Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions frontend/src/features/call/components/CallParticipantsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,39 @@ export const CallParticipantsGrid = ({
containerRef,
isFullscreen,
}: CallParticipantsGridProps) => {
const handleLocalVideoRef = useCallback(
(element: HTMLVideoElement | null) => {
localVideoRef.current = element;
const attachStreamToElement = useCallback(
(element: HTMLVideoElement | null, stream: MediaStream | null | undefined) => {
if (!element) {
return;
}

const stream = localStreamRef.current;
if (stream && element.srcObject !== stream) {
const currentStream = element.srcObject as MediaStream | null;
let streamAttached = false;

if (stream && currentStream !== stream) {
element.srcObject = stream;
streamAttached = true;
}

element.muted = true;
if (element.srcObject) {
if ((streamAttached || element.paused) && element.srcObject) {
element.play().catch(() => undefined);
Comment on lines +21 to 36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Clear video element when stream disappears

The new attachStreamToElement helper only sets element.srcObject when the incoming stream is truthy. When a remote participant’s stream becomes null or undefined, the function skips assignment and never removes the previous stream, so the element keeps rendering and playing the stale video/audio. The prior implementation assigned element.srcObject = participant.stream regardless of value, which detached the stream when absent. This regression leaves orphaned media playing whenever a participant leaves or a camera stops.

Useful? React with 👍 / 👎.

}
},
[localStreamRef, localVideoRef],
[],
);

const handleLocalVideoRef = useCallback(
(element: HTMLVideoElement | null) => {
localVideoRef.current = element;
if (!element) {
return;
}

const stream = localStreamRef.current;
element.muted = true;
attachStreamToElement(element, stream);
},
[attachStreamToElement, localStreamRef, localVideoRef],
);

if (isFullscreen && remoteParticipants.length > 0) {
Expand All @@ -48,10 +63,7 @@ export const CallParticipantsGrid = ({
autoPlay
playsInline
ref={(element) => {
if (element) {
element.srcObject = primaryParticipant.stream;
element.play().catch(() => undefined);
}
attachStreamToElement(element, primaryParticipant.stream);
}}
className="video-stage__primary-video"
/>
Expand All @@ -77,10 +89,7 @@ export const CallParticipantsGrid = ({
autoPlay
playsInline
ref={(element) => {
if (element) {
element.srcObject = participant.stream;
element.play().catch(() => undefined);
}
attachStreamToElement(element, participant.stream);
}}
className="video-stage__thumbnail-video"
/>
Expand All @@ -105,10 +114,7 @@ export const CallParticipantsGrid = ({
autoPlay
playsInline
ref={(element) => {
if (element) {
element.srcObject = participant.stream;
element.play().catch(() => undefined);
}
attachStreamToElement(element, participant.stream);
}}
/>
<span className="video-grid__label">{participant.id.slice(0, 6)}</span>
Expand Down