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(), remoteCameraStreamIds: new Set(), remoteScreenShareStreamIds: new Set() }; 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; }