7281475271
kurento-utils is unmaintained. It's served us well, but its age shows. We need to transition to something else if we want to have better maintainability and include simulcast, multistream, ... This introduces a simplified/leaner wrapper kit that's almost API-compatible with what we use right now - so widespread changes are minimal). It's easier to maintain/read/transition from. This can be read as an intermediate step to transitioning to something definitive (ie mediasoup-client).
327 lines
9.3 KiB
JavaScript
327 lines
9.3 KiB
JavaScript
import { EventEmitter2 } from 'eventemitter2';
|
|
import {
|
|
stopStream,
|
|
stopTrack,
|
|
silentConsole,
|
|
} from '/imports/ui/services/webrtc-base/utils';
|
|
|
|
export default class WebRtcPeer extends EventEmitter2 {
|
|
constructor(mode, options = {}) {
|
|
super({ newListener: true });
|
|
|
|
this.mode = mode;
|
|
|
|
this.options = options;
|
|
this.peerConnection = this.options.peerConnection;
|
|
this.videoStream = this.options.videoStream;
|
|
this.audioStream = this.options.audioStream;
|
|
this.mediaConstraints = this.options.mediaConstraints;
|
|
this.trace = this.options.trace;
|
|
this.configuration = this.options.configuration;
|
|
this.onicecandidate = this.options.onicecandidate;
|
|
this.oncandidategatheringdone = this.options.oncandidategatheringdone;
|
|
this.candidateGatheringDone = false;
|
|
|
|
this._outboundCandidateQueue = [];
|
|
this._inboundCandidateQueue = [];
|
|
this._handleIceCandidate = this._handleIceCandidate.bind(this);
|
|
this._handleSignalingStateChange = this._handleSignalingStateChange.bind(this);
|
|
|
|
if (this.onicecandidate) this.on('icecandidate', this.onicecandidate);
|
|
if (this.oncandidategatheringdone) this.on('candidategatheringdone', this.oncandidategatheringdone);
|
|
}
|
|
|
|
_flushInboundCandidateQueue() {
|
|
while (this._inboundCandidateQueue.length) {
|
|
const entry = this._inboundCandidateQueue.shift();
|
|
if (entry.candidate && entry.promise) {
|
|
try {
|
|
if (this.isPeerConnectionClosed()) {
|
|
entry.promise.resolve();
|
|
} else {
|
|
this.peerConnection.addIceCandidate(entry.candidate)
|
|
.then(entry.promise.resolve)
|
|
.catch(entry.promise.reject);
|
|
}
|
|
} catch (error) {
|
|
entry.promise.reject(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_trackQueueFlushEvents() {
|
|
this.on('newListener', (event) => {
|
|
if (event === 'icecandidate' || event === 'candidategatheringdone') {
|
|
while (this._outboundCandidateQueue.length) {
|
|
const candidate = this._outboundCandidateQueue.shift();
|
|
|
|
if (!candidate) this._emitCandidateGatheringDone();
|
|
}
|
|
}
|
|
});
|
|
|
|
this.peerConnection?.addEventListener('signalingstatechange', this._handleSignalingStateChange);
|
|
}
|
|
|
|
_emitCandidateGatheringDone() {
|
|
if (!this.candidateGatheringDone) {
|
|
this.emit('candidategatheringdone');
|
|
this.candidateGatheringDone = true;
|
|
}
|
|
}
|
|
|
|
_handleIceCandidate({ candidate }) {
|
|
if (this.hasListeners('icecandidate') || this.hasListeners('candidategatheringdone')) {
|
|
if (candidate) {
|
|
this.emit('icecandidate', candidate);
|
|
this.candidateGatheringDone = false;
|
|
} else this._emitCandidateGatheringDone();
|
|
} else if (!this.candidateGatheringDone) {
|
|
this._outboundCandidateQueue.push(candidate);
|
|
if (!candidate) this.candidateGatheringDone = true;
|
|
}
|
|
}
|
|
|
|
_handleSignalingStateChange() {
|
|
if (this.peerConnection?.signalingState === 'stable') {
|
|
this._flushInboundCandidateQueue();
|
|
}
|
|
}
|
|
|
|
set peerConnection(pc) {
|
|
this._pc = pc;
|
|
}
|
|
|
|
get peerConnection() {
|
|
return this._pc;
|
|
}
|
|
|
|
get logger() {
|
|
if (this.trace) return console;
|
|
return silentConsole;
|
|
}
|
|
|
|
getLocalSessionDescriptor() {
|
|
return this.peerConnection?.localDescription;
|
|
}
|
|
|
|
getRemoteSessionDescriptor() {
|
|
return this.peerConnection?.remoteDescription;
|
|
}
|
|
|
|
getLocalStream() {
|
|
if (this.localStream) {
|
|
return this.localStream;
|
|
}
|
|
|
|
if (this.peerConnection) {
|
|
this.localStream = new MediaStream();
|
|
const senders = this.peerConnection.getSenders();
|
|
senders.forEach(({ track }) => {
|
|
if (track) {
|
|
this.localStream.addTrack(track);
|
|
}
|
|
});
|
|
|
|
return this.localStream;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getRemoteStream() {
|
|
if (this.remoteStream) {
|
|
return this.remoteStream;
|
|
}
|
|
|
|
if (this.peerConnection) {
|
|
this.remoteStream = new MediaStream();
|
|
this.peerConnection.getReceivers().forEach(({ track }) => {
|
|
if (track) {
|
|
this.remoteStream.addTrack(track);
|
|
}
|
|
});
|
|
return this.remoteStream;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
isPeerConnectionClosed() {
|
|
return !this.peerConnection || this.peerConnection.signalingState === 'closed';
|
|
}
|
|
|
|
start() {
|
|
// Init PeerConnection
|
|
if (!this.peerConnection) {
|
|
this.peerConnection = new RTCPeerConnection(this.configuration);
|
|
}
|
|
|
|
if (this.isPeerConnectionClosed()) {
|
|
this.logger.trace('BBB::WebRtcPeer::start - peer connection closed');
|
|
throw new Error('Invalid peer state: closed');
|
|
}
|
|
|
|
this.peerConnection.addEventListener('icecandidate', this._handleIceCandidate);
|
|
this._trackQueueFlushEvents();
|
|
|
|
if (this.videoStream) {
|
|
this.videoStream.getTracks().forEach((track) => {
|
|
this.peerConnection.addTrack(track, this.videoStream);
|
|
});
|
|
}
|
|
|
|
if (this.audioStream) {
|
|
this.audioStream.getTracks().forEach((track) => {
|
|
this.peerConnection.addTrack(track, this.audioStream);
|
|
});
|
|
}
|
|
}
|
|
|
|
addIceCandidate(iceCandidate) {
|
|
const candidate = new RTCIceCandidate(iceCandidate);
|
|
|
|
switch (this.peerConnection?.signalingState) {
|
|
case 'closed':
|
|
this.logger.trace('BBB::WebRtcPeer::addIceCandidate - peer connection closed');
|
|
throw new Error('PeerConnection object is closed');
|
|
case 'stable': {
|
|
if (this.peerConnection.remoteDescription) {
|
|
this.logger.debug('BBB::WebRtcPeer::addIceCandidate - adding candidate', candidate);
|
|
return this.peerConnection.addIceCandidate(candidate);
|
|
}
|
|
}
|
|
// eslint-ignore-next-line no-fallthrough
|
|
default: {
|
|
this.logger.debug('BBB::WebRtcPeer::addIceCandidate - buffering inbound candidate', candidate);
|
|
const promise = new Promise();
|
|
this._inboundCandidateQueue.push({
|
|
candidate,
|
|
promise,
|
|
});
|
|
return promise;
|
|
}
|
|
}
|
|
}
|
|
|
|
generateOffer() {
|
|
switch (this.mode) {
|
|
case 'recvonly': {
|
|
const useAudio = this.mediaConstraints
|
|
&& ((typeof this.mediaConstraints.audio === 'boolean')
|
|
|| (typeof this.mediaConstraints.audio === 'object'));
|
|
const useVideo = this.mediaConstraints
|
|
&& ((typeof this.mediaConstraints.video === 'boolean')
|
|
|| (typeof this.mediaConstraints.video === 'object'));
|
|
|
|
if (useAudio) {
|
|
this.peerConnection.addTransceiver('audio', {
|
|
direction: 'recvonly',
|
|
});
|
|
}
|
|
|
|
if (useVideo) {
|
|
this.peerConnection.addTransceiver('video', {
|
|
direction: 'recvonly',
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'sendonly': {
|
|
this.peerConnection.getTransceivers().forEach((transceiver) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
transceiver.direction = 'sendonly';
|
|
});
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return this.peerConnection.createOffer()
|
|
.then((offer) => {
|
|
this.logger.debug('BBB::WebRtcPeer::generateOffer - created offer', offer);
|
|
return this.peerConnection.setLocalDescription(offer);
|
|
})
|
|
.then(() => {
|
|
const localDescription = this.getLocalSessionDescriptor();
|
|
this.logger.debug('BBB::WebRtcPeer::generateOffer - local description set', localDescription);
|
|
return localDescription.sdp;
|
|
});
|
|
}
|
|
|
|
processAnswer(sdp) {
|
|
const answer = new RTCSessionDescription({
|
|
type: 'answer',
|
|
sdp,
|
|
});
|
|
|
|
if (this.isPeerConnectionClosed()) {
|
|
this.logger.error('BBB::WebRtcPeer::processAnswer - peer connection closed');
|
|
throw new Error('Peer connection is closed');
|
|
}
|
|
|
|
this.logger.debug('BBB::WebRtcPeer::processAnswer - setting remote description');
|
|
|
|
return this.peerConnection.setRemoteDescription(answer);
|
|
}
|
|
|
|
processOffer(sdp) {
|
|
const offer = new RTCSessionDescription({
|
|
type: 'offer',
|
|
sdp,
|
|
});
|
|
|
|
if (this.isPeerConnectionClosed()) {
|
|
this.logger.error('BBB::WebRtcPeer::processOffer - peer connection closed');
|
|
throw new Error('Peer connection is closed');
|
|
}
|
|
|
|
this.logger.debug('BBB::WebRtcPeer::processOffer - setting remote description', offer);
|
|
|
|
return this.peerConnection.setRemoteDescription(offer)
|
|
.then(() => this.peerConnection.createAnswer())
|
|
.then((answer) => {
|
|
this.logger.debug('BBB::WebRtcPeer::processOffer - created answer', answer);
|
|
return this.peerConnection.setLocalDescription(answer);
|
|
})
|
|
.then(() => {
|
|
const localDescription = this.getLocalSessionDescriptor();
|
|
this.logger.debug('BBB::WebRtcPeer::processOffer - local description set', localDescription.sdp);
|
|
return localDescription.sdp;
|
|
});
|
|
}
|
|
|
|
dispose() {
|
|
this.logger.debug('BBB::WebRtcPeer::dispose');
|
|
|
|
try {
|
|
if (this.peerConnection) {
|
|
this.peerConnection.getSenders().forEach(({ track }) => stopTrack(track));
|
|
if (!this.isPeerConnectionClosed()) this.peerConnection.close();
|
|
this.peerConnection = null;
|
|
}
|
|
|
|
if (this.localStream) {
|
|
stopStream(this.localStream);
|
|
this.localStream = null;
|
|
}
|
|
|
|
if (this.remoteStream) {
|
|
stopStream(this.remoteStream);
|
|
this.remoteStream = null;
|
|
}
|
|
|
|
this._outboundCandidateQueue = [];
|
|
this.candidateGatheringDone = false;
|
|
} catch (error) {
|
|
this.logger.trace('BBB::WebRtcPeer::dispose - failed', error);
|
|
}
|
|
|
|
this.removeAllListeners();
|
|
}
|
|
}
|