Files
Toju/toju-app/src/app/infrastructure/realtime/peer-connection-manager/connection/create-peer-connection.ts
2026-03-30 03:10:44 +02:00

192 lines
5.8 KiB
TypeScript

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<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;
}