Files
Toju/toju-app/src/app/infrastructure/realtime/peer-connection-manager/connection/create-peer-connection.ts
2026-06-05 06:16:02 +02:00

230 lines
6.9 KiB
TypeScript

import {
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_CONNECTED,
CONNECTION_STATE_DISCONNECTED,
CONNECTION_STATE_FAILED,
DATA_CHANNEL_LABEL,
SIGNALING_TYPE_ICE_CANDIDATE,
TRACK_KIND_AUDIO,
TRACK_KIND_VIDEO,
TRANSCEIVER_RECV_ONLY,
TRANSCEIVER_SEND_RECV
} from '../../realtime.constants';
import { recordDebugNetworkConnectionState } from '../../logging/debug-network-metrics';
import { PeerData } from '../../realtime.types';
import { ConnectionLifecycleHandlers, PeerConnectionManagerContext } from '../shared';
/**
* Create and configure a new RTCPeerConnection for a remote peer.
*/
export function createPeerConnection(
context: PeerConnectionManagerContext,
remotePeerId: string,
isInitiator: boolean,
handlers: ConnectionLifecycleHandlers
): PeerData {
const { callbacks, logger, state } = context;
logger.info('Creating peer connection', { remotePeerId, isInitiator });
const connection = new RTCPeerConnection({ iceServers: callbacks.getIceServers() });
let dataChannel: RTCDataChannel | null = null;
let peerData: PeerData | null = null;
const adoptDataChannel = (channel: RTCDataChannel): void => {
const primaryChannel = dataChannel;
const shouldAdoptAsPrimary = !primaryChannel || primaryChannel.readyState === 'closed';
if (shouldAdoptAsPrimary) {
dataChannel = channel;
if (peerData) {
peerData.dataChannel = channel;
}
const existing = state.activePeerConnections.get(remotePeerId);
if (existing) {
existing.dataChannel = channel;
}
} else if (primaryChannel !== channel) {
logger.info('Received secondary data channel while primary channel is still active', {
channelLabel: channel.label,
primaryChannelLabel: primaryChannel.label,
primaryReadyState: primaryChannel.readyState,
remotePeerId
});
}
handlers.setupDataChannel(channel, remotePeerId);
};
connection.onicecandidate = (event) => {
if (event.candidate) {
if (!callbacks.isSignalingConnected()) {
return;
}
logger.info('ICE candidate gathered', {
remotePeerId,
candidateType: (event.candidate as RTCIceCandidate & { type?: string }).type
});
callbacks.sendRawMessage({
type: SIGNALING_TYPE_ICE_CANDIDATE,
targetUserId: remotePeerId,
payload: { candidate: event.candidate }
});
}
};
connection.onconnectionstatechange = () => {
logger.info('connectionstatechange', {
remotePeerId,
state: connection.connectionState
});
// Ignore events from a connection that was already replaced in the Map
// (e.g. handleOffer recreated the peer while this handler was still queued).
const currentPeer = state.activePeerConnections.get(remotePeerId);
if (currentPeer && currentPeer.connection !== connection) {
logger.info('Ignoring stale connectionstatechange', {
remotePeerId,
state: connection.connectionState
});
return;
}
recordDebugNetworkConnectionState(remotePeerId, connection.connectionState);
switch (connection.connectionState) {
case CONNECTION_STATE_CONNECTED:
handlers.clearPeerDisconnectGraceTimer(remotePeerId);
handlers.addToConnectedPeers(remotePeerId);
state.peerConnected$.next(remotePeerId);
handlers.clearPeerReconnectTimer(remotePeerId);
state.disconnectedPeerTracker.delete(remotePeerId);
handlers.requestVoiceStateFromPeer(remotePeerId);
break;
case CONNECTION_STATE_DISCONNECTED:
handlers.schedulePeerDisconnectRecovery(remotePeerId);
break;
case CONNECTION_STATE_FAILED:
handlers.trackDisconnectedPeer(remotePeerId);
handlers.removePeer(remotePeerId, { preserveReconnectState: true });
handlers.schedulePeerReconnect(remotePeerId);
break;
case CONNECTION_STATE_CLOSED:
handlers.removePeer(remotePeerId);
break;
}
};
connection.oniceconnectionstatechange = () => {
logger.info('iceconnectionstatechange', {
remotePeerId,
state: connection.iceConnectionState
});
};
connection.onsignalingstatechange = () => {
logger.info('signalingstatechange', {
remotePeerId,
state: connection.signalingState
});
};
connection.onnegotiationneeded = () => {
logger.info('negotiationneeded', { remotePeerId });
};
connection.ontrack = (event) => {
handlers.handleRemoteTrack(event, remotePeerId);
};
connection.ondatachannel = (event) => {
logger.info('Received data channel', { remotePeerId });
adoptDataChannel(event.channel);
};
if (isInitiator) {
dataChannel = connection.createDataChannel(DATA_CHANNEL_LABEL, { ordered: true });
handlers.setupDataChannel(dataChannel, remotePeerId);
}
peerData = {
connection,
dataChannel,
createdAt: Date.now(),
isInitiator,
pendingIceCandidates: [],
audioSender: undefined,
videoSender: undefined,
remoteVoiceStreamIds: new Set<string>(),
remoteCameraStreamIds: new Set<string>(),
remoteScreenShareStreamIds: new Set<string>()
};
if (isInitiator) {
const audioTransceiver = connection.addTransceiver(TRACK_KIND_AUDIO, {
direction: TRANSCEIVER_SEND_RECV
});
const videoTransceiver = connection.addTransceiver(TRACK_KIND_VIDEO, {
direction: TRANSCEIVER_RECV_ONLY
});
peerData.audioSender = audioTransceiver.sender;
peerData.videoSender = videoTransceiver.sender;
}
state.activePeerConnections.set(remotePeerId, peerData);
const localStream = callbacks.getLocalMediaStream();
if (localStream && isInitiator) {
logger.logStream(`localStream->${remotePeerId}`, localStream);
localStream.getTracks().forEach((track) => {
if (track.kind === TRACK_KIND_AUDIO && peerData.audioSender) {
if (typeof peerData.audioSender.setStreams === 'function') {
peerData.audioSender.setStreams(localStream);
}
peerData.audioSender
.replaceTrack(track)
.then(() => logger.info('audio replaceTrack (init) ok', { remotePeerId }))
.catch((error) =>
logger.error('audio replaceTrack failed at createPeerConnection', error)
);
} else if (track.kind === TRACK_KIND_VIDEO && peerData.videoSender) {
if (typeof peerData.videoSender.setStreams === 'function') {
peerData.videoSender.setStreams(localStream);
}
peerData.videoSender
.replaceTrack(track)
.then(() => logger.info('video replaceTrack (init) ok', { remotePeerId }))
.catch((error) =>
logger.error('video replaceTrack failed at createPeerConnection', error)
);
} else {
const sender = connection.addTrack(track, localStream);
if (track.kind === TRACK_KIND_AUDIO)
peerData.audioSender = sender;
if (track.kind === TRACK_KIND_VIDEO)
peerData.videoSender = sender;
}
});
}
return peerData;
}