import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_CONNECTED, CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_FAILED, DATA_CHANNEL_LABEL, ICE_SERVERS, 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: ICE_SERVERS }); let dataChannel: RTCDataChannel | null = null; connection.onicecandidate = (event) => { if (event.candidate) { 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 }); 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); }; if (isInitiator) { dataChannel = connection.createDataChannel(DATA_CHANNEL_LABEL, { ordered: true }); handlers.setupDataChannel(dataChannel, remotePeerId); } else { connection.ondatachannel = (event) => { logger.info('Received data channel', { remotePeerId }); dataChannel = event.channel; const existing = state.activePeerConnections.get(remotePeerId); if (existing) { existing.dataChannel = dataChannel; } handlers.setupDataChannel(dataChannel, remotePeerId); }; } const peerData: PeerData = { connection, dataChannel, 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; }