176 lines
5.1 KiB
JavaScript
176 lines
5.1 KiB
JavaScript
import { createLogger } from '../../shared/logger.js';
|
|
|
|
const logger = createLogger('server-recording-peer');
|
|
|
|
export class ServerRecordingPeer {
|
|
constructor({
|
|
rtcConfiguration,
|
|
getLocalStream,
|
|
getSignaling,
|
|
getConnectionId,
|
|
getParticipantId
|
|
}) {
|
|
this.rtcConfiguration = rtcConfiguration;
|
|
this.getLocalStream = getLocalStream;
|
|
this.getSignaling = getSignaling;
|
|
this.getConnectionId = getConnectionId;
|
|
this.getParticipantId = getParticipantId;
|
|
this.peers = new Map();
|
|
}
|
|
|
|
async start(request) {
|
|
if (!request || !request.recordingId) {
|
|
return;
|
|
}
|
|
|
|
this.stop(request.recordingId);
|
|
|
|
const localStream = this.getLocalStream();
|
|
const tracks = localStream ? localStream.getTracks().filter(track => track.readyState !== 'ended') : [];
|
|
if (tracks.length === 0) {
|
|
this._sendStatus(request, 'no-local-media');
|
|
return;
|
|
}
|
|
|
|
const pc = new RTCPeerConnection(this.rtcConfiguration);
|
|
const state = {
|
|
pc,
|
|
recordingId: request.recordingId,
|
|
connectionId: request.connectionId || this.getConnectionId(),
|
|
pendingCandidates: []
|
|
};
|
|
this.peers.set(request.recordingId, state);
|
|
|
|
pc.onicecandidate = (event) => {
|
|
if (!event.candidate) {
|
|
return;
|
|
}
|
|
|
|
this._sendCandidate(state, event.candidate);
|
|
};
|
|
pc.onconnectionstatechange = () => {
|
|
logger.debug(`recording peer ${request.recordingId} state: ${pc.connectionState}`);
|
|
};
|
|
|
|
tracks.forEach(track => {
|
|
pc.addTransceiver(track, {
|
|
direction: 'sendonly',
|
|
streams: localStream ? [localStream] : []
|
|
});
|
|
});
|
|
|
|
const offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
this._sendOffer(state);
|
|
}
|
|
|
|
async applyAnswer(answer) {
|
|
const state = this.peers.get(answer?.recordingId);
|
|
if (!state || !answer?.sdp) {
|
|
return;
|
|
}
|
|
|
|
await state.pc.setRemoteDescription(new RTCSessionDescription({
|
|
type: 'answer',
|
|
sdp: answer.sdp
|
|
}));
|
|
await this._flushPendingCandidates(state);
|
|
}
|
|
|
|
async addIceCandidate(candidate) {
|
|
const state = this.peers.get(candidate?.recordingId);
|
|
if (!state || !candidate?.candidate) {
|
|
return;
|
|
}
|
|
|
|
const iceCandidate = new RTCIceCandidate({
|
|
candidate: candidate.candidate,
|
|
sdpMid: candidate.sdpMid,
|
|
sdpMLineIndex: candidate.sdpMLineIndex
|
|
});
|
|
|
|
if (!state.pc.remoteDescription) {
|
|
state.pendingCandidates.push(iceCandidate);
|
|
return;
|
|
}
|
|
|
|
await state.pc.addIceCandidate(iceCandidate);
|
|
}
|
|
|
|
stop(recordingId) {
|
|
if (!recordingId) {
|
|
this.peers.forEach(peerState => this._closePeer(peerState));
|
|
this.peers.clear();
|
|
return;
|
|
}
|
|
|
|
const state = this.peers.get(recordingId);
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
this._closePeer(state);
|
|
this.peers.delete(recordingId);
|
|
}
|
|
|
|
_closePeer(state) {
|
|
state.pendingCandidates = [];
|
|
state.pc.close();
|
|
}
|
|
|
|
async _flushPendingCandidates(state) {
|
|
if (!state?.pc?.remoteDescription || !state.pendingCandidates.length) {
|
|
return;
|
|
}
|
|
|
|
const pendingCandidates = state.pendingCandidates.splice(0, state.pendingCandidates.length);
|
|
for (const candidate of pendingCandidates) {
|
|
await state.pc.addIceCandidate(candidate);
|
|
}
|
|
}
|
|
|
|
_sendOffer(state) {
|
|
const signaling = this.getSignaling();
|
|
if (!signaling || typeof signaling.sendRecordingOffer !== 'function') {
|
|
return;
|
|
}
|
|
|
|
signaling.sendRecordingOffer({
|
|
recordingId: state.recordingId,
|
|
connectionId: state.connectionId,
|
|
participantId: this.getParticipantId() || '',
|
|
sdp: state.pc.localDescription?.sdp || ''
|
|
});
|
|
}
|
|
|
|
_sendCandidate(state, candidate) {
|
|
const signaling = this.getSignaling();
|
|
if (!signaling || typeof signaling.sendRecordingCandidate !== 'function') {
|
|
return;
|
|
}
|
|
|
|
signaling.sendRecordingCandidate({
|
|
recordingId: state.recordingId,
|
|
connectionId: state.connectionId,
|
|
participantId: this.getParticipantId() || '',
|
|
candidate: candidate.candidate,
|
|
sdpMid: candidate.sdpMid,
|
|
sdpMLineIndex: candidate.sdpMLineIndex
|
|
});
|
|
}
|
|
|
|
_sendStatus(request, status) {
|
|
const signaling = this.getSignaling();
|
|
if (!signaling || typeof signaling.sendRecordingStatus !== 'function') {
|
|
return;
|
|
}
|
|
|
|
signaling.sendRecordingStatus({
|
|
recordingId: request.recordingId,
|
|
connectionId: request.connectionId || this.getConnectionId(),
|
|
participantId: this.getParticipantId() || '',
|
|
status
|
|
});
|
|
}
|
|
}
|