Refactor and code designing
This commit is contained in:
@@ -52,17 +52,14 @@ import {
|
||||
export class WebRTCService implements OnDestroy {
|
||||
private readonly timeSync = inject(TimeSyncService);
|
||||
|
||||
// ─── Logger ────────────────────────────────────────────────────────
|
||||
private readonly logger = new WebRTCLogger(/* debugEnabled */ true);
|
||||
|
||||
// ─── Identity & server membership ──────────────────────────────────
|
||||
private lastIdentifyCredentials: IdentifyCredentials | null = null;
|
||||
private lastJoinedServer: JoinedServerInfo | null = null;
|
||||
private readonly memberServerIds = new Set<string>();
|
||||
private activeServerId: string | null = null;
|
||||
private readonly serviceDestroyed$ = new Subject<void>();
|
||||
|
||||
// ─── Angular signals (reactive state) ──────────────────────────────
|
||||
private readonly _localPeerId = signal<string>(uuidv4());
|
||||
private readonly _isSignalingConnected = signal(false);
|
||||
private readonly _isVoiceConnected = signal(false);
|
||||
@@ -73,10 +70,12 @@ export class WebRTCService implements OnDestroy {
|
||||
private readonly _screenStreamSignal = signal<MediaStream | null>(null);
|
||||
private readonly _hasConnectionError = signal(false);
|
||||
private readonly _connectionErrorMessage = signal<string | null>(null);
|
||||
private readonly _hasEverConnected = signal(false);
|
||||
|
||||
// Public computed signals (unchanged external API)
|
||||
readonly peerId = computed(() => this._localPeerId());
|
||||
readonly isConnected = computed(() => this._isSignalingConnected());
|
||||
readonly hasEverConnected = computed(() => this._hasEverConnected());
|
||||
readonly isVoiceConnected = computed(() => this._isVoiceConnected());
|
||||
readonly connectedPeers = computed(() => this._connectedPeers());
|
||||
readonly isMuted = computed(() => this._isMuted());
|
||||
@@ -91,7 +90,6 @@ export class WebRTCService implements OnDestroy {
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Public observables (unchanged external API) ───────────────────
|
||||
private readonly signalingMessage$ = new Subject<SignalingMessage>();
|
||||
readonly onSignalingMessage = this.signalingMessage$.asObservable();
|
||||
|
||||
@@ -102,8 +100,6 @@ export class WebRTCService implements OnDestroy {
|
||||
get onRemoteStream(): Observable<{ peerId: string; stream: MediaStream }> { return this.peerManager.remoteStream$.asObservable(); }
|
||||
get onVoiceConnected(): Observable<void> { return this.mediaManager.voiceConnected$.asObservable(); }
|
||||
|
||||
// ─── Sub-managers ──────────────────────────────────────────────────
|
||||
|
||||
private readonly signalingManager: SignalingManager;
|
||||
private readonly peerManager: PeerConnectionManager;
|
||||
private readonly mediaManager: MediaManager;
|
||||
@@ -162,12 +158,11 @@ export class WebRTCService implements OnDestroy {
|
||||
this.wireManagerEvents();
|
||||
}
|
||||
|
||||
// ─── Event wiring ──────────────────────────────────────────────────
|
||||
|
||||
private wireManagerEvents(): void {
|
||||
// Signaling → connection status
|
||||
this.signalingManager.connectionStatus$.subscribe(({ connected, errorMessage }) => {
|
||||
this._isSignalingConnected.set(connected);
|
||||
if (connected) this._hasEverConnected.set(true);
|
||||
this._hasConnectionError.set(!connected);
|
||||
this._connectionErrorMessage.set(connected ? null : (errorMessage ?? null));
|
||||
});
|
||||
@@ -187,8 +182,6 @@ export class WebRTCService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Signaling message routing ─────────────────────────────────────
|
||||
|
||||
private handleSignalingMessage(message: any): void {
|
||||
this.signalingMessage$.next(message);
|
||||
this.logger.info('Signaling message', { type: message.type });
|
||||
@@ -242,8 +235,6 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Voice state snapshot ──────────────────────────────────────────
|
||||
|
||||
private getCurrentVoiceState(): VoiceStateSnapshot {
|
||||
return {
|
||||
isConnected: this._isVoiceConnected(),
|
||||
@@ -259,41 +250,85 @@ export class WebRTCService implements OnDestroy {
|
||||
// PUBLIC API – matches the old monolithic service's interface
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ─── Signaling ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 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.signalingManager.connect(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 this.signalingManager.ensureConnected(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.signalingManager.sendSignalingMessage(message, this._localPeerId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a raw JSON payload through the signaling WebSocket.
|
||||
*
|
||||
* @param message - Arbitrary JSON message.
|
||||
*/
|
||||
sendRawMessage(message: Record<string, unknown>): void {
|
||||
this.signalingManager.sendRawMessage(message);
|
||||
}
|
||||
|
||||
// ─── Server membership ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Track the currently-active server ID (for server-scoped operations).
|
||||
*
|
||||
* @param serverId - The server to mark as active.
|
||||
*/
|
||||
setCurrentServer(serverId: string): void {
|
||||
this.activeServerId = serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): void {
|
||||
this.lastIdentifyCredentials = { oderId, displayName };
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_IDENTIFY, oderId, displayName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): void {
|
||||
this.lastJoinedServer = { serverId: roomId, userId };
|
||||
this.memberServerIds.add(roomId);
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_JOIN_SERVER, serverId: roomId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): void {
|
||||
this.lastJoinedServer = { serverId, userId };
|
||||
|
||||
@@ -307,6 +342,14 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (serverId) {
|
||||
this.memberServerIds.delete(serverId);
|
||||
@@ -323,86 +366,159 @@ export class WebRTCService implements OnDestroy {
|
||||
this.fullCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the local client has joined a given server.
|
||||
*
|
||||
* @param serverId - The server to check.
|
||||
*/
|
||||
hasJoinedServer(serverId: string): boolean {
|
||||
return this.memberServerIds.has(serverId);
|
||||
}
|
||||
|
||||
/** Returns a read-only set of all currently-joined server IDs. */
|
||||
getJoinedServerIds(): ReadonlySet<string> {
|
||||
return this.memberServerIds;
|
||||
}
|
||||
|
||||
// ─── Peer messaging ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Broadcast a {@link ChatEvent} to every connected peer.
|
||||
*
|
||||
* @param event - The chat event to send.
|
||||
*/
|
||||
broadcastMessage(event: ChatEvent): void {
|
||||
this.peerManager.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.peerManager.sendToPeer(peerId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this.peerManager.sendToPeerBuffered(peerId, event);
|
||||
}
|
||||
|
||||
/** Returns an array of currently-connected peer IDs. */
|
||||
getConnectedPeers(): string[] {
|
||||
return this.peerManager.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.peerManager.remotePeerStreams.get(peerId) ?? null;
|
||||
}
|
||||
|
||||
// ─── Voice / Media ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Request microphone access and start sending audio to all peers.
|
||||
*
|
||||
* @returns The captured local {@link MediaStream}.
|
||||
*/
|
||||
async enableVoice(): Promise<MediaStream> {
|
||||
const stream = await this.mediaManager.enableVoice();
|
||||
this.syncMediaSignals();
|
||||
return stream;
|
||||
}
|
||||
|
||||
/** Stop local voice capture and remove audio senders from peers. */
|
||||
disableVoice(): void {
|
||||
this.mediaManager.disableVoice();
|
||||
this._isVoiceConnected.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an externally-obtained media stream as the local voice source.
|
||||
*
|
||||
* @param stream - The media stream to use.
|
||||
*/
|
||||
setLocalStream(stream: MediaStream): void {
|
||||
this.mediaManager.setLocalStream(stream);
|
||||
this.syncMediaSignals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the local microphone mute state.
|
||||
*
|
||||
* @param muted - Explicit state; if omitted, the current state is toggled.
|
||||
*/
|
||||
toggleMute(muted?: boolean): void {
|
||||
this.mediaManager.toggleMute(muted);
|
||||
this._isMuted.set(this.mediaManager.getIsMicMuted());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle self-deafen (suppress incoming audio playback).
|
||||
*
|
||||
* @param deafened - Explicit state; if omitted, the current state is toggled.
|
||||
*/
|
||||
toggleDeafen(deafened?: boolean): void {
|
||||
this.mediaManager.toggleDeafen(deafened);
|
||||
this._isDeafened.set(this.mediaManager.getIsSelfDeafened());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the output volume for remote audio playback.
|
||||
*
|
||||
* @param volume - Normalised volume (0–1).
|
||||
*/
|
||||
setOutputVolume(volume: number): void {
|
||||
this.mediaManager.setOutputVolume(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 this.mediaManager.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 this.mediaManager.setLatencyProfile(profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start broadcasting voice-presence heartbeats to all peers.
|
||||
*
|
||||
* @param roomId - The voice channel room ID.
|
||||
* @param serverId - The voice channel server ID.
|
||||
*/
|
||||
startVoiceHeartbeat(roomId?: string, serverId?: string): void {
|
||||
this.mediaManager.startVoiceHeartbeat(roomId, serverId);
|
||||
}
|
||||
|
||||
/** Stop the voice-presence heartbeat. */
|
||||
stopVoiceHeartbeat(): void {
|
||||
this.mediaManager.stopVoiceHeartbeat();
|
||||
}
|
||||
|
||||
// ─── Screen share ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Start sharing the screen (or a window) with all connected peers.
|
||||
*
|
||||
* @param includeAudio - Whether to capture and mix system audio.
|
||||
* @returns The screen-capture {@link MediaStream}.
|
||||
*/
|
||||
async startScreenShare(includeAudio: boolean = false): Promise<MediaStream> {
|
||||
const stream = await this.screenShareManager.startScreenShare(includeAudio);
|
||||
this._isScreenSharing.set(true);
|
||||
@@ -410,24 +526,26 @@ export class WebRTCService implements OnDestroy {
|
||||
return stream;
|
||||
}
|
||||
|
||||
/** Stop screen sharing and restore microphone audio on all peers. */
|
||||
stopScreenShare(): void {
|
||||
this.screenShareManager.stopScreenShare();
|
||||
this._isScreenSharing.set(false);
|
||||
this._screenStreamSignal.set(null);
|
||||
}
|
||||
|
||||
// ─── Disconnect / cleanup ─────────────────────────────────────────
|
||||
|
||||
/** Disconnect from the signaling server and clean up all state. */
|
||||
disconnect(): void {
|
||||
this.leaveRoom();
|
||||
this.mediaManager.stopVoiceHeartbeat();
|
||||
this.signalingManager.close();
|
||||
this._isSignalingConnected.set(false);
|
||||
this._hasEverConnected.set(false);
|
||||
this._hasConnectionError.set(false);
|
||||
this._connectionErrorMessage.set(null);
|
||||
this.serviceDestroyed$.next();
|
||||
}
|
||||
|
||||
/** Alias for {@link disconnect}. */
|
||||
disconnectAll(): void {
|
||||
this.disconnect();
|
||||
}
|
||||
@@ -442,8 +560,6 @@ export class WebRTCService implements OnDestroy {
|
||||
this._screenStreamSignal.set(null);
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/** Synchronise Angular signals from the MediaManager's internal state. */
|
||||
private syncMediaSignals(): void {
|
||||
this._isVoiceConnected.set(this.mediaManager.getIsVoiceActive());
|
||||
@@ -451,8 +567,6 @@ export class WebRTCService implements OnDestroy {
|
||||
this._isDeafened.set(this.mediaManager.getIsSelfDeafened());
|
||||
}
|
||||
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────────
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.disconnect();
|
||||
this.serviceDestroyed$.complete();
|
||||
|
||||
Reference in New Issue
Block a user