UNPKG

ortc-adapter

Version:
914 lines (837 loc) 27.3 kB
'use strict'; var MediaSection = require('./mediasection'); var MediaStreamEvent = require('./mediastreamevent'); var RTCIceCandidate = require('./rtcicecandidate'); var RTCPeerConnectionIceEvent = require('./rtcpeerconnectioniceevent'); var RTCSessionDescription = require('./rtcsessiondescription'); var sdpTransform = require('sdp-transform'); var sdpUtils = require('./sdp-utils'); /** * Construct an {@link RTCPeerConnection}. * @class * @classdesc This {@link RTCPeerConnection} is implemented in terms of ORTC APIs. * @param {RTCConfiguration} configuration * @property {string} iceConnectionState * @property {string} iceGatheringState * @property {?RTCSessionDescription} localDescription * @property {?function} onaddstream * @property {?function} onicecandidate * @property {?function} oniceconnectionstatechange * @property {?function} onsignalingstatechange * @property {?RTCSessionDescription} remoteDescription * @property {string} signalingState */ function RTCPeerConnection(configuration) { if (!(this instanceof RTCPeerConnection)) { return new RTCPeerConnection(configuration); } // ICE Gatherer var gatherOptions = makeGatherOptions(configuration); /* global RTCIceGatherer:true */ var iceGatherer = new RTCIceGatherer(gatherOptions); var iceGatheringCompleted = false; iceGatherer.onlocalcandidate = this._onlocalcandidate.bind(this); var onicecandidate = null; var onicecandidateWasSet = false; var iceCandidatesAdded = 0; // ICE Transport /* global RTCIceTransport:true */ var iceTransport = new RTCIceTransport(); var oniceconnectionstatechange = null; iceTransport.onicestatechange = this._onicestatechange.bind(this); // DTLS Transport /* global RTCDtlsTransport:true */ var dtlsTransport = new RTCDtlsTransport(iceTransport); dtlsTransport.ondtlsstatechange = this._ondtlsstatechange.bind(this); // Descriptions var signalingState = 'stable'; var onsignalingstatechange = null; var localDescription = null; var remoteDescription = null; // Streams var onaddstream = null; Object.defineProperties(this, { _dtlsTransport: { value: dtlsTransport }, _dtmfSenders: { value: new Map() }, _gatherOptions: { value: gatherOptions }, _iceCandidatesAdded: { get: function() { return iceCandidatesAdded; }, set: function(_iceCandidatesAdded) { iceCandidatesAdded = _iceCandidatesAdded; } }, _iceGatherer: { value: iceGatherer }, _iceGatheringCompleted: { get: function() { return iceGatheringCompleted; }, set: function(_iceGatheringCompleted) { iceGatheringCompleted = _iceGatheringCompleted; } }, _iceTransport: { value: iceTransport }, _localCandidates: { value: new Set() }, _localDescription: { get: function() { return localDescription; }, set: function(_localDescription) { localDescription = _localDescription; } }, _localStreams: { value: [] }, _midCounters: { value: { audio: 0, video: 0 } }, _remoteCandidates: { value: new Set() }, _remoteDescription: { get: function() { return remoteDescription; }, set: function(_remoteDescription) { remoteDescription = _remoteDescription; } }, _remoteStreams: { value: [] }, _rtpReceivers: { value: new Map() }, _rtpSenders: { value: new Map() }, _signalingState: { get: function() { return signalingState; }, set: function(_signalingState) { signalingState = _signalingState; if (this.onsignalingstatechange) { this.onsignalingstatechange(); } } }, _streamIds: { value: new Map() }, iceConnectionState: { enumerable: true, get: function() { return iceTransport.state; } }, iceGatheringState: { enumerable: true, get: function() { return iceGatheringCompleted ? 'gathering' : 'complete'; } }, localDescription: { enumerable: true, get: function() { return localDescription; } }, onaddstream: { enumerable: true, get: function() { return onaddstream; }, set: function(_onaddstream) { onaddstream = _onaddstream; } }, onicecandidate: { enumerable: true, get: function() { return onicecandidate; }, set: function(_onicecandidate) { onicecandidate = _onicecandidate; if (!onicecandidateWasSet) { try { iceGatherer.getLocalCandidates() .forEach(iceGatherer.onlocalcandidate); } catch (error) { // Do nothing. } } onicecandidateWasSet = true; } }, oniceconnectionstatechange: { enumerable: true, get: function() { return oniceconnectionstatechange; }, set: function(_oniceconnectionstatechange) { oniceconnectionstatechange = _oniceconnectionstatechange; } }, onsignalingstatechange: { enumerable: true, get: function() { return onsignalingstatechange; }, set: function(_onsignalingstatechange) { onsignalingstatechange = _onsignalingstatechange; } }, remoteDescription: { enumerable: true, get: function() { return remoteDescription; } }, signalingState: { enumerable: true, get: function() { return signalingState; } } }); } RTCPeerConnection.prototype._makeMid = function _makeMid(kind) { return kind + ++this._midCounters[kind]; }; /** * This method is assigned to the {@link RTCDtlsTransport}'s "ondtlsstatechange" event handler. * @access private * @param {object} event */ RTCPeerConnection.prototype._ondtlsstatechange = function _ondtlsstatechange(event) { void event; }; /** * This method is assigned to the {@link RTCIceTransport}'s "onicestatechange" event handler. * @access private * @param {object} event */ RTCPeerConnection.prototype._onicestatechange = function _onicestatechange(event) { if (this.oniceconnectionstatechange) { this.oniceconnectionstatechange(event); } }; /** * This method is assigned to the {@link RTCIceGatherer}'s "onlocalcandidate" event handler. * @access private * @param {object} event */ RTCPeerConnection.prototype._onlocalcandidate = function _onlocalcandidate(event) { if (isEmptyObject(event.candidate)) { this._iceGatheringCompleted = true; } this._localCandidates.add(event.candidate); if (this.onicecandidate) { var webrtcCandidate = makeWebRTCCandidate(event.candidate); this.onicecandidate(makeOnIceCandidateEvent(webrtcCandidate)); } }; /** * Start sending RTP. * @access private * @param {MediaSection} mediaSection * @returns {this} */ RTCPeerConnection.prototype._sendRtp = function _sendRtp(mediaSection) { var kind = mediaSection.kind; // FIXME(mroberts): This is not right. this._rtpSenders.forEach(function(rtpSender) { if (rtpSender.track.kind !== kind) { return; } rtpSender.send(mediaSection.capabilities); }, this); return this; }; /** * Start sending and receiving RTP for the given {@link MediaSection}s. * @access private * @param {Array<MediaSection>} mediaSections * @returns {this} */ RTCPeerConnection.prototype._sendAndReceiveRtp = function _sendAndReceiveRtp(mediaSections) { mediaSections.forEach(function(mediaSection) { if (mediaSection.direction === 'sendrecv' || mediaSection.direction === 'sendonly') { this._sendRtp(mediaSection); } if (mediaSection.direction === 'sendrecv' || mediaSection.direction === 'recvonly') { this._receiveRtp(mediaSection); } }, this); return this; }; /** * Start receiving RTP. * @access private * @param {MediaSection} mediaSection * @returns {this} */ RTCPeerConnection.prototype._receiveRtp = function _receiveRtp(mediaSection) { var kind = mediaSection.capabilities.type; /* global RTCRtpReceiver:true */ var rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); rtpReceiver.receive(mediaSection.capabilities); var track = rtpReceiver.track; this._rtpReceivers.set(track, rtpReceiver); // NOTE(mroberts): Without any source-level msid attribute, we are just // going to assume a one-to-one mapping between MediaStreams and // MediaStreamTracks. /* global MediaStream:true */ var mediaStream = new MediaStream(); mediaStream.addTrack(track); this._remoteStreams.push(mediaStream); if (this.onaddstream) { this.onaddstream(makeOnAddStreamEvent(mediaStream)); } return this; }; /** * Start the {@link RTCDtlsTransport}. * @access private * @param {RTCDtlsParameters} dtlsParameters - the remote DTLS parameters * @returns {this} */ RTCPeerConnection.prototype._startDtlsTransport = function _startDtlsTransport(dtlsParameters) { this._dtlsTransport.start(dtlsParameters); return this; }; /** * Start the {@link RTCIceTransport}. * @access private * @param {RTCIceParameters} iceParameters - the remote ICE parameters * @returns {this} */ RTCPeerConnection.prototype._startIceTransport = function _startIceTransport(iceParameters) { var role = this.signalingState === 'have-local-offer' ? 'controlling' : 'controlled'; this._iceTransport.start(this._iceGatherer, iceParameters, role); return this; }; /** * Add an {@link RTCIceCandidate} to the {@link RTCPeerConnection}. * @param {RTCIceCandidate} candidate - the remote ICE candidate * @param {function} onSuccess * @param {function} onFailure *//** * Add an {@link RTCIceCandidate} to the {@link RTCPeerConnection}. * @param {RTCIceCandidate} candidate -the remote ICE candidate * @returns {Promise} */ RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate(candidate, onSuccess, onFailure) { if (!onSuccess) { return new Promise(this.addIceCandidate.bind(this, candidate)); } // NOTE(mroberts): I'm not sure there is a scenario where we'd ever call // onFailure. void onFailure; this._iceCandidatesAdded++; var ortcCandidate = makeORTCCandidate(candidate); // A candidate is identified by a triple of IP address, port, and protocol. // ORTC ICE candidates have no component ID, and so we need to deduplicate // the RTP and RTCP candidates when we're muxing. var triple = [ortcCandidate.ip, ortcCandidate.port, ortcCandidate.transport].join(' '); if (!this._remoteCandidates.has(triple)) { this._remoteCandidates.add(triple); this._iceTransport.addRemoteCandidate(ortcCandidate); } if (onSuccess) { onSuccess(); } }; /** * Add a {@link MediaStream} to the {@link RTCPeerConnection}. * @param {MediaStream} stream */ RTCPeerConnection.prototype.addStream = function addStream(mediaStream) { this._localStreams.push(mediaStream); mediaStream.getTracks().forEach(function(track) { /* eslint no-invalid-this:0 */ /* global RTCRtpSender:true */ var rtpSender = new RTCRtpSender(track, this._dtlsTransport); this._rtpSenders.set(track, rtpSender); this._streamIds.set(track, mediaStream.id); }, this); }; /** * Close the {@link RTCPeerConnection}. */ RTCPeerConnection.prototype.close = function close() { this._signalingState = 'closed'; this._rtpReceivers.forEach(function(rtpReceiver) { rtpReceiver.stop(); }); this._dtlsTransport.stop(); this._iceTransport.stop(); }; /** * Construct an {@link RTCSessionDescription} containing an SDP offer. * @param {RTCSessionDescriptionCallback} onSuccess * @param {function} onFailure *//** * Construct an {@link RTCSessionDescription} containing an SDP offer. * @returns {Promise<RTCSessionDescription>} */ RTCPeerConnection.prototype.createAnswer = function createAnswer(onSuccess, onFailure) { if (typeof onSuccess !== 'function') { return new Promise(this.createAnswer.bind(this)); } if (this.signalingState !== 'have-remote-offer') { return void onFailure(invalidSignalingState(this.signalingState)); } // draft-ietf-rtcweb-jsep-11, Section 5.3.1: // // The next step is to go through each offered m= section. If there is a // local MediaStreamTrack of the same type which has been added to the // PeerConnection via addStream and not yet associated with a m= section, // and the specific m= section is either sendrecv or recvonly, the // MediaStreamTrack will be associated with the m= section at this time. // MediaStreamTracks are assigned using the canonical order described in // Section 5.2.1. // var remote = sdpUtils.parseDescription(this.remoteDescription); // sdpTransform.parse(this.remoteDescription.sdp); var streams = this.getLocalStreams(); var tracks = { audio: [], video: [] }; streams.forEach(function(stream) { tracks.audio = tracks.audio.concat(stream.getAudioTracks()); tracks.video = tracks.video.concat(stream.getVideoTracks()); }); var mediaSections = remote.mediaSections.map(function(remoteMediaSection) { var kind = remoteMediaSection.kind; var remoteDirection = remoteMediaSection.direction; var remoteCapabilities = remoteMediaSection.capabilities; var localCapabilities = RTCRtpSender.getCapabilities(kind); var sharedCodecs = intersectCodecs(remoteCapabilities.codecs, localCapabilities.codecs); var sharedCapabilities = { codecs: sharedCodecs }; var capabilities = sharedCapabilities; var direction; var track; // RFC 3264, Section 6.1: // // If the answerer has no media formats in common for a particular // offered stream, the answerer MUST reject that media stream by // setting the port to zero. // if (!sharedCodecs.length) { return remoteMediaSection.copyAndReject(); } // RFC 3264, Section 6.1: // // For streams marked as inactive in the answer, the list of media // formats is constructed based on the offer. If the offer was // sendonly, the list is constructed as if the answer were recvonly. // Similarly, if the offer was recvonly, the list is constructed as if // the answer were sendonly, and if the offer was sendrecv, the list is // constructed as if the answer were sendrecv. If the offer was // inactive, the list is constructed as if the offer were actually // sendrecv and the answer were sendrecv. // if (remoteDirection === 'inactive' || remoteDirection === 'recvonly' && !tracks[kind].length) { direction = 'inactive'; } else if (remoteDirection === 'recvonly') { track = tracks[kind].shift(); direction = 'sendonly'; } else if (remoteDirection === 'sendrecv') { track = tracks[kind].shift(); direction = track ? 'sendrecv' : 'recvonly'; } else { // sendonly direction = 'recvonly'; } var streamId = this._streamIds.get(track); var mediaSection = remoteMediaSection.copy(null, null, capabilities, direction, null, streamId, track); return mediaSection; }, this); // FIXME(mroberts): We should probably provision an ICE transport for each // MediaSection in the event BUNDLE is not supported. mediaSections.forEach(function(mediaSection) { this._localCandidates.forEach(mediaSection.addCandidate, mediaSection); }, this); var sdp = sdpUtils.makeInitialSDPBlob(); sdpUtils.addMediaSectionsToSDPBlob(sdp, mediaSections); sdpUtils.addIceParametersToSDPBlob(sdp, this._iceGatherer.getLocalParameters()); sdpUtils.addDtlsParametersToSDPBlob(sdp, this._dtlsTransport.getLocalParameters()); var description = new RTCSessionDescription({ sdp: sdpTransform.write(sdp), type: 'answer' }); onSuccess(description); }; RTCPeerConnection.prototype.createDTMFSender = function createDTMFSender(track) { if (!this._dtmfSenders.has(track)) { var rtpSender = this._rtpSenders.get(track); /* global RTCDtmfSender:true */ var dtmfSender = new RTCDtmfSender(rtpSender); this._dtmfSenders.set(track, dtmfSender); } return this._dtmfSenders.get(track); }; /** * Construct an {@link RTCSessionDescription} containing an SDP offer. * @param {RTCSessionDescriptionCallback} onSuccess * @param {function} onFailure * @param {?RTCOfferOptions} [options] *//** * Construct an {@link RTCSessionDescription} containing an SDP offer. * @param {?RTCOfferOptions} [options] * @returns {Promise<RTCSessionDescription>} */ RTCPeerConnection.prototype.createOffer = function createOffer(onSuccess, onFailure, options) { if (typeof onSuccess !== 'function') { return new Promise(function(resolve, reject) { this.createOffer(resolve, reject, onSuccess); }.bind(this)); } // draft-ieft-rtcweb-jsep-11, Section 5.2.3: // // If the 'OfferToReceiveAudio' option is specified, with an integer value // of N, and M audio MediaStreamTracks have been added to the // PeerConnection, the offer MUST include M non-rejected m= sections with // media type 'audio', even if N is greater than M. ... the directional // attribute on the N-M audio m= sections without associated // MediaStreamTracks MUST be set to recvonly. // // ... // // For backwards compatibility with pre-standards versions of this // specification, a value of 'true' is interpreted as equivalent to N=1, // and 'false' as N=0. // var N = { audio: null, video: null }; var M = { audio: 0, video: 0 }; options = options || {}; ['optional', 'mandatory'].forEach(function(optionType) { if (!(optionType in options)) { return; } if ('OfferToReceiveAudio' in options[optionType]) { N.audio = Number(options[optionType].OfferToReceiveAudio); } if ('OfferToReceiveVideo' in options[optionType]) { N.video = Number(options[optionType].OfferToReceiveVideo); } }); var mediaSections = []; // draft-ietf-rtcweb-jsep-11, Section 5.2.1: // // m=sections MUST be sorted first by the order in which the MediaStreams // were added to the PeerConnection, and then by the alphabetical // ordering of the media type for the MediaStreamTrack. // var _N = { audio: N.audio, video: N.video }; var streams = this.getLocalStreams(); streams.forEach(function(stream) { var audioTracks = stream.getAudioTracks(); var videoTracks = stream.getVideoTracks(); M.audio += audioTracks.length; M.video += videoTracks.length; var tracks = audioTracks.concat(videoTracks); tracks.forEach(function(track) { var kind = track.kind; var capabilities = RTCRtpSender.getCapabilities(kind); var direction; var mid = this._makeMid(kind); if (_N.audio === null) { direction = 'sendrecv'; } else if (!_N[kind]) { direction = 'sendonly'; } else { _N[kind]--; direction = 'sendrecv'; } var mediaSection = new MediaSection(null, null, capabilities, direction, kind, mid, null, null, stream.id, track); mediaSections.push(mediaSection); }, this); }, this); // Add the N-M recvonly m=sections. ['audio', 'video'].forEach(function(kind) { var k = Math.max(N[kind] - M[kind], 0); if (!k) { return; } var capabilities = RTCRtpSender.getCapabilities(kind); var direction = 'recvonly'; var mid; var mediaSection; while (k--) { mid = this._makeMid(kind); mediaSection = new MediaSection(null, null, capabilities, direction, kind, mid); mediaSections.push(mediaSection); } }, this); // FIXME(mroberts): We should probably provision an ICE transport for each // MediaSection in the event BUNDLE is not supported. mediaSections.forEach(function(mediaSection) { this._localCandidates.forEach(mediaSection.addCandidate, mediaSection); }, this); var sdp = sdpUtils.makeInitialSDPBlob(); sdpUtils.addMediaSectionsToSDPBlob(sdp, mediaSections); sdpUtils.addIceParametersToSDPBlob(sdp, this._iceGatherer.getLocalParameters()); sdpUtils.addDtlsParametersToSDPBlob(sdp, this._dtlsTransport.getLocalParameters()); var description = new RTCSessionDescription({ sdp: sdpTransform.write(sdp), type: 'offer' }); onSuccess(description); }; /** * Get the {@link MediaStream}s that are currently or will be sent with this * {@link RTCPeerConnection}. * @returns {Array<MediaStream>} */ RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { return this._localStreams.slice(); }; /** * Get the {@link MediaStreams} that are currently received by this * {@link RTCPeerConnection}. * @returns {Array<MediaStream>} */ RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() { return this._remoteStreams.slice(); }; /** * Apply the supplied {@link RTCSessionDescription} as the local description. * @param {RTCSessionDescription} * @param {function} onSuccess * @param {function} onFailure *//** * Apply the supplied {@link RTCSessionDescription} as the local description. * @param {RTCSessionDescription} * @returns {Promise} */ RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription(description, onSuccess, onFailure) { if (!onSuccess) { return new Promise(this.setLocalDescription.bind(this, description)); } var nextSignalingState; switch (this.signalingState) { case 'stable': nextSignalingState = 'have-local-offer'; break; case 'have-remote-offer': nextSignalingState = 'stable'; break; default: return void onFailure(invalidSignalingState(this.signalingState)); } var parsed = sdpUtils.parseDescription(description); if (this.signalingState === 'have-remote-offer') { this._sendAndReceiveRtp(parsed.mediaSections); } this._localDescription = description; this._signalingState = nextSignalingState; onSuccess(); }; /** * Apply the supplied {@link RTCSessionDescription} as the remote offer or answer. * @param {RTCSessionDescription} * @param {function} onSuccess * @param {function} onFailure *//** * Apply the supplied {@link RTCSessionDescription} as the remote offer or answer. * @param {RTCSessionDescription} * @returns {Promise} */ RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(description, onSuccess, onFailure) { if (!onSuccess) { return new Promise(this.setRemoteDescription.bind(this, description)); } var nextSignalingState; switch (this.signalingState) { case 'stable': nextSignalingState = 'have-remote-offer'; break; case 'have-local-offer': nextSignalingState = 'stable'; break; default: return void onFailure(invalidSignalingState(this.signalingState)); } var parsed = sdpUtils.parseDescription(description); parsed.mediaSections.forEach(function(mediaSection) { mediaSection.candidates.forEach(this._iceTransport.addRemoteCandidate, this._iceTransport); }, this); this._startIceTransport(parsed.iceParameters[0]); this._startDtlsTransport(parsed.dtlsParameters[0]); if (this.signalingState === 'have-local-offer') { this._sendAndReceiveRtp(parsed.mediaSections); } this._remoteDescription = description; this._signalingState = nextSignalingState; onSuccess(); }; /** * Construct an "invalid signaling state" {@link Error}. * @access private * @param {string} singalingState * @returns {Error} */ function invalidSignalingState(signalingState) { return new Error('Invalid signaling state: ' + signalingState); } /** * Check if an object is empty (i.e. the object contains no keys). * @access private * @param {object} object * @returns {boolean} */ function isEmptyObject(object) { return !Object.keys(object).length; } /** * Construct {@link RTCIceGatherOptions} from an {@link RTCConfiguration}. * @access private * @param {RTCConfiguration} configuration * @returns {RTCIceGatherOptions} */ function makeGatherOptions(configuration) { // Filter STUN servers, since these appear to be broken. var iceServers = (configuration.iceServers || []) .filter(function(iceServer) { return !iceServer.urls.match(/^stun:/); }); return { gatherPolicy: configuration.gatherPolicy || 'all', iceServers: iceServers }; } /** * Construct an "addstream" {@link MediaStreamEvent}. * @access private * @param {MediaStream} stream * @returns {MediaStreamEvent} */ function makeOnAddStreamEvent(stream) { return new MediaStreamEvent('addstream', { stream: stream }); } /** * Construct an "icecandidate" {@link RTCPeerConnectionIceEvent}. * @access private * @param {RTCIceCandidate} candidate * @returns {RTCPeerConnectionIceEvent} */ function makeOnIceCandidateEvent(candidate) { return new RTCPeerConnectionIceEvent('icecandidate', { candidate: candidate }); } /** * Construct an ORTC ICE candidate from a WebRTC ICE candidate. * @access private * @param {RTCIceCandidate} candidate - an WebRTC ICE candidate * @returns {RTCIceCanddidate} */ function makeORTCCandidate(candidate) { if (!candidate) { return {}; } var start = candidate.candidate.indexOf('candidate:'); var line = candidate.candidate .slice(start + 10) .replace(/ +/g, ' ') .split(' '); var ortcIceCandidate = { foundation: line[0], protocol: line[2], priority: parseInt(line[3]), ip: line[4], port: parseInt(line[5]), type: line[7], relatedAddress: null, relatedPort: 0, tcpType: 'active' }; if (ortcIceCandidate.type !== 'host') { ortcIceCandidate.relatedAddress = line[9]; ortcIceCandidate.relatedPort = parseInt(line[11]); } return ortcIceCandidate; } /** * Construct a WebRTC ICE candidate from an ORTC ICE candidate. * @access private * @param {RTCIceCandidate} candidate - an ORTC ICE candidate * @returns {RTCIceCandidate} */ function makeWebRTCCandidate(candidate) { if (isEmptyObject(candidate)) { return null; } var line = [ 'a=candidate', candidate.foundation, 1, candidate.protocol, candidate.priority, candidate.ip, candidate.port, candidate.type ]; if (candidate.relatedAddress) { line = line.concat([ 'raddr', candidate.relatedAddress, 'rport', candidate.relatedPort ]); } line.push('generation 0'); return new RTCIceCandidate({ candidate: line.join(' '), sdpMLineIndex: 0 }); } /** * Intersect codecs. * @param {Array<object>} localCodecs * @param {Array<object>} remoteCodecs * @returns {Array<object>} */ function intersectCodecs(localCodecs, remoteCodecs) { var sharedCodecs = []; localCodecs.forEach(function(localCodec) { remoteCodecs.forEach(function(remoteCodec) { if (localCodec.name === remoteCodec.name && localCodec.clockRate === remoteCodec.clockRate && localCodec.numChannels === remoteCodec.numChannels) { sharedCodecs.push(remoteCodec); } }); }); return sharedCodecs; } module.exports = RTCPeerConnection;