Outgoing voice was gated on the observer's roster copy of the remote user's voice state, which is signaling gossip. The signal server broadcasts `user_left` for any socket it declares dead, so a suspended laptop or a flaky hop wiped that copy and the observer detached its microphone from a peer that never left the channel - a silent member with no way back through the UI. `decideVoicePathRouting` now closes a path only on positive evidence: we left voice, the peer itself reported another channel or none, or the connection is gone. Missing gossip holds an established path instead. Opening still needs confirmation, so a guess never starts sending; the same rule gates playback, camera video, and the microphone a new connection puts in its first offer. Peers are also asked for their voice state when a connection or data channel comes up, so a rebuilt path re-confirms itself. Alongside it, the microphone can be switched mid-call: capture moves to a device service and rules, the live track is swapped with `replaceTrack` so the session is never renegotiated, and the speaking indicator follows the new stream.
147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { Observable } from 'rxjs';
|
|
import { ChatEvent } from '../../../shared-kernel';
|
|
import { ScreenShareStartOptions } from '../screen-share.config';
|
|
import { PeerData } from '../realtime.types';
|
|
import { MediaManager } from '../media/media.manager';
|
|
import { PeerScopedIdentity } from '../peer-connection-manager/messaging/data-channel';
|
|
import { PeerConnectionManager } from '../peer-connection-manager/peer-connection.manager';
|
|
import { ScreenShareManager } from '../media/screen-share.manager';
|
|
|
|
interface PeerMediaFacadeDependencies {
|
|
peerManager: PeerConnectionManager;
|
|
mediaManager: MediaManager;
|
|
screenShareManager: ScreenShareManager;
|
|
}
|
|
|
|
export class PeerMediaFacade {
|
|
constructor(
|
|
private readonly dependencies: PeerMediaFacadeDependencies
|
|
) {}
|
|
|
|
get onMessageReceived(): Observable<ChatEvent> {
|
|
return this.dependencies.peerManager.messageReceived$.asObservable();
|
|
}
|
|
|
|
get onPeerConnected(): Observable<string> {
|
|
return this.dependencies.peerManager.peerConnected$.asObservable();
|
|
}
|
|
|
|
get onPeerDisconnected(): Observable<string> {
|
|
return this.dependencies.peerManager.peerDisconnected$.asObservable();
|
|
}
|
|
|
|
get onRemoteStream(): Observable<{ peerId: string; stream: MediaStream }> {
|
|
return this.dependencies.peerManager.remoteStream$.asObservable();
|
|
}
|
|
|
|
get onVoiceConnected(): Observable<void> {
|
|
return this.dependencies.mediaManager.voiceConnected$.asObservable();
|
|
}
|
|
|
|
getActivePeers(): Map<string, PeerData> {
|
|
return this.dependencies.peerManager.activePeerConnections;
|
|
}
|
|
|
|
async renegotiate(peerId: string): Promise<void> {
|
|
return await this.dependencies.peerManager.renegotiate(peerId);
|
|
}
|
|
|
|
broadcastMessage(event: ChatEvent): void {
|
|
this.dependencies.peerManager.broadcastMessage(event);
|
|
}
|
|
|
|
broadcastIdentityScopedMessage(buildEvent: (identity: PeerScopedIdentity) => ChatEvent): void {
|
|
this.dependencies.peerManager.broadcastIdentityScopedMessage(buildEvent);
|
|
}
|
|
|
|
sendToPeer(peerId: string, event: ChatEvent): boolean {
|
|
return this.dependencies.peerManager.sendToPeer(peerId, event);
|
|
}
|
|
|
|
async sendToPeerBuffered(peerId: string, event: ChatEvent): Promise<void> {
|
|
return await this.dependencies.peerManager.sendToPeerBuffered(peerId, event);
|
|
}
|
|
|
|
broadcastCurrentStates(): void {
|
|
this.dependencies.peerManager.broadcastCurrentStates();
|
|
}
|
|
|
|
getConnectedPeerIds(): string[] {
|
|
return this.dependencies.peerManager.getConnectedPeerIds();
|
|
}
|
|
|
|
getRemoteStream(peerId: string): MediaStream | null {
|
|
return this.dependencies.peerManager.remotePeerStreams.get(peerId) ?? null;
|
|
}
|
|
|
|
getRemoteVoiceStream(peerId: string): MediaStream | null {
|
|
return this.dependencies.peerManager.remotePeerVoiceStreams.get(peerId) ?? null;
|
|
}
|
|
|
|
getRemoteCameraStream(peerId: string): MediaStream | null {
|
|
return this.dependencies.peerManager.remotePeerCameraStreams.get(peerId) ?? null;
|
|
}
|
|
|
|
getRemoteScreenShareStream(peerId: string): MediaStream | null {
|
|
return this.dependencies.peerManager.remotePeerScreenShareStreams.get(peerId) ?? null;
|
|
}
|
|
|
|
clearRemoteScreenShareStream(peerId: string): void {
|
|
this.dependencies.peerManager.clearRemoteScreenShareStream(peerId);
|
|
}
|
|
|
|
closeAllPeers(): void {
|
|
this.dependencies.peerManager.closeAllPeers();
|
|
}
|
|
|
|
getLocalStream(): MediaStream | null {
|
|
return this.dependencies.mediaManager.getLocalStream();
|
|
}
|
|
|
|
getLocalCameraStream(): MediaStream | null {
|
|
return this.dependencies.mediaManager.getLocalCameraStream();
|
|
}
|
|
|
|
getRawMicStream(): MediaStream | null {
|
|
return this.dependencies.mediaManager.getRawMicStream();
|
|
}
|
|
|
|
mayOpenVoicePathToPeer(peerId: string): boolean {
|
|
return this.dependencies.mediaManager.mayOpenVoicePathToPeer(peerId);
|
|
}
|
|
|
|
isScreenShareActive(): boolean {
|
|
return this.dependencies.screenShareManager.getIsScreenActive();
|
|
}
|
|
|
|
async startScreenShare(options: ScreenShareStartOptions): Promise<MediaStream> {
|
|
return await this.dependencies.screenShareManager.startScreenShare(options);
|
|
}
|
|
|
|
stopScreenShare(): void {
|
|
this.dependencies.screenShareManager.stopScreenShare();
|
|
}
|
|
|
|
requestScreenShareForPeer(peerId: string): void {
|
|
this.dependencies.screenShareManager.requestScreenShareForPeer(peerId);
|
|
}
|
|
|
|
stopScreenShareForPeer(peerId: string): void {
|
|
this.dependencies.screenShareManager.stopScreenShareForPeer(peerId);
|
|
}
|
|
|
|
clearScreenShareRequest(peerId: string): void {
|
|
this.dependencies.screenShareManager.clearScreenShareRequest(peerId);
|
|
}
|
|
|
|
syncScreenShareToPeer(peerId: string): void {
|
|
this.dependencies.screenShareManager.syncScreenShareToPeer(peerId);
|
|
}
|
|
|
|
destroy(): void {
|
|
this.dependencies.peerManager.destroy();
|
|
this.dependencies.mediaManager.destroy();
|
|
this.dependencies.screenShareManager.destroy();
|
|
}
|
|
}
|