fix bug with voice being global

This commit is contained in:
2026-03-02 03:55:50 +01:00
parent e231f4ed05
commit 47304254f3
6 changed files with 366 additions and 4 deletions

View File

@@ -58,6 +58,10 @@ export class WebRTCService implements OnDestroy {
private lastJoinedServer: JoinedServerInfo | null = null;
private readonly memberServerIds = new Set<string>();
private activeServerId: string | null = null;
/** The server ID where voice is currently active, or `null` when not in voice. */
private voiceServerId: string | null = null;
/** Maps each remote peer ID to the server they were discovered from. */
private readonly peerServerMap = new Map<string, string>();
private readonly serviceDestroyed$ = new Subject<void>();
private readonly _localPeerId = signal<string>(uuidv4());
@@ -194,18 +198,40 @@ export class WebRTCService implements OnDestroy {
}
break;
case SIGNALING_TYPE_SERVER_USERS:
this.logger.info('Server users', { count: Array.isArray(message.users) ? message.users.length : 0 });
case SIGNALING_TYPE_SERVER_USERS: {
this.logger.info('Server users', { count: Array.isArray(message.users) ? message.users.length : 0, serverId: message.serverId });
// Only create peer connections for the voice server (if in voice)
// or the currently active/viewed server (if not in voice).
const effectiveServerId = this.voiceServerId || this.activeServerId;
if (message.serverId && effectiveServerId && message.serverId !== effectiveServerId) {
this.logger.info('Skipping peer connections for non-target server', {
messageServerId: message.serverId,
effectiveServerId,
voiceActive: !!this.voiceServerId,
});
break;
}
if (message.users && Array.isArray(message.users)) {
// Close stale peer connections from other servers
if (message.serverId) {
this.closePeersNotInServer(message.serverId);
}
message.users.forEach((user: { oderId: string; displayName: string }) => {
if (user.oderId && !this.peerManager.activePeerConnections.has(user.oderId)) {
this.logger.info('Create peer connection to existing user', { oderId: user.oderId });
this.logger.info('Create peer connection to existing user', { oderId: user.oderId, serverId: message.serverId });
this.peerManager.createPeerConnection(user.oderId, true);
this.peerManager.createAndSendOffer(user.oderId);
if (message.serverId) {
this.peerServerMap.set(user.oderId, message.serverId);
}
}
});
}
break;
}
case SIGNALING_TYPE_USER_JOINED:
this.logger.info('User joined', { displayName: message.displayName, oderId: message.oderId });
@@ -217,6 +243,11 @@ export class WebRTCService implements OnDestroy {
case SIGNALING_TYPE_OFFER:
if (message.fromUserId && message.payload?.sdp) {
// Track inbound peer as belonging to our effective server
const offerEffectiveServer = this.voiceServerId || this.activeServerId;
if (offerEffectiveServer && !this.peerServerMap.has(message.fromUserId)) {
this.peerServerMap.set(message.fromUserId, offerEffectiveServer);
}
this.peerManager.handleOffer(message.fromUserId, message.payload.sdp);
}
break;
@@ -235,6 +266,28 @@ export class WebRTCService implements OnDestroy {
}
}
/**
* Close all peer connections that were discovered from a server
* other than `serverId`. Also removes their entries from
* {@link peerServerMap} so the bookkeeping stays clean.
*
* This ensures audio (and data channels) are scoped to only
* the voice-active (or currently viewed) server.
*/
private closePeersNotInServer(serverId: string): void {
const peersToClose: string[] = [];
this.peerServerMap.forEach((peerServerId, peerId) => {
if (peerServerId !== serverId) {
peersToClose.push(peerId);
}
});
for (const peerId of peersToClose) {
this.logger.info('Closing peer from different server', { peerId, currentServer: serverId });
this.peerManager.removePeer(peerId);
this.peerServerMap.delete(peerId);
}
}
private getCurrentVoiceState(): VoiceStateSnapshot {
return {
isConnected: this._isVoiceConnected(),
@@ -424,6 +477,15 @@ export class WebRTCService implements OnDestroy {
return this.peerManager.remotePeerStreams.get(peerId) ?? null;
}
/**
* Get the current local media stream (microphone audio).
*
* @returns The local {@link MediaStream}, or `null` if voice is not active.
*/
getLocalStream(): MediaStream | null {
return this.mediaManager.getLocalStream();
}
/**
* Request microphone access and start sending audio to all peers.
*
@@ -437,6 +499,7 @@ export class WebRTCService implements OnDestroy {
/** Stop local voice capture and remove audio senders from peers. */
disableVoice(): void {
this.voiceServerId = null;
this.mediaManager.disableVoice();
this._isVoiceConnected.set(false);
}
@@ -501,10 +564,20 @@ export class WebRTCService implements OnDestroy {
/**
* Start broadcasting voice-presence heartbeats to all peers.
*
* Also marks the given server as the active voice server and closes
* any peer connections that belong to other servers so that audio
* is isolated to the correct voice channel.
*
* @param roomId - The voice channel room ID.
* @param serverId - The voice channel server ID.
*/
startVoiceHeartbeat(roomId?: string, serverId?: string): void {
if (serverId) {
this.voiceServerId = serverId;
// Remove peer connections that belong to a different server
// so audio does not leak across voice channels.
this.closePeersNotInServer(serverId);
}
this.mediaManager.startVoiceHeartbeat(roomId, serverId);
}
@@ -535,6 +608,8 @@ export class WebRTCService implements OnDestroy {
/** Disconnect from the signaling server and clean up all state. */
disconnect(): void {
this.voiceServerId = null;
this.peerServerMap.clear();
this.leaveRoom();
this.mediaManager.stopVoiceHeartbeat();
this.signalingManager.close();
@@ -551,6 +626,8 @@ export class WebRTCService implements OnDestroy {
}
private fullCleanup(): void {
this.voiceServerId = null;
this.peerServerMap.clear();
this.peerManager.closeAllPeers();
this._connectedPeers.set([]);
this.mediaManager.disableVoice();