UNPKG

ortc-adapter

Version:
1,716 lines (1,578 loc) 70.7 kB
/*! ortc-adapter.js 0.1.7 The following license applies to all parts of this software except as documented below. Copyright (c) 2015, Twilio, inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Twilio nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software includes sdp-transform under the following license: (The MIT License) Copyright (c) 2013 Eirik Albrigtsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* eslint strict:0 */ (function(root, selectWebRTC) { var webrtc = selectWebRTC(); /* global define:true */ if (typeof define === 'function' && define.amd) { define([], webrtc); /* global module:true */ } else if (typeof module === 'object' && module.exports) { module.exports = webrtc; } else { for (var key in webrtc) { root[key] = webrtc[key]; } } }(this, function() { /* eslint max-len:0 */ var ortcAdapterBundle = (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 'use strict'; module.exports.RTCIceCandidate = require('./rtcicecandidate'); module.exports.RTCPeerConnection = require('./rtcpeerconnection'); module.exports.RTCSessionDescription = require('./rtcsessiondescription'); },{"./rtcicecandidate":4,"./rtcpeerconnection":5,"./rtcsessiondescription":7}],2:[function(require,module,exports){ 'use strict'; /** * Construct a {@link MediaSection}. * @class * @classdesc * @param {?string} [address="0.0.0.0"] * @param {?Array<RTCIceCandidate>} [candidates=[]] * @param {object} capabilities * @param {string} direction - one of "sendrecv", "sendonly", "recvonly", or "inactive" * @param {string} kind - one of "audio" or "video" * @param {string} mid * @param {?number} [port=9] * @param {?boolean} [rtcpMux=true] * @param {?string} streamId * @param {?MediaStreamTrack} track * @property {Array<RTCIceCandidate>} candidates * @property {object} capabilities * @property {?RTCIceCandidate} defaultCandidate * @property {string} direction - one of "sendrecv", "sendonly", "recvonly", or "inactive" * @property {string} kind - one of "audio" or "video" * @property {string} mid * @property {number} port * @property {boolean} rtcpMux * @property {?string} streamId * @property {?MediaStreamTrack} track */ function MediaSection(address, _candidates, capabilities, direction, kind, mid, port, rtcpMux, streamId, track) { if (!(this instanceof MediaSection)) { return new MediaSection(address, _candidates, capabilities, direction, kind, mid, port, rtcpMux, streamId, track); } var rejected = false; address = address || '0.0.0.0'; port = typeof port === 'number' ? port : 9; rtcpMux = typeof rtcpMux === 'boolean' ? rtcpMux : true; streamId = streamId || null; track = track || null; Object.defineProperties(this, { _address: { get: function() { return address; }, set: function(_address) { address = _address; } }, _candidates: { value: [] }, _port: { get: function() { return port; }, set: function(_port) { port = _port; } }, _rejected: { get: function() { return rejected; }, set: function(_rejected) { rejected = _rejected; } }, _streamId: { get: function() { return streamId; }, set: function(_streamId) { streamId = _streamId; } }, _track: { get: function() { return track; }, set: function(_track) { track = _track; } }, _triples: { value: new Set() }, candidates: { enumerable: true, get: function() { return this._candidates.slice(); } }, capabilities: { enumerable: true, value: capabilities }, defaultCandidate: { enumerable: true, get: function() { return this._candidates.length ? this._candidates[0] : null; } }, direction: { enumerable: true, value: direction }, kind: { enumerable: true, value: kind }, port: { enumerable: true, get: function() { return port; } }, rtcpMux: { enumerable: true, value: rtcpMux }, streamId: { enumerable: true, get: function() { return streamId; } }, track: { enumerable: true, get: function() { return track; } } }); if (_candidates) { _candidates.forEach(this.addCandidate, this); } } /** * Add an RTCIceCandidate to the {@link MediaSection}. * @param {RTCIceCandidate} candidate * @returns {boolean} */ MediaSection.prototype.addCandidate = function addCandidate(candidate) { var triple = [ candidate.ip, candidate.port, candidate.protocol ].join(' '); if (!this._triples.has(triple)) { this._triples.add(triple); this._candidates.push(candidate); return true; } return false; }; /** * Copy the {@link MediaSection}. * @param {?string} address - if unsupplied, use the {@link MediaSection} defaults * @param {?Array<RTCIceCandidates> candidates - if unsupplied, use the {@link MediaSection} defaults * @param {?string} capabilities - if unsupplied, copy the existing capabilities * @param {?string} direction - if unsupplied, copy the existing direction * @param {?number} port - if unsupplied, use the {@link MediaSection} defaults * @param {?string} streamId - if unsupplied, set to null * @param {?MediaStreamTrack} track - if unsupplied, set to null * @returns {MediaSection} */ MediaSection.prototype.copy = function copy(address, candidates, capabilities, direction, port, streamId, track) { return new MediaSection(this.address, candidates, capabilities || this.capabilities, direction || this.direction, this.kind, this.mid, port, this.rtcpMux, streamId, track); }; /** * Copy and reject the {@link MediaSection}. * @returns {MediaSection}. */ MediaSection.prototype.copyAndReject = function copyAndReject() { var mediaSection = new MediaSection(null, this.candidates, this.capabilities, this.direction, this.kind, this.mid, null, this.rtcpMux); return mediaSection.reject(); }; /** * Reject the {@link MediaSection}. * @returns {MediaSection} */ MediaSection.prototype.reject = function reject() { // RFC 3264, Section 6: // // To reject an offered stream, the port number in the corresponding // stream in the answer MUST be set to zero. Any media formats listed // are ignored. At least one MUST be present, as specified by SDP. // this.setPort(0); return this; }; /** * Set the {@link MediaSection}'s address. * @param {string} address * @returns {MediaSection} */ MediaSection.prototype.setAddress = function setAddress(address) { this._address = address; return this; }; /** * Set the {@link MediaSection}'s port. * @param {number} port * @returns {MediaSection} */ MediaSection.prototype.setPort = function setPort(port) { this._port = port; return this; }; /* MediaSection.prototype.setStreamId = function setStreamId(streamId) { this._streamId = streamId; return this; }; MediaSection.prototype.setTrack = function setTrack(track) { this._track = track; return this; }; */ module.exports = MediaSection; },{}],3:[function(require,module,exports){ 'use strict'; /** * Construct a {@link MediaStreamEvent}. * @class * @classdesc * @extends Event * @param {string} type - one of "addstream" or "removestream" * @param {object} init * @property {MediaStream} stream */ function MediaStreamEvent(type, init) { if (!(this instanceof MediaStreamEvent)) { return new MediaStreamEvent(type, init); } Event.call(this, type, init); Object.defineProperties(this, { stream: { enumerable: true, value: init.stream } }); } module.exports = MediaStreamEvent; },{}],4:[function(require,module,exports){ 'use strict'; /** * Construct an {@link RTCIceCandidate}. * @class * @classdesc * @param {object} candidate * @property {string} candidate * @property {number} sdpMLineIndex */ function RTCIceCandidate(candidate) { if (!(this instanceof RTCIceCandidate)) { return new RTCIceCandidate(candidate); } Object.defineProperties(this, { candidate: { enumerable: true, value: candidate.candidate }, sdpMLineIndex: { enumerable: true, value: candidate.sdpMLineIndex } }); } module.exports = RTCIceCandidate; },{}],5:[function(require,module,exports){ '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; },{"./mediasection":2,"./mediastreamevent":3,"./rtcicecandidate":4,"./rtcpeerconnectioniceevent":6,"./rtcsessiondescription":7,"./sdp-utils":8,"sdp-transform":10}],6:[function(require,module,exports){ 'use strict'; /** * Construct an {@link RTCPeerConnectionIceEvent}. * @class * @classdesc * @extends Event * @param {string} type - "icecandidate" * @param {object} init * @property {MediaStream} stream */ function RTCPeerConnectionIceEvent(type, init) { if (!(this instanceof RTCPeerConnectionIceEvent)) { return new RTCPeerConnectionIceEvent(type, init); } Event.call(this, type, init); Object.defineProperties(this, { candidate: { enumerable: true, value: init.candidate } }); } module.exports = RTCPeerConnectionIceEvent; },{}],7:[function(require,module,exports){ 'use strict'; /** * Construct an {@link RTCSessionDescription}. * @class * @classdesc * @param {object} description * @property {string} sdp * @property {string} type - one of "offer" or "answer" */ function RTCSessionDescription(description) { if (!(this instanceof RTCSessionDescription)) { return new RTCSessionDescription(description); } Object.defineProperties(this, { sdp: { enumerable: true, value: description.sdp }, type: { enumerable: true, value: description.type } }); } module.exports = RTCSessionDescription; },{}],8:[function(require,module,exports){ 'use strict'; var MediaSection = require('./mediasection'); var sdpTransform = require('sdp-transform'); /** * Add ICE candidates to an arbitrary level of an SDP blob. * @param {?object} [level={}] * @param {?Array<RTCIceCandidate>} [candidates] * @param {?number} [component] - if unspecified, add both RTP and RTCP candidates * @returns {object} */ function addCandidatesToLevel(level, candidates, component) { level = level || {}; level.candidates = level.candidates || []; if (!candidates) { return level; } candidates.forEach(function(candidate) { // TODO(mroberts): Empty dictionary check. if (!candidate.foundation) { level.endOfCandidates = 'end-of-candidates'; return; } var candidate1 = { foundation: candidate.foundation, transport: candidate.protocol, priority: candidate.priority, ip: candidate.ip, port: candidate.port, type: candidate.type, generation: 0 }; if (candidate.relatedAddress) { candidate1.raddr = candidate.relatedAddress; candidate1.rport = candidate.relatedPort; } if (typeof component === 'number') { candidate1.component = component; level.candidates.push(candidate1); return; } // RTP candidate candidate1.component = 1; level.candidates.push(candidate1); // RTCP candidate var candidate2 = {}; for (var key in candidate1) { candidate2[key] = candidate1[key]; } candidate2.component = 2; level.candidates.push(candidate2); }); return level; } /** * Add ICE candidates to the media-levels of an SDP blob. Since this adds to * the media-levels, you should call this after you have added all your media. * @param {?object} [sdp={}] * @param {?Array<RTCIceCandidate>} [candidates] * @param {?number} [component] - if unspecified, add both RTP and RTCP candidates * @returns {object} */ function addCandidatesToMediaLevels(sdp, candidates, component) { sdp = sdp || {}; if (!sdp.media) { return sdp; } sdp.media.forEach(function(media) { addCandidatesToLevel(media, candidates, component); }); return sdp; } /** * Add ICE candidates to the media-levels of an SDP blob. Since * this adds to the media-levels, you should call this after you have added * all your media. * @param {?object} [sdp={}] * @param {?Array<RTCIceCandidate>} [candidates] * @param {?number} [component] - if unspecified, add both RTP and RTCP candidates * @returns {object} */ function addCandidatesToSDPBlob(sdp, candidates, component) { sdp = sdp || {}; // addCandidatesToSessionLevel(sdp, candidates, component); addCandidatesToMediaLevels(sdp, candidates, component); return sdp; } /** * Add the DTLS fingerprint to the media-levels of an SDP blob. * Since this adds to media-levels, you should call this after you have added * all your media. * @param {?object} [sdp={}] * @param {RTCDtlsParameters} dtlsParameters * @returns {object} */ function addDtlsParametersToSDPBlob(sdp, dtlsParameters) { sdp = sdp || {}; // addDtlsParametersToSessionLevel(sdp, dtlsParameters); addDtlsParametersToMediaLevels(sdp, dtlsParameters); return sdp; } /** * Add the DTLS fingerprint to an arbitrary level of an SDP blob. * @param {?object} [sdp={}] * @param {RTCDtlsParameters} dtlsParameters * @returns {object} */ function addDtlsParametersToLevel(level, dtlsParameters) { level = level || {}; var fingerprints = dtlsParameters.fingerprints; if (fingerprints.length) { level.fingerprint = { type: fingerprints[0].algorithm, hash: fingerprints[0].value }; } return level; } /** * Add the DTLS fingerprint to the media-levels of an SDP blob. Since this adds * to the media-levels, you should call this after you have added all of your * media. * @param {?object} [sdp={}] * @param {RTCDtlsParameters} dtlsParameters * @returns {object} */ function addDtlsParametersToMediaLevels(sdp, dtlsParameters) { sdp = sdp || {}; if (!sdp.media) { return sdp; } sdp.media.forEach(function(media) { addDtlsParametersToLevel(media, dtlsParameters); }); return sdp; } /** * Add the ICE username fragment and password to the media-levels * of an SDP blob. Since this adds to media-levels, you should call this after * you have added all your media. * @param {?object} [sdp={}] * @param {RTCIceParameters} parameters * @returns {object} */ function addIceParametersToSDPBlob(sdp, iceParameters) { sdp = sdp || {}; // addIceParametersToSessionLevel(sdp, iceParameters); addIceParametersToMediaLevels(sdp, iceParameters); return sdp; } /** * Add the ICE username fragment and password to the media-levels of an SDP * blob. Since this adds to media-levels, you should call this after you have * added all your media. * @param {?object} [sdp={}] * @param {RTCIceParameters} iceParameters * @returns {object} */ function addIceParametersToMediaLevels(sdp, iceParameters) { sdp = sdp || {}; if (!sdp.media) { return sdp; } sdp.media.forEach(function(media) { addIceParametersToLevel(media, iceParameters); }); return sdp; } /** * Add the ICE username fragment and password to an arbitrary level of an SDP * blob. * @param {?object} [level={}] * @param {RTCIceParameters} iceParameters * @returns {object} */ function addIceParametersToLevel(level, iceParameters) { level = level || {}; level.iceUfrag = iceParameters.usernameFragment; level.icePwd = iceParameters.password; return level; } /** * Add a {@link MediaSection} to an SDP blob. * @param {object} sdp * @param {MediaSection} mediaSection * @returns {object} */ function addMediaSectionToSDPBlob(sdp, mediaSection) { var streamId = mediaSection.streamId; if (streamId) { sdp.msidSemantic = sdp.msidSemantic || { semantic: 'WMS', token: [] }; sdp.msidSemantic.token.push(streamId); } var mid = mediaSection.mid; if (mid) { sdp.groups = sdp.groups || []; var foundBundle = false; sdp.groups.forEach(function(group) { if (group.type === 'BUNDLE') { group.mids.push(mid); foundBundle = true; } }); if (!foundBundle) { sdp.groups.push({ type: 'BUNDLE', mids: [mid] }); } } var payloads = []; var rtps = []; var fmtps = []; mediaSection.capabilities.codecs.forEach(function(codec) { var payload = codec.preferredPayloadType; payloads.push(payload); var rtp = { payload: payload, codec: codec.name, rate: codec.clockRate }; if (codec.numChannels > 1) { rtp.encoding = codec.numChannels; } rtps.push(rtp); switch (codec.name) { case 'telephone-event': if (codec.parameters && codec.parameters.events) { fmtps.push({ payload: payload, config: codec.parameters.events }); } break; } }); var ssrcs = []; if (streamId && mediaSection.track) { var ssrc = Math.floor(Math.random() * 4294967296); var cname = makeCname(); var trackId = mediaSection.track.id; ssrcs = ssrcs.concat([ { id: ssrc, attribute: 'cname', value: cname }, { id: ssrc, attribute: 'msid', value: mediaSection.streamId + ' ' + trackId }, { id: ssrc, attribute: 'mslabel', value: trackId }, { id: ssrc, attribute: 'label', value: trackId } ]); } // draft-ietf-rtcweb-jsep-11, Section 5.2.2: // // Each "m=" and c=" line MUST be filled in with the port, protocol, // and address of the default candidate for the m= section, as // described in [RFC5245], Section 4.3. Each "a=rtcp" attribute line // MUST also be filled in with the port and address of the // appropriate default candidate, either the default RTP or RTCP // candidate, depending on whether RTCP multiplexing is currently // active or not. // var defaultCandidate = mediaSection.defaultCandidate; var media = { rtp: rtps, fmtp: fmtps, type: mediaSection.kind, port: defaultCandidate ? defaultCandidate.port : 9, payloads: payloads.join(' '), protocol: 'RTP/SAVPF', direction: mediaSection.direction, connection: { version: 4, ip: defaultCandidate ? defaultCandidate.ip : '0.0.0.0' }, rtcp: { port: defaultCandidate ? defaultCandidate.port : 9, netType: 'IN', ipVer: 4, address: defaultCandidate ? defaultCandidate.ip : '0.0.0.0' }, ssrcs: ssrcs }; if (mid) { media.mid = mid; } if (mediaSection.rtcpMux) { media.rtcpMux = 'rtcp-mux'; } addCandidatesToLevel(media, mediaSection.candidates); sdp.media.push(media); return sdp; } function addMediaSectionsToSDPBlob(sdp, mediaSections) { mediaSections.forEach(addMediaSectionToSDPBlob.bind(null, sdp)); return sdp; } /** * Construct an initial SDP blob. * @param {?number} [sessionId] * @returns {object} */ function makeInitialSDPBlob(sessionId) { sessionId = sessionId || Math.floor(Math.random() * 4294967296); return { version: 0, origin: { username: '-', sessionId: sessionId, sessionVersion: 0, netType: 'IN', ipVer: 4, address: '127.0.0.1' }, name: '-', timing: { start: 0, stop: 0 }, connection: { version: 4, ip: '0.0.0.0' }, media: [] }; } /** * Parse the SDP contained in an {@link RTCSessionDescription} into individual * {@link RTCIceParameters}, {@link RTCDtlsParameters}, and * {@link RTCRtpParameters}. * @access private * @param {RTCSessionDescription} description * @returns {object} */ function p