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.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;

View File

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

View File

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

View File

@@ -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<string, MediaStream>,
remotePeerId: 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 {
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,

View File

@@ -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