Skip to content

Conversation

@sarthakNITT
Copy link
Contributor

@sarthakNITT sarthakNITT commented Oct 25, 2025

🌟 Pre-submission Checklist

  • ⭐ I have starred the repository (mandatory before contributing)
  • πŸ’¬ I am a member of the Discord server: Join Here
  • πŸ“ I have signed up at helixque.netlify.app
  • πŸ“’ I have checked the #pull-request channel to ensure no one else is working on this issue
  • πŸ“ I have mentioned this PR in the Discord #pull-request channel

Summary

  • changes made in the room.tsx and webrtc-utils.tsx files

Type of Changes

  • πŸš€ Feature addition
  • πŸ› Bug fix
  • πŸ“š Documentation update
  • πŸ”§ Refactoring
  • 🎨 UI/UX improvements
  • ⚑ Performance optimizations
  • πŸ“± Mobile responsiveness
  • β™Ώ Accessibility improvements
  • Other: _____

Testing Completed

  • βœ… I have tested these changes locally
  • πŸ”§ Backend functionality works properly (if applicable)
  • 🎨 Frontend functionality works properly (if applicable)
  • 🌐 WebRTC connections work properly (if applicable)
  • πŸ“± Tested on different screen sizes/devices
  • πŸ”„ Tested edge cases (disconnections, reconnections, etc.)
  • πŸ§ͺ All existing functionality remains unaffected

Development Setup Verification

  • πŸ“¦ Dependencies installed for both frontend and backend
  • πŸš€ Development servers start without errors
  • πŸ—οΈ Code builds successfully

Code Quality

  • πŸ“ Follows existing TypeScript and React patterns
  • πŸ“ Uses meaningful variable and function names
  • πŸ’‘ Added comments for complex logic
  • 🎯 Code is properly formatted
  • πŸ” Self-review of the code has been performed

Related Issues

Closes #77

Screenshots/Videos

Send in discord group

Additional Notes

Any additional information or context about the changes.

Summary by CodeRabbit

  • Bug Fixes

    • Improved media state synchronization for users joining mid-session.
    • Refined connection cleanup to prevent stale media state accumulation.
    • Prevent duplicate remote video tracks by resetting remote streams before reconnects.
  • Enhancements

    • Video capture now only starts when camera is enabled, avoiding unintended video sending.
    • Media state broadcasts no longer include sender-identifying info.
    • Chat state now aligns reliably with connection attempts.

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Walkthrough

Adds per-socket media-state caching and cleanup on the backend, simplifies chat join handling, and stops including sender id in media broadcasts. Frontend gates video acquisition on camOn, resets remote streams before new connections, ensures tracks are replaced (not duplicated), and propagates camOn/micOn into peer teardown and renegotiation flows.

Changes

Cohort / File(s) Summary
Backend media state management
backend/src/index.ts
Added in-module lastMediaState cache keyed by socket to store {camOn,micOn} for late joiners; simplified chat:join handler to directly update UserManager and socket room join; media:state broadcasts no longer include the sender id; lastMediaState cleaned up on disconnect.
Frontend RTC β€” Room logic
frontend/components/RTC/Room.tsx
Added peerCamOnRef and syncing effect; gate video acquisition/sending on camOn; reset/recreate remote MediaStream before new connections; remove existing remote video tracks before adding incoming ones; emit chat:join on offer/send-offer paths; pass camOn and micOn to teardownPeers at all teardown/next/leave points; minor error/cleanup refinements.
Frontend RTC β€” WebRTC utilities
frontend/components/RTC/webrtc-utils.tsx
teardownPeers signature extended to accept camOn and micOn and it uses those values to set peer UI state; toggleCameraTrack improved with replaceTrack fallback handling (remove/re-add) and emits media:cam then attempts renegotiation with error handling; other signaling/track management try/catch additions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Backend
    participant Peers

    rect rgb(200,220,255)
    note right of Client: Client updates local media state
    Client->>Backend: emit media:state {camOn, micOn}
    Backend->>Backend: lastMediaState[socketId] = {camOn,micOn}
    Backend->>Peers: broadcast media:state {camOn, micOn}  -- without sender id
    end
