192 lines
5.8 KiB
TypeScript
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;
|
|
}
|