Allow multiple signal servers (might need rollback)
All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 9m58s
Queue Release Build / build-linux (push) Successful in 26m26s
Queue Release Build / build-windows (push) Successful in 25m3s
Queue Release Build / finalize (push) Successful in 1m43s
All checks were successful
Queue Release Build / prepare (push) Successful in 17s
Deploy Web Apps / deploy (push) Successful in 9m58s
Queue Release Build / build-linux (push) Successful in 26m26s
Queue Release Build / build-windows (push) Successful in 25m3s
Queue Release Build / finalize (push) Successful in 1m43s
This commit is contained in:
@@ -19,7 +19,12 @@ import {
|
||||
inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import {
|
||||
Observable,
|
||||
of,
|
||||
Subject,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SignalingMessage, ChatEvent } from '../models/index';
|
||||
import { TimeSyncService } from './time-sync.service';
|
||||
@@ -88,8 +93,13 @@ export class WebRTCService implements OnDestroy {
|
||||
private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
|
||||
|
||||
private lastIdentifyCredentials: IdentifyCredentials | null = null;
|
||||
private lastJoinedServer: JoinedServerInfo | null = null;
|
||||
private readonly memberServerIds = new Set<string>();
|
||||
private readonly lastJoinedServerBySignalUrl = new Map<string, JoinedServerInfo>();
|
||||
private readonly memberServerIdsBySignalUrl = new Map<string, Set<string>>();
|
||||
private readonly serverSignalingUrlMap = new Map<string, string>();
|
||||
private readonly peerSignalingUrlMap = new Map<string, string>();
|
||||
private readonly signalingManagers = new Map<string, SignalingManager>();
|
||||
private readonly signalingSubscriptions = new Map<string, Subscription[]>();
|
||||
private readonly signalingConnectionStates = new Map<string, boolean>();
|
||||
private activeServerId: string | null = null;
|
||||
/** The server ID where voice is currently active, or `null` when not in voice. */
|
||||
private voiceServerId: string | null = null;
|
||||
@@ -168,20 +178,12 @@ export class WebRTCService implements OnDestroy {
|
||||
return this.mediaManager.voiceConnected$.asObservable();
|
||||
}
|
||||
|
||||
private readonly signalingManager: SignalingManager;
|
||||
private readonly peerManager: PeerConnectionManager;
|
||||
private readonly mediaManager: MediaManager;
|
||||
private readonly screenShareManager: ScreenShareManager;
|
||||
|
||||
constructor() {
|
||||
// Create managers with null callbacks first to break circular initialization
|
||||
this.signalingManager = new SignalingManager(
|
||||
this.logger,
|
||||
() => this.lastIdentifyCredentials,
|
||||
() => this.lastJoinedServer,
|
||||
() => this.memberServerIds
|
||||
);
|
||||
|
||||
this.peerManager = new PeerConnectionManager(this.logger, null!);
|
||||
|
||||
this.mediaManager = new MediaManager(this.logger, null!);
|
||||
@@ -190,7 +192,7 @@ export class WebRTCService implements OnDestroy {
|
||||
|
||||
// Now wire up cross-references (all managers are instantiated)
|
||||
this.peerManager.setCallbacks({
|
||||
sendRawMessage: (msg: Record<string, unknown>) => this.signalingManager.sendRawMessage(msg),
|
||||
sendRawMessage: (msg: Record<string, unknown>) => this.sendRawMessage(msg),
|
||||
getLocalMediaStream: (): MediaStream | null => this.mediaManager.getLocalStream(),
|
||||
isSignalingConnected: (): boolean => this._isSignalingConnected(),
|
||||
getVoiceStateSnapshot: (): VoiceStateSnapshot => this.getCurrentVoiceState(),
|
||||
@@ -231,23 +233,6 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
// Signaling → message routing
|
||||
this.signalingManager.messageReceived$.subscribe((msg) => this.handleSignalingMessage(msg));
|
||||
|
||||
// Signaling → heartbeat → broadcast states
|
||||
this.signalingManager.heartbeatTick$.subscribe(() => this.peerManager.broadcastCurrentStates());
|
||||
|
||||
// Internal control-plane messages for on-demand screen-share delivery.
|
||||
this.peerManager.messageReceived$.subscribe((event) => this.handlePeerControlMessage(event));
|
||||
|
||||
@@ -277,6 +262,7 @@ export class WebRTCService implements OnDestroy {
|
||||
this.peerManager.peerDisconnected$.subscribe((peerId) => {
|
||||
this.activeRemoteScreenSharePeers.delete(peerId);
|
||||
this.peerServerMap.delete(peerId);
|
||||
this.peerSignalingUrlMap.delete(peerId);
|
||||
this.screenShareManager.clearScreenShareRequest(peerId);
|
||||
});
|
||||
|
||||
@@ -293,37 +279,145 @@ export class WebRTCService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private handleSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private ensureSignalingManager(signalUrl: string): SignalingManager {
|
||||
const existingManager = this.signalingManagers.get(signalUrl);
|
||||
|
||||
if (existingManager) {
|
||||
return existingManager;
|
||||
}
|
||||
|
||||
const manager = new SignalingManager(
|
||||
this.logger,
|
||||
() => this.lastIdentifyCredentials,
|
||||
() => this.lastJoinedServerBySignalUrl.get(signalUrl) ?? null,
|
||||
() => this.getMemberServerIdsForSignalUrl(signalUrl)
|
||||
);
|
||||
const subscriptions: Subscription[] = [
|
||||
manager.connectionStatus$.subscribe(({ connected, errorMessage }) =>
|
||||
this.handleSignalingConnectionStatus(signalUrl, connected, errorMessage)
|
||||
),
|
||||
manager.messageReceived$.subscribe((message) => this.handleSignalingMessage(message, signalUrl)),
|
||||
manager.heartbeatTick$.subscribe(() => this.peerManager.broadcastCurrentStates())
|
||||
];
|
||||
|
||||
this.signalingManagers.set(signalUrl, manager);
|
||||
this.signalingSubscriptions.set(signalUrl, subscriptions);
|
||||
return manager;
|
||||
}
|
||||
|
||||
private handleSignalingConnectionStatus(
|
||||
signalUrl: string,
|
||||
connected: boolean,
|
||||
errorMessage?: string
|
||||
): void {
|
||||
this.signalingConnectionStates.set(signalUrl, connected);
|
||||
|
||||
if (connected)
|
||||
this._hasEverConnected.set(true);
|
||||
|
||||
const anyConnected = this.isAnySignalingConnected();
|
||||
|
||||
this._isSignalingConnected.set(anyConnected);
|
||||
this._hasConnectionError.set(!anyConnected);
|
||||
this._connectionErrorMessage.set(anyConnected ? null : (errorMessage ?? 'Disconnected from signaling server'));
|
||||
}
|
||||
|
||||
private isAnySignalingConnected(): boolean {
|
||||
for (const manager of this.signalingManagers.values()) {
|
||||
if (manager.isSocketOpen()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getConnectedSignalingManagers(): { signalUrl: string; manager: SignalingManager }[] {
|
||||
const connectedManagers: { signalUrl: string; manager: SignalingManager }[] = [];
|
||||
|
||||
for (const [signalUrl, manager] of this.signalingManagers.entries()) {
|
||||
if (!manager.isSocketOpen()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
connectedManagers.push({ signalUrl,
|
||||
manager });
|
||||
}
|
||||
|
||||
return connectedManagers;
|
||||
}
|
||||
|
||||
private getOrCreateMemberServerSet(signalUrl: string): Set<string> {
|
||||
const existingSet = this.memberServerIdsBySignalUrl.get(signalUrl);
|
||||
|
||||
if (existingSet) {
|
||||
return existingSet;
|
||||
}
|
||||
|
||||
const createdSet = new Set<string>();
|
||||
|
||||
this.memberServerIdsBySignalUrl.set(signalUrl, createdSet);
|
||||
return createdSet;
|
||||
}
|
||||
|
||||
private getMemberServerIdsForSignalUrl(signalUrl: string): ReadonlySet<string> {
|
||||
return this.memberServerIdsBySignalUrl.get(signalUrl) ?? new Set<string>();
|
||||
}
|
||||
|
||||
private isJoinedServer(serverId: string): boolean {
|
||||
for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
|
||||
if (memberServerIds.has(serverId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getJoinedServerCount(): number {
|
||||
let joinedServerCount = 0;
|
||||
|
||||
for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
|
||||
joinedServerCount += memberServerIds.size;
|
||||
}
|
||||
|
||||
return joinedServerCount;
|
||||
}
|
||||
|
||||
private handleSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
this.signalingMessage$.next(message);
|
||||
this.logger.info('Signaling message', { type: message.type });
|
||||
this.logger.info('Signaling message', {
|
||||
signalUrl,
|
||||
type: message.type
|
||||
});
|
||||
|
||||
switch (message.type) {
|
||||
case SIGNALING_TYPE_CONNECTED:
|
||||
this.handleConnectedSignalingMessage(message);
|
||||
this.handleConnectedSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_SERVER_USERS:
|
||||
this.handleServerUsersSignalingMessage(message);
|
||||
this.handleServerUsersSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_USER_JOINED:
|
||||
this.handleUserJoinedSignalingMessage(message);
|
||||
this.handleUserJoinedSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_USER_LEFT:
|
||||
this.handleUserLeftSignalingMessage(message);
|
||||
this.handleUserLeftSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_OFFER:
|
||||
this.handleOfferSignalingMessage(message);
|
||||
this.handleOfferSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_ANSWER:
|
||||
this.handleAnswerSignalingMessage(message);
|
||||
this.handleAnswerSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
case SIGNALING_TYPE_ICE_CANDIDATE:
|
||||
this.handleIceCandidateSignalingMessage(message);
|
||||
this.handleIceCandidateSignalingMessage(message, signalUrl);
|
||||
return;
|
||||
|
||||
default:
|
||||
@@ -331,26 +425,40 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private handleConnectedSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
this.logger.info('Server connected', { oderId: message.oderId });
|
||||
private handleConnectedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
this.logger.info('Server connected', {
|
||||
oderId: message.oderId,
|
||||
signalUrl
|
||||
});
|
||||
|
||||
if (message.serverId) {
|
||||
this.serverSignalingUrlMap.set(message.serverId, signalUrl);
|
||||
}
|
||||
|
||||
if (typeof message.serverTime === 'number') {
|
||||
this.timeSync.setFromServerTime(message.serverTime);
|
||||
}
|
||||
}
|
||||
|
||||
private handleServerUsersSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleServerUsersSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
const users = Array.isArray(message.users) ? message.users : [];
|
||||
|
||||
this.logger.info('Server users', {
|
||||
count: users.length,
|
||||
signalUrl,
|
||||
serverId: message.serverId
|
||||
});
|
||||
|
||||
if (message.serverId) {
|
||||
this.serverSignalingUrlMap.set(message.serverId, signalUrl);
|
||||
}
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.oderId)
|
||||
continue;
|
||||
|
||||
this.peerSignalingUrlMap.set(user.oderId, signalUrl);
|
||||
|
||||
if (message.serverId) {
|
||||
this.trackPeerInServer(user.oderId, message.serverId);
|
||||
}
|
||||
@@ -376,21 +484,31 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private handleUserJoinedSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleUserJoinedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
this.logger.info('User joined', {
|
||||
displayName: message.displayName,
|
||||
oderId: message.oderId
|
||||
oderId: message.oderId,
|
||||
signalUrl
|
||||
});
|
||||
|
||||
if (message.serverId) {
|
||||
this.serverSignalingUrlMap.set(message.serverId, signalUrl);
|
||||
}
|
||||
|
||||
if (message.oderId) {
|
||||
this.peerSignalingUrlMap.set(message.oderId, signalUrl);
|
||||
}
|
||||
|
||||
if (message.oderId && message.serverId) {
|
||||
this.trackPeerInServer(message.oderId, message.serverId);
|
||||
}
|
||||
}
|
||||
|
||||
private handleUserLeftSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleUserLeftSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
this.logger.info('User left', {
|
||||
displayName: message.displayName,
|
||||
oderId: message.oderId,
|
||||
signalUrl,
|
||||
serverId: message.serverId
|
||||
});
|
||||
|
||||
@@ -404,17 +522,20 @@ export class WebRTCService implements OnDestroy {
|
||||
if (!hasRemainingSharedServers) {
|
||||
this.peerManager.removePeer(message.oderId);
|
||||
this.peerServerMap.delete(message.oderId);
|
||||
this.peerSignalingUrlMap.delete(message.oderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleOfferSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleOfferSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
const fromUserId = message.fromUserId;
|
||||
const sdp = message.payload?.sdp;
|
||||
|
||||
if (!fromUserId || !sdp)
|
||||
return;
|
||||
|
||||
this.peerSignalingUrlMap.set(fromUserId, signalUrl);
|
||||
|
||||
const offerEffectiveServer = this.voiceServerId || this.activeServerId;
|
||||
|
||||
if (offerEffectiveServer && !this.peerServerMap.has(fromUserId)) {
|
||||
@@ -424,23 +545,27 @@ export class WebRTCService implements OnDestroy {
|
||||
this.peerManager.handleOffer(fromUserId, sdp);
|
||||
}
|
||||
|
||||
private handleAnswerSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleAnswerSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
const fromUserId = message.fromUserId;
|
||||
const sdp = message.payload?.sdp;
|
||||
|
||||
if (!fromUserId || !sdp)
|
||||
return;
|
||||
|
||||
this.peerSignalingUrlMap.set(fromUserId, signalUrl);
|
||||
|
||||
this.peerManager.handleAnswer(fromUserId, sdp);
|
||||
}
|
||||
|
||||
private handleIceCandidateSignalingMessage(message: IncomingSignalingMessage): void {
|
||||
private handleIceCandidateSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
|
||||
const fromUserId = message.fromUserId;
|
||||
const candidate = message.payload?.candidate;
|
||||
|
||||
if (!fromUserId || !candidate)
|
||||
return;
|
||||
|
||||
this.peerSignalingUrlMap.set(fromUserId, signalUrl);
|
||||
|
||||
this.peerManager.handleIceCandidate(fromUserId, candidate);
|
||||
}
|
||||
|
||||
@@ -467,6 +592,7 @@ export class WebRTCService implements OnDestroy {
|
||||
|
||||
this.peerManager.removePeer(peerId);
|
||||
this.peerServerMap.delete(peerId);
|
||||
this.peerSignalingUrlMap.delete(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +616,18 @@ export class WebRTCService implements OnDestroy {
|
||||
* @returns An observable that emits `true` once connected.
|
||||
*/
|
||||
connectToSignalingServer(serverUrl: string): Observable<boolean> {
|
||||
return this.signalingManager.connect(serverUrl);
|
||||
const manager = this.ensureSignalingManager(serverUrl);
|
||||
|
||||
if (manager.isSocketOpen()) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return manager.connect(serverUrl);
|
||||
}
|
||||
|
||||
/** Returns true when the signaling socket for a given URL is currently open. */
|
||||
isSignalingConnectedTo(serverUrl: string): boolean {
|
||||
return this.signalingManagers.get(serverUrl)?.isSocketOpen() ?? false;
|
||||
}
|
||||
|
||||
private trackPeerInServer(peerId: string, serverId: string): void {
|
||||
@@ -504,7 +641,7 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
|
||||
private replacePeerSharedServers(peerId: string, serverIds: string[]): boolean {
|
||||
const sharedServerIds = serverIds.filter((serverId) => this.memberServerIds.has(serverId));
|
||||
const sharedServerIds = serverIds.filter((serverId) => this.isJoinedServer(serverId));
|
||||
|
||||
if (sharedServerIds.length === 0) {
|
||||
this.peerServerMap.delete(peerId);
|
||||
@@ -539,7 +676,17 @@ export class WebRTCService implements OnDestroy {
|
||||
* @returns `true` if connected within the timeout.
|
||||
*/
|
||||
async ensureSignalingConnected(timeoutMs?: number): Promise<boolean> {
|
||||
return this.signalingManager.ensureConnected(timeoutMs);
|
||||
if (this.isAnySignalingConnected()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const manager of this.signalingManagers.values()) {
|
||||
if (await manager.ensureConnected(timeoutMs)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -548,7 +695,32 @@ export class WebRTCService implements OnDestroy {
|
||||
* @param message - The signaling message payload (excluding `from` / `timestamp`).
|
||||
*/
|
||||
sendSignalingMessage(message: Omit<SignalingMessage, 'from' | 'timestamp'>): void {
|
||||
this.signalingManager.sendSignalingMessage(message, this._localPeerId());
|
||||
const targetPeerId = message.to;
|
||||
|
||||
if (targetPeerId) {
|
||||
const targetSignalUrl = this.peerSignalingUrlMap.get(targetPeerId);
|
||||
|
||||
if (targetSignalUrl) {
|
||||
const targetManager = this.ensureSignalingManager(targetSignalUrl);
|
||||
|
||||
targetManager.sendSignalingMessage(message, this._localPeerId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const connectedManagers = this.getConnectedSignalingManagers();
|
||||
|
||||
if (connectedManagers.length === 0) {
|
||||
this.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), {
|
||||
type: message.type
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { manager } of connectedManagers) {
|
||||
manager.sendSignalingMessage(message, this._localPeerId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -557,7 +729,50 @@ export class WebRTCService implements OnDestroy {
|
||||
* @param message - Arbitrary JSON message.
|
||||
*/
|
||||
sendRawMessage(message: Record<string, unknown>): void {
|
||||
this.signalingManager.sendRawMessage(message);
|
||||
const targetPeerId = typeof message['targetUserId'] === 'string' ? message['targetUserId'] : null;
|
||||
|
||||
if (targetPeerId) {
|
||||
const targetSignalUrl = this.peerSignalingUrlMap.get(targetPeerId);
|
||||
|
||||
if (targetSignalUrl && this.sendRawMessageToSignalUrl(targetSignalUrl, message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const serverId = typeof message['serverId'] === 'string' ? message['serverId'] : null;
|
||||
|
||||
if (serverId) {
|
||||
const serverSignalUrl = this.serverSignalingUrlMap.get(serverId);
|
||||
|
||||
if (serverSignalUrl && this.sendRawMessageToSignalUrl(serverSignalUrl, message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const connectedManagers = this.getConnectedSignalingManagers();
|
||||
|
||||
if (connectedManagers.length === 0) {
|
||||
this.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), {
|
||||
type: typeof message['type'] === 'string' ? message['type'] : 'unknown'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { manager } of connectedManagers) {
|
||||
manager.sendRawMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private sendRawMessageToSignalUrl(signalUrl: string, message: Record<string, unknown>): boolean {
|
||||
const manager = this.signalingManagers.get(signalUrl);
|
||||
|
||||
if (!manager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
manager.sendRawMessage(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,7 +791,15 @@ export class WebRTCService implements OnDestroy {
|
||||
|
||||
/** The last signaling URL used by the client, if any. */
|
||||
getCurrentSignalingUrl(): string | null {
|
||||
return this.signalingManager.getLastUrl();
|
||||
if (this.activeServerId) {
|
||||
const activeServerSignalUrl = this.serverSignalingUrlMap.get(this.activeServerId);
|
||||
|
||||
if (activeServerSignalUrl) {
|
||||
return activeServerSignalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getConnectedSignalingManagers()[0]?.signalUrl ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,13 +810,22 @@ export class WebRTCService implements OnDestroy {
|
||||
* @param oderId - The user's unique order/peer ID.
|
||||
* @param displayName - The user's display name.
|
||||
*/
|
||||
identify(oderId: string, displayName: string): void {
|
||||
identify(oderId: string, displayName: string, signalUrl?: string): void {
|
||||
this.lastIdentifyCredentials = { oderId,
|
||||
displayName };
|
||||
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_IDENTIFY,
|
||||
const identifyMessage = {
|
||||
type: SIGNALING_TYPE_IDENTIFY,
|
||||
oderId,
|
||||
displayName });
|
||||
displayName
|
||||
};
|
||||
|
||||
if (signalUrl) {
|
||||
this.sendRawMessageToSignalUrl(signalUrl, identifyMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendRawMessage(identifyMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,13 +834,27 @@ export class WebRTCService implements OnDestroy {
|
||||
* @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 };
|
||||
joinRoom(roomId: string, userId: string, signalUrl?: string): void {
|
||||
const resolvedSignalUrl = signalUrl
|
||||
?? this.serverSignalingUrlMap.get(roomId)
|
||||
?? this.getCurrentSignalingUrl();
|
||||
|
||||
this.memberServerIds.add(roomId);
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_JOIN_SERVER,
|
||||
serverId: roomId });
|
||||
if (!resolvedSignalUrl) {
|
||||
this.logger.warn('[signaling] Cannot join room without a signaling URL', { roomId });
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverSignalingUrlMap.set(roomId, resolvedSignalUrl);
|
||||
this.lastJoinedServerBySignalUrl.set(resolvedSignalUrl, {
|
||||
serverId: roomId,
|
||||
userId
|
||||
});
|
||||
|
||||
this.getOrCreateMemberServerSet(resolvedSignalUrl).add(roomId);
|
||||
this.sendRawMessageToSignalUrl(resolvedSignalUrl, {
|
||||
type: SIGNALING_TYPE_JOIN_SERVER,
|
||||
serverId: roomId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,26 +864,46 @@ export class WebRTCService implements OnDestroy {
|
||||
* @param serverId - The target server ID.
|
||||
* @param userId - The local user ID.
|
||||
*/
|
||||
switchServer(serverId: string, userId: string): void {
|
||||
this.lastJoinedServer = { serverId,
|
||||
userId };
|
||||
switchServer(serverId: string, userId: string, signalUrl?: string): void {
|
||||
const resolvedSignalUrl = signalUrl
|
||||
?? this.serverSignalingUrlMap.get(serverId)
|
||||
?? this.getCurrentSignalingUrl();
|
||||
|
||||
if (this.memberServerIds.has(serverId)) {
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_VIEW_SERVER,
|
||||
serverId });
|
||||
if (!resolvedSignalUrl) {
|
||||
this.logger.warn('[signaling] Cannot switch server without a signaling URL', { serverId });
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverSignalingUrlMap.set(serverId, resolvedSignalUrl);
|
||||
this.lastJoinedServerBySignalUrl.set(resolvedSignalUrl, {
|
||||
serverId,
|
||||
userId
|
||||
});
|
||||
|
||||
const memberServerIds = this.getOrCreateMemberServerSet(resolvedSignalUrl);
|
||||
|
||||
if (memberServerIds.has(serverId)) {
|
||||
this.sendRawMessageToSignalUrl(resolvedSignalUrl, {
|
||||
type: SIGNALING_TYPE_VIEW_SERVER,
|
||||
serverId
|
||||
});
|
||||
|
||||
this.logger.info('Viewed server (already joined)', {
|
||||
serverId,
|
||||
signalUrl: resolvedSignalUrl,
|
||||
userId,
|
||||
voiceConnected: this._isVoiceConnected()
|
||||
});
|
||||
} else {
|
||||
this.memberServerIds.add(serverId);
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_JOIN_SERVER,
|
||||
serverId });
|
||||
memberServerIds.add(serverId);
|
||||
this.sendRawMessageToSignalUrl(resolvedSignalUrl, {
|
||||
type: SIGNALING_TYPE_JOIN_SERVER,
|
||||
serverId
|
||||
});
|
||||
|
||||
this.logger.info('Joined new server via switch', {
|
||||
serverId,
|
||||
signalUrl: resolvedSignalUrl,
|
||||
userId,
|
||||
voiceConnected: this._isVoiceConnected()
|
||||
});
|
||||
@@ -654,25 +920,47 @@ export class WebRTCService implements OnDestroy {
|
||||
*/
|
||||
leaveRoom(serverId?: string): void {
|
||||
if (serverId) {
|
||||
this.memberServerIds.delete(serverId);
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_LEAVE_SERVER,
|
||||
serverId });
|
||||
const resolvedSignalUrl = this.serverSignalingUrlMap.get(serverId);
|
||||
|
||||
if (resolvedSignalUrl) {
|
||||
this.getOrCreateMemberServerSet(resolvedSignalUrl).delete(serverId);
|
||||
this.sendRawMessageToSignalUrl(resolvedSignalUrl, {
|
||||
type: SIGNALING_TYPE_LEAVE_SERVER,
|
||||
serverId
|
||||
});
|
||||
} else {
|
||||
this.sendRawMessage({
|
||||
type: SIGNALING_TYPE_LEAVE_SERVER,
|
||||
serverId
|
||||
});
|
||||
|
||||
for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
|
||||
memberServerIds.delete(serverId);
|
||||
}
|
||||
}
|
||||
|
||||
this.serverSignalingUrlMap.delete(serverId);
|
||||
|
||||
this.logger.info('Left server', { serverId });
|
||||
|
||||
if (this.memberServerIds.size === 0) {
|
||||
if (this.getJoinedServerCount() === 0) {
|
||||
this.fullCleanup();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.memberServerIds.forEach((sid) => {
|
||||
this.sendRawMessage({ type: SIGNALING_TYPE_LEAVE_SERVER,
|
||||
serverId: sid });
|
||||
});
|
||||
for (const [signalUrl, memberServerIds] of this.memberServerIdsBySignalUrl.entries()) {
|
||||
for (const sid of memberServerIds) {
|
||||
this.sendRawMessageToSignalUrl(signalUrl, {
|
||||
type: SIGNALING_TYPE_LEAVE_SERVER,
|
||||
serverId: sid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.memberServerIds.clear();
|
||||
this.memberServerIdsBySignalUrl.clear();
|
||||
this.serverSignalingUrlMap.clear();
|
||||
this.fullCleanup();
|
||||
}
|
||||
|
||||
@@ -682,12 +970,18 @@ export class WebRTCService implements OnDestroy {
|
||||
* @param serverId - The server to check.
|
||||
*/
|
||||
hasJoinedServer(serverId: string): boolean {
|
||||
return this.memberServerIds.has(serverId);
|
||||
return this.isJoinedServer(serverId);
|
||||
}
|
||||
|
||||
/** Returns a read-only set of all currently-joined server IDs. */
|
||||
getJoinedServerIds(): ReadonlySet<string> {
|
||||
return this.memberServerIds;
|
||||
const joinedServerIds = new Set<string>();
|
||||
|
||||
for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
|
||||
memberServerIds.forEach((serverId) => joinedServerIds.add(serverId));
|
||||
}
|
||||
|
||||
return joinedServerIds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -942,11 +1236,15 @@ export class WebRTCService implements OnDestroy {
|
||||
|
||||
/** Disconnect from the signaling server and clean up all state. */
|
||||
disconnect(): void {
|
||||
this.leaveRoom();
|
||||
this.voiceServerId = null;
|
||||
this.peerServerMap.clear();
|
||||
this.leaveRoom();
|
||||
this.peerSignalingUrlMap.clear();
|
||||
this.lastJoinedServerBySignalUrl.clear();
|
||||
this.memberServerIdsBySignalUrl.clear();
|
||||
this.serverSignalingUrlMap.clear();
|
||||
this.mediaManager.stopVoiceHeartbeat();
|
||||
this.signalingManager.close();
|
||||
this.destroyAllSignalingManagers();
|
||||
this._isSignalingConnected.set(false);
|
||||
this._hasEverConnected.set(false);
|
||||
this._hasConnectionError.set(false);
|
||||
@@ -962,6 +1260,7 @@ export class WebRTCService implements OnDestroy {
|
||||
private fullCleanup(): void {
|
||||
this.voiceServerId = null;
|
||||
this.peerServerMap.clear();
|
||||
this.peerSignalingUrlMap.clear();
|
||||
this.remoteScreenShareRequestsEnabled = false;
|
||||
this.desiredRemoteScreenSharePeers.clear();
|
||||
this.activeRemoteScreenSharePeers.clear();
|
||||
@@ -1040,10 +1339,25 @@ export class WebRTCService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private destroyAllSignalingManagers(): void {
|
||||
for (const subscriptions of this.signalingSubscriptions.values()) {
|
||||
for (const subscription of subscriptions) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
for (const manager of this.signalingManagers.values()) {
|
||||
manager.destroy();
|
||||
}
|
||||
|
||||
this.signalingSubscriptions.clear();
|
||||
this.signalingManagers.clear();
|
||||
this.signalingConnectionStates.clear();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.disconnect();
|
||||
this.serviceDestroyed$.complete();
|
||||
this.signalingManager.destroy();
|
||||
this.peerManager.destroy();
|
||||
this.mediaManager.destroy();
|
||||
this.screenShareManager.destroy();
|
||||
|
||||
Reference in New Issue
Block a user