Clean up stream audio on ending stream

This commit is contained in:
2026-03-09 23:59:59 +01:00
parent dc6746c882
commit ca74836c52
5 changed files with 86 additions and 55 deletions

View File

@@ -623,7 +623,7 @@ class DebugNetworkSnapshotBuilder {
node.isMuted = user.voiceState?.isMuted === true; node.isMuted = user.voiceState?.isMuted === true;
node.isDeafened = user.voiceState?.isDeafened === true; node.isDeafened = user.voiceState?.isDeafened === true;
node.isSpeaking = user.voiceState?.isSpeaking === true || node.isSpeaking; 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) if (user.voiceState?.isConnected !== true)
node.streams.audio = 0; node.streams.audio = 0;

View File

@@ -924,6 +924,11 @@ export class WebRTCService implements OnDestroy {
return; 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) { if (event.type === P2P_TYPE_SCREEN_SHARE_REQUEST) {
this.screenShareManager.requestScreenShareForPeer(event.fromPeerId); this.screenShareManager.requestScreenShareForPeer(event.fromPeerId);
return; return;
@@ -951,15 +956,12 @@ export class WebRTCService implements OnDestroy {
const connectedPeerIds = new Set(this.peerManager.getConnectedPeerIds()); const connectedPeerIds = new Set(this.peerManager.getConnectedPeerIds());
for (const peerId of peerIds) { for (const peerId of peerIds) {
if (!this.activeRemoteScreenSharePeers.has(peerId)) { if (this.activeRemoteScreenSharePeers.has(peerId) && connectedPeerIds.has(peerId)) {
continue;
}
if (connectedPeerIds.has(peerId)) {
this.peerManager.sendToPeer(peerId, { type: P2P_TYPE_SCREEN_SHARE_STOP }); this.peerManager.sendToPeer(peerId, { type: P2P_TYPE_SCREEN_SHARE_STOP });
} }
this.activeRemoteScreenSharePeers.delete(peerId); this.activeRemoteScreenSharePeers.delete(peerId);
this.peerManager.clearRemoteScreenShareStream(peerId);
} }
} }

View File

@@ -34,7 +34,7 @@ import {
schedulePeerReconnect, schedulePeerReconnect,
trackDisconnectedPeer trackDisconnectedPeer
} from './recovery/peer-recovery'; } from './recovery/peer-recovery';
import { handleRemoteTrack } from './streams/remote-streams'; import { clearRemoteScreenShareStream as clearManagedRemoteScreenShareStream, handleRemoteTrack } from './streams/remote-streams';
import { import {
ConnectionLifecycleHandlers, ConnectionLifecycleHandlers,
createPeerConnectionManagerState, createPeerConnectionManagerState,
@@ -223,6 +223,11 @@ export class PeerConnectionManager {
return getConnectedPeerIds(this.state); 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. */ /** Reset the connected peers list to empty and notify subscribers. */
resetConnectedPeers(): void { resetConnectedPeers(): void {
resetConnectedPeers(this.state); resetConnectedPeers(this.state);

View File

@@ -53,22 +53,33 @@ export function handleRemoteTrack(
state.remotePeerScreenShareStreams.set(remotePeerId, screenShareStream); state.remotePeerScreenShareStreams.set(remotePeerId, screenShareStream);
} }
state.remoteStream$.next({ publishRemoteStreamUpdate(context, remotePeerId, compositeStream);
peerId: remotePeerId, }
stream: compositeStream
});
recordDebugNetworkStreams(remotePeerId, { export function clearRemoteScreenShareStream(
audio: compositeStream.getAudioTracks().length, context: PeerConnectionManagerContext,
video: compositeStream.getVideoTracks().length remotePeerId: string
}); ): void {
const { state } = context;
const screenShareStream = state.remotePeerScreenShareStreams.get(remotePeerId);
logger.info('Remote stream updated', { if (!screenShareStream) {
audioTrackCount: compositeStream.getAudioTracks().length, return;
}
const screenShareTrackIds = new Set(
screenShareStream.getTracks().map((track) => track.id)
);
const compositeStream = removeTracksFromStreamMap(
state.remotePeerStreams,
remotePeerId, remotePeerId,
trackCount: compositeStream.getTracks().length, screenShareTrackIds
videoTrackCount: compositeStream.getVideoTracks().length );
});
removeTracksFromStreamMap(state.remotePeerVoiceStreams, remotePeerId, screenShareTrackIds);
state.remotePeerScreenShareStreams.delete(remotePeerId);
publishRemoteStreamUpdate(context, remotePeerId, compositeStream);
} }
function buildCompositeRemoteStream( function buildCompositeRemoteStream(
@@ -140,48 +151,27 @@ function removeRemoteTrack(
remotePeerId: string, remotePeerId: string,
trackId: string trackId: string
): void { ): void {
const { logger, state } = context; const { state } = context;
const compositeStream = removeTrackFromStreamMap(state.remotePeerStreams, remotePeerId, trackId); const compositeStream = removeTrackFromStreamMap(state.remotePeerStreams, remotePeerId, trackId);
removeTrackFromStreamMap(state.remotePeerVoiceStreams, remotePeerId, trackId); removeTrackFromStreamMap(state.remotePeerVoiceStreams, remotePeerId, trackId);
removeTrackFromStreamMap(state.remotePeerScreenShareStreams, remotePeerId, trackId); removeTrackFromStreamMap(state.remotePeerScreenShareStreams, remotePeerId, trackId);
if (!compositeStream) { publishRemoteStreamUpdate(context, remotePeerId, 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
});
} }
function removeTrackFromStreamMap( function removeTrackFromStreamMap(
streamMap: Map<string, MediaStream>, streamMap: Map<string, MediaStream>,
remotePeerId: string, remotePeerId: string,
trackId: string trackId: string
): MediaStream | null {
return removeTracksFromStreamMap(streamMap, remotePeerId, new Set([trackId]));
}
function removeTracksFromStreamMap(
streamMap: Map<string, MediaStream>,
remotePeerId: string,
trackIds: ReadonlySet<string>
): MediaStream | null { ): MediaStream | null {
const currentStream = streamMap.get(remotePeerId); const currentStream = streamMap.get(remotePeerId);
@@ -191,7 +181,7 @@ function removeTrackFromStreamMap(
const remainingTracks = currentStream const remainingTracks = currentStream
.getTracks() .getTracks()
.filter((existingTrack) => existingTrack.id !== trackId && existingTrack.readyState === 'live'); .filter((existingTrack) => !trackIds.has(existingTrack.id) && existingTrack.readyState === 'live');
if (remainingTracks.length === currentStream.getTracks().length) { if (remainingTracks.length === currentStream.getTracks().length) {
return currentStream; return currentStream;
@@ -208,6 +198,32 @@ function removeTrackFromStreamMap(
return nextStream; 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( function isVoiceAudioTrack(
context: PeerConnectionManagerContext, context: PeerConnectionManagerContext,
event: RTCTrackEvent, event: RTCTrackEvent,

View File

@@ -141,9 +141,17 @@ export class ScreenShareViewerComponent implements OnDestroy {
// Subscribe to remote streams with video (screen shares) // Subscribe to remote streams with video (screen shares)
// NOTE: We no longer auto-display remote streams. Users must click "Live" to view. // 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 subscription is kept for potential future use (e.g., tracking available streams)
this.remoteStreamSub = this.webrtcService.onRemoteStream.subscribe(({ peerId, stream }) => { this.remoteStreamSub = this.webrtcService.onRemoteStream.subscribe(({ peerId }) => {
// Do nothing on remote stream - user must explicitly click "Live" to view if (peerId !== this.watchingUserId() || this.isLocalShare()) {
// The stream is still stored in webrtcService.remoteStreams and can be accessed via getRemoteStream() 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 // Listen for focus events dispatched by other components