@@ -467,19 +467,35 @@ export function useWebRTC({
467467
468468 // Handle incoming tracks (video + audio from host and relayed viewers)
469469 pc . ontrack = ( event ) => {
470- const stream = event . streams [ 0 ] ;
471- if ( ! stream ) return ;
470+ const incomingStream = event . streams [ 0 ] ;
472471
472+ // Browsers may deliver audio/video on separate streams (or streamless) across
473+ // renegotiation, which can cause us to drop audio if we simply replace the stream.
473474 const current = remoteStreamRef . current ;
474- const currentHasVideo = Boolean ( current && current . getVideoTracks ( ) . length > 0 ) ;
475- const incomingHasVideo = stream . getVideoTracks ( ) . length > 0 ;
476-
477- // Avoid replacing an active video stream with a later audio-only stream.
478- if ( ! current || ( ! currentHasVideo && incomingHasVideo ) ) {
479- remoteStreamRef . current = stream ;
480- setRemoteStream ( stream ) ;
481- onStreamReady ?.( stream ) ;
475+ const composite = current ?? new MediaStream ( ) ;
476+
477+ const addTrackIfMissing = ( track : MediaStreamTrack ) => {
478+ const exists = composite . getTracks ( ) . some ( ( existing ) => existing . id === track . id ) ;
479+ if ( ! exists ) {
480+ const sameKind = composite . getTracks ( ) . find ( ( existing ) => existing . kind === track . kind ) ;
481+ if ( sameKind ) {
482+ composite . removeTrack ( sameKind ) ;
483+ }
484+ composite . addTrack ( track ) ;
485+ }
486+ } ;
487+
488+ if ( incomingStream ) {
489+ incomingStream . getTracks ( ) . forEach ( addTrackIfMissing ) ;
490+ } else {
491+ addTrackIfMissing ( event . track ) ;
482492 }
493+
494+ // Re-emit a fresh stream so the video element updates when tracks arrive later.
495+ const mergedStream = new MediaStream ( composite . getTracks ( ) ) ;
496+ remoteStreamRef . current = mergedStream ;
497+ setRemoteStream ( mergedStream ) ;
498+ onStreamReady ?.( mergedStream ) ;
483499 } ;
484500
485501 // Handle ICE candidates
0 commit comments