Race condition on startup of AV still...

This commit is contained in:
James Ketr 2025-08-26 19:07:07 -07:00
parent b26366eb05
commit 6588672a3c
2 changed files with 371 additions and 185 deletions

View File

@ -13,6 +13,113 @@ import { Session } from "./GlobalContext";
const debug = true; const debug = true;
const createAnimatedVideoTrack = ({ width = 320, height = 240 } = {}): MediaStreamTrack => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not get canvas context");
// Ball properties
const ball = {
x: width / 2,
y: height / 2,
radius: Math.min(width, height) * 0.06,
dx: 3,
dy: 2,
color: "#00ff88",
};
// Create stream BEFORE starting animation
const stream = canvas.captureStream(15);
const track = stream.getVideoTracks()[0];
function drawFrame() {
if (!ctx) return;
// Clear canvas
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
// Update ball position
ball.x += ball.dx;
ball.y += ball.dy;
// Bounce off walls
if (ball.x + ball.radius >= width || ball.x - ball.radius <= 0) {
ball.dx = -ball.dx;
}
if (ball.y + ball.radius >= height || ball.y - ball.radius <= 0) {
ball.dy = -ball.dy;
}
// Keep ball in bounds
ball.x = Math.max(ball.radius, Math.min(width - ball.radius, ball.x));
ball.y = Math.max(ball.radius, Math.min(height - ball.radius, ball.y));
// Draw ball
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
// Add frame number or timestamp for debugging
ctx.fillStyle = "#ffffff";
ctx.font = "12px Arial";
ctx.fillText(`Frame: ${Date.now() % 10000}`, 10, 20);
}
// Draw initial frame
drawFrame();
// Start animation - CRITICAL: Request animation frame for better performance
function animate() {
drawFrame();
requestAnimationFrame(animate);
}
animate();
track.enabled = true;
return track;
};
const createBlackVideoTrack = ({ width = 320, height = 240 } = {}): MediaStreamTrack => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
}
// Use 1 FPS instead of 30 for synthetic tracks
const stream = canvas.captureStream(1);
return stream.getVideoTracks()[0];
};
// Helper function to create a silent audio track
const createSilentAudioTrack = (): MediaStreamTrack => {
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
const destination = audioContext.createMediaStreamDestination();
// Set gain to 0 for silence
gainNode.gain.value = 0;
// Connect: oscillator -> gain -> destination
oscillator.connect(gainNode);
gainNode.connect(destination);
oscillator.start();
const track = destination.stream.getAudioTracks()[0];
track.enabled = true;
return track;
};
// Types for peer and track context // Types for peer and track context
interface Peer { interface Peer {
session_id: string; session_id: string;
@ -25,13 +132,14 @@ interface Peer {
local: boolean; local: boolean;
dead: boolean; dead: boolean;
connection?: RTCPeerConnection; connection?: RTCPeerConnection;
queuedCandidates?: RTCIceCandidateInit[];
} }
export type { Peer }; export type { Peer };
interface TrackContext { interface TrackContext {
media: MediaStream | null; media: MediaStream | null;
audio: boolean; has_audio: boolean;
video: boolean; has_video: boolean;
} }
interface AddPeerConfig { interface AddPeerConfig {
@ -63,24 +171,42 @@ interface VideoProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
const Video: React.FC<VideoProps> = ({ srcObject, local, ...props }) => { const Video: React.FC<VideoProps> = ({ srcObject, local, ...props }) => {
const refVideo = useRef<HTMLVideoElement>(null); const refVideo = useRef<HTMLVideoElement>(null);
useEffect(() => { useEffect(() => {
if (!refVideo.current) { if (!refVideo.current || !srcObject) {
return; return;
} }
const ref = refVideo.current; const ref = refVideo.current;
if (debug) console.log("media-control - video <video> bind"); console.log("Setting video srcObject:", srcObject);
ref.srcObject = srcObject; ref.srcObject = srcObject;
if (local) { if (local) {
ref.muted = true; ref.muted = true;
} }
// Force play the video
const playVideo = async () => {
try {
await ref.play();
console.log("Video started playing");
} catch (error) {
console.error("Error playing video:", error);
}
};
// Play after a short delay to ensure srcObject is set
setTimeout(playVideo, 100);
return () => { return () => {
if (debug) console.log("media-control - <video> unbind"); console.log("Cleaning up video srcObject");
if (ref) { if (ref) {
(ref as any).srcObject = undefined; ref.srcObject = null;
} }
}; };
}, [srcObject, local]); }, [srcObject, local]);
return <video ref={refVideo} {...props} />;
return <video ref={refVideo} autoPlay playsInline muted={local} {...props} />;
}; };
type MediaAgentProps = { type MediaAgentProps = {
@ -93,7 +219,7 @@ type MediaAgentProps = {
const MediaAgent = (props: MediaAgentProps) => { const MediaAgent = (props: MediaAgentProps) => {
const { peers, setPeers, socketUrl, session } = props; const { peers, setPeers, socketUrl, session } = props;
// track: null = no local media, TrackContext = local media // track: null = no local media, TrackContext = local media
const [track, setTrack] = useState<TrackContext | null>(null); const [context, setContext] = useState<TrackContext | null>(null);
const { sendJsonMessage, lastJsonMessage } = useWebSocket(socketUrl, { const { sendJsonMessage, lastJsonMessage } = useWebSocket(socketUrl, {
share: true, share: true,
@ -183,6 +309,12 @@ const MediaAgent = (props: MediaAgentProps) => {
peer.connection = connection; peer.connection = connection;
connection.addEventListener("connectionstatechange", (event) => { connection.addEventListener("connectionstatechange", (event) => {
console.log(`media-agent - connectionstatechange - `, connection.connectionState, event); console.log(`media-agent - connectionstatechange - `, connection.connectionState, event);
if (connection.connectionState === "failed") {
console.log("Connection failed, attempting recovery...");
// Implement retry logic or notify user
peer.dead = true;
setPeers({ ...peers });
}
}); });
connection.addEventListener("negotiationneeded", (event) => { connection.addEventListener("negotiationneeded", (event) => {
console.log(`media-agent - negotiationneeded - `, connection.connectionState, event); console.log(`media-agent - negotiationneeded - `, connection.connectionState, event);
@ -215,12 +347,31 @@ const MediaAgent = (props: MediaAgentProps) => {
}, },
}); });
}; };
connection.ontrack = (e: RTCTrackEvent) => refOnTrack.current(e); connection.ontrack = (e: RTCTrackEvent) => {
console.log("media-agent - ontrack event received", e);
console.log("Stream:", e.streams[0]);
console.log("Track:", e.track);
refOnTrack.current(e);
};
connection.oniceconnectionstatechange = (event) => {
console.log(`media-agent - iceconnectionstatechange - `, connection.iceConnectionState, event);
if (connection.iceConnectionState === "failed") {
console.log("ICE connection failed, attempting recovery...");
// Implement retry logic or notify user
peer.dead = true;
setPeers({ ...peers });
}
};
// Only add local tracks if present // Only add local tracks if present
if (track && track.media) { if (context && context.media) {
track.media.getTracks().forEach((t) => { console.log("Adding local tracks to new peer connection");
connection.addTrack(t, track.media!); context.media.getTracks().forEach((t) => {
console.log("Adding track:", t.kind, t.enabled);
connection.addTrack(t, context.media!);
}); });
} else {
console.log("No local tracks available when creating peer");
} }
if (config.should_create_offer) { if (config.should_create_offer) {
connection connection
@ -241,55 +392,60 @@ const MediaAgent = (props: MediaAgentProps) => {
}); });
} }
}, },
[peers, setPeers, track, sendJsonMessage] [peers, setPeers, context, sendJsonMessage]
); );
const sessionDescription = useCallback( const sessionDescription = useCallback(
({ peer_id, session_description }: SessionDescriptionData) => { ({ peer_id, session_description }: SessionDescriptionData) => {
const peer = peers[peer_id]; const peer = peers[peer_id];
if (!peer) { console.log(`media-agent - sessionDescription - `, { peer_id, session_description, peer });
console.error(`media-agent - sessionDescription - No peer for ${peer_id}`); if (!peer?.connection) return;
return;
}
const { connection } = peer;
if (!connection) {
console.error(`media-agent - sessionDescription - No connection for peer ${peer_id}`);
return;
}
const desc = new RTCSessionDescription(session_description); const desc = new RTCSessionDescription(session_description);
connection peer.connection
.setRemoteDescription(desc) .setRemoteDescription(desc)
.then(() => { .then(() => {
if (debug) console.log(`media-agent - sessionDescription - setRemoteDescription succeeded`); console.log("Remote description set successfully");
if (session_description.type === "offer") {
if (debug) console.log(`media-agent - sessionDescription - Creating answer`); // Process queued ICE candidates after remote description is set
connection if (peer.queuedCandidates && peer.queuedCandidates.length > 0) {
.createAnswer() console.log(`Processing ${peer.queuedCandidates.length} queued candidates`);
.then((local_description) => { const candidatePromises = peer.queuedCandidates.map((candidate) =>
if (debug) console.log(`media-agent - sessionDescription - Answer description is: `, local_description); peer.connection!.addIceCandidate(new RTCIceCandidate(candidate))
connection );
.setLocalDescription(local_description)
.then(() => { Promise.all(candidatePromises)
sendJsonMessage({ .then(() => {
type: "relaySessionDescription", console.log("All queued candidates processed");
config: { peer.queuedCandidates = [];
peer_id,
session_description: local_description,
},
});
if (debug) console.log(`media-agent - sessionDescription - Answer setLocalDescription succeeded`);
})
.catch(() => {
console.error(`media-agent - sessionDescription - Answer setLocalDescription failed!`);
});
}) })
.catch((error) => { .catch((err) => console.error("Error processing queued candidates:", err));
console.error(error); }
// Handle offer/answer logic...
if (session_description.type === "offer") {
console.log("Creating answer for received offer");
return peer.connection!.createAnswer();
}
return null;
})
.then((answer) => {
if (answer && session_description.type === "offer") {
return peer.connection!.setLocalDescription(answer).then(() => {
sendJsonMessage({
type: "relaySessionDescription",
config: {
peer_id: peer_id,
session_description: answer,
},
}); });
console.log("Answer sent successfully");
});
} }
}) })
.catch((error) => { .catch((error) => {
console.log(`media-agent - sessionDescription - setRemoteDescription error: `, error); console.error("Failed to set remote description:", error);
}); });
}, },
[peers, sendJsonMessage] [peers, sendJsonMessage]
@ -316,28 +472,28 @@ const MediaAgent = (props: MediaAgentProps) => {
const iceCandidate = useCallback( const iceCandidate = useCallback(
({ peer_id, candidate }: IceCandidateData) => { ({ peer_id, candidate }: IceCandidateData) => {
/**
* The offerer will send a number of ICE Candidate blobs to the
* answerer so they can begin trying to find the best path to one
* another on the net.
*/
const peer = peers[peer_id]; const peer = peers[peer_id];
if (!peer) { console.log(`media-agent - iceCandidate - `, { peer_id, candidate, peer });
console.error(`media-agent - iceCandidate - No peer for ${peer_id}`, peers); if (!peer?.connection) {
console.error(`No peer or connection for ${peer_id}`);
return; return;
} }
peer.connection
?.addIceCandidate(new RTCIceCandidate(candidate)) // Critical fix: Queue candidates if remote description not set
.then(() => { if (peer.connection.remoteDescription) {
if (debug) console.log(`media-agent - iceCandidate - Successfully added Ice Candidate for ${peer_id}`); peer.connection
}) .addIceCandidate(new RTCIceCandidate(candidate))
.catch((error) => { .then(() => console.log(`Added ICE candidate for ${peer_id}`))
console.error(error, peer, candidate); .catch((err) => console.error("Failed to add ICE candidate:", err));
}); } else {
// Queue the candidate for later processing
if (!peer.queuedCandidates) peer.queuedCandidates = [];
peer.queuedCandidates.push(candidate);
console.log(`Queued ICE candidate for ${peer_id} (no remote description yet)`);
}
}, },
[peers] [peers]
); );
useEffect(() => { useEffect(() => {
if (!lastJsonMessage) { if (!lastJsonMessage) {
return; return;
@ -346,10 +502,6 @@ const MediaAgent = (props: MediaAgentProps) => {
if (!session) { if (!session) {
return; return;
} }
console.log("media-agent - message", data);
if (["addPeer", "removePeer", "iceCandidate", "sessionDescription"].includes(data.type)) {
console.log(`media-agent - message - ${data.type}`, peers);
}
switch (data.type) { switch (data.type) {
case "addPeer": case "addPeer":
addPeer(data.data); addPeer(data.data);
@ -373,56 +525,77 @@ const MediaAgent = (props: MediaAgentProps) => {
}); });
useEffect(() => { useEffect(() => {
console.log(`media-control - Track changed`, track); console.log(`media-control - Context changed`, context);
const join = () => { const join = () => {
sendJsonMessage({ sendJsonMessage({
type: "join", type: "join",
data: { data: {
has_audio: track && track.audio ? track.audio : false, has_audio: context && context.has_audio ? context.has_audio : false,
has_video: track && track.video ? track.video : false, has_video: context && context.has_video ? context.has_video : false,
}, },
}); });
}; };
if (track !== undefined) { if (context) {
console.log(`media-control - issuing join request: `, track); console.log(`media-control - issuing join request: `, context);
for (let peer in peers) { for (let peer in peers) {
if (peers[peer].local && peers[peer].dead) { if (peers[peer].local && peers[peer].dead) {
/* Allocate a new Object so <MediaControl> will trigger */
peers[peer] = Object.assign({}, peers[peer]);
// Mark as alive // Mark as alive
peers[peer].dead = false; peers[peer] = { ...peers[peer], dead: false };
setPeers(Object.assign({}, peers)); setPeers({ ...peers });
} }
} }
join(); join();
} }
}, [track, peers, setPeers, sendJsonMessage]); }, [context, peers, setPeers, sendJsonMessage]);
useEffect(() => {
if (!context || !context.media) return;
console.log("Track changed, updating all peer connections");
// Add tracks to all existing peer connections
for (let peer_id in peers) {
const peer = peers[peer_id];
if (peer.connection && !peer.local && !peer.dead) {
console.log(`Adding tracks to existing peer ${peer.peerName}`);
if (!context || !context.media) return;
context.media.getTracks().forEach((t) => {
// Check if track is already added
const senders = peer.connection!.getSenders();
const trackAlreadyAdded = senders.some((sender) => sender.track === t);
if (!trackAlreadyAdded) {
console.log(`Adding ${t.kind} track to ${peer.peerName}`);
peer.connection!.addTrack(t, context.media!);
}
});
}
}
}, [context, peers]);
useEffect(() => { useEffect(() => {
if (!session) { if (!session) {
return; return;
} }
let update = false; let update = false;
if (track) { if (context && !(session.id in peers)) {
if (!(session.id in peers)) { update = true;
update = true; peers[session.id] = {
peers[session.id] = { peerName: session.name || "Unknown",
peerName: session.name || "Unknown", session_id: session.id,
session_id: session.id, local: true,
muted: true,
video_on: false,
has_video: context.has_video,
has_audio: context.has_audio,
attributes: {
local: true, local: true,
muted: true, srcObject: context.media,
video_on: false, },
has_video: track.video, dead: false,
has_audio: track.audio, };
attributes: {
local: true,
srcObject: track.media,
},
dead: false,
};
}
} }
/* Renaming the local connection requires the peer to be deleted /* Renaming the local connection requires the peer to be deleted
@ -438,74 +611,47 @@ const MediaAgent = (props: MediaAgentProps) => {
if (debug) console.log(`media-agent - Setting global peers`, peers); if (debug) console.log(`media-agent - Setting global peers`, peers);
setPeers(Object.assign({}, peers)); setPeers(Object.assign({}, peers));
} }
}, [peers, setPeers, track, session]); }, [peers, setPeers, context, session]);
const setup_local_media = async (): Promise<TrackContext> => { const setup_local_media = async (): Promise<TrackContext> => {
console.log(`media-agent - Requesting access to local audio / video inputs`); console.log(`media-agent - Requesting access to local audio / video inputs`);
const context: TrackContext = { media: null, audio: true, video: true }; const context: TrackContext = { media: null, has_audio: true, has_video: true };
// Try to get user media with fallback logic // Try to get user media with fallback logic
while (context.audio || context.video) { while (context.has_audio || context.has_video) {
console.log(context);
try { try {
context.media = await navigator.mediaDevices.getUserMedia({ const constraints: any = {};
audio: context.audio, if (context.has_audio) {
video: context.video, constraints.audio = true;
}); }
break; if (context.has_video) {
constraints.video = true;
}
console.log(
`media-agent - Attempting to get user media: audio=${context.has_audio}, video=${context.has_video}`
);
context.media = await navigator.mediaDevices.getUserMedia(constraints);
/* Success -- on failure, an exception is thrown */
return context;
} catch (error) { } catch (error) {
console.error(`media-agent - Error accessing local media: `, error); if (context.has_video && context.has_audio) {
if (context.video) { console.log(`media-agent - Disabling video and trying just audio`);
console.log(`media-agent - Disabling video and trying again`); context.has_video = false;
context.video = false; context.has_audio = true;
} else if (context.audio) { } else if (context.has_audio && !context.has_video) {
console.log(`media-agent - Disabling audio and trying again`); console.log(`media-agent - Disabling audio and trying just video`);
context.audio = false; context.has_video = true;
context.has_audio = false;
} else { } else {
console.log(`media-agent - No media available`); console.log(`media-agent - No media available`);
break; context.has_video = false;
context.has_audio = false;
} }
} }
} }
// Helper function to create a black video track
const createBlackVideoTrack = ({ width = 640, height = 480 } = {}): MediaStreamTrack => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
}
const stream = canvas.captureStream(30); // 30 FPS
const track = stream.getVideoTracks()[0];
track.enabled = true;
return track;
};
// Helper function to create a silent audio track
const createSilentAudioTrack = (): MediaStreamTrack => {
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
const destination = audioContext.createMediaStreamDestination();
// Set gain to 0 for silence
gainNode.gain.value = 0;
// Connect: oscillator -> gain -> destination
oscillator.connect(gainNode);
gainNode.connect(destination);
oscillator.start();
const track = destination.stream.getAudioTracks()[0];
track.enabled = true;
return track;
};
// Process the results and create appropriate media stream // Process the results and create appropriate media stream
const tracks: MediaStreamTrack[] = []; const tracks: MediaStreamTrack[] = [];
let hasRealAudio = false; let hasRealAudio = false;
@ -540,7 +686,7 @@ const MediaAgent = (props: MediaAgentProps) => {
} }
if (!hasRealVideo) { if (!hasRealVideo) {
tracks.push(createBlackVideoTrack()); tracks.push(createAnimatedVideoTrack());
console.log("media-agent - Using synthetic black video"); console.log("media-agent - Using synthetic black video");
} }
@ -548,8 +694,8 @@ const MediaAgent = (props: MediaAgentProps) => {
context.media = new MediaStream(tracks); context.media = new MediaStream(tracks);
// Update context flags to reflect what we actually have // Update context flags to reflect what we actually have
context.audio = hasRealAudio; context.has_audio = true; //hasRealAudio;
context.video = hasRealVideo; context.has_video = true; //hasRealVideo;
const mediaType = const mediaType =
hasRealAudio && hasRealVideo hasRealAudio && hasRealVideo
@ -570,15 +716,19 @@ const MediaAgent = (props: MediaAgentProps) => {
return; return;
} }
if (track === null) { if (context === null) {
setup_local_media() setup_local_media()
.then((context) => { .then((context) => {
sendJsonMessage({ type: "media_status", ...context, media: undefined }); sendJsonMessage({ type: "media_status", ...context, media: undefined });
setTrack(context); setContext(context);
}) })
.catch(() => setTrack(null)); .catch((error) => {
console.error("media-agent - Failed to get local media:", error);
sendJsonMessage({ type: "media_status", has_audio: false, has_video: false, media: undefined });
setContext(null);
});
} }
}, [track, session, sendJsonMessage]); }, [context, session, sendJsonMessage]);
return <></>; return <></>;
}; };
@ -599,8 +749,10 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
}); });
useEffect(() => { useEffect(() => {
console.log(`media-control - peer changed`, peer);
if (peer && peer.peerName) { if (peer && peer.peerName) {
const el = document.querySelector(`.MediaControl[data-peer="${peer.session_id}"]`); const el = document.querySelector(`.MediaControl[data-peer="${peer.session_id}"]`);
console.log(`media-control - setting target for ${peer.peerName}`, el);
setTarget(el ?? undefined); setTarget(el ?? undefined);
} }
}, [setTarget, peer]); }, [setTarget, peer]);
@ -646,13 +798,13 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
if (!media || media.dead || !peer) { if (!media || media.dead || !peer) {
return; return;
} }
console.log(`media-control - media changed`, media);
if (media.attributes.srcObject) { if (media.attributes.srcObject) {
console.log(`media-control - audio enable - ${peer.peerName}:${!muted}`);
(media.attributes.srcObject.getAudioTracks() as MediaStreamTrack[]).forEach((track: MediaStreamTrack) => { (media.attributes.srcObject.getAudioTracks() as MediaStreamTrack[]).forEach((track: MediaStreamTrack) => {
track.enabled = media.has_audio && !muted; track.enabled = media.has_audio && !muted;
}); });
} }
}); }, [muted, media, peer]);
useEffect(() => { useEffect(() => {
if (!media || media.dead || !peer) { if (!media || media.dead || !peer) {

View File

@ -172,18 +172,20 @@ async def join(
if not session.ws: if not session.ws:
logger.error( logger.error(
f"{session.short}:{session.name} - No WebSocket connection. Media not available." f"{getSessionName(session)} - No WebSocket connection. Media not available."
) )
return return
logger.info(f"{getSessionName(session)} <- join({getLobbyName(lobby)})") logger.info(f"{getSessionName(session)} <- join({getLobbyName(lobby)})")
if session.id in lobby.peers: # if session.id in lobby.peers:
logger.info(f"{getSessionName(session)} - Already joined to Media.") # logger.info(f"{getSessionName(session)} - Already joined to Media.")
return # return
# Notify all existing RTC peers # Notify all existing RTC peers
for peer_session in lobby.peers.values(): for peer_session in lobby.peers.values():
if peer_session.id == session.id:
continue
if not peer_session.ws: if not peer_session.ws:
logger.warning( logger.warning(
f"{getSessionName(peer_session)} - No WebSocket connection. Skipping." f"{getSessionName(peer_session)} - No WebSocket connection. Skipping."
@ -191,7 +193,7 @@ async def join(
continue continue
logger.info( logger.info(
f"{getSessionName(peer_session)} -> addPeer({getSessionName(session), getLobbyName(lobby)})" f"{getSessionName(peer_session)} -> addPeer({getSessionName(session), getLobbyName(lobby)}, video={has_video}, audio={has_audio}, should_create_offer=False)"
) )
await peer_session.ws.send_json( await peer_session.ws.send_json(
{ {
@ -209,7 +211,7 @@ async def join(
# Add each other peer to the caller # Add each other peer to the caller
if session.ws: if session.ws:
logger.info( logger.info(
f"{getSessionName(session)} -> addPeer({getSessionName(peer_session), getLobbyName(lobby)})" f"{getSessionName(session)} -> addPeer({getSessionName(peer_session), getLobbyName(lobby)}, video={peer_session.has_video}, audio={peer_session.has_audio}, should_create_offer=True)"
) )
await session.ws.send_json( await session.ws.send_json(
{ {
@ -327,7 +329,7 @@ async def websocket_lobby(
return return
session = getSession(session_id) session = getSession(session_id)
if not session: if not session:
logger.error(f"Invalid session ID {session_id}") # logger.error(f"Invalid session ID {session_id}")
await websocket.send_json( await websocket.send_json(
{"type": "error", "error": f"Invalid session ID {session_id}"} {"type": "error", "error": f"Invalid session ID {session_id}"}
) )
@ -381,8 +383,8 @@ async def websocket_lobby(
await update_users(lobby, session) await update_users(lobby, session)
case "media_status": case "media_status":
has_audio = data.get("audio", False) has_audio = data.get("has_audio", False)
has_video = data.get("video", False) has_video = data.get("has_video", False)
logger.info( logger.info(
f"{getSessionName(session)}: <- media-status(audio: {has_audio}, video: {has_video})" f"{getSessionName(session)}: <- media-status(audio: {has_audio}, video: {has_video})"
) )
@ -390,8 +392,9 @@ async def websocket_lobby(
session.has_video = has_video session.has_video = has_video
case "join": case "join":
has_audio = data.get("audio", False) logger.info(f"{getSessionName(session)} <- join {data}")
has_video = data.get("video", False) has_audio = data.get("has_audio", False)
has_video = data.get("has_video", False)
await join(lobby, session, has_video, has_audio) await join(lobby, session, has_video, has_audio)
case "part": case "part":
@ -401,11 +404,26 @@ async def websocket_lobby(
logger.info(f"{getSessionName(session)} <- relayICECandidate") logger.info(f"{getSessionName(session)} <- relayICECandidate")
if session.id not in lobby.peers: if session.id not in lobby.peers:
logger.error( logger.error(
f"{session.short}:{session.name} <- relayICECandidate - Not an RTC peer" f"{session.short}:{session.name} <- relayICECandidate - Not an RTC peer ({session.id})"
) )
return await websocket.send_json(
{"type": "error", "error": "Not joined to media session"}
)
continue
peer_id = data.get("config", {}).get("peer_id") peer_id = data.get("config", {}).get("peer_id")
if peer_id not in lobby.peers:
logger.error(
f"{getSessionName(session)} <- relayICECandidate - Not an RTC peer({peer_id})"
)
await websocket.send_json(
{
"type": "error",
"error": f"Target peer {peer_id} not found",
}
)
continue
candidate = data.get("config", {}).get("candidate") candidate = data.get("config", {}).get("candidate")
message = { message = {
@ -419,7 +437,10 @@ async def websocket_lobby(
logger.warning( logger.warning(
f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping." f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping."
) )
return break
logger.info(
f"{getSessionName(session)} -> iceCandidate({getSessionName(lobby.peers[peer_id])})"
)
await ws.send_json(message) await ws.send_json(message)
case "relaySessionDescription": case "relaySessionDescription":
@ -428,8 +449,14 @@ async def websocket_lobby(
logger.error( logger.error(
f"{session.short}:{session.name} - relaySessionDescription - Not an RTC peer" f"{session.short}:{session.name} - relaySessionDescription - Not an RTC peer"
) )
return break
peer_id = data.get("config", {}).get("peer_id") peer_id = data.get("config", {}).get("peer_id")
peer = lobby.peers.get(peer_id, None)
if not peer:
logger.error(
f"{getSessionName(session)} <- relaySessionDescription - Not an RTC peer({peer_id})"
)
break
session_description = data.get("config", {}).get( session_description = data.get("config", {}).get(
"session_description" "session_description"
) )
@ -440,14 +467,16 @@ async def websocket_lobby(
"session_description": session_description, "session_description": session_description,
}, },
} }
if peer_id in lobby.peers: if not peer.ws:
ws = lobby.peers[peer_id].ws logger.warning(
if not ws: f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping."
logger.warning( )
f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping." break
)
return logger.info(
await ws.send_json(message) f"{getSessionName(session)} -> sessionDescription({getSessionName(lobby.peers[peer_id])})"
)
await peer.ws.send_json(message)
case _: case _:
await websocket.send_json( await websocket.send_json(
@ -461,11 +490,16 @@ async def websocket_lobby(
logger.info(f"{getSessionName(session)} <- WebSocket disconnected for user.") logger.info(f"{getSessionName(session)} <- WebSocket disconnected for user.")
# Cleanup: remove session from lobby and sessions dict # Cleanup: remove session from lobby and sessions dict
session.ws = None session.ws = None
if lobby and session: if session.id in lobby.peers:
await part(lobby, session) await part(lobby, session)
await update_users(lobby) await update_users(lobby)
# if session_id in sessions:
# del sessions[session_id] # Clean up empty lobbies
if not lobby.sessions:
if lobby.id in lobbies:
del lobbies[lobby.id]
logger.info(f"Cleaned up empty lobby {lobby.short}")
# Serve static files or proxy to frontend development server # Serve static files or proxy to frontend development server