From ca74836c52eb70736cccf698e6a02cb01047d76e Mon Sep 17 00:00:00 2001 From: Myx Date: Mon, 9 Mar 2026 23:59:59 +0100 Subject: [PATCH] Clean up stream audio on ending stream --- .../debugging-network-snapshot.builder.ts | 2 +- src/app/core/services/webrtc.service.ts | 12 +- .../peer-connection.manager.ts | 7 +- .../streams/remote-streams.ts | 106 ++++++++++-------- .../screen-share-viewer.component.ts | 14 ++- 5 files changed, 86 insertions(+), 55 deletions(-) diff --git a/src/app/core/services/debugging/debugging-network-snapshot.builder.ts b/src/app/core/services/debugging/debugging-network-snapshot.builder.ts index 36bb288..e03027b 100644 --- a/src/app/core/services/debugging/debugging-network-snapshot.builder.ts +++ b/src/app/core/services/debugging/debugging-network-snapshot.builder.ts @@ -623,7 +623,7 @@ class DebugNetworkSnapshotBuilder { node.isMuted = user.voiceState?.isMuted === true; node.isDeafened = user.voiceState?.isDeafened === true; node.isSpeaking = user.voiceState?.isSpeaking === true || node.isSpeaking; - node.isStreaming = user.screenShareState?.isSharing === true || node.isStreaming; + node.isStreaming = user.screenShareState?.isSharing === true; if (user.voiceState?.isConnected !== true) node.streams.audio = 0; diff --git a/src/app/core/services/webrtc.service.ts b/src/app/core/services/webrtc.service.ts index c35d709..ccdeb90 100644 --- a/src/app/core/services/webrtc.service.ts +++ b/src/app/core/services/webrtc.service.ts @@ -924,6 +924,11 @@ export class WebRTCService implements OnDestroy { return; } + if (event.type === P2P_TYPE_SCREEN_STATE && event.isScreenSharing === false) { + this.peerManager.clearRemoteScreenShareStream(event.fromPeerId); + return; + } + if (event.type === P2P_TYPE_SCREEN_SHARE_REQUEST) { this.screenShareManager.requestScreenShareForPeer(event.fromPeerId); return; @@ -951,15 +956,12 @@ export class WebRTCService implements OnDestroy { const connectedPeerIds = new Set(this.peerManager.getConnectedPeerIds()); for (const peerId of peerIds) { - if (!this.activeRemoteScreenSharePeers.has(peerId)) { - continue; - } - - if (connectedPeerIds.has(peerId)) { + if (this.activeRemoteScreenSharePeers.has(peerId) && connectedPeerIds.has(peerId)) { this.peerManager.sendToPeer(peerId, { type: P2P_TYPE_SCREEN_SHARE_STOP }); } this.activeRemoteScreenSharePeers.delete(peerId); + this.peerManager.clearRemoteScreenShareStream(peerId); } } diff --git a/src/app/core/services/webrtc/peer-connection-manager/peer-connection.manager.ts b/src/app/core/services/webrtc/peer-connection-manager/peer-connection.manager.ts index d15ed71..ef7ba1e 100644 --- a/src/app/core/services/webrtc/peer-connection-manager/peer-connection.manager.ts +++ b/src/app/core/services/webrtc/peer-connection-manager/peer-connection.manager.ts @@ -34,7 +34,7 @@ import { schedulePeerReconnect, trackDisconnectedPeer } from './recovery/peer-recovery'; -import { handleRemoteTrack } from './streams/remote-streams'; +import { clearRemoteScreenShareStream as clearManagedRemoteScreenShareStream, handleRemoteTrack } from './streams/remote-streams'; import { ConnectionLifecycleHandlers, createPeerConnectionManagerState, @@ -223,6 +223,11 @@ export class PeerConnectionManager { return getConnectedPeerIds(this.state); } + /** Remove any cached remote screen-share tracks for a peer. */ + clearRemoteScreenShareStream(peerId: string): void { + clearManagedRemoteScreenShareStream(this.context, peerId); + } + /** Reset the connected peers list to empty and notify subscribers. */ resetConnectedPeers(): void { resetConnectedPeers(this.state); diff --git a/src/app/core/services/webrtc/peer-connection-manager/streams/remote-streams.ts b/src/app/core/services/webrtc/peer-connection-manager/streams/remote-streams.ts index bad190e..d40ee14 100644 --- a/src/app/core/services/webrtc/peer-connection-manager/streams/remote-streams.ts +++ b/src/app/core/services/webrtc/peer-connection-manager/streams/remote-streams.ts @@ -53,22 +53,33 @@ export function handleRemoteTrack( state.remotePeerScreenShareStreams.set(remotePeerId, screenShareStream); } - state.remoteStream$.next({ - peerId: remotePeerId, - stream: compositeStream - }); + publishRemoteStreamUpdate(context, remotePeerId, compositeStream); +} - recordDebugNetworkStreams(remotePeerId, { - audio: compositeStream.getAudioTracks().length, - video: compositeStream.getVideoTracks().length - }); +export function clearRemoteScreenShareStream( + context: PeerConnectionManagerContext, + remotePeerId: string +): void { + const { state } = context; + const screenShareStream = state.remotePeerScreenShareStreams.get(remotePeerId); - logger.info('Remote stream updated', { - audioTrackCount: compositeStream.getAudioTracks().length, + if (!screenShareStream) { + return; + } + + const screenShareTrackIds = new Set( + screenShareStream.getTracks().map((track) => track.id) + ); + const compositeStream = removeTracksFromStreamMap( + state.remotePeerStreams, remotePeerId, - trackCount: compositeStream.getTracks().length, - videoTrackCount: compositeStream.getVideoTracks().length - }); + screenShareTrackIds + ); + + removeTracksFromStreamMap(state.remotePeerVoiceStreams, remotePeerId, screenShareTrackIds); + state.remotePeerScreenShareStreams.delete(remotePeerId); + + publishRemoteStreamUpdate(context, remotePeerId, compositeStream); } function buildCompositeRemoteStream( @@ -140,48 +151,27 @@ function removeRemoteTrack( remotePeerId: string, trackId: string ): void { - const { logger, state } = context; + const { state } = context; const compositeStream = removeTrackFromStreamMap(state.remotePeerStreams, remotePeerId, trackId); removeTrackFromStreamMap(state.remotePeerVoiceStreams, remotePeerId, trackId); removeTrackFromStreamMap(state.remotePeerScreenShareStreams, remotePeerId, trackId); - if (!compositeStream) { - recordDebugNetworkStreams(remotePeerId, { audio: 0, - video: 0 }); - - logger.info('Remote stream updated', { - audioTrackCount: 0, - remotePeerId, - trackCount: 0, - videoTrackCount: 0 - }); - - return; - } - - state.remoteStream$.next({ - peerId: remotePeerId, - stream: compositeStream - }); - - recordDebugNetworkStreams(remotePeerId, { - audio: compositeStream.getAudioTracks().length, - video: compositeStream.getVideoTracks().length - }); - - logger.info('Remote stream updated', { - audioTrackCount: compositeStream.getAudioTracks().length, - remotePeerId, - trackCount: compositeStream.getTracks().length, - videoTrackCount: compositeStream.getVideoTracks().length - }); + publishRemoteStreamUpdate(context, remotePeerId, compositeStream); } function removeTrackFromStreamMap( streamMap: Map, remotePeerId: string, trackId: string +): MediaStream | null { + return removeTracksFromStreamMap(streamMap, remotePeerId, new Set([trackId])); +} + +function removeTracksFromStreamMap( + streamMap: Map, + remotePeerId: string, + trackIds: ReadonlySet ): MediaStream | null { const currentStream = streamMap.get(remotePeerId); @@ -191,7 +181,7 @@ function removeTrackFromStreamMap( const remainingTracks = currentStream .getTracks() - .filter((existingTrack) => existingTrack.id !== trackId && existingTrack.readyState === 'live'); + .filter((existingTrack) => !trackIds.has(existingTrack.id) && existingTrack.readyState === 'live'); if (remainingTracks.length === currentStream.getTracks().length) { return currentStream; @@ -208,6 +198,32 @@ function removeTrackFromStreamMap( return nextStream; } +function publishRemoteStreamUpdate( + context: PeerConnectionManagerContext, + remotePeerId: string, + compositeStream: MediaStream | null +): void { + const { logger, state } = context; + const stream = compositeStream ?? new MediaStream(); + + state.remoteStream$.next({ + peerId: remotePeerId, + stream + }); + + recordDebugNetworkStreams(remotePeerId, { + audio: stream.getAudioTracks().length, + video: stream.getVideoTracks().length + }); + + logger.info('Remote stream updated', { + audioTrackCount: stream.getAudioTracks().length, + remotePeerId, + trackCount: stream.getTracks().length, + videoTrackCount: stream.getVideoTracks().length + }); +} + function isVoiceAudioTrack( context: PeerConnectionManagerContext, event: RTCTrackEvent, diff --git a/src/app/features/voice/screen-share-viewer/screen-share-viewer.component.ts b/src/app/features/voice/screen-share-viewer/screen-share-viewer.component.ts index b2ed6d4..0fb5216 100644 --- a/src/app/features/voice/screen-share-viewer/screen-share-viewer.component.ts +++ b/src/app/features/voice/screen-share-viewer/screen-share-viewer.component.ts @@ -141,9 +141,17 @@ export class ScreenShareViewerComponent implements OnDestroy { // Subscribe to remote streams with video (screen shares) // NOTE: We no longer auto-display remote streams. Users must click "Live" to view. // This subscription is kept for potential future use (e.g., tracking available streams) - this.remoteStreamSub = this.webrtcService.onRemoteStream.subscribe(({ peerId, stream }) => { - // Do nothing on remote stream - user must explicitly click "Live" to view - // The stream is still stored in webrtcService.remoteStreams and can be accessed via getRemoteStream() + this.remoteStreamSub = this.webrtcService.onRemoteStream.subscribe(({ peerId }) => { + if (peerId !== this.watchingUserId() || this.isLocalShare()) { + return; + } + + const stream = this.webrtcService.getRemoteScreenShareStream(peerId); + const hasActiveVideo = stream?.getVideoTracks().some((track) => track.readyState === 'live') ?? false; + + if (!hasActiveVideo) { + this.stopWatching(); + } }); // Listen for focus events dispatched by other components