624 lines
24 KiB
TypeScript
624 lines
24 KiB
TypeScript
/**
|
|
* WebRTCService - thin Angular service that composes specialised managers.
|
|
*
|
|
* Each concern lives in its own file under `./`:
|
|
* • SignalingManager - WebSocket lifecycle & reconnection
|
|
* • PeerConnectionManager - RTCPeerConnection, offers/answers, ICE, data channels
|
|
* • MediaManager - mic voice, mute, deafen, bitrate
|
|
* • ScreenShareManager - screen capture & mixed audio
|
|
* • WebRTCLogger - debug / diagnostic logging
|
|
*
|
|
* This file wires them together and exposes a public API that is
|
|
* identical to the old monolithic service so consumers don't change.
|
|
*/
|
|
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-non-null-assertion */
|
|
import {
|
|
Injectable,
|
|
inject,
|
|
OnDestroy
|
|
} from '@angular/core';
|
|
import { Observable, Subject } from 'rxjs';
|
|
import { ChatEvent } from '../../shared-kernel';
|
|
import type { SignalingMessage } from '../../shared-kernel';
|
|
import { TimeSyncService } from '../../core/services/time-sync.service';
|
|
import { DebuggingService } from '../../core/services/debugging';
|
|
import { ScreenShareSourcePickerService } from '../../domains/screen-share';
|
|
import { MediaManager } from './media/media.manager';
|
|
import { ScreenShareManager } from './media/screen-share.manager';
|
|
import { VoiceSessionController } from './media/voice-session-controller';
|
|
import type { PeerData, VoiceStateSnapshot } from './realtime.types';
|
|
import { LatencyProfile } from './realtime.constants';
|
|
import { ScreenShareStartOptions } from './screen-share.config';
|
|
import { WebRTCLogger } from './logging/webrtc-logger';
|
|
import { PeerConnectionManager } from './peer-connection-manager/peer-connection.manager';
|
|
import { PeerMediaFacade } from './streams/peer-media-facade';
|
|
import { RemoteScreenShareRequestController } from './streams/remote-screen-share-request-controller';
|
|
import { IncomingSignalingMessage, IncomingSignalingMessageHandler } from './signaling/signaling-message-handler';
|
|
import { ServerMembershipSignalingHandler } from './signaling/server-membership-signaling-handler';
|
|
import { ServerSignalingCoordinator } from './signaling/server-signaling-coordinator';
|
|
import { SignalingManager } from './signaling/signaling.manager';
|
|
import { SignalingTransportHandler } from './signaling/signaling-transport-handler';
|
|
import { WebRtcStateController } from './state/webrtc-state-controller';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class WebRTCService implements OnDestroy {
|
|
private readonly timeSync = inject(TimeSyncService);
|
|
private readonly debugging = inject(DebuggingService);
|
|
private readonly screenShareSourcePicker = inject(ScreenShareSourcePickerService);
|
|
|
|
private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
|
|
private readonly state = new WebRtcStateController();
|
|
|
|
readonly peerId = this.state.peerId;
|
|
readonly isConnected = this.state.isConnected;
|
|
readonly hasEverConnected = this.state.hasEverConnected;
|
|
readonly isVoiceConnected = this.state.isVoiceConnected;
|
|
readonly connectedPeers = this.state.connectedPeers;
|
|
readonly isMuted = this.state.isMuted;
|
|
readonly isDeafened = this.state.isDeafened;
|
|
readonly isScreenSharing = this.state.isScreenSharing;
|
|
readonly isNoiseReductionEnabled = this.state.isNoiseReductionEnabled;
|
|
readonly screenStream = this.state.screenStream;
|
|
readonly isScreenShareRemotePlaybackSuppressed = this.state.isScreenShareRemotePlaybackSuppressed;
|
|
readonly forceDefaultRemotePlaybackOutput = this.state.forceDefaultRemotePlaybackOutput;
|
|
readonly hasConnectionError = this.state.hasConnectionError;
|
|
readonly connectionErrorMessage = this.state.connectionErrorMessage;
|
|
readonly shouldShowConnectionError = this.state.shouldShowConnectionError;
|
|
readonly peerLatencies = this.state.peerLatencies;
|
|
|
|
private readonly signalingMessage$ = new Subject<IncomingSignalingMessage>();
|
|
readonly onSignalingMessage = this.signalingMessage$.asObservable();
|
|
|
|
// Delegates to managers
|
|
get onMessageReceived(): Observable<ChatEvent> {
|
|
return this.peerMediaFacade.onMessageReceived;
|
|
}
|
|
get onPeerConnected(): Observable<string> {
|
|
return this.peerMediaFacade.onPeerConnected;
|
|
}
|
|
get onPeerDisconnected(): Observable<string> {
|
|
return this.peerMediaFacade.onPeerDisconnected;
|
|
}
|
|
get onRemoteStream(): Observable<{ peerId: string; stream: MediaStream }> {
|
|
return this.peerMediaFacade.onRemoteStream;
|
|
}
|
|
get onVoiceConnected(): Observable<void> {
|
|
return this.peerMediaFacade.onVoiceConnected;
|
|
}
|
|
|
|
private readonly peerManager: PeerConnectionManager;
|
|
private readonly mediaManager: MediaManager;
|
|
private readonly screenShareManager: ScreenShareManager;
|
|
private readonly peerMediaFacade: PeerMediaFacade;
|
|
private readonly voiceSessionController: VoiceSessionController;
|
|
private readonly signalingCoordinator: ServerSignalingCoordinator<IncomingSignalingMessage>;
|
|
private readonly signalingTransportHandler: SignalingTransportHandler<IncomingSignalingMessage>;
|
|
private readonly signalingMessageHandler: IncomingSignalingMessageHandler;
|
|
private readonly serverMembershipSignalingHandler: ServerMembershipSignalingHandler<IncomingSignalingMessage>;
|
|
private readonly remoteScreenShareRequestController: RemoteScreenShareRequestController;
|
|
|
|
constructor() {
|
|
// Create managers with null callbacks first to break circular initialization
|
|
this.peerManager = new PeerConnectionManager(this.logger, null!);
|
|
|
|
this.mediaManager = new MediaManager(this.logger, null!);
|
|
|
|
this.screenShareManager = new ScreenShareManager(this.logger, null!);
|
|
|
|
this.peerMediaFacade = new PeerMediaFacade({
|
|
peerManager: this.peerManager,
|
|
mediaManager: this.mediaManager,
|
|
screenShareManager: this.screenShareManager
|
|
});
|
|
|
|
this.voiceSessionController = new VoiceSessionController({
|
|
mediaManager: this.mediaManager,
|
|
getIsScreenSharing: () => this.state.isScreenSharingActive(),
|
|
setVoiceConnected: (connected) => this.state.setVoiceConnected(connected),
|
|
setMuted: (muted) => this.state.setMuted(muted),
|
|
setDeafened: (deafened) => this.state.setDeafened(deafened),
|
|
setNoiseReductionEnabled: (enabled) => this.state.setNoiseReductionEnabled(enabled)
|
|
});
|
|
|
|
this.signalingCoordinator = new ServerSignalingCoordinator({
|
|
createManager: (_signalUrl, getLastJoinedServer, getMemberServerIds) => new SignalingManager(
|
|
this.logger,
|
|
() => this.signalingTransportHandler.getIdentifyCredentials(),
|
|
getLastJoinedServer,
|
|
getMemberServerIds
|
|
),
|
|
handleConnectionStatus: (_signalUrl, connected, errorMessage) =>
|
|
this.handleSignalingConnectionStatus(connected, errorMessage),
|
|
handleHeartbeatTick: () => this.peerMediaFacade.broadcastCurrentStates(),
|
|
handleMessage: (message, signalUrl) => this.handleSignalingMessage(message, signalUrl)
|
|
});
|
|
|
|
this.signalingTransportHandler = new SignalingTransportHandler({
|
|
signalingCoordinator: this.signalingCoordinator,
|
|
logger: this.logger,
|
|
getLocalPeerId: () => this.state.getLocalPeerId()
|
|
});
|
|
|
|
// Now wire up cross-references (all managers are instantiated)
|
|
this.peerManager.setCallbacks({
|
|
sendRawMessage: (msg: Record<string, unknown>) => this.signalingTransportHandler.sendRawMessage(msg),
|
|
getLocalMediaStream: (): MediaStream | null => this.peerMediaFacade.getLocalStream(),
|
|
isSignalingConnected: (): boolean => this.state.isSignalingConnected(),
|
|
getVoiceStateSnapshot: (): VoiceStateSnapshot => this.voiceSessionController.getCurrentVoiceState(),
|
|
getIdentifyCredentials: () => this.signalingTransportHandler.getIdentifyCredentials(),
|
|
getLocalPeerId: (): string => this.state.getLocalPeerId(),
|
|
isScreenSharingActive: (): boolean => this.state.isScreenSharingActive()
|
|
});
|
|
|
|
this.mediaManager.setCallbacks({
|
|
getActivePeers: (): Map<string, PeerData> => this.peerMediaFacade.getActivePeers(),
|
|
renegotiate: (peerId: string): Promise<void> => this.peerMediaFacade.renegotiate(peerId),
|
|
broadcastMessage: (event: ChatEvent): void => this.peerMediaFacade.broadcastMessage(event),
|
|
getIdentifyOderId: (): string => this.signalingTransportHandler.getIdentifyOderId(),
|
|
getIdentifyDisplayName: (): string => this.signalingTransportHandler.getIdentifyDisplayName()
|
|
});
|
|
|
|
this.screenShareManager.setCallbacks({
|
|
getActivePeers: (): Map<string, PeerData> => this.peerMediaFacade.getActivePeers(),
|
|
getLocalMediaStream: (): MediaStream | null => this.peerMediaFacade.getLocalStream(),
|
|
renegotiate: (peerId: string): Promise<void> => this.peerMediaFacade.renegotiate(peerId),
|
|
broadcastCurrentStates: (): void => this.peerMediaFacade.broadcastCurrentStates(),
|
|
selectDesktopSource: async (sources, options) => await this.screenShareSourcePicker.open(
|
|
sources,
|
|
options.includeSystemAudio
|
|
),
|
|
updateLocalScreenShareState: (state): void => this.state.applyLocalScreenShareState(state)
|
|
});
|
|
|
|
this.signalingMessageHandler = new IncomingSignalingMessageHandler({
|
|
getEffectiveServerId: () => this.voiceSessionController.getEffectiveServerId(this.state.currentServerId),
|
|
peerManager: this.peerManager,
|
|
setServerTime: (serverTime) => this.timeSync.setFromServerTime(serverTime),
|
|
signalingCoordinator: this.signalingCoordinator,
|
|
logger: this.logger
|
|
});
|
|
|
|
this.serverMembershipSignalingHandler = new ServerMembershipSignalingHandler({
|
|
signalingCoordinator: this.signalingCoordinator,
|
|
signalingTransport: this.signalingTransportHandler,
|
|
logger: this.logger,
|
|
getActiveServerId: () => this.state.currentServerId,
|
|
isVoiceConnected: () => this.state.isVoiceConnectedActive(),
|
|
runFullCleanup: () => this.fullCleanup()
|
|
});
|
|
|
|
this.remoteScreenShareRequestController = new RemoteScreenShareRequestController({
|
|
getConnectedPeerIds: () => this.peerMediaFacade.getConnectedPeerIds(),
|
|
sendToPeer: (peerId, event) => this.peerMediaFacade.sendToPeer(peerId, event),
|
|
clearRemoteScreenShareStream: (peerId) => this.peerMediaFacade.clearRemoteScreenShareStream(peerId),
|
|
requestScreenShareForPeer: (peerId) => this.peerMediaFacade.requestScreenShareForPeer(peerId),
|
|
stopScreenShareForPeer: (peerId) => this.peerMediaFacade.stopScreenShareForPeer(peerId),
|
|
clearScreenShareRequest: (peerId) => this.peerMediaFacade.clearScreenShareRequest(peerId)
|
|
});
|
|
|
|
this.wireManagerEvents();
|
|
}
|
|
|
|
private wireManagerEvents(): void {
|
|
// Internal control-plane messages for on-demand screen-share delivery.
|
|
this.peerManager.messageReceived$.subscribe((event) =>
|
|
this.remoteScreenShareRequestController.handlePeerControlMessage(event)
|
|
);
|
|
|
|
// Peer manager → connected peers signal
|
|
this.peerManager.connectedPeersChanged$.subscribe((peers: string[]) =>
|
|
this.state.setConnectedPeers(peers)
|
|
);
|
|
|
|
// If we are already sharing when a new peer connection finishes, push the
|
|
// current screen-share tracks to that peer and renegotiate.
|
|
this.peerManager.peerConnected$.subscribe((peerId) => {
|
|
if (this.peerMediaFacade.isScreenShareActive()) {
|
|
this.peerMediaFacade.syncScreenShareToPeer(peerId);
|
|
}
|
|
|
|
this.remoteScreenShareRequestController.handlePeerConnected(peerId);
|
|
});
|
|
|
|
this.peerManager.peerDisconnected$.subscribe((peerId) => {
|
|
this.remoteScreenShareRequestController.handlePeerDisconnected(peerId);
|
|
this.signalingCoordinator.deletePeerTracking(peerId);
|
|
});
|
|
|
|
// Media manager → voice connected signal
|
|
this.mediaManager.voiceConnected$.subscribe(() => {
|
|
this.voiceSessionController.handleVoiceConnected();
|
|
});
|
|
|
|
// Peer manager → latency updates
|
|
this.peerManager.peerLatencyChanged$.subscribe(() =>
|
|
this.state.syncPeerLatencies(this.peerManager.peerLatencies)
|
|
);
|
|
}
|
|
|
|
private handleSignalingConnectionStatus(connected: boolean, errorMessage?: string): void {
|
|
this.state.updateSignalingConnectionStatus(
|
|
this.signalingCoordinator.isAnySignalingConnected(),
|
|
connected,
|
|
errorMessage
|
|
);
|
|
}
|
|
|
|
private handleSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
|
this.signalingMessage$.next(message);
|
|
this.signalingMessageHandler.handleMessage(message, signalUrl);
|
|
}
|
|
|
|
// PUBLIC API - matches the old monolithic service's interface
|
|
|
|
/**
|
|
* Connect to a signaling server via WebSocket.
|
|
*
|
|
* @param serverUrl - The WebSocket URL of the signaling server.
|
|
* @returns An observable that emits `true` once connected.
|
|
*/
|
|
connectToSignalingServer(serverUrl: string): Observable<boolean> {
|
|
return this.signalingTransportHandler.connectToSignalingServer(serverUrl);
|
|
}
|
|
|
|
/** Returns true when the signaling socket for a given URL is currently open. */
|
|
isSignalingConnectedTo(serverUrl: string): boolean {
|
|
return this.signalingTransportHandler.isSignalingConnectedTo(serverUrl);
|
|
}
|
|
|
|
/**
|
|
* Ensure the signaling WebSocket is connected, reconnecting if needed.
|
|
*
|
|
* @param timeoutMs - Maximum time (ms) to wait for the connection.
|
|
* @returns `true` if connected within the timeout.
|
|
*/
|
|
async ensureSignalingConnected(timeoutMs?: number): Promise<boolean> {
|
|
return await this.signalingTransportHandler.ensureSignalingConnected(timeoutMs);
|
|
}
|
|
|
|
/**
|
|
* Send a signaling-level message (with `from` and `timestamp` auto-populated).
|
|
*
|
|
* @param message - The signaling message payload (excluding `from` / `timestamp`).
|
|
*/
|
|
sendSignalingMessage(message: Omit<SignalingMessage, 'from' | 'timestamp'>): void {
|
|
this.signalingTransportHandler.sendSignalingMessage(message);
|
|
}
|
|
|
|
/**
|
|
* Send a raw JSON payload through the signaling WebSocket.
|
|
*
|
|
* @param message - Arbitrary JSON message.
|
|
*/
|
|
sendRawMessage(message: Record<string, unknown>): void {
|
|
this.signalingTransportHandler.sendRawMessage(message);
|
|
}
|
|
|
|
/**
|
|
* Track the currently-active server ID (for server-scoped operations).
|
|
*
|
|
* @param serverId - The server to mark as active.
|
|
*/
|
|
setCurrentServer(serverId: string): void {
|
|
this.state.setCurrentServer(serverId);
|
|
}
|
|
|
|
/** The server ID currently being viewed / active, or `null`. */
|
|
get currentServerId(): string | null {
|
|
return this.state.currentServerId;
|
|
}
|
|
|
|
/** The last signaling URL used by the client, if any. */
|
|
getCurrentSignalingUrl(): string | null {
|
|
return this.signalingTransportHandler.getCurrentSignalingUrl(this.state.currentServerId);
|
|
}
|
|
|
|
/**
|
|
* Send an identify message to the signaling server.
|
|
*
|
|
* The credentials are cached so they can be replayed after a reconnect.
|
|
*
|
|
* @param oderId - The user's unique order/peer ID.
|
|
* @param displayName - The user's display name.
|
|
*/
|
|
identify(oderId: string, displayName: string, signalUrl?: string): void {
|
|
this.signalingTransportHandler.identify(oderId, displayName, signalUrl);
|
|
}
|
|
|
|
/**
|
|
* Join a server (room) on the signaling server.
|
|
*
|
|
* @param roomId - The server / room ID to join.
|
|
* @param userId - The local user ID.
|
|
*/
|
|
joinRoom(roomId: string, userId: string, signalUrl?: string): void {
|
|
this.serverMembershipSignalingHandler.joinRoom(roomId, userId, signalUrl);
|
|
}
|
|
|
|
/**
|
|
* Switch to a different server. If already a member, sends a view event;
|
|
* otherwise joins the server.
|
|
*
|
|
* @param serverId - The target server ID.
|
|
* @param userId - The local user ID.
|
|
*/
|
|
switchServer(serverId: string, userId: string, signalUrl?: string): void {
|
|
this.serverMembershipSignalingHandler.switchServer(serverId, userId, signalUrl);
|
|
}
|
|
|
|
/**
|
|
* Leave one or all servers.
|
|
*
|
|
* If `serverId` is provided, leaves only that server.
|
|
* Otherwise leaves every joined server and performs a full cleanup.
|
|
*
|
|
* @param serverId - Optional server to leave; omit to leave all.
|
|
*/
|
|
leaveRoom(serverId?: string): void {
|
|
this.serverMembershipSignalingHandler.leaveRoom(serverId);
|
|
}
|
|
|
|
/**
|
|
* Check whether the local client has joined a given server.
|
|
*
|
|
* @param serverId - The server to check.
|
|
*/
|
|
hasJoinedServer(serverId: string): boolean {
|
|
return this.signalingCoordinator.hasJoinedServer(serverId);
|
|
}
|
|
|
|
/** Returns a read-only set of all currently-joined server IDs. */
|
|
getJoinedServerIds(): ReadonlySet<string> {
|
|
return this.signalingCoordinator.getJoinedServerIds();
|
|
}
|
|
|
|
/**
|
|
* Broadcast a {@link ChatEvent} to every connected peer.
|
|
*
|
|
* @param event - The chat event to send.
|
|
*/
|
|
broadcastMessage(event: ChatEvent): void {
|
|
this.peerMediaFacade.broadcastMessage(event);
|
|
}
|
|
|
|
/**
|
|
* Send a {@link ChatEvent} to a specific peer.
|
|
*
|
|
* @param peerId - The target peer ID.
|
|
* @param event - The chat event to send.
|
|
*/
|
|
sendToPeer(peerId: string, event: ChatEvent): void {
|
|
this.peerMediaFacade.sendToPeer(peerId, event);
|
|
}
|
|
|
|
syncRemoteScreenShareRequests(peerIds: string[], enabled: boolean): void {
|
|
this.remoteScreenShareRequestController.syncRemoteScreenShareRequests(peerIds, enabled);
|
|
}
|
|
|
|
/**
|
|
* Send a {@link ChatEvent} to a peer with back-pressure awareness.
|
|
*
|
|
* @param peerId - The target peer ID.
|
|
* @param event - The chat event to send.
|
|
*/
|
|
async sendToPeerBuffered(peerId: string, event: ChatEvent): Promise<void> {
|
|
return await this.peerMediaFacade.sendToPeerBuffered(peerId, event);
|
|
}
|
|
|
|
/** Returns an array of currently-connected peer IDs. */
|
|
getConnectedPeers(): string[] {
|
|
return this.peerMediaFacade.getConnectedPeerIds();
|
|
}
|
|
|
|
/**
|
|
* Get the composite remote {@link MediaStream} for a connected peer.
|
|
*
|
|
* @param peerId - The remote peer whose stream to retrieve.
|
|
* @returns The stream, or `null` if the peer has no active stream.
|
|
*/
|
|
getRemoteStream(peerId: string): MediaStream | null {
|
|
return this.peerMediaFacade.getRemoteStream(peerId);
|
|
}
|
|
|
|
/**
|
|
* Get the remote voice-only stream for a connected peer.
|
|
*
|
|
* @param peerId - The remote peer whose voice stream to retrieve.
|
|
* @returns The stream, or `null` if the peer has no active voice audio.
|
|
*/
|
|
getRemoteVoiceStream(peerId: string): MediaStream | null {
|
|
return this.peerMediaFacade.getRemoteVoiceStream(peerId);
|
|
}
|
|
|
|
/**
|
|
* Get the remote screen-share stream for a connected peer.
|
|
*
|
|
* This contains the screen video track and any audio track that belongs to
|
|
* the screen share itself, not the peer's normal voice-chat audio.
|
|
*
|
|
* @param peerId - The remote peer whose screen-share stream to retrieve.
|
|
* @returns The stream, or `null` if the peer has no active screen share.
|
|
*/
|
|
getRemoteScreenShareStream(peerId: string): MediaStream | null {
|
|
return this.peerMediaFacade.getRemoteScreenShareStream(peerId);
|
|
}
|
|
|
|
/**
|
|
* 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.peerMediaFacade.getLocalStream();
|
|
}
|
|
|
|
/**
|
|
* Get the raw local microphone stream before gain / RNNoise processing.
|
|
*
|
|
* @returns The raw microphone {@link MediaStream}, or `null` if voice is not active.
|
|
*/
|
|
getRawMicStream(): MediaStream | null {
|
|
return this.peerMediaFacade.getRawMicStream();
|
|
}
|
|
|
|
/**
|
|
* Request microphone access and start sending audio to all peers.
|
|
*
|
|
* @returns The captured local {@link MediaStream}.
|
|
*/
|
|
async enableVoice(): Promise<MediaStream> {
|
|
return await this.voiceSessionController.enableVoice();
|
|
}
|
|
|
|
/** Stop local voice capture and remove audio senders from peers. */
|
|
disableVoice(): void {
|
|
this.voiceSessionController.disableVoice();
|
|
}
|
|
|
|
/**
|
|
* Inject an externally-obtained media stream as the local voice source.
|
|
*
|
|
* @param stream - The media stream to use.
|
|
*/
|
|
async setLocalStream(stream: MediaStream): Promise<void> {
|
|
await this.voiceSessionController.setLocalStream(stream);
|
|
}
|
|
|
|
/**
|
|
* Toggle the local microphone mute state.
|
|
*
|
|
* @param muted - Explicit state; if omitted, the current state is toggled.
|
|
*/
|
|
toggleMute(muted?: boolean): void {
|
|
this.voiceSessionController.toggleMute(muted);
|
|
}
|
|
|
|
/**
|
|
* Toggle self-deafen (suppress incoming audio playback).
|
|
*
|
|
* @param deafened - Explicit state; if omitted, the current state is toggled.
|
|
*/
|
|
toggleDeafen(deafened?: boolean): void {
|
|
this.voiceSessionController.toggleDeafen(deafened);
|
|
}
|
|
|
|
/**
|
|
* Toggle RNNoise noise reduction on the local microphone.
|
|
*
|
|
* When enabled, the raw mic audio is routed through an AudioWorklet
|
|
* that applies neural-network noise suppression before being sent
|
|
* to peers.
|
|
*
|
|
* @param enabled - Explicit state; if omitted, the current state is toggled.
|
|
*/
|
|
async toggleNoiseReduction(enabled?: boolean): Promise<void> {
|
|
await this.voiceSessionController.toggleNoiseReduction(enabled);
|
|
}
|
|
|
|
/**
|
|
* Set the output volume for remote audio playback.
|
|
*
|
|
* @param volume - Normalised volume (0-1).
|
|
*/
|
|
setOutputVolume(volume: number): void {
|
|
this.voiceSessionController.setOutputVolume(volume);
|
|
}
|
|
|
|
/**
|
|
* Set the input (microphone) volume.
|
|
*
|
|
* Adjusts a Web Audio GainNode on the local mic stream so the level
|
|
* sent to peers changes in real time without renegotiation.
|
|
*
|
|
* @param volume - Normalised volume (0-1).
|
|
*/
|
|
setInputVolume(volume: number): void {
|
|
this.voiceSessionController.setInputVolume(volume);
|
|
}
|
|
|
|
/**
|
|
* Set the maximum audio bitrate for all peer connections.
|
|
*
|
|
* @param kbps - Target bitrate in kilobits per second.
|
|
*/
|
|
async setAudioBitrate(kbps: number): Promise<void> {
|
|
return await this.voiceSessionController.setAudioBitrate(kbps);
|
|
}
|
|
|
|
/**
|
|
* Apply a predefined latency profile that maps to a specific bitrate.
|
|
*
|
|
* @param profile - One of `'low'`, `'balanced'`, or `'high'`.
|
|
*/
|
|
async setLatencyProfile(profile: LatencyProfile): Promise<void> {
|
|
return await this.voiceSessionController.setLatencyProfile(profile);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
this.voiceSessionController.startVoiceHeartbeat(roomId, serverId);
|
|
}
|
|
|
|
/** Stop the voice-presence heartbeat. */
|
|
stopVoiceHeartbeat(): void {
|
|
this.voiceSessionController.stopVoiceHeartbeat();
|
|
}
|
|
|
|
/**
|
|
* Start sharing the screen (or a window) with all connected peers.
|
|
*
|
|
* @param options - Screen-share capture options.
|
|
* @returns The screen-capture {@link MediaStream}.
|
|
*/
|
|
async startScreenShare(options: ScreenShareStartOptions): Promise<MediaStream> {
|
|
return await this.peerMediaFacade.startScreenShare(options);
|
|
}
|
|
|
|
/** Stop screen sharing and restore microphone audio on all peers. */
|
|
stopScreenShare(): void {
|
|
this.peerMediaFacade.stopScreenShare();
|
|
}
|
|
|
|
/** Disconnect from the signaling server and clean up all state. */
|
|
disconnect(): void {
|
|
this.leaveRoom();
|
|
this.destroyAllSignalingManagers();
|
|
this.state.resetConnectionState();
|
|
}
|
|
|
|
/** Alias for {@link disconnect}. */
|
|
disconnectAll(): void {
|
|
this.disconnect();
|
|
}
|
|
|
|
private fullCleanup(): void {
|
|
this.signalingCoordinator.clearPeerTracking();
|
|
this.remoteScreenShareRequestController.clear();
|
|
this.peerMediaFacade.closeAllPeers();
|
|
this.state.clearPeerViewState();
|
|
this.voiceSessionController.resetVoiceSession();
|
|
this.peerMediaFacade.stopScreenShare();
|
|
this.state.clearScreenShareState();
|
|
}
|
|
|
|
private destroyAllSignalingManagers(): void {
|
|
this.signalingCoordinator.destroy();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.disconnect();
|
|
this.peerMediaFacade.destroy();
|
|
}
|
|
}
|