Loading
sequenceDiagram
    participant Local as Local Client
    participant Remote as Remote Peer
    participant Signaling as Signaling Server

    rect rgb(220,255,220)
    Local->>Local: camOn true/false (gate)
    alt camOn true
        Local->>Local: acquire video track & add to PC
        Local->>Signaling: send offer (with video)
    else camOn false
        Local->>Local: skip acquiring video track
        Local->>Signaling: send offer (no video)
    end
    Signaling->>Remote: deliver offer
    Remote->>Remote: reset/create remote MediaStream
    Remote->>Remote: ontrack -> replace existing video tracks then add new
    end

    rect rgb(255,235,205)
    Local->>Local: click Next -> teardownPeers(reason, camOn, micOn)
    note right of Local: teardown uses current cam/mic to update UI/state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to backend lastMediaState lifecycle and race conditions with disconnects and late-joiners.
  • Verify all call sites updated for the teardownPeers signature and correct propagation of camOn/micOn.
  • Check replaceTrack fallback logic and renegotiation error handling for RTCPeerConnection compatibility across browsers.

Possibly related PRs

Suggested labels

UI-UX, hacktoberfest2025

Suggested reviewers

  • javatcoding1

Poem

🐰
I hop and stash a tiny state,
So late friends see the truth, not fate.
Cameras gated, tracks set right,
Next connectionβ€”soft and bright.
A little hop, and all’s in sync.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Issue#77' is vague and generic, merely referencing an issue number without describing the actual change being made. Replace the title with a descriptive summary of the main change, e.g., 'Fix camera state handling when navigating between users' or 'Preserve camera state on Next navigation'.
βœ… Passed checks (3 passed)
Check name Status Explanation
Description check βœ… Passed The description follows the template structure and includes checklist completion, testing confirmation, and type of changes, though the Summary section is minimal.
Linked Issues check βœ… Passed The code changes directly address issue #77's requirements: camera state is now preserved during navigation, physical camera streams are properly managed, and state is communicated to peers via media:state events.
Out of Scope Changes check βœ… Passed All changes are scoped to fixing camera state handling; backend cache cleanup, chat:join handling, and teardownPeers refactoring all directly support the camera state preservation objective.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/components/RTC/webrtc-utils.tsx (1)

274-291: Answerer path lacks renegotiation after addTrack; remote won’t receive camera.

When pc === receivingPcRef.current, you add a video track but never renegotiate.

Apply this minimal fix:

       if (videoSenderRef.current) {
         await videoSenderRef.current.replaceTrack(track);
       } else if (pc) {
         const sender = pc.addTrack(track);
         videoSenderRef.current = sender;
         // console.log("Added new video track to existing connection");
-        
-        if (sendingPcRef.current === pc) {
-          const offer = await pc.createOffer();
-          await pc.setLocalDescription(offer);
-          socketRef.current?.emit("renegotiate-offer", { 
-            roomId, 
-            sdp: offer, 
-            role: "caller" 
-          });
-          // console.log("πŸ“€ Sent renegotiation offer for camera turn on");
-        }
+        const role = sendingPcRef.current === pc ? "caller" : "answerer";
+        const offer = await pc.createOffer();
+        await pc.setLocalDescription(offer);
+        socketRef.current?.emit("renegotiate-offer", { roomId, sdp: offer, role });
+        // console.log("πŸ“€ Sent renegotiation offer for camera turn on");
       }

Optional follow‑up: centralize on pc.onnegotiationneeded in setupPeerConnection to avoid manual branches.

frontend/components/RTC/Room.tsx (2)

486-491: Screen share UI/stream mismatch on Next.

You intentionally no‑op setScreenShareOn, but teardownPeers still stops screen share tracks; UI shows β€œon” with no stream.

Either preserve streams on Next or reset the UI. Minimal fix (reset UI to match teardown):

-        setScreenShareOn: () => {}, // Don't reset screen share on next
+        setScreenShareOn,

If the goal is to preserve screen share, gate teardownPeers’ screen-share cleanup on reason === "next".


588-601: Duplicate chat:join emits.

You emit chat:join immediately and again after 100 ms; risks duplicate joins.

-      s.emit("chat:join", { roomId: rid, name });
...
-        setTimeout(() => {
-          s.emit("chat:join", { roomId: rid, name });
-        }, 100);
+        s.emit("chat:join", { roomId: rid, name });

Apply in both send-offer and offer handlers.

Also applies to: 623-635

🧹 Nitpick comments (4)
frontend/components/RTC/webrtc-utils.tsx (3)

321-325: Stopping a prop-owned localVideoTrack may have unintended side‑effects.

localVideoTrack is passed in via props; stopping it here can affect upstream owners.

Guard to only stop tracks created by this module (currentVideoTrackRef), or pass an explicit flag indicating ownership.


249-251: Type the socket ref.

Use Socket | null instead of any for socketRef param to catch event payload mistakes.

-  socketRef: React.RefObject<any>,
+  socketRef: React.RefObject<Socket | null>,

120-122: All teardownPeers call sites are correctly orderedβ€”refactoring to options object remains a good practice but not urgent.

Verification found 2 call sites (lines 399 and 470 in Room.tsx), both passing arguments in correct order: reason, camOn, micOn. While positional parameters are fragile compared to an options object, no actual misorderings were detected. The suggested refactoring to an options object would improve API resilience but is not critical.

frontend/components/RTC/Room.tsx (1)

119-136: If no initial localVideoTrack, new camera track isn’t attached to local preview.

When camOn is true and you create a fresh track, you add it to pc but not to localVideoRef.

     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;
+          // Ensure local preview reflects the new camera track
+          if (localVideoRef.current) {
+            const ms = (localVideoRef.current.srcObject as MediaStream) || new MediaStream();
+            ms.getVideoTracks().forEach(t => ms.removeTrack(t));
+            ms.addTrack(videoTrack);
+            if (!localVideoRef.current.srcObject) localVideoRef.current.srcObject = ms;
+            await localVideoRef.current.play().catch(() => {});
+          }
         } catch (err) {
           console.error("Error creating video track:", err);
           videoTrack = null;
         }
       }

(Note: localVideoRef is in closure scope; if not, pass it into setupPeerConnection.)

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between b9fad5d and f496fef.

β›” Files ignored due to path filters (2)
  • backend/bun.lockb is excluded by !**/bun.lockb
  • frontend/bun.lockb is excluded by !**/bun.lockb
πŸ“’ Files selected for processing (3)
  • backend/src/index.ts (4 hunks)
  • frontend/components/RTC/Room.tsx (9 hunks)
  • frontend/components/RTC/webrtc-utils.tsx (2 hunks)
πŸ”‡ Additional comments (2)
backend/src/index.ts (1)

162-163: Good cleanup

Removing the socket’s entry from lastMediaState on disconnect avoids leaks. LGTM.

Confirm that chat.ts (disconnecting handler) already notifies peers so UI can clear avatars/tracks promptly.

frontend/components/RTC/Room.tsx (1)

559-562: Good: sync ref with peerCamOn.

Keeps the ref in lockstep for render-agnostic checks.

Comment on lines +18 to +19
// store latest media state per-socket so late joiners can be informed
const lastMediaState = new Map<string, { micOn?: boolean; camOn?: boolean }>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

lastMediaState is never written/read β€” late‑joiner hydration won’t work

You 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).

Comment on lines +78 to 82
// ⬇️ 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);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Normalize room names and hydrate late joiners on chat:join

The handler joins the raw roomId while most emits use the chat: namespace. This mismatch will drop events. Also, use this hook to send existing peers’ media states to the joiner.

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 normalizeRoom idempotent to avoid double prefixing.

// replace existing normalizeRoom
const normalizeRoom = (r: string) => (r ? (r.startsWith("chat:") ? r : `chat:${r}`) : "");
πŸ€– Prompt for AI Agents
In backend/src/index.ts around lines 78 to 82, the chat:join handler currently
uses the raw roomId which causes a mismatch with events that use the "chat:"
prefix and doesn't hydrate late joiners; update the handler to call
normalizeRoom(roomId) and use that normalizedRoom for userManager.setRoom and
socket.join, and after joining emit the current peers' media states to the
joining socket (e.g., iterate known peers in the room and send their media state
events to the joiner); also replace or ensure normalizeRoom is idempotent so it
prefixes "chat:" only once (optionally use the provided implementation).

Comment on lines 118 to 120
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 });
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

media:state drops from and uses raw roomId; also not updating cache

Without from, clients can’t attribute state changes; using raw roomId bypasses the chat: namespace. Also, not persisting to lastMediaState blocks late‑joiner sync.

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 peer:media-state:


🏁 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 -100

Length 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.ts

Length of output: 1388


Add from, use toRoom(), and update cache consistently across all media handlers

The media:state handler is indeed inconsistent with media:cam and media:micβ€”it bypasses the chat: namespace normalization and omits the sender ID. However, the cache persistence gap is wider: none of the media handlers update lastMediaState.

Apply the diff as provided, but also update media:cam and media:mic to persist state:

  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 });
  });

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In backend/src/index.ts around lines 118 to 120, the media handlers are
inconsistent: update the "media:state" handler to include the sender id ("from")
and emit using toRoom(chatNamespace, roomId) (i.e., normalize the chat:
namespace) instead of socket.to(roomId).emit, and persist the new state into
lastMediaState cache; likewise, modify the "media:cam" and "media:mic" handlers
to normalize the namespace via toRoom(chatNamespace, roomId), include the sender
id in emitted payloads, and update lastMediaState with the combined per-user
state (micOn/camOn) so the cache is consistently persisted across all three
handlers.

Comment on lines +472 to 474
camOn,
micOn,
sendingPcRef,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
camOn,
micOn,
sendingPcRef,
currentCamState,
currentMicState,
sendingPcRef,
πŸ€– Prompt for AI Agents
In frontend/components/RTC/Room.tsx around lines 472 to 474, the teardownPeers
call is passing camOn and micOn which may be stale; instead pass the computed
currentCamState and currentMicState that reflect actual media liveness (e.g.,
derived from track.enabled/readyState or your currentCamState/currentMicState
variables). Update the teardownPeers invocation to use currentCamState and
currentMicState (ensure those variables are in scope at this call site) so peer
teardown uses the real-time media state.

Comment on lines +211 to 213
setters.setPeerMicOn(micOn);
setters.setPeerCamOn(camOn);
setters.setScreenShareOn(false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setters.setPeerMicOn(micOn);
setters.setPeerCamOn(camOn);
setters.setScreenShareOn(false);
// Reset to a neutral/unknown state; will be updated by peer events
setters.setPeerMicOn(false);
setters.setPeerCamOn(false);
setters.setScreenShareOn(false);
πŸ€– Prompt for AI Agents
In frontend/components/RTC/webrtc-utils.tsx around lines 211 to 213, the code
sets peer state using local micOn/camOn flags which mirrors the local user into
the peer UI; change these to neutral defaults so the peer UI doesn’t falsely
reflect local state while waiting for the peer’s actual state β€” e.g.,
setPeerMicOn(false) and setPeerCamOn(false) (or null/undefined if your state
model supports an "unknown" value) and keep setScreenShareOn(false), so the peer
controls remain neutral until the peer’s presence/state update arrives.

@javatcoding1
Copy link
Collaborator

@sarthakNITT will review it

@javatcoding1
Copy link
Collaborator

Screenshot 2025-10-28 at 4 25 03β€―PM Screenshot 2025-10-28 at 4 24 12β€―PM when i click on next the and turn on the camera the video tracks are not visible properly i mean it's totally blank @sarthakNITT

@jayanthjamin-web
Copy link
Member

@sarthakNITT status?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/components/RTC/Room.tsx (1)

578-594: Remove duplicate chat:join emissions.

Both send-offer and offer handlers emit chat:join twice: once immediately (lines 580, 614) and again after a 100ms delay (lines 592, 626). This duplication is unnecessary and may cause backend state issues or race conditions.

Remove the immediate emission and keep only the delayed one:

     s.on("send-offer", async ({ roomId: rid }) => {
       setRoomId(rid);
-      s.emit("chat:join", { roomId: rid, name });
       setLobby(false);

And:

     s.on("offer", async ({ roomId: rid, sdp: remoteSdp }) => {
       setRoomId(rid);
-      s.emit("chat:join", { roomId: rid, name });
       setLobby(false);

Also applies to: 612-628

♻️ Duplicate comments (2)
frontend/components/RTC/Room.tsx (1)

464-465: Pass computed actual media state to teardownPeers.

You compute actualCamState and actualMicState at lines 358-359 but still pass the potentially stale camOn and micOn state variables to teardownPeers. This does not address the previous review comment.

Apply this diff:

       reason,
-      camOn,
-      micOn,
+      actualCamState,
+      actualMicState,
       sendingPcRef,
frontend/components/RTC/webrtc-utils.tsx (1)

211-212: Reset peer media state to neutral defaults, not local state.

Setting setPeerMicOn(micOn) and setPeerCamOn(camOn) uses the local user's media state to represent the peer's state, which is semantically incorrect. After teardown there is no peer, so these should be reset to neutral defaults.

Apply this diff:

   // Reset UI states
   setters.setShowChat(false);
-  setters.setPeerMicOn(micOn);
-  setters.setPeerCamOn(camOn);
+  setters.setPeerMicOn(false);
+  setters.setPeerCamOn(false);
   setters.setScreenShareOn(false);
   setters.setPeerScreenShareOn(false);
🧹 Nitpick comments (2)
frontend/components/RTC/Room.tsx (2)

74-74: Remove unused peerCamOnRef or use it.

peerCamOnRef is initialized and synced via useEffect but never referenced elsewhere in the codebase. This appears to be dead code from an incomplete refactoring.

Remove the unused ref and effect:

-  const peerCamOnRef = useRef<boolean>(false);

And:

-  useEffect(() => {
-    peerCamOnRef.current = peerCamOn;
-  }, [peerCamOn]);

Also applies to: 551-553


391-394: Compute actual media state before passing to teardownPeers.

For consistency with handleNextConnection, compute the actual live state of camera and microphone tracks before passing to teardownPeers.

Apply this diff:

+    const actualCamState = !!(currentVideoTrackRef.current && currentVideoTrackRef.current.readyState === "live" && camOn);
+    const actualMicState = !!(localAudioTrack && localAudioTrack.readyState === "live" && micOn);
+
     teardownPeers(
       "teardown",
-      camOn,
-      micOn,
+      actualCamState,
+      actualMicState,
       sendingPcRef,
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between f496fef and 1018c49.

πŸ“’ Files selected for processing (2)
  • frontend/components/RTC/Room.tsx (9 hunks)
  • frontend/components/RTC/webrtc-utils.tsx (3 hunks)
πŸ”‡ Additional comments (7)
frontend/components/RTC/Room.tsx (4)

119-136: LGTM: Video acquisition correctly gated on camera state.

The video track is now only acquired and added to the peer connection when camOn is true. This properly implements the requirement to stop the physical camera stream when the camera is toggled off.


142-147: LGTM: Remote video tracks handled correctly.

The ontrack handler now removes existing video tracks before adding the new incoming track, preventing duplicates. The track is always added regardless of peer camera state, which correctly addresses the previous review concern about permanent blank video.


595-599: LGTM: Remote stream properly reset before new connections.

Resetting the remote stream and clearing video/audio elements before establishing new peer connections ensures a clean slate and prevents track accumulation. This correctly supports the camera state preservation requirements.

Also applies to: 629-632


354-369: LGTM: Next button handler correctly computes media state.

The handleNext function properly computes the actual camera and microphone state before cleanup and navigation. Ensure these computed states are used in the subsequent teardownPeers call within handleNextConnection (see related comment).

frontend/components/RTC/webrtc-utils.tsx (3)

289-289: Verify that roomId is not null before emitting.

The media:cam emission on line 289 uses roomId which may be null if the user toggles the camera before joining a room or during transitions.

Add a check:

-      try { socketRef.current?.emit("media:cam", { roomId, on: true }); } catch {}
+      try { 
+        if (socketRef.current && roomId) {
+          socketRef.current.emit("media:cam", { roomId, on: true }); 
+        }
+      } catch {}

290-302: Add null check for roomId in renegotiation flow.

The renegotiation logic emits an offer with roomId which may be null during transitions or before room establishment. While wrapped in try/catch, a null roomId would send invalid data to the backend.

Apply this diff:

       try {
-        if (pc) {
+        if (pc && roomId) {
           const offer = await pc.createOffer();
           await pc.setLocalDescription(offer);
           socketRef.current?.emit("renegotiate-offer", {

275-287: LGTM: ReplaceTrack fallback is defensive.

The fallback logic that removes and re-adds the video track when replaceTrack fails is a good defensive approach for handling browser incompatibilities or edge cases. The implementation correctly updates the videoSenderRef after adding the new track.

@sarthakNITT
Copy link
Contributor Author

sarthakNITT commented Nov 5, 2025

@jayanthjamin-web Sorry for late response. I forgot to push the got back then.
With these changes i have tested it multiple times. It is working fine now.

  • The video stream sharing working fine now. Theres no black screen shown on toggling camera state.
  • when peers click next by keeping their initial camera state as false then after successful connection their video stream is not shared.

video - https://drive.google.com/file/d/1zM7Atiab1yFersqS--tCYcAMHXTtVohZ/view?usp=sharing

@javatcoding1
Copy link
Collaborator

@sarthakNITT will check you code

@javatcoding1
Copy link
Collaborator

Hey @sarthakNITT, great job on this! πŸ‘
Just a small issue when we turn off the camera for 4 peers and click β€œNext” on 2 peers, one peer’s camera gets turned on automatically. Could you please check that as well?

@javatcoding1
Copy link
Collaborator

@sarthakNITT status

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix camera state handling when users click "Next"

4 participants