-
-
Notifications
You must be signed in to change notification settings - Fork 75
Issue#77 #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue#77 #191
Changes from all commits
c3979d2
2f5158a
8f314c2
efecea9
ed58d52
f496fef
1018c49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ const app = express(); | |
| const server = http.createServer(app); | ||
|
|
||
| const io = new Server(server, { cors: { origin: "*" } }); | ||
| // store latest media state per-socket so late joiners can be informed | ||
| const lastMediaState = new Map<string, { micOn?: boolean; camOn?: boolean }>(); | ||
| // io.adapter(createAdapter(pubClient, subClient)); | ||
|
|
||
| const userManager = new UserManager(); | ||
|
|
@@ -73,16 +75,10 @@ io.on("connection", (socket: Socket) => { | |
| userManager.setRoom(socket.id, initialRoomId); | ||
| } | ||
|
|
||
| // Keep UserManager in sync when client explicitly joins later | ||
| socket.on("chat:join", ({ roomId }: ChatJoinPayload) => { | ||
| try { | ||
| if (!roomId || typeof roomId !== "string") return; | ||
| const namespaced = normalizeRoom(roomId.trim()); | ||
| // Keep UserManager in sync only; actual join + announcements are handled in chat.ts | ||
| userManager.setRoom(socket.id, namespaced); | ||
| } catch (err) { | ||
| console.warn("[chat:join] error", err); | ||
| } | ||
| // โฌ๏ธ Keep UserManager in sync when client explicitly joins later | ||
| socket.on("chat:join", ({ roomId }: { roomId: string; name?: string }) => { | ||
| if (roomId) userManager.setRoom(socket.id, roomId); | ||
| if (roomId) socket.join(roomId); | ||
| }); | ||
|
Comment on lines
+78
to
82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize room names and hydrate late joiners on The handler joins the raw roomId while most emits use the Apply this diff: - // โฌ๏ธ Keep UserManager in sync when client explicitly joins later
- socket.on("chat:join", ({ roomId }: { roomId: string; name?: string }) => {
- if (roomId) userManager.setRoom(socket.id, roomId);
- if (roomId) socket.join(roomId);
- });
+ // โฌ๏ธ Keep UserManager in sync when client explicitly joins later
+ socket.on("chat:join", async ({ roomId }: { roomId: string; name?: string }) => {
+ const r = normalizeRoom(roomId);
+ if (!r) return;
+ userManager.setRoom(socket.id, r);
+ socket.join(r);
+ // Hydrate the joiner with current peers' media states
+ const socketsInRoom = await io.in(r).fetchSockets();
+ const peers = socketsInRoom
+ .filter((s) => s.id !== socket.id)
+ .map((s) => ({ id: s.id, state: lastMediaState.get(s.id) ?? {} }));
+ socket.emit("room:peers-media-state", { peers });
+ });Optional hardening (outside this range): make // replace existing normalizeRoom
const normalizeRoom = (r: string) => (r ? (r.startsWith("chat:") ? r : `chat:${r}`) : "");๐ค Prompt for AI Agents |
||
|
|
||
| // Screen share + media + renegotiation handlers (same behavior, use namespaced rooms) | ||
|
|
@@ -120,8 +116,7 @@ io.on("connection", (socket: Socket) => { | |
|
|
||
| // Media state | ||
| socket.on("media:state", ({ roomId, state }: { roomId: string; state: { micOn?: boolean; camOn?: boolean } }) => { | ||
| const r = toRoom(roomId); | ||
| if (r) socket.to(r).emit("peer:media-state", { state, from: socket.id }); | ||
| socket.to(roomId).emit("peer:media-state", { state }); | ||
| }); | ||
|
Comment on lines
118
to
120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐งฉ Analysis chain
Without Apply this diff: - socket.on("media:state", ({ roomId, state }: { roomId: string; state: { micOn?: boolean; camOn?: boolean } }) => {
- socket.to(roomId).emit("peer:media-state", { state });
- });
+ socket.on("media:state", ({ roomId, state }: { roomId: string; state: { micOn?: boolean; camOn?: boolean } }) => {
+ const r = normalizeRoom(roomId);
+ // persist latest media state for late joiners
+ lastMediaState.set(socket.id, { ...(lastMediaState.get(socket.id) ?? {}), ...state });
+ if (r) socket.to(r).emit("peer:media-state", { state, from: socket.id });
+ });Keep the cache in sync for individual toggles too (outside-range snippet): // media:cam
socket.on("media:cam", ({ roomId, on }: { roomId: string; on: boolean }) => {
const r = normalizeRoom(roomId);
lastMediaState.set(socket.id, { ...(lastMediaState.get(socket.id) ?? {}), camOn: on });
if (r) socket.to(r).emit("media:cam", { on, from: socket.id });
});
// media:mic
socket.on("media:mic", ({ roomId, on }: { roomId: string; on: boolean }) => {
const r = normalizeRoom(roomId);
lastMediaState.set(socket.id, { ...(lastMediaState.get(socket.id) ?? {}), micOn: on });
if (r) socket.to(r).emit("media:mic", { on, from: socket.id });
});Verify frontend expectations for ๐ Script executed: #!/bin/bash
# Find handlers/usages of 'peer:media-state' and check if 'from' is referenced.
rg -n -C2 --type ts --type tsx "peer:media-state|from" frontend || rg -n -C2 "peer:media-state|from"Length of output: 30928 Now let me search for the actual functions used in the codebase: ๐ Script executed: #!/bin/bash
# Find toRoom function and lastMediaState usage
rg -n -B5 -A10 'toRoom|lastMediaState' backend/src/index.ts | head -100Length of output: 60 ๐ Script executed: #!/bin/bash
# Find toRoom function definition and lastMediaState usage in backend
rg -n -B2 -A8 'const toRoom|function toRoom|toRoom\s*=' backend/src/index.ts
echo "---"
# Find lastMediaState declaration and usage
rg -n 'lastMediaState' backend/src/index.ts | head -20
echo "---"
# Show context around media:state handler and nearby handlers
sed -n '115,135p' backend/src/index.tsLength of output: 1388 Add The Apply the diff as provided, but also update socket.on("media:cam", ({ roomId, on }: { roomId: string; on: boolean }) => {
const r = toRoom(roomId);
+ lastMediaState.set(socket.id, { ...(lastMediaState.get(socket.id) ?? {}), camOn: on });
if (r) socket.to(r).emit("media:cam", { on, from: socket.id });
});
socket.on("media:mic", ({ roomId, on }: { roomId: string; on: boolean }) => {
const r = toRoom(roomId);
+ lastMediaState.set(socket.id, { ...(lastMediaState.get(socket.id) ?? {}), micOn: on });
if (r) socket.to(r).emit("media:mic", { on, from: socket.id });
});
๐ค Prompt for AI Agents |
||
|
|
||
| socket.on("media:cam", ({ roomId, on }: { roomId: string; on: boolean }) => { | ||
|
|
@@ -164,6 +159,8 @@ io.on("connection", (socket: Socket) => { | |
|
|
||
| // chat.ts handles leave announcements in its disconnecting handler | ||
|
|
||
| // cleanup stored media state | ||
| lastMediaState.delete(socket.id); | ||
| userManager.removeUser(socket.id); | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -71,6 +71,7 @@ export default function Room({ | |||||||||||||
| const currentScreenShareTrackRef = useRef<MediaStreamTrack | null>(null); | ||||||||||||||
| const localScreenShareStreamRef = useRef<MediaStream | null>(null); | ||||||||||||||
| const remoteStreamRef = useRef<MediaStream | null>(null); | ||||||||||||||
| const peerCamOnRef = useRef<boolean>(false); | ||||||||||||||
|
|
||||||||||||||
| // ICE candidate queues for handling candidates before remote description is set | ||||||||||||||
| const senderIceCandidatesQueue = useRef<RTCIceCandidate[]>([]); | ||||||||||||||
|
|
@@ -115,33 +116,35 @@ export default function Room({ | |||||||||||||
| pc.addTrack(localAudioTrack); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| let videoTrack = currentVideoTrackRef.current; | ||||||||||||||
| if (!videoTrack || videoTrack.readyState === "ended") { | ||||||||||||||
| try { | ||||||||||||||
| const stream = await navigator.mediaDevices.getUserMedia({ video: true }); | ||||||||||||||
| videoTrack = stream.getVideoTracks()[0]; | ||||||||||||||
| currentVideoTrackRef.current = videoTrack; | ||||||||||||||
| } catch (err) { | ||||||||||||||
| console.error("Error creating video track:", err); | ||||||||||||||
| videoTrack = null; | ||||||||||||||
| if (camOn) { | ||||||||||||||
| let videoTrack = currentVideoTrackRef.current; | ||||||||||||||
| if (!videoTrack || videoTrack.readyState === "ended") { | ||||||||||||||
| try { | ||||||||||||||
| const stream = await navigator.mediaDevices.getUserMedia({ video: true }); | ||||||||||||||
| videoTrack = stream.getVideoTracks()[0]; | ||||||||||||||
| currentVideoTrackRef.current = videoTrack; | ||||||||||||||
| } catch (err) { | ||||||||||||||
| console.error("Error creating video track:", err); | ||||||||||||||
| videoTrack = null; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (videoTrack && videoTrack.readyState === "live") { | ||||||||||||||
| const vs = pc.addTrack(videoTrack); | ||||||||||||||
| videoSenderRef.current = vs; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (videoTrack && videoTrack.readyState === "live") { | ||||||||||||||
| const vs = pc.addTrack(videoTrack); | ||||||||||||||
| videoSenderRef.current = vs; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| ensureRemoteStreamLocal(); | ||||||||||||||
| pc.ontrack = (e) => { | ||||||||||||||
| if (!remoteStreamRef.current) remoteStreamRef.current = new MediaStream(); | ||||||||||||||
| if (e.track.kind === 'video') { | ||||||||||||||
| remoteStreamRef.current.getVideoTracks().forEach(track => | ||||||||||||||
| remoteStreamRef.current?.removeTrack(track) | ||||||||||||||
| ); | ||||||||||||||
| remoteStreamRef.current.getVideoTracks().forEach(t => { try { remoteStreamRef.current?.removeTrack(t); } catch {} }); | ||||||||||||||
| remoteStreamRef.current.addTrack(e.track); | ||||||||||||||
| } else { | ||||||||||||||
| remoteStreamRef.current.addTrack(e.track); | ||||||||||||||
| } | ||||||||||||||
| remoteStreamRef.current.addTrack(e.track); | ||||||||||||||
| ensureRemoteStreamLocal(); | ||||||||||||||
| ensureRemoteStreamLocal(); | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| pc.onicecandidate = (e) => { | ||||||||||||||
|
|
@@ -387,6 +390,8 @@ export default function Room({ | |||||||||||||
|
|
||||||||||||||
| teardownPeers( | ||||||||||||||
| "teardown", | ||||||||||||||
| camOn, | ||||||||||||||
| micOn, | ||||||||||||||
| sendingPcRef, | ||||||||||||||
| receivingPcRef, | ||||||||||||||
| remoteStreamRef, | ||||||||||||||
|
|
@@ -456,6 +461,8 @@ export default function Room({ | |||||||||||||
|
|
||||||||||||||
| teardownPeers( | ||||||||||||||
| reason, | ||||||||||||||
| camOn, | ||||||||||||||
| micOn, | ||||||||||||||
| sendingPcRef, | ||||||||||||||
|
Comment on lines
+464
to
466
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass actual media liveness to teardownPeers during Next. You compute currentCamState/currentMicState but pass camOn/micOn, which can be stale (e.g., track ended). - camOn,
- micOn,
+ currentCamState,
+ currentMicState,๐ Committable suggestion
Suggested change
๐ค Prompt for AI Agents |
||||||||||||||
| receivingPcRef, | ||||||||||||||
| remoteStreamRef, | ||||||||||||||
|
|
@@ -541,6 +548,10 @@ export default function Room({ | |||||||||||||
| socketRef.current.emit("media:state", { roomId, state: { micOn, camOn } }); | ||||||||||||||
| }, [micOn, camOn, roomId]); | ||||||||||||||
|
|
||||||||||||||
| useEffect(() => { | ||||||||||||||
| peerCamOnRef.current = peerCamOn; | ||||||||||||||
| }, [peerCamOn]); | ||||||||||||||
|
|
||||||||||||||
| // Main socket connection effect - simplified, actual WebRTC logic would be here | ||||||||||||||
| useEffect(() => { | ||||||||||||||
| if (socketRef.current) return; | ||||||||||||||
|
|
@@ -566,6 +577,7 @@ export default function Room({ | |||||||||||||
| // ----- CALLER ----- | ||||||||||||||
| s.on("send-offer", async ({ roomId: rid }) => { | ||||||||||||||
| setRoomId(rid); | ||||||||||||||
| s.emit("chat:join", { roomId: rid, name }); | ||||||||||||||
| setLobby(false); | ||||||||||||||
| setStatus("Connectingโฆ"); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -580,7 +592,11 @@ export default function Room({ | |||||||||||||
| s.emit("chat:join", { roomId: rid, name }); | ||||||||||||||
| }, 100); | ||||||||||||||
| }, 100); | ||||||||||||||
|
|
||||||||||||||
| try { remoteStreamRef.current?.getTracks().forEach(t => t.stop()); } catch {} | ||||||||||||||
| remoteStreamRef.current = new MediaStream(); | ||||||||||||||
| if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = null; try { remoteVideoRef.current.load(); } catch {} } | ||||||||||||||
| if (remoteAudioRef.current) remoteAudioRef.current.srcObject = null; | ||||||||||||||
|
|
||||||||||||||
| const pc = new RTCPeerConnection(); | ||||||||||||||
| sendingPcRef.current = pc; | ||||||||||||||
| peerIdRef.current = rid; | ||||||||||||||
|
|
@@ -595,6 +611,7 @@ export default function Room({ | |||||||||||||
| // ----- ANSWERER ----- | ||||||||||||||
| s.on("offer", async ({ roomId: rid, sdp: remoteSdp }) => { | ||||||||||||||
| setRoomId(rid); | ||||||||||||||
| s.emit("chat:join", { roomId: rid, name }); | ||||||||||||||
| setLobby(false); | ||||||||||||||
| setStatus("Connectingโฆ"); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -609,6 +626,10 @@ export default function Room({ | |||||||||||||
| s.emit("chat:join", { roomId: rid, name }); | ||||||||||||||
| }, 100); | ||||||||||||||
| }, 100); | ||||||||||||||
| try { remoteStreamRef.current?.getTracks().forEach(t => t.stop()); } catch {} | ||||||||||||||
| remoteStreamRef.current = new MediaStream(); | ||||||||||||||
| if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = null; try { remoteVideoRef.current.load(); } catch {} } | ||||||||||||||
| if (remoteAudioRef.current) remoteAudioRef.current.srcObject = null; | ||||||||||||||
|
|
||||||||||||||
| const pc = new RTCPeerConnection(); | ||||||||||||||
| receivingPcRef.current = pc; | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -117,6 +117,8 @@ export function stopProvidedTracks( | |||||||||||||||
|
|
||||||||||||||||
| export function teardownPeers( | ||||||||||||||||
| reason: string, | ||||||||||||||||
| camOn: boolean, | ||||||||||||||||
| micOn: boolean, | ||||||||||||||||
| sendingPcRef: React.RefObject<RTCPeerConnection | null>, | ||||||||||||||||
| receivingPcRef: React.RefObject<RTCPeerConnection | null>, | ||||||||||||||||
| remoteStreamRef: React.RefObject<MediaStream | null>, | ||||||||||||||||
|
|
@@ -206,8 +208,8 @@ export function teardownPeers( | |||||||||||||||
|
|
||||||||||||||||
| // Reset UI states | ||||||||||||||||
| setters.setShowChat(false); | ||||||||||||||||
| setters.setPeerMicOn(true); | ||||||||||||||||
| setters.setPeerCamOn(true); | ||||||||||||||||
| setters.setPeerMicOn(micOn); | ||||||||||||||||
| setters.setPeerCamOn(camOn); | ||||||||||||||||
| setters.setScreenShareOn(false); | ||||||||||||||||
|
Comment on lines
+211
to
213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Peer state reset uses local flags; likely incorrect UI semantics. Setting setPeerMicOn(micOn) / setPeerCamOn(camOn) mirrors self into โpeerโ state and can mislead UI during lobby/transition. Apply neutral defaults until the peerโs state arrives: - setters.setPeerMicOn(micOn);
- setters.setPeerCamOn(camOn);
+ // Reset to a neutral/unknown state; will be updated by peer events
+ setters.setPeerMicOn(false);
+ setters.setPeerCamOn(false);๐ Committable suggestion
Suggested change
๐ค Prompt for AI Agents |
||||||||||||||||
| setters.setPeerScreenShareOn(false); | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -270,23 +272,34 @@ export async function toggleCameraTrack( | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (videoSenderRef.current) { | ||||||||||||||||
| await videoSenderRef.current.replaceTrack(track); | ||||||||||||||||
| try { | ||||||||||||||||
| await videoSenderRef.current.replaceTrack(track); | ||||||||||||||||
| } catch (err) { | ||||||||||||||||
| try { | ||||||||||||||||
| pc?.getSenders().forEach(s => { if (s.track?.kind === 'video') try { pc.removeTrack(s); } catch {} }); | ||||||||||||||||
| } catch {} | ||||||||||||||||
| const sender = pc?.addTrack(track) || null; | ||||||||||||||||
| if (sender) videoSenderRef.current = sender; | ||||||||||||||||
| } | ||||||||||||||||
| } else if (pc) { | ||||||||||||||||
| const sender = pc.addTrack(track); | ||||||||||||||||
| videoSenderRef.current = sender; | ||||||||||||||||
| // console.log("Added new video track to existing connection"); | ||||||||||||||||
|
|
||||||||||||||||
| if (sendingPcRef.current === pc) { | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| try { socketRef.current?.emit("media:cam", { roomId, on: true }); } catch {} | ||||||||||||||||
| try { | ||||||||||||||||
| if (pc) { | ||||||||||||||||
| const offer = await pc.createOffer(); | ||||||||||||||||
| await pc.setLocalDescription(offer); | ||||||||||||||||
| socketRef.current?.emit("renegotiate-offer", { | ||||||||||||||||
| roomId, | ||||||||||||||||
| sdp: offer, | ||||||||||||||||
| role: "caller" | ||||||||||||||||
| socketRef.current?.emit("renegotiate-offer", { | ||||||||||||||||
| roomId, | ||||||||||||||||
| sdp: offer, | ||||||||||||||||
| role: sendingPcRef.current === pc ? "caller" : "answerer" | ||||||||||||||||
| }); | ||||||||||||||||
| // console.log("๐ค Sent renegotiation offer for camera turn on"); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } catch (err) { | ||||||||||||||||
| console.warn("renegotiation failed:", err); | ||||||||||||||||
| } | ||||||||||||||||
| } else { | ||||||||||||||||
| if (videoSenderRef.current) { | ||||||||||||||||
| await videoSenderRef.current.replaceTrack(null); | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lastMediaStateis never written/read โ lateโjoiner hydration wonโt workYou introduce the cache but donโt persist to it or read from it. As-is, late joiners canโt receive existing peersโ media states; this misses the issue #77 requirement.
Hook writes in media events and hydrate on join (see fixes on Lines 78-82 and 118-120).