From b86b6e695782435e14269efdad9e2796b49069be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Baz=20Castillo?= Date: Wed, 21 Jun 2017 17:09:50 +0200 Subject: [PATCH] Add suport for Safari 11 and Microsoft Edge --- app/lib/Client.js | 35 +- app/lib/components/RemoteVideo.jsx | 2 +- app/lib/components/Room.jsx | 33 +- app/lib/components/Stats.jsx | 90 +- app/lib/components/Video.jsx | 4 +- app/lib/edge/RTCPeerConnection.js | 4902 ++++++++++++++-------------- app/lib/index.jsx | 14 +- app/lib/utils.js | 14 + app/package.json | 33 +- server/lib/Room.js | 3 +- server/package.json | 8 +- 11 files changed, 2651 insertions(+), 2487 deletions(-) diff --git a/app/lib/Client.js b/app/lib/Client.js index 8bc3d69..523fdf9 100644 --- a/app/lib/Client.js +++ b/app/lib/Client.js @@ -133,7 +133,9 @@ export default class Client extends events.EventEmitter } videoTrack.stop(); + stream.removeTrack(videoTrack); + // New API. if (this._peerconnection.removeTrack) { let sender; @@ -146,9 +148,11 @@ export default class Client extends events.EventEmitter this._peerconnection.removeTrack(sender); } + // Old API. else { - stream.removeTrack(videoTrack); + this._peerconnection.removeStream(stream); + this._peerconnection.addStream(stream); } if (!dontNegotiate) @@ -187,19 +191,33 @@ export default class Client extends events.EventEmitter if (stream) { - // Fucking hack for adapter.js in Chrome. - if (this._peerconnection.removeStream) - this._peerconnection.removeStream(stream); - stream.addTrack(newVideoTrack); + // New API. if (this._peerconnection.addTrack) + { this._peerconnection.addTrack(newVideoTrack, stream); + } + // Old API. + else + { + this._peerconnection.addStream(stream); + } } else { this._localStream = newStream; - this._peerconnection.addStream(newStream); + + // New API. + if (this._peerconnection.addTrack) + { + this._peerconnection.addTrack(newVideoTrack, stream); + } + // Old API. + else + { + this._peerconnection.addStream(stream); + } } this.emit('localstream', this._localStream, videoResolution); @@ -803,7 +821,10 @@ export default class Client extends events.EventEmitter { let state = this._peerconnection.iceConnectionState; - logger.debug('peerconnection "iceconnectionstatechange" event [state:%s]', state); + if (state === 'failed') + logger.warn('peerconnection "iceconnectionstatechange" event [state:failed]'); + else + logger.debug('peerconnection "iceconnectionstatechange" event [state:%s]', state); this.emit('connectionstate', state); }); diff --git a/app/lib/components/RemoteVideo.jsx b/app/lib/components/RemoteVideo.jsx index 6c6633d..68b2d37 100644 --- a/app/lib/components/RemoteVideo.jsx +++ b/app/lib/components/RemoteVideo.jsx @@ -104,7 +104,7 @@ export default class RemoteVideo extends React.Component let videoTrack = this.props.stream.getVideoTracks()[0]; let videoEnabled = videoTrack && videoTrack.enabled; let stream = this.props.stream; - let msid = stream.id; + let msid = stream.jitsiRemoteId || stream.id; if (videoEnabled) { diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 8e69884..ca8d9af 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -41,6 +41,9 @@ export default class Room extends React.Component this._client = null; // Timer to retrieve RTC stats. this._statsTimer = null; + + // TODO: TMP + global.ROOM = this; } render() @@ -221,6 +224,13 @@ export default class Room extends React.Component { logger.debug('handleLocalResolutionChange()'); + if (!utils.canChangeResolution()) + { + logger.warn('changing local resolution not implemented for this browser'); + + return; + } + this._client.changeVideoResolution(); } @@ -395,8 +405,23 @@ export default class Room extends React.Component let peers = this.state.peers; + peer = peers[peer.id]; + if (!peer) + return; + delete peers[peer.id]; - this.setState({ peers }); + + // NOTE: This shouldn't be needed but Safari 11 does not fire pc "removestream" + // nor stream "removetrack" nor track "ended", so we need to cleanup remote + // streams when a peer leaves. + let remoteStreams = this.state.remoteStreams; + + for (let msid of peer.msids) + { + delete remoteStreams[msid]; + } + + this.setState({ peers, remoteStreams }); }); this._client.on('connectionstate', (state) => @@ -407,16 +432,18 @@ export default class Room extends React.Component this._client.on('addstream', (stream) => { let remoteStreams = this.state.remoteStreams; + let streamId = stream.jitsiRemoteId || stream.id; - remoteStreams[stream.id] = stream; + remoteStreams[streamId] = stream; this.setState({ remoteStreams }); }); this._client.on('removestream', (stream) => { let remoteStreams = this.state.remoteStreams; + let streamId = stream.jitsiRemoteId || stream.id; - delete remoteStreams[stream.id]; + delete remoteStreams[streamId]; this.setState({ remoteStreams }); }); diff --git a/app/lib/components/Stats.jsx b/app/lib/components/Stats.jsx index 20452cd..e758cea 100644 --- a/app/lib/components/Stats.jsx +++ b/app/lib/components/Stats.jsx @@ -96,6 +96,8 @@ export default class Stats extends React.Component _processStats(stats) { + // global.STATS = stats; // TODO: REMOVE + if (browser.check({ chrome: '58' }, true)) { this._processStatsChrome58(stats); @@ -108,6 +110,10 @@ export default class Stats extends React.Component { this._processStatsFirefox(stats); } + else if (browser.check({ safari: '11' }, true)) + { + this._processStatsSafari11(stats); + } else { logger.warn('_processStats() | unsupported browser [name:"%s", version:%s]', @@ -388,9 +394,6 @@ export default class Stats extends React.Component for (let group of stats.values()) { - // TODO: REMOVE - global.STATS = stats; - switch (group.type) { case 'candidate-pair': @@ -489,6 +492,87 @@ export default class Stats extends React.Component } }); } + + _processStatsSafari11(stats) + { + let transport = {}; + let audio = {}; + let video = {}; + + for (let group of stats.values()) + { + switch (group.type) + { + case 'candidate-pair': + { + if (!group.writable) + break; + + transport['bytes sent'] = group.bytesSent; + transport['bytes received'] = group.bytesReceived; + transport['available bitrate'] = + Math.round(group.availableOutgoingBitrate / 1000) + ' kbps'; + transport['current RTT'] = + Math.round(group.currentRoundTripTime * 1000) + ' ms'; + + break; + } + + case 'outbound-rtp': + { + if (group.isRemote) + break; + + let block; + + switch (group.mediaType) + { + case 'audio': + block = audio; + break; + case 'video': + block = video; + break; + } + + if (!block) + break; + + block['ssrc'] = group.ssrc; + block['bytes sent'] = group.bytesSent; + block['packets sent'] = group.packetsSent; + + if (block === video) + block['frames encoded'] = group.framesEncoded; + + block['NACK count'] = group.nackCount; + block['PLI count'] = group.pliCount; + block['FIR count'] = group.firCount; + + break; + } + } + } + + // Post checks. + + if (!video.ssrc) + video = {}; + + if (!audio.ssrc) + audio = {}; + + // Set state. + this.setState( + { + stats : + { + transport, + audio, + video + } + }); + } } Stats.propTypes = diff --git a/app/lib/components/Video.jsx b/app/lib/components/Video.jsx index 92a8b3f..7556eaa 100644 --- a/app/lib/components/Video.jsx +++ b/app/lib/components/Video.jsx @@ -161,7 +161,9 @@ export default class Video extends React.Component return stream.getTracks() .map((track) => { - return track.id; + let trackId = track.jitsiRemoteId || track.id; + + return trackId; }) .join('|'); } diff --git a/app/lib/edge/RTCPeerConnection.js b/app/lib/edge/RTCPeerConnection.js index 55e5338..1ddfbb1 100644 --- a/app/lib/edge/RTCPeerConnection.js +++ b/app/lib/edge/RTCPeerConnection.js @@ -11,16 +11,16 @@ import { InvalidStateError } from './errors'; const logger = new Logger('edge/RTCPeerConnection'); const RTCSignalingState = { - stable: 'stable', - haveLocalOffer: 'have-local-offer', - haveRemoteOffer: 'have-remote-offer', - closed: 'closed' + stable: 'stable', + haveLocalOffer: 'have-local-offer', + haveRemoteOffer: 'have-remote-offer', + closed: 'closed' }; const RTCIceGatheringState = { - new: 'new', - gathering: 'gathering', - complete: 'complete' + new: 'new', + gathering: 'gathering', + complete: 'complete' }; const CNAME = `jitsi-ortc-cname-${utils.randomNumber()}`; @@ -37,2444 +37,2452 @@ const CNAME = `jitsi-ortc-cname-${utils.randomNumber()}`; * transport and rtcp-mux. */ export default class ortcRTCPeerConnection extends yaeti.EventTarget { - /** - */ - constructor(pcConfig) { - super(); - - // TODO: TMP - window.PC = this; - - logger.debug('constructor() pcConfig:', pcConfig); - - // Buffered local ICE candidates (in WebRTC format). - // @type {sequence} - this._bufferedIceCandidates = []; - - // Closed flag. - // @type {Boolean} - this._closed = false; - - // RTCDtlsTransport. - // @type {RTCDtlsTransport} - this._dtlsTransport = null; - - // RTCIceGatherer. - // @type {RTCIceGatherer} - this._iceGatherer = null; - - // RTCPeerConnection iceGatheringState. - // NOTE: This should not be needed, but Edge does not implement - // iceGatherer.state. - // @type {RTCIceGatheringState} - this._iceGatheringState = RTCIceGatheringState.new; - - // RTCIceTransport. - // @type {RTCIceTransport} - this._iceTransport = null; - - // Local RTP capabilities (filtered with remote ones). - // @type {RTCRtpCapabilities} - this._localCapabilities = null; - - // Local RTCSessionDescription. - // @type {RTCSessionDescription} - this._localDescription = null; - - // Map with info regarding local media. - // - index: MediaStreamTrack.id - // - value: Object - // - rtpSender: Associated RTCRtpSender instance - // - stream: Associated MediaStream instance - // - ssrc: Provisional or definitive SSRC - // - rtxSsrc: Provisional or definitive SSRC for RTX - // - sending: Boolean indicating whether rtpSender.send() was called. - this._localTrackInfos = new Map(); - - // Ordered Map with MID as key and kind as value. - // @type {map} - this._mids = new Map(); - - // Remote RTCSessionDescription. - // @type {RTCSessionDescription} - this._remoteDescription = null; - - // Map of remote streams. - // - index: MediaStream.jitsiRemoteId (as signaled in remote SDP) - // - value: MediaStream (locally generated so id does not match) - // @type {map} - this._remoteStreams = new Map(); - - // Map with info about receiving media. - // - index: Media SSRC - // - value: Object - // - kind: 'audio' / 'video' - // - ssrc: Media SSRC - // - rtxSsrc: RTX SSRC (may be unset) - // - streamId: MediaStream.jitsiRemoteId - // - trackId: MediaStreamTrack.jitsiRemoteId - // - cname: CNAME - // - stream: MediaStream - // - rtpReceiver: Associated RTCRtpReceiver instance - // @type {map} - this._remoteTrackInfos = new Map(); - - // Local SDP global fields. - this._sdpGlobalFields = { - id: utils.randomNumber(), - version: 0 - }; - - // RTCPeerConnection signalingState. - // @type {RTCSignalingState} - this._signalingState = RTCSignalingState.stable; - - // Create the RTCIceGatherer. - this._setIceGatherer(pcConfig); - - // Create the RTCIceTransport. - this._setIceTransport(this._iceGatherer); - - // Create the RTCDtlsTransport. - this._setDtlsTransport(this._iceTransport); - } - - /** - * Current ICE+DTLS connection state. - * @return {RTCPeerConnectionState} - */ - get connectionState() { - return this._dtlsTransport.state; - } - - /** - * Current ICE connection state. - * @return {RTCIceConnectionState} - */ - get iceConnectionState() { - return this._iceTransport.state; - } - - /** - * Current ICE gathering state. - * @return {RTCIceGatheringState} - */ - get iceGatheringState() { - return this._iceGatheringState; - } - - /** - * Gets the local description. - * @return {RTCSessionDescription} - */ - get localDescription() { - return this._localDescription; - } - - /** - * Gets the remote description. - * @return {RTCSessionDescription} - */ - get remoteDescription() { - return this._remoteDescription; - } - - /** - * Current signaling state. - * @return {RTCSignalingState} - */ - get signalingState() { - return this._signalingState; - } - - /** - * Adds a remote ICE candidate. Implements both the old callbacks based - * signature and the new Promise based style. - * - * Arguments in Promise mode: - * @param {RTCIceCandidate} candidate - * - * Arguments in callbacks mode: - * @param {RTCIceCandidate} candidate - * @param {function()} callback - * @param {function(error)} errback - */ - addIceCandidate(candidate, ...args) { - let usePromise; - let callback; - let errback; - - if (!candidate) { - throw new TypeError('candidate missing'); - } - - if (args.length === 0) { - usePromise = true; - } else { - usePromise = false; - callback = args[0]; - errback = args[1]; - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('addIceCandidate() candidate:', candidate); - - if (usePromise) { - return this._addIceCandidate(candidate); - } - - this._addIceCandidate(candidate) - .then(() => callback()) - .catch(error => errback(error)); - } - - /** - * Adds a local MediaStream. - * @param {MediaStream} stream. - * NOTE: Deprecated API. - */ - addStream(stream) { - logger.debug('addStream()'); - - this._addStream(stream); - } - - /** - * Closes the RTCPeerConnection and all the underlying ORTC objects. - */ - close() { - if (this._closed) { - return; - } - - this._closed = true; - - logger.debug('close()'); - - this._updateAndEmitSignalingStateChange(RTCSignalingState.closed); - - // Close RTCIceGatherer. - // NOTE: Not yet implemented by Edge. - try { - this._iceGatherer.close(); - } catch (error) { - logger.warn(`iceGatherer.close() failed:${error}`); - } - - // Close RTCIceTransport. - try { - this._iceTransport.stop(); - } catch (error) { - logger.warn(`iceTransport.stop() failed:${error}`); - } - - // Close RTCDtlsTransport. - try { - this._dtlsTransport.stop(); - } catch (error) { - logger.warn(`dtlsTransport.stop() failed:${error}`); - } - - // Close and clear RTCRtpSenders. - for (const info of this._localTrackInfos.values()) { - const rtpSender = info.rtpSender; - - try { - rtpSender.stop(); - } catch (error) { - logger.warn(`rtpSender.stop() failed:${error}`); - } - } - - this._localTrackInfos.clear(); - - // Close and clear RTCRtpReceivers. - for (const info of this._remoteTrackInfos.values()) { - const rtpReceiver = info.rtpReceiver; - - try { - rtpReceiver.stop(); - } catch (error) { - logger.warn(`rtpReceiver.stop() failed:${error}`); - } - } - - this._remoteTrackInfos.clear(); - - // Clear remote streams. - this._remoteStreams.clear(); - } - - /** - * Creates a local answer. Implements both the old callbacks based signature - * and the new Promise based style. - * - * Arguments in Promise mode: - * @param {RTCOfferOptions} [options] - * - * Arguments in callbacks mode: - * @param {function(desc)} callback - * @param {function(error)} errback - * @param {MediaConstraints} [constraints] - */ - createAnswer(...args) { - let usePromise; - let options; - let callback; - let errback; - - if (args.length <= 1) { - usePromise = true; - options = args[0]; - } else { - usePromise = false; - callback = args[0]; - errback = args[1]; - options = args[2]; - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('createAnswer() options:', options); - - if (usePromise) { - return this._createAnswer(options); - } - - this._createAnswer(options) - .then(desc => callback(desc)) - .catch(error => errback(error)); - } - - /** - * Creates a RTCDataChannel. - */ - createDataChannel() { - logger.debug('createDataChannel()'); - - // NOTE: DataChannels not implemented in Edge. - throw new Error('createDataChannel() not supported in Edge'); - } - - /** - * Creates a local offer. Implements both the old callbacks based signature - * and the new Promise based style. - * - * Arguments in Promise mode: - * @param {RTCOfferOptions} [options] - * - * Arguments in callbacks mode: - * @param {function(desc)} callback - * @param {function(error)} errback - * @param {MediaConstraints} [constraints] - */ - createOffer(...args) { - let usePromise; - let options; - let callback; - let errback; - - if (args.length <= 1) { - usePromise = true; - options = args[0]; - } else { - usePromise = false; - callback = args[0]; - errback = args[1]; - options = args[2]; - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('createOffer() options:', options); - - if (usePromise) { - return this._createOffer(options); - } - - this._createOffer(options) - .then(desc => callback(desc)) - .catch(error => errback(error)); - } - - /** - * Gets a sequence of local MediaStreams. - * @return {sequence} - */ - getLocalStreams() { - return Array.from(this._localTrackInfos.values()) - .map(info => info.stream) - .filter((elem, pos, arr) => arr.indexOf(elem) === pos); - } - - /** - * Gets a sequence of remote MediaStreams. - * @return {sequence} - */ - getRemoteStreams() { - return Array.from(this._remoteStreams.values()); - } - - /** - * Get RTP statistics. Implements both the old callbacks based signature - * and the new Promise based style. - * - * Arguments in Promise mode: - * @param {MediaStreamTrack} [selector] - * - * Arguments in callbacks mode: - * @param {MediaStreamTrack} [selector] - * @param {function(desc)} callback - * @param {function(error)} errback - */ - getStats(...args) { - let usePromise; - let selector; - let callback; - let errback; - - if (args.length <= 1) { - usePromise = true; - selector = args[0]; - } else { - usePromise = false; - - if (args.length === 2) { - callback = args[0]; - errback = args[1]; - } else { - selector = args[0]; - callback = args[1]; - errback = args[2]; - } - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('getStats()'); - - if (usePromise) { - return this._getStats(selector); - } - - this._getStats(selector) - .then(stats => callback(stats)) - .catch(error => errback(error)); - } - - /** - * Removes a local MediaStream. - * @param {MediaStream} stream. - * NOTE: Deprecated API. - */ - removeStream(stream) { - logger.debug('removeStream()'); - - this._removeStream(stream); - } - - /** - * Applies a local description. Implements both the old callbacks based - * signature and the new Promise based style. - * - * Arguments in Promise mode: - * @param {RTCSessionDescriptionInit} desc - * - * Arguments in callbacks mode: - * @param {RTCSessionDescription} desc - * @param {function()} callback - * @param {function(error)} errback - */ - setLocalDescription(desc, ...args) { - let usePromise; - let callback; - let errback; - - if (!desc) { - throw new TypeError('description missing'); - } - - if (args.length === 0) { - usePromise = true; - } else { - usePromise = false; - callback = args[0]; - errback = args[1]; - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('setLocalDescription() desc:', desc); - - if (usePromise) { - return this._setLocalDescription(desc); - } - - this._setLocalDescription(desc) - .then(() => callback()) - .catch(error => errback(error)); - } - - /** - * Applies a remote description. Implements both the old callbacks based - * signature and the new Promise based style. - * - * Arguments in Promise mode: - * @param {RTCSessionDescriptionInit} desc - * - * Arguments in callbacks mode: - * @param {RTCSessionDescription} desc - * @param {function()} callback - * @param {function(error)} errback - */ - setRemoteDescription(desc, ...args) { - let usePromise; - let callback; - let errback; - - if (!desc) { - throw new TypeError('description missing'); - } - - if (args.length === 0) { - usePromise = true; - } else { - usePromise = false; - callback = args[0]; - errback = args[1]; - - if (typeof callback !== 'function') { - throw new TypeError('callback missing'); - } - - if (typeof errback !== 'function') { - throw new TypeError('errback missing'); - } - } - - logger.debug('setRemoteDescription() desc:', desc); - - if (usePromise) { - return this._setRemoteDescription(desc); - } - - this._setRemoteDescription(desc) - .then(() => callback()) - .catch(error => errback(error)); - } - - /** - * Promise based implementation for addIceCandidate(). - * @return {Promise} - * @private - */ - _addIceCandidate(candidate) { // eslint-disable-line no-unused-vars - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - // NOTE: Edge does not support Trickle-ICE so just candidates in the - // remote SDP are applied. Candidates given later would be just - // ignored, so notify the called about that. - return Promise.reject(new Error('addIceCandidate() not supported')); - } - - /** - * Implementation for addStream(). - * @private - */ - _addStream(stream) { - if (this._closed) { - throw new InvalidStateError('RTCPeerConnection closed'); - } - - // Create a RTCRtpSender for each track. - for (const track of stream.getTracks()) { - // Ignore if ended. - if (track.readyState === 'ended') { - logger.warn('ignoring ended MediaStreamTrack'); - - continue; // eslint-disable-line no-continue - } - - // Ignore if track is already present. - if (this._localTrackInfos.has(track.id)) { - logger.warn('ignoring already handled MediaStreamTrack'); - - continue; // eslint-disable-line no-continue - } - - const rtpSender = new RTCRtpSender(track, this._dtlsTransport); - - // Store it in the map. - this._localTrackInfos.set(track.id, { - rtpSender, - stream - }); - } - - // It may need to renegotiate. - this._emitNegotiationNeeded(); - } - - /** - * Promise based implementation for createAnswer(). - * @returns {Promise} - * @private - */ - _createAnswer(options) { // eslint-disable-line no-unused-vars - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - if (this.signalingState !== RTCSignalingState.haveRemoteOffer) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - // Create an answer. - const localDescription = this._createLocalDescription('answer'); - - // Resolve with it. - return Promise.resolve(localDescription); - } - - /** - * Creates the local RTCSessionDescription. - * @param {String} type - 'offer' / 'answer'. - * @return {RTCSessionDescription} - */ - _createLocalDescription(type) { - const sdpObject = {}; - const localIceParameters = this._iceGatherer.getLocalParameters(); - const localIceCandidates = this._iceGatherer.getLocalCandidates(); - const localDtlsParameters = this._dtlsTransport.getLocalParameters(); - const remoteDtlsParameters = this._dtlsTransport.getRemoteParameters(); - const localCapabilities = this._localCapabilities; - const localTrackInfos = this._localTrackInfos; - - // TODO: TMP - logger.warn(`_createLocalDescription() ICE local [username:${localIceParameters.usernameFragment}, password:${localIceParameters.password}`); - - // Increase SDP version if an offer. - if (type === 'offer') { - this._sdpGlobalFields.version++; - } - - // SDP global fields. - sdpObject.version = 0; - sdpObject.origin = { - address: '127.0.0.1', - ipVer: 4, - netType: 'IN', - sessionId: this._sdpGlobalFields.id, - sessionVersion: this._sdpGlobalFields.version, - username: 'jitsi-ortc-webrtc-shim' - }; - sdpObject.name = '-'; - sdpObject.timing = { - start: 0, - stop: 0 - }; - sdpObject.msidSemantic = { - semantic: 'WMS', - token: '*' - }; - sdpObject.groups = [ - { - mids: Array.from(this._mids.keys()).join(' '), - type: 'BUNDLE' - } - ]; - sdpObject.media = []; - - // DTLS fingerprint. - sdpObject.fingerprint = { - hash: localDtlsParameters.fingerprints[0].value, - type: localDtlsParameters.fingerprints[0].algorithm - }; - - // Let's check whether there is video RTX. - let hasVideoRtx = false; - - for (const codec of localCapabilities.codecs) { - if (codec.kind === 'video' && codec.name === 'rtx') { - hasVideoRtx = true; - break; - } - } - - // Add m= sections. - for (const [ mid, kind ] of this._mids) { - addMediaSection.call(this, mid, kind); - } - - // Create a RTCSessionDescription. - const localDescription = new RTCSessionDescription({ - type, - _sdpObject: sdpObject - }); - - logger.debug('_createLocalDescription():', localDescription); - - return localDescription; - - /** - * Add a m= section. - */ - function addMediaSection(mid, kind) { - const mediaObject = {}; - - // m= line. - mediaObject.type = kind; - - switch (kind) { - case 'audio': - case 'video': - mediaObject.protocol = 'RTP/SAVPF'; - mediaObject.port = 9; - mediaObject.direction = 'sendrecv'; - break; - case 'application': - mediaObject.protocol = 'DTLS/SCTP'; - mediaObject.port = 0; // Reject m section. - mediaObject.payloads = '0'; // Just put something. - mediaObject.direction = 'inactive'; - break; - } - - // c= line. - mediaObject.connection = { - ip: '127.0.0.1', - version: 4 - }; - - // a=mid attribute. - mediaObject.mid = mid; - - // ICE. - mediaObject.iceUfrag = localIceParameters.usernameFragment; - mediaObject.icePwd = localIceParameters.password; - mediaObject.candidates = []; - - for (const candidate of localIceCandidates) { - const candidateObject = {}; - - // rtcp-mux is assumed, so component is always 1 (RTP). - candidateObject.component = 1; - candidateObject.foundation = candidate.foundation; - candidateObject.ip = candidate.ip; - candidateObject.port = candidate.port; - candidateObject.priority = candidate.priority; - candidateObject.transport - = candidate.protocol.toLowerCase(); - candidateObject.type = candidate.type; - if (candidateObject.transport === 'tcp') { - candidateObject.tcptype = candidate.tcpType; - } - - mediaObject.candidates.push(candidateObject); - } - - mediaObject.endOfCandidates = 'end-of-candidates'; - - // DTLS. - // If 'offer' always use 'actpass'. - if (type === 'offer') { - mediaObject.setup = 'actpass'; - } else { - mediaObject.setup = remoteDtlsParameters.role === 'server' - ? 'active' : 'passive'; - } - - if (kind === 'audio' || kind === 'video') { - mediaObject.rtp = []; - mediaObject.rtcpFb = []; - mediaObject.fmtp = []; - - // Array of payload types. - const payloads = []; - - // Add codecs. - for (const codec of localCapabilities.codecs) { - if (codec.kind && codec.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - payloads.push(codec.preferredPayloadType); - - const rtpObject = { - codec: codec.name, - payload: codec.preferredPayloadType, - rate: codec.clockRate - }; - - if (codec.numChannels > 1) { - rtpObject.encoding = codec.numChannels; - } - - mediaObject.rtp.push(rtpObject); - - // If codec has parameters add them into a=fmtp attributes. - if (codec.parameters) { - const paramFmtp = { - config: '', - payload: codec.preferredPayloadType - }; - - for (const name of Object.keys(codec.parameters)) { - /* eslint-disable max-depth */ - if (paramFmtp.config) { - paramFmtp.config += ';'; - } - /* eslint-enable max-depth */ - - paramFmtp.config - += `${name}=${codec.parameters[name]}`; - } - - if (paramFmtp.config) { - mediaObject.fmtp.push(paramFmtp); - } - } - - // Set RTCP feedback. - for (const fb of codec.rtcpFeedback || []) { - mediaObject.rtcpFb.push({ - payload: codec.preferredPayloadType, - subtype: fb.parameter || undefined, - type: fb.type - }); - } - } - - // If there are no codecs, set this m section as unavailable. - if (payloads.length === 0) { - mediaObject.payloads = '9'; // Just put something. - mediaObject.port = 0; - mediaObject.direction = 'inactive'; - } else { - mediaObject.payloads = payloads.join(' '); - } - - // SSRCs. - mediaObject.ssrcs = []; - mediaObject.ssrcGroups = []; - - // Add RTP sending stuff. - for (const info of localTrackInfos.values()) { - const rtpSender = info.rtpSender; - const streamId = info.stream.id; - const track = rtpSender.track; - - // Ignore if ended. - if (track.readyState === 'ended') { - continue; // eslint-disable-line no-continue - } - - if (track.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - // Set a random provisional SSRC if not set. - if (!info.ssrc) { - info.ssrc = utils.randomNumber(); - } - - // Whether RTX should be enabled. - const enableRtx = hasVideoRtx && track.kind === 'video'; - - // Set a random provisional RTX SSRC if not set. - if (enableRtx && !info.rtxSsrc) { - info.rtxSsrc = info.ssrc + 1; - } - - mediaObject.ssrcs.push({ - attribute: 'cname', - id: info.ssrc, - value: CNAME - }); - - mediaObject.ssrcs.push({ - attribute: 'msid', - id: info.ssrc, - value: `${streamId} ${track.id}` - }); - - mediaObject.ssrcs.push({ - attribute: 'mslabel', - id: info.ssrc, - value: streamId - }); - - mediaObject.ssrcs.push({ - attribute: 'label', - id: info.ssrc, - value: track.id - }); - - if (enableRtx) { - mediaObject.ssrcs.push({ - attribute: 'cname', - id: info.rtxSsrc, - value: CNAME - }); - - mediaObject.ssrcs.push({ - attribute: 'msid', - id: info.rtxSsrc, - value: `${streamId} ${track.id}` - }); - - mediaObject.ssrcs.push({ - attribute: 'mslabel', - id: info.rtxSsrc, - value: streamId - }); - - mediaObject.ssrcs.push({ - attribute: 'label', - id: info.rtxSsrc, - value: track.id - }); - - mediaObject.ssrcGroups.push({ - semantics: 'FID', - ssrcs: `${info.ssrc} ${info.rtxSsrc}` - }); - } - } - - // RTP header extensions. - mediaObject.ext = []; - - for (const extension of localCapabilities.headerExtensions) { - if (extension.kind && extension.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - mediaObject.ext.push({ - value: extension.preferredId, - uri: extension.uri - }); - } - - // a=rtcp-mux attribute. - mediaObject.rtcpMux = 'rtcp-mux'; - - // a=rtcp-rsize. - mediaObject.rtcpRsize = 'rtcp-rsize'; - } - - // Add the media section. - sdpObject.media.push(mediaObject); - } - } - - /** - * Promise based implementation for createOffer(). - * @returns {Promise} - * @private - */ - _createOffer(options) { // eslint-disable-line no-unused-vars - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - if (this.signalingState !== RTCSignalingState.stable) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - // NOTE: P2P mode not yet supported, so createOffer() should never be - // called. - // return Promise.reject(new Error('createoOffer() not yet supported')); - - // HACK: Create an offer assuming this is called before any - // setRemoteDescription() and assuming that setLocalDescription() - // wont be called with this offer. - - const sdpObject = {}; - const localIceParameters = this._iceGatherer.getLocalParameters(); - const localIceCandidates = this._iceGatherer.getLocalCandidates(); - const localDtlsParameters = this._dtlsTransport.getLocalParameters(); - const localCapabilities = RTCRtpReceiver.getCapabilities(); - const localTrackInfos = this._localTrackInfos; - const mids = new Map([ ['audio', 'audio'], ['video', 'video'] ]); - - // SDP global fields. - sdpObject.version = 0; - sdpObject.origin = { - address: '127.0.0.1', - ipVer: 4, - netType: 'IN', - sessionId: this._sdpGlobalFields.id, - sessionVersion: this._sdpGlobalFields.version, - username: 'jitsi-ortc-webrtc-shim' - }; - sdpObject.name = '-'; - sdpObject.timing = { - start: 0, - stop: 0 - }; - sdpObject.msidSemantic = { - semantic: 'WMS', - token: '*' - }; - sdpObject.groups = [ - { - mids: Array.from(mids.keys()).join(' '), - type: 'BUNDLE' - } - ]; - sdpObject.media = []; - - // DTLS fingerprint. - sdpObject.fingerprint = { - hash: localDtlsParameters.fingerprints[0].value, - type: localDtlsParameters.fingerprints[0].algorithm - }; - - // Let's check whether there is video RTX. - let hasVideoRtx = false; - - for (const codec of localCapabilities.codecs) { - if (codec.kind === 'video' && codec.name === 'rtx') { - hasVideoRtx = true; - break; - } - } - - // Add m= sections. - for (const [ mid, kind ] of mids) { - addMediaSection.call(this, mid, kind); - } - - // Create a RTCSessionDescription. - const localDescription = new RTCSessionDescription({ - type: 'offer', - _sdpObject: sdpObject - }); - - logger.debug('_createLocalDescription():', localDescription); - - // Resolve with it. - return Promise.resolve(localDescription); - - /** - * Add a m= section. - */ - function addMediaSection(mid, kind) { - const mediaObject = {}; - - // m= line. - mediaObject.type = kind; - - switch (kind) { - case 'audio': - case 'video': - mediaObject.protocol = 'RTP/SAVPF'; - mediaObject.port = 9; - mediaObject.direction = 'sendrecv'; - break; - case 'application': - mediaObject.protocol = 'DTLS/SCTP'; - mediaObject.port = 0; // Reject m section. - mediaObject.payloads = '0'; // Just put something. - mediaObject.direction = 'inactive'; - break; - } - - // c= line. - mediaObject.connection = { - ip: '127.0.0.1', - version: 4 - }; - - // a=mid attribute. - mediaObject.mid = mid; - - // ICE. - mediaObject.iceUfrag = localIceParameters.usernameFragment; - mediaObject.icePwd = localIceParameters.password; - mediaObject.candidates = []; - - for (const candidate of localIceCandidates) { - const candidateObject = {}; - - // rtcp-mux is assumed, so component is always 1 (RTP). - candidateObject.component = 1; - candidateObject.foundation = candidate.foundation; - candidateObject.ip = candidate.ip; - candidateObject.port = candidate.port; - candidateObject.priority = candidate.priority; - candidateObject.transport - = candidate.protocol.toLowerCase(); - candidateObject.type = candidate.type; - if (candidateObject.transport === 'tcp') { - candidateObject.tcptype = candidate.tcpType; - } - - mediaObject.candidates.push(candidateObject); - } - - mediaObject.endOfCandidates = 'end-of-candidates'; - - // DTLS. - // 'offer' so always use 'actpass'. - mediaObject.setup = 'actpass'; - - if (kind === 'audio' || kind === 'video') { - mediaObject.rtp = []; - mediaObject.rtcpFb = []; - mediaObject.fmtp = []; - - // Array of payload types. - const payloads = []; - - // Add codecs. - for (const codec of localCapabilities.codecs) { - if (codec.kind && codec.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - payloads.push(codec.preferredPayloadType); - - const rtpObject = { - codec: codec.name, - payload: codec.preferredPayloadType, - rate: codec.clockRate - }; - - if (codec.numChannels > 1) { - rtpObject.encoding = codec.numChannels; - } - - mediaObject.rtp.push(rtpObject); - - // If codec has parameters add them into a=fmtp attributes. - if (codec.parameters) { - const paramFmtp = { - config: '', - payload: codec.preferredPayloadType - }; - - for (const name of Object.keys(codec.parameters)) { - /* eslint-disable max-depth */ - if (paramFmtp.config) { - paramFmtp.config += ';'; - } - /* eslint-enable max-depth */ - - paramFmtp.config - += `${name}=${codec.parameters[name]}`; - } - - if (paramFmtp.config) { - mediaObject.fmtp.push(paramFmtp); - } - } - - // Set RTCP feedback. - for (const fb of codec.rtcpFeedback || []) { - mediaObject.rtcpFb.push({ - payload: codec.preferredPayloadType, - subtype: fb.parameter || undefined, - type: fb.type - }); - } - } - - // If there are no codecs, set this m section as unavailable. - if (payloads.length === 0) { - mediaObject.payloads = '9'; // Just put something. - mediaObject.port = 0; - mediaObject.direction = 'inactive'; - } else { - mediaObject.payloads = payloads.join(' '); - } - - // SSRCs. - mediaObject.ssrcs = []; - mediaObject.ssrcGroups = []; - - // Add RTP sending stuff. - for (const info of localTrackInfos.values()) { - const rtpSender = info.rtpSender; - const streamId = info.stream.id; - const track = rtpSender.track; - - // Ignore if ended. - if (track.readyState === 'ended') { - continue; // eslint-disable-line no-continue - } - - if (track.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - // Set a random provisional SSRC if not set. - if (!info.ssrc) { - info.ssrc = utils.randomNumber(); - } - - // Whether RTX should be enabled. - const enableRtx = hasVideoRtx && track.kind === 'video'; - - // Set a random provisional RTX SSRC if not set. - if (enableRtx && !info.rtxSsrc) { - info.rtxSsrc = info.ssrc + 1; - } - - mediaObject.ssrcs.push({ - attribute: 'cname', - id: info.ssrc, - value: CNAME - }); - - mediaObject.ssrcs.push({ - attribute: 'msid', - id: info.ssrc, - value: `${streamId} ${track.id}` - }); - - mediaObject.ssrcs.push({ - attribute: 'mslabel', - id: info.ssrc, - value: streamId - }); - - mediaObject.ssrcs.push({ - attribute: 'label', - id: info.ssrc, - value: track.id - }); - - if (enableRtx) { - mediaObject.ssrcs.push({ - attribute: 'cname', - id: info.rtxSsrc, - value: CNAME - }); - - mediaObject.ssrcs.push({ - attribute: 'msid', - id: info.rtxSsrc, - value: `${streamId} ${track.id}` - }); - - mediaObject.ssrcs.push({ - attribute: 'mslabel', - id: info.rtxSsrc, - value: streamId - }); - - mediaObject.ssrcs.push({ - attribute: 'label', - id: info.rtxSsrc, - value: track.id - }); - - mediaObject.ssrcGroups.push({ - semantics: 'FID', - ssrcs: `${info.ssrc} ${info.rtxSsrc}` - }); - } - } - - // RTP header extensions. - mediaObject.ext = []; - - for (const extension of localCapabilities.headerExtensions) { - if (extension.kind && extension.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - mediaObject.ext.push({ - value: extension.preferredId, - uri: extension.uri - }); - } - - // a=rtcp-mux attribute. - mediaObject.rtcpMux = 'rtcp-mux'; - - // a=rtcp-rsize. - mediaObject.rtcpRsize = 'rtcp-rsize'; - } - - // Add the media section. - sdpObject.media.push(mediaObject); - } - } - - /** - * Emit 'addstream' event. - * @private - */ - _emitAddStream(stream) { - if (this._closed) { - return; - } - - logger.debug('emitting "addstream"'); - - const event = new yaeti.Event('addstream'); - - event.stream = stream; - this.dispatchEvent(event); - } - - /** - * May emit buffered ICE candidates. - * @private - */ - _emitBufferedIceCandidates() { - if (this._closed) { - return; - } - - for (const sdpCandidate of this._bufferedIceCandidates) { - if (!sdpCandidate) { - continue; // eslint-disable-line no-continue - } - - // Now we have set the MID values of the SDP O/A, so let's fill the - // sdpMIndex of the candidate. - sdpCandidate.sdpMIndex = this._mids.keys().next().value; - - logger.debug( - 'emitting buffered "icecandidate", candidate:', sdpCandidate); - - const event = new yaeti.Event('icecandidate'); - - event.candidate = sdpCandidate; - this.dispatchEvent(event); - } - - this._bufferedIceCandidates = []; - } - - /** - * May emit 'connectionstatechange' event. - * @private - */ - _emitConnectionStateChange() { - if (this._closed && this.connectionState !== 'closed') { - return; - } - - logger.debug( - 'emitting "connectionstatechange", connectionState:', - this.connectionState); - - const event = new yaeti.Event('connectionstatechange'); - - this.dispatchEvent(event); - } - - /** - * May emit 'icecandidate' event. - * @private - */ - _emitIceCandidate(candidate) { - if (this._closed) { - return; - } - - let sdpCandidate = null; - - if (candidate) { - // NOTE: We assume BUNDLE so let's just emit candidates for the - // first m= section. - const sdpMIndex = this._mids.keys().next().value; - const sdpMLineIndex = 0; - let sdpAttribute - = `candidate:${candidate.foundation} 1 ${candidate.protocol}` - + ` ${candidate.priority} ${candidate.ip} ${candidate.port}` - + ` typ ${candidate.type}`; - - if (candidate.relatedAddress) { - sdpAttribute += ` raddr ${candidate.relatedAddress}`; - } - if (candidate.relatedPort) { - sdpAttribute += ` rport ${candidate.relatedPort}`; - } - if (candidate.protocol === 'tcp') { - sdpAttribute += ` tcptype ${candidate.tcpType}`; - } - - sdpCandidate = { - candidate: sdpAttribute, - component: 1, // rtcp-mux assumed, so always 1 (RTP). - foundation: candidate.foundation, - ip: candidate.ip, - port: candidate.port, - priority: candidate.priority, - protocol: candidate.protocol, - type: candidate.type, - sdpMIndex, - sdpMLineIndex - }; - - if (candidate.protocol === 'tcp') { - sdpCandidate.tcptype = candidate.tcpType; - } - if (candidate.relatedAddress) { - sdpCandidate.relatedAddress = candidate.relatedAddress; - } - if (candidate.relatedPort) { - sdpCandidate.relatedPort = candidate.relatedPort; - } - } - - // If we don't have yet a local description, buffer the candidate. - if (this._localDescription) { - logger.debug( - 'emitting "icecandidate", candidate:', sdpCandidate); - - const event = new yaeti.Event('icecandidate'); - - event.candidate = sdpCandidate; - this.dispatchEvent(event); - } else { - logger.debug( - 'buffering gathered ICE candidate:', sdpCandidate); - - this._bufferedIceCandidates.push(sdpCandidate); - } - } - - /** - * May emit 'iceconnectionstatechange' event. - * @private - */ - _emitIceConnectionStateChange() { - if (this._closed && this.iceConnectionState !== 'closed') { - return; - } - - logger.debug( - 'emitting "iceconnectionstatechange", iceConnectionState:', - this.iceConnectionState); - - const event = new yaeti.Event('iceconnectionstatechange'); - - this.dispatchEvent(event); - } - - /** - * May emit 'negotiationneeded' event. - * @private - */ - _emitNegotiationNeeded() { - // Ignore if signalingState is not 'stable'. - if (this.signalingState !== RTCSignalingState.stable) { - return; - } - - logger.debug('emitting "negotiationneeded"'); - - const event = new yaeti.Event('negotiationneeded'); - - this.dispatchEvent(event); - } - - /** - * Emit 'removestream' event. - * @private - */ - _emitRemoveStream(stream) { - if (this._closed) { - return; - } - - logger.debug('emitting "removestream"'); - - const event = new yaeti.Event('removestream'); - - event.stream = stream; - this.dispatchEvent(event); - } - - /** - * Get RTP parameters for a RTCRtpReceiver. - * @private - * @return {RTCRtpParameters} - */ - _getParametersForRtpReceiver(kind, data) { - const ssrc = data.ssrc; - const rtxSsrc = data.rtxSsrc; - const cname = data.cname; - const localCapabilities = this._localCapabilities; - const parameters = { - codecs: [], - degradationPreference: 'balanced', - encodings: [], - headerExtensions: [], - muxId: '', - rtcp: { - cname, - compound: true, // NOTE: Implemented in Edge. - mux: true, - reducedSize: true // NOTE: Not yet implemented in Edge. - } - }; - - const codecs = []; - let codecPayloadType; - - for (const codecCapability of localCapabilities.codecs) { - if (codecCapability.kind !== kind - || codecCapability.name === 'rtx') { - continue; // eslint-disable-line no-continue - } - - codecPayloadType = codecCapability.preferredPayloadType; - codecs.push({ - clockRate: codecCapability.clockRate, - maxptime: codecCapability.maxptime, - mimeType: codecCapability.mimeType, - name: codecCapability.name, - numChannels: codecCapability.numChannels, - parameters: codecCapability.parameters, - payloadType: codecCapability.preferredPayloadType, - ptime: codecCapability.ptime, - rtcpFeedback: codecCapability.rtcpFeedback - }); - - break; - } - - if (rtxSsrc) { - for (const codecCapability of localCapabilities.codecs) { - if (codecCapability.kind !== kind - || codecCapability.name !== 'rtx') { - continue; // eslint-disable-line no-continue - } - - codecs.push({ - clockRate: codecCapability.clockRate, - mimeType: codecCapability.mimeType, - name: 'rtx', - parameters: codecCapability.parameters, - payloadType: codecCapability.preferredPayloadType, - rtcpFeedback: codecCapability.rtcpFeedback - }); - - break; - } - } - - parameters.codecs = codecs; - - const encoding = { - active: true, - codecPayloadType, - ssrc - }; - - if (rtxSsrc) { - encoding.rtx = { - ssrc: rtxSsrc - }; - } - - parameters.encodings.push(encoding); - - for (const extension of localCapabilities.headerExtensions) { - if (extension.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - parameters.headerExtensions.push({ - encrypt: extension.preferredEncrypt, - id: extension.preferredId, - uri: extension.uri - }); - } - - return parameters; - } - - /** - * Get RTP parameters for a RTCRtpSender. - * @private - * @return {RTCRtpParameters} - */ - _getParametersForRtpSender(kind, data) { - const ssrc = data.ssrc; - const rtxSsrc = data.rtxSsrc; - const cname = CNAME; - const localCapabilities = this._localCapabilities; - const parameters = { - codecs: [], - degradationPreference: 'balanced', - encodings: [], - headerExtensions: [], - muxId: '', - rtcp: { - cname, - compound: true, // NOTE: Implemented in Edge. - mux: true, - reducedSize: true // NOTE: Not yet implemented in Edge. - } - }; - - const codecs = []; - let codecPayloadType; - - for (const codecCapability of localCapabilities.codecs) { - if (codecCapability.kind !== kind - || codecCapability.name === 'rtx') { - continue; // eslint-disable-line no-continue - } - - codecPayloadType = codecCapability.preferredPayloadType; - codecs.push({ - clockRate: codecCapability.clockRate, - maxptime: codecCapability.maxptime, - mimeType: codecCapability.mimeType, - name: codecCapability.name, - numChannels: codecCapability.numChannels, - parameters: codecCapability.parameters, - payloadType: codecCapability.preferredPayloadType, - ptime: codecCapability.ptime, - rtcpFeedback: codecCapability.rtcpFeedback - }); - - break; - } - - if (rtxSsrc) { - for (const codecCapability of localCapabilities.codecs) { - if (codecCapability.kind !== kind - || codecCapability.name !== 'rtx') { - continue; // eslint-disable-line no-continue - } - - codecs.push({ - clockRate: codecCapability.clockRate, - mimeType: codecCapability.mimeType, - name: 'rtx', - parameters: codecCapability.parameters, - payloadType: codecCapability.preferredPayloadType, - rtcpFeedback: codecCapability.rtcpFeedback - }); - - break; - } - } - - parameters.codecs = codecs; - - const encoding = { - active: true, - codecPayloadType, - ssrc - }; - - if (rtxSsrc) { - encoding.rtx = { - ssrc: rtxSsrc - }; - } - - parameters.encodings.push(encoding); - - for (const extension of localCapabilities.headerExtensions) { - if (extension.kind !== kind) { - continue; // eslint-disable-line no-continue - } - - parameters.headerExtensions.push({ - encrypt: extension.preferredEncrypt, - id: extension.preferredId, - uri: extension.uri - }); - } - - return parameters; - } - - /** - * Promise based implementation for getStats(). - * @return {Promise} - * @private - */ - _getStats(selector) { // eslint-disable-line no-unused-vars - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - // TODO: TBD - return Promise.reject(new Error('getStats() not yet implemented')); - } - - /** - * Handles the local initial answer. - * @return {Promise} - * @private - */ - _handleLocalInitialAnswer(desc) { - logger.debug('_handleLocalInitialAnswer(), desc:', desc); - - const sdpObject = desc.sdpObject; - - // Update local capabilities as decided by the app. - this._localCapabilities = ortcUtils.extractCapabilities(sdpObject); - - logger.debug('local capabilities:', this._localCapabilities); - - // TODO: Should inspect the answer given by the app and update our - // sending RTP parameters if, for example, the app modified SSRC - // values. - } - - /** - * Handles a local re-answer. - * @return {Promise} - * @private - */ - _handleLocalReAnswer(desc) { - logger.debug('_handleLocalReAnswer(), desc:', desc); - - const sdpObject = desc.sdpObject; - - // Update local capabilities as decided by the app. - this._localCapabilities = ortcUtils.extractCapabilities(sdpObject); - - logger.debug('local capabilities:', this._localCapabilities); - - // TODO: Should inspect the answer given by the app and update our - // sending RTP parameters if, for example, the app modified SSRC - // values. - } - - /** - * Handles the remote initial offer. - * @return {Promise} - * @private - */ - _handleRemoteInitialOffer(desc) { - logger.debug('_handleRemoteInitialOffer(), desc:', desc); - - const sdpObject = desc.sdpObject; - - // Set MID values. - this._mids = ortcUtils.extractMids(sdpObject); - - // Get remote RTP capabilities. - const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject); - - logger.debug('remote capabilities:', remoteCapabilities); - - // Get local RTP capabilities (filter them with remote capabilities). - this._localCapabilities - = ortcUtils.getLocalCapabilities(remoteCapabilities); - - // Start ICE and DTLS. - this._startIceAndDtls(desc); - } - - /** - * Handles a remote re-offer. - * @return {Promise} - * @private - */ - _handleRemoteReOffer(desc) { - logger.debug('_handleRemoteReOffer(), desc:', desc); - - const sdpObject = desc.sdpObject; - - // Update MID values (just in case). - this._mids = ortcUtils.extractMids(sdpObject); - - // Get remote RTP capabilities (filter them with remote capabilities). - const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject); - - logger.debug('remote capabilities:', remoteCapabilities); - - // Update local RTP capabilities (just in case). - this._localCapabilities - = ortcUtils.getLocalCapabilities(remoteCapabilities); - } - - /** - * Start receiving remote media. - */ - _receiveMedia() { - logger.debug('_receiveMedia()'); - - const currentRemoteSsrcs = new Set(this._remoteTrackInfos.keys()); - const newRemoteTrackInfos - = ortcUtils.extractTrackInfos(this._remoteDescription.sdpObject); - - // Map of new remote MediaStream indexed by MediaStream.jitsiRemoteId. - const addedRemoteStreams = new Map(); - - // Map of remote MediaStream indexed by added MediaStreamTrack. - // NOTE: Just filled for already existing streams. - const addedRemoteTracks = new Map(); - - // Map of remote MediaStream indexed by removed MediaStreamTrack. - const removedRemoteTracks = new Map(); - - logger.debug( - '_receiveMedia() remote track infos:', newRemoteTrackInfos); - - // Check new tracks. - for (const [ ssrc, info ] of newRemoteTrackInfos) { - // If already handled, ignore it. - if (currentRemoteSsrcs.has(ssrc)) { - continue; // eslint-disable-line no-continue - } - - logger.debug(`_receiveMedia() new remote track, ssrc:${ssrc}`); - - // Otherwise append to the map. - this._remoteTrackInfos.set(ssrc, info); - - const kind = info.kind; - const rtxSsrc = info.rtxSsrc; - const streamRemoteId = info.streamId; - const trackRemoteId = info.trackId; - const cname = info.cname; - const isNewStream = !this._remoteStreams.has(streamRemoteId); - let stream; - - if (isNewStream) { - logger.debug( - `_receiveMedia() new remote stream, id:${streamRemoteId}`); - - // Create a new MediaStream. - stream = new MediaStream(); - - // Set custom property with the remote id. - stream.jitsiRemoteId = streamRemoteId; - - addedRemoteStreams.set(streamRemoteId, stream); - this._remoteStreams.set(streamRemoteId, stream); - } else { - stream = this._remoteStreams.get(streamRemoteId); - } - - const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); - const parameters = this._getParametersForRtpReceiver(kind, { - ssrc, - rtxSsrc, - cname - }); - - // Fill the info with the stream and rtpReceiver. - info.stream = stream; - info.rtpReceiver = rtpReceiver; - - logger.debug( - 'calling rtpReceiver.receive(), parameters:', parameters); - - // Start receiving media. - try { - rtpReceiver.receive(parameters); - - // Get the associated MediaStreamTrack. - const track = rtpReceiver.track; - - // Set custom property with the remote id. - track.jitsiRemoteId = trackRemoteId; - - // Add the track to the stream. - stream.addTrack(track); - - if (!addedRemoteStreams.has(streamRemoteId)) { - addedRemoteTracks.set(track, stream); - } - } catch (error) { - logger.error(`rtpReceiver.receive() failed:${error.message}`); - logger.error(error); - } - } - - // Check track removal. - for (const ssrc of currentRemoteSsrcs) { - if (newRemoteTrackInfos.has(ssrc)) { - continue; // eslint-disable-line no-continue - } - - logger.debug(`_receiveMedia() remote track removed, ssrc:${ssrc}`); - - const info = this._remoteTrackInfos.get(ssrc); - const stream = info.stream; - const rtpReceiver = info.rtpReceiver; - const track = rtpReceiver.track; - - try { - rtpReceiver.stop(); - } catch (error) { - logger.warn(`rtpReceiver.stop() failed:${error}`); - } - - removedRemoteTracks.set(track, stream); - stream.removeTrack(track); - this._remoteTrackInfos.delete(ssrc); - } - - // Emit MediaStream 'addtrack' for new tracks in already existing - // streams. - for (const [ track, stream ] of addedRemoteTracks) { - const event = new Event('addtrack'); - - event.track = track; - stream.dispatchEvent(event); - } - - // Emit MediaStream 'removetrack' for removed tracks. - for (const [ track, stream ] of removedRemoteTracks) { - const event = new Event('removetrack'); - - event.track = track; - stream.dispatchEvent(event); - } - - // Emit RTCPeerConnection 'addstream' for new remote streams. - for (const stream of addedRemoteStreams.values()) { - // Check whether at least a track was added, otherwise ignore it. - if (stream.getTracks().length === 0) { - logger.warn( - 'ignoring new stream for which no track could be added'); - - addedRemoteStreams.delete(stream.jitsiRemoteId); - this._remoteStreams.delete(stream.jitsiRemoteId); - } else { - this._emitAddStream(stream); - } - } - - // Emit RTCPeerConnection 'removestream' for removed remote streams. - for (const [ streamRemoteId, stream ] of this._remoteStreams) { - if (stream.getTracks().length > 0) { - continue; // eslint-disable-line no-continue - } - - this._remoteStreams.delete(streamRemoteId); - this._emitRemoveStream(stream); - } - } - - /** - * Implementation for removeStream(). - * @private - */ - _removeStream(stream) { - if (this._closed) { - throw new InvalidStateError('RTCPeerConnection closed'); - } - - // Stop and remove the RTCRtpSender associated to each track. - for (const track of stream.getTracks()) { - // Ignore if track not present. - if (!this._localTrackInfos.has(track.id)) { - continue; // eslint-disable-line no-continue - } - - const rtpSender = this._localTrackInfos.get(track.id).rtpSender; - - try { - rtpSender.stop(); - } catch (error) { - logger.warn(`rtpSender.stop() failed:${error}`); - } - - // Remove from the map. - this._localTrackInfos.delete(track.id); - } - - // It may need to renegotiate. - this._emitNegotiationNeeded(); - } - - /** - * Start sending our media to the remote. - */ - _sendMedia() { - logger.debug('_sendMedia()'); - - for (const info of this._localTrackInfos.values()) { - // Ignore if already sending. - if (info.sending) { - continue; // eslint-disable-line no-continue - } - - // Update sending field. - info.sending = true; - - const rtpSender = info.rtpSender; - const ssrc = info.ssrc; - const rtxSsrc = info.rtxSsrc; - const track = rtpSender.track; - const kind = track.kind; - - const parameters = this._getParametersForRtpSender(kind, { - ssrc, - rtxSsrc - }); - - logger.debug( - 'calling rtpSender.send(), parameters:', parameters); - - // Start rsending media. - try { - rtpSender.send(parameters); - } catch (error) { - logger.error(`rtpSender.send() failed:${error.message}`); - logger.error(error); - } - } - } - - /** - * Creates the RTCDtlsTransport. - * @private - */ - _setDtlsTransport(iceTransport) { - const dtlsTransport = new RTCDtlsTransport(iceTransport); - - // NOTE: Not yet implemented by Edge. - dtlsTransport.onstatechange = () => { - logger.debug( - 'dtlsTransport "statechange" event, ' - + `state:${dtlsTransport.state}`); - - this._emitConnectionStateChange(); - }; - - // NOTE: Not standard, but implemented by Edge. - dtlsTransport.ondtlsstatechange = () => { - logger.debug( - 'dtlsTransport "dtlsstatechange" event, ' - + `state:${dtlsTransport.state}`); - - this._emitConnectionStateChange(); - }; - - dtlsTransport.onerror = ev => { - let message; - - if (ev.message) { - message = ev.message; - } else if (ev.error) { - message = ev.error.message; - } - - logger.error(`dtlsTransport "error" event, message:${message}`); - - // TODO: Edge does not set state to 'failed' on error. We should - // hack it. - - this._emitConnectionStateChange(); - }; - - this._dtlsTransport = dtlsTransport; - } - - /** - * Creates the RTCIceGatherer. - * @private - */ - _setIceGatherer(pcConfig) { - const iceGatherOptions = { - gatherPolicy: pcConfig.iceTransportPolicy || 'all', - iceServers: pcConfig.iceServers || [] - }; - const iceGatherer = new RTCIceGatherer(iceGatherOptions); - - // NOTE: Not yet implemented by Edge. - iceGatherer.onstatechange = () => { - logger.debug( - `iceGatherer "statechange" event, state:${iceGatherer.state}`); - - this._updateAndEmitIceGatheringStateChange(iceGatherer.state); - }; - - iceGatherer.onlocalcandidate = ev => { - let candidate = ev.candidate; - - // NOTE: Not yet implemented by Edge. - const complete = ev.complete; - - logger.debug( - 'iceGatherer "localcandidate" event, candidate:', candidate); - - // NOTE: Instead of null candidate or complete:true, current Edge - // signals end of gathering with an empty candidate object. - if (complete - || !candidate - || Object.keys(candidate).length === 0) { - - candidate = null; - - this._updateAndEmitIceGatheringStateChange( - RTCIceGatheringState.complete); - this._emitIceCandidate(null); - } else { - this._emitIceCandidate(candidate); - } - }; - - iceGatherer.onerror = ev => { - const errorCode = ev.errorCode; - const errorText = ev.errorText; - - logger.error( - `iceGatherer "error" event, errorCode:${errorCode}, ` - + `errorText:${errorText}`); - }; - - // NOTE: Not yet implemented by Edge, which starts gathering - // automatically. - try { - iceGatherer.gather(); - } catch (error) { - logger.warn(`iceGatherer.gather() failed:${error}`); - } - - this._iceGatherer = iceGatherer; - } - - /** - * Creates the RTCIceTransport. - * @private - */ - _setIceTransport(iceGatherer) { - const iceTransport = new RTCIceTransport(iceGatherer); - - // NOTE: Not yet implemented by Edge. - iceTransport.onstatechange = () => { - logger.debug( - 'iceTransport "statechange" event, ' - + `state:${iceTransport.state}`); - - this._emitIceConnectionStateChange(); - }; - - // NOTE: Not standard, but implemented by Edge. - iceTransport.onicestatechange = () => { - logger.debug( - 'iceTransport "icestatechange" event, ' - + `state:${iceTransport.state}`); - - if (iceTransport.state === 'completed') { - logger.debug( - 'nominated candidate pair:', - iceTransport.getNominatedCandidatePair()); - - // TODO: TMP - logger.warn( - 'nominated candidate pair:', - iceTransport.getNominatedCandidatePair()); - } - - this._emitIceConnectionStateChange(); - }; - - iceTransport.oncandidatepairchange = ev => { - logger.debug( - 'iceTransport "candidatepairchange" event, ' - + `pair:${ev.pair}`); - }; - - this._iceTransport = iceTransport; - } - - /** - * Promise based implementation for setLocalDescription(). - * @returns {Promise} - * @private - */ - _setLocalDescription(desc) { - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - let localDescription; - - try { - localDescription = new RTCSessionDescription(desc); - } catch (error) { - return Promise.reject(new TypeError( - `invalid RTCSessionDescriptionInit: ${error}`)); - } - - switch (desc.type) { - case 'offer': { - if (this.signalingState !== RTCSignalingState.stable) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - // NOTE: P2P mode not yet supported, so createOffer() should never - // has been called, neither setLocalDescription() with an offer. - return Promise.reject(new TypeError( - 'setLocalDescription() with type "offer" not supported')); - } - case 'answer': { - if (this.signalingState !== RTCSignalingState.haveRemoteOffer) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - const isLocalInitialAnswer = Boolean(!this._localDescription); - - return Promise.resolve() - .then(() => { - // Different handling for initial answer and re-answer. - if (isLocalInitialAnswer) { - return this._handleLocalInitialAnswer(localDescription); - } else { // eslint-disable-line no-else-return - return this._handleLocalReAnswer(localDescription); - } - }) - .then(() => { - logger.debug('setLocalDescription() succeed'); - - // Update local description. - this._localDescription = localDescription; - - // Update signaling state. - this._updateAndEmitSignalingStateChange( - RTCSignalingState.stable); - - // If initial answer, emit buffered ICE candidates. - if (isLocalInitialAnswer) { - this._emitBufferedIceCandidates(); - } - - // Send our RTP. - this._sendMedia(); - - // Receive remote RTP. - this._receiveMedia(); - }) - .catch(error => { - logger.error( - `setLocalDescription() failed: ${error.message}`); - logger.error(error); - - throw error; - }); - } - default: - return Promise.reject(new TypeError( - `unsupported description.type "${desc.type}"`)); - } - } - - /** - * Promise based implementation for setRemoteDescription(). - * @returns {Promise} - * @private - */ - _setRemoteDescription(desc) { - if (this._closed) { - return Promise.reject( - new InvalidStateError('RTCPeerConnection closed')); - } - - let remoteDescription; - - try { - remoteDescription = new RTCSessionDescription(desc); - } catch (error) { - return Promise.reject(new TypeError( - `invalid RTCSessionDescriptionInit: ${error}`)); - } - - switch (desc.type) { - case 'offer': { - if (this.signalingState !== RTCSignalingState.stable) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - const isRemoteInitialOffer = Boolean(!this._remoteDescription); - - return Promise.resolve() - .then(() => { - // Different handling for initial answer and re-answer. - if (isRemoteInitialOffer) { - return this._handleRemoteInitialOffer( - remoteDescription); - } else { // eslint-disable-line no-else-return - return this._handleRemoteReOffer(remoteDescription); - } - }) - .then(() => { - logger.debug('setRemoteDescription() succeed'); - - // Update remote description. - this._remoteDescription = remoteDescription; - - // Update signaling state. - this._updateAndEmitSignalingStateChange( - RTCSignalingState.haveRemoteOffer); - }) - .catch(error => { - logger.error(`setRemoteDescription() failed: ${error}`); - - throw error; - }); - } - case 'answer': { - if (this.signalingState !== RTCSignalingState.haveLocalOffer) { - return Promise.reject(new InvalidStateError( - `invalid signalingState "${this.signalingState}"`)); - } - - // NOTE: P2P mode not yet supported, so createOffer() should never - // has been called, neither setRemoteDescription() with an answer. - return Promise.reject(new TypeError( - 'setRemoteDescription() with type "answer" not supported')); - } - default: - return Promise.reject(new TypeError( - `unsupported description.type "${desc.type}"`)); - } - } - - /** - * Start ICE and DTLS connection procedures. - * @param {RTCSessionDescription} desc - Remote description. - */ - _startIceAndDtls(desc) { - const sdpObject = desc.sdpObject; - const remoteIceParameters - = ortcUtils.extractIceParameters(sdpObject); - const remoteIceCandidates - = ortcUtils.extractIceCandidates(sdpObject); - const remoteDtlsParameters - = ortcUtils.extractDtlsParameters(sdpObject); - - // TODO: TMP - logger.warn(`_startIceAndDtls() ICE remote [username:${remoteIceParameters.usernameFragment}, password:${remoteIceParameters.password}`); - logger.warn('_startIceAndDtls() remoteIceParameters:'); - console.warn(remoteIceParameters); - - // Start the RTCIceTransport. - switch (desc.type) { - case 'offer': - this._iceTransport.start( - this._iceGatherer, remoteIceParameters, 'controlled'); - break; - case 'answer': - this._iceTransport.start( - this._iceGatherer, remoteIceParameters, 'controlling'); - break; - } - - // Add remote ICE candidates. - // NOTE: Remove candidates that Edge doesn't like. - for (const candidate of remoteIceCandidates) { - if (candidate.port === 0 || candidate.port === 9) { - continue; // eslint-disable-line no-continue - } - - this._iceTransport.addRemoteCandidate(candidate); - } - - // Also signal a 'complete' candidate as per spec. - // NOTE: It should be {complete: true} but Edge prefers {}. - // NOTE: We know that addCandidate() is never used so we need to signal - // end of candidates (otherwise the RTCIceTransport never enters the - // 'completed' state). - this._iceTransport.addRemoteCandidate({}); - - // Set desired remote DTLS role (as we receive the offer). - switch (desc.type) { - case 'offer': - remoteDtlsParameters.role = 'server'; - break; - case 'answer': - remoteDtlsParameters.role = 'client'; - break; - } - - // Start RTCDtlsTransport. - this._dtlsTransport.start(remoteDtlsParameters); - } - - /** - * May update iceGatheringState and emit 'icegatheringstatechange' event. - * @private - */ - _updateAndEmitIceGatheringStateChange(state) { - if (this._closed || state === this.iceGatheringState) { - return; - } - - this._iceGatheringState = state; - - logger.debug( - 'emitting "icegatheringstatechange", iceGatheringState:', - this.iceGatheringState); - - const event = new yaeti.Event('icegatheringstatechange'); - - this.dispatchEvent(event); - } - - /** - * May update signalingState and emit 'signalingstatechange' event. - * @private - */ - _updateAndEmitSignalingStateChange(state) { - if (state === this.signalingState) { - return; - } - - this._signalingState = state; - - logger.debug( - 'emitting "signalingstatechange", signalingState:', - this.signalingState); - - const event = new yaeti.Event('signalingstatechange'); - - this.dispatchEvent(event); - } + /** + */ + constructor(pcConfig) { + super(); + + logger.debug('constructor() pcConfig:', pcConfig); + + // Buffered local ICE candidates (in WebRTC format). + // @type {sequence} + this._bufferedIceCandidates = []; + + // Closed flag. + // @type {Boolean} + this._closed = false; + + // RTCDtlsTransport. + // @type {RTCDtlsTransport} + this._dtlsTransport = null; + + // RTCIceGatherer. + // @type {RTCIceGatherer} + this._iceGatherer = null; + + // RTCPeerConnection iceGatheringState. + // NOTE: This should not be needed, but Edge does not implement + // iceGatherer.state. + // @type {RTCIceGatheringState} + this._iceGatheringState = RTCIceGatheringState.new; + + // RTCIceTransport. + // @type {RTCIceTransport} + this._iceTransport = null; + + // Local RTP capabilities (filtered with remote ones). + // @type {RTCRtpCapabilities} + this._localCapabilities = null; + + // Local RTCSessionDescription. + // @type {RTCSessionDescription} + this._localDescription = null; + + // Map with info regarding local media. + // - index: MediaStreamTrack.id + // - value: Object + // - rtpSender: Associated RTCRtpSender instance + // - stream: Associated MediaStream instance + // - ssrc: Provisional or definitive SSRC + // - rtxSsrc: Provisional or definitive SSRC for RTX + // - sending: Boolean indicating whether rtpSender.send() was called. + this._localTrackInfos = new Map(); + + // Ordered Map with MID as key and kind as value. + // @type {map} + this._mids = new Map(); + + // Remote RTCSessionDescription. + // @type {RTCSessionDescription} + this._remoteDescription = null; + + // Map of remote streams. + // - index: MediaStream.jitsiRemoteId (as signaled in remote SDP) + // - value: MediaStream (locally generated so id does not match) + // @type {map} + this._remoteStreams = new Map(); + + // Map with info about receiving media. + // - index: Media SSRC + // - value: Object + // - kind: 'audio' / 'video' + // - ssrc: Media SSRC + // - rtxSsrc: RTX SSRC (may be unset) + // - streamId: MediaStream.jitsiRemoteId + // - trackId: MediaStreamTrack.jitsiRemoteId + // - cname: CNAME + // - stream: MediaStream + // - track: MediaStreamTrack + // - rtpReceiver: Associated RTCRtpReceiver instance + // @type {map} + this._remoteTrackInfos = new Map(); + + // Local SDP global fields. + this._sdpGlobalFields = { + id: utils.randomNumber(), + version: 0 + }; + + // RTCPeerConnection signalingState. + // @type {RTCSignalingState} + this._signalingState = RTCSignalingState.stable; + + // Create the RTCIceGatherer. + this._setIceGatherer(pcConfig); + + // Create the RTCIceTransport. + this._setIceTransport(this._iceGatherer); + + // Create the RTCDtlsTransport. + this._setDtlsTransport(this._iceTransport); + } + + /** + * Current ICE+DTLS connection state. + * @return {RTCPeerConnectionState} + */ + get connectionState() { + return this._dtlsTransport.state; + } + + /** + * Current ICE connection state. + * @return {RTCIceConnectionState} + */ + get iceConnectionState() { + return this._iceTransport.state; + } + + /** + * Current ICE gathering state. + * @return {RTCIceGatheringState} + */ + get iceGatheringState() { + return this._iceGatheringState; + } + + /** + * Gets the local description. + * @return {RTCSessionDescription} + */ + get localDescription() { + return this._localDescription; + } + + /** + * Gets the remote description. + * @return {RTCSessionDescription} + */ + get remoteDescription() { + return this._remoteDescription; + } + + /** + * Current signaling state. + * @return {RTCSignalingState} + */ + get signalingState() { + return this._signalingState; + } + + /** + * Adds a remote ICE candidate. Implements both the old callbacks based + * signature and the new Promise based style. + * + * Arguments in Promise mode: + * @param {RTCIceCandidate} candidate + * + * Arguments in callbacks mode: + * @param {RTCIceCandidate} candidate + * @param {function()} callback + * @param {function(error)} errback + */ + addIceCandidate(candidate, ...args) { + let usePromise; + let callback; + let errback; + + if (!candidate) { + throw new TypeError('candidate missing'); + } + + if (args.length === 0) { + usePromise = true; + } else { + usePromise = false; + callback = args[0]; + errback = args[1]; + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('addIceCandidate() candidate:', candidate); + + if (usePromise) { + return this._addIceCandidate(candidate); + } + + this._addIceCandidate(candidate) + .then(() => callback()) + .catch(error => errback(error)); + } + + /** + * Adds a local MediaStream. + * @param {MediaStream} stream. + * NOTE: Deprecated API. + */ + addStream(stream) { + logger.debug('addStream()'); + + this._addStream(stream); + } + + /** + * Closes the RTCPeerConnection and all the underlying ORTC objects. + */ + close() { + if (this._closed) { + return; + } + + this._closed = true; + + logger.debug('close()'); + + this._updateAndEmitSignalingStateChange(RTCSignalingState.closed); + + // Close RTCIceGatherer. + // NOTE: Not yet implemented by Edge. + try { + this._iceGatherer.close(); + } catch (error) { + logger.warn(`iceGatherer.close() failed:${error}`); + } + + // Close RTCIceTransport. + try { + this._iceTransport.stop(); + } catch (error) { + logger.warn(`iceTransport.stop() failed:${error}`); + } + + // Close RTCDtlsTransport. + try { + this._dtlsTransport.stop(); + } catch (error) { + logger.warn(`dtlsTransport.stop() failed:${error}`); + } + + // Close and clear RTCRtpSenders. + for (const info of this._localTrackInfos.values()) { + const rtpSender = info.rtpSender; + + try { + rtpSender.stop(); + } catch (error) { + logger.warn(`rtpSender.stop() failed:${error}`); + } + } + + this._localTrackInfos.clear(); + + // Close and clear RTCRtpReceivers. + for (const info of this._remoteTrackInfos.values()) { + const rtpReceiver = info.rtpReceiver; + + try { + rtpReceiver.stop(); + } catch (error) { + logger.warn(`rtpReceiver.stop() failed:${error}`); + } + } + + this._remoteTrackInfos.clear(); + + // Clear remote streams. + this._remoteStreams.clear(); + } + + /** + * Creates a local answer. Implements both the old callbacks based signature + * and the new Promise based style. + * + * Arguments in Promise mode: + * @param {RTCOfferOptions} [options] + * + * Arguments in callbacks mode: + * @param {function(desc)} callback + * @param {function(error)} errback + * @param {MediaConstraints} [constraints] + */ + createAnswer(...args) { + let usePromise; + let options; + let callback; + let errback; + + if (args.length <= 1) { + usePromise = true; + options = args[0]; + } else { + usePromise = false; + callback = args[0]; + errback = args[1]; + options = args[2]; + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('createAnswer() options:', options); + + if (usePromise) { + return this._createAnswer(options); + } + + this._createAnswer(options) + .then(desc => callback(desc)) + .catch(error => errback(error)); + } + + /** + * Creates a RTCDataChannel. + */ + createDataChannel() { + logger.debug('createDataChannel()'); + + // NOTE: DataChannels not implemented in Edge. + throw new Error('createDataChannel() not supported in Edge'); + } + + /** + * Creates a local offer. Implements both the old callbacks based signature + * and the new Promise based style. + * + * Arguments in Promise mode: + * @param {RTCOfferOptions} [options] + * + * Arguments in callbacks mode: + * @param {function(desc)} callback + * @param {function(error)} errback + * @param {MediaConstraints} [constraints] + */ + createOffer(...args) { + let usePromise; + let options; + let callback; + let errback; + + if (args.length <= 1) { + usePromise = true; + options = args[0]; + } else { + usePromise = false; + callback = args[0]; + errback = args[1]; + options = args[2]; + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('createOffer() options:', options); + + if (usePromise) { + return this._createOffer(options); + } + + this._createOffer(options) + .then(desc => callback(desc)) + .catch(error => errback(error)); + } + + /** + * Gets a sequence of local MediaStreams. + * @return {sequence} + */ + getLocalStreams() { + return Array.from(this._localTrackInfos.values()) + .map(info => info.stream) + .filter((elem, pos, arr) => arr.indexOf(elem) === pos); + } + + /** + * Gets a sequence of remote MediaStreams. + * @return {sequence} + */ + getRemoteStreams() { + return Array.from(this._remoteStreams.values()); + } + + /** + * Get RTP statistics. Implements both the old callbacks based signature + * and the new Promise based style. + * + * Arguments in Promise mode: + * @param {MediaStreamTrack} [selector] + * + * Arguments in callbacks mode: + * @param {MediaStreamTrack} [selector] + * @param {function(desc)} callback + * @param {function(error)} errback + */ + getStats(...args) { + let usePromise; + let selector; + let callback; + let errback; + + if (args.length <= 1) { + usePromise = true; + selector = args[0]; + } else { + usePromise = false; + + if (args.length === 2) { + callback = args[0]; + errback = args[1]; + } else { + selector = args[0]; + callback = args[1]; + errback = args[2]; + } + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('getStats()'); + + if (usePromise) { + return this._getStats(selector); + } + + this._getStats(selector) + .then(stats => callback(stats)) + .catch(error => errback(error)); + } + + /** + * Removes a local MediaStream. + * @param {MediaStream} stream. + * NOTE: Deprecated API. + */ + removeStream(stream) { + logger.debug('removeStream()'); + + this._removeStream(stream); + } + + /** + * Applies a local description. Implements both the old callbacks based + * signature and the new Promise based style. + * + * Arguments in Promise mode: + * @param {RTCSessionDescriptionInit} desc + * + * Arguments in callbacks mode: + * @param {RTCSessionDescription} desc + * @param {function()} callback + * @param {function(error)} errback + */ + setLocalDescription(desc, ...args) { + let usePromise; + let callback; + let errback; + + if (!desc) { + throw new TypeError('description missing'); + } + + if (args.length === 0) { + usePromise = true; + } else { + usePromise = false; + callback = args[0]; + errback = args[1]; + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('setLocalDescription() desc:', desc); + + if (usePromise) { + return this._setLocalDescription(desc); + } + + this._setLocalDescription(desc) + .then(() => callback()) + .catch(error => errback(error)); + } + + /** + * Applies a remote description. Implements both the old callbacks based + * signature and the new Promise based style. + * + * Arguments in Promise mode: + * @param {RTCSessionDescriptionInit} desc + * + * Arguments in callbacks mode: + * @param {RTCSessionDescription} desc + * @param {function()} callback + * @param {function(error)} errback + */ + setRemoteDescription(desc, ...args) { + let usePromise; + let callback; + let errback; + + if (!desc) { + throw new TypeError('description missing'); + } + + if (args.length === 0) { + usePromise = true; + } else { + usePromise = false; + callback = args[0]; + errback = args[1]; + + if (typeof callback !== 'function') { + throw new TypeError('callback missing'); + } + + if (typeof errback !== 'function') { + throw new TypeError('errback missing'); + } + } + + logger.debug('setRemoteDescription() desc:', desc); + + if (usePromise) { + return this._setRemoteDescription(desc); + } + + this._setRemoteDescription(desc) + .then(() => callback()) + .catch(error => errback(error)); + } + + /** + * Promise based implementation for addIceCandidate(). + * @return {Promise} + * @private + */ + _addIceCandidate(candidate) { // eslint-disable-line no-unused-vars + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + // NOTE: Edge does not support Trickle-ICE so just candidates in the + // remote SDP are applied. Candidates given later would be just + // ignored, so notify the called about that. + return Promise.reject(new Error('addIceCandidate() not supported')); + } + + /** + * Implementation for addStream(). + * @private + */ + _addStream(stream) { + if (this._closed) { + throw new InvalidStateError('RTCPeerConnection closed'); + } + + // Create a RTCRtpSender for each track. + for (const track of stream.getTracks()) { + // Ignore if ended. + if (track.readyState === 'ended') { + logger.warn('ignoring ended MediaStreamTrack'); + + continue; // eslint-disable-line no-continue + } + + // Ignore if track is already present. + if (this._localTrackInfos.has(track.id)) { + logger.warn('ignoring already handled MediaStreamTrack'); + + continue; // eslint-disable-line no-continue + } + + const rtpSender = new RTCRtpSender(track, this._dtlsTransport); + + // Store it in the map. + this._localTrackInfos.set(track.id, { + rtpSender, + stream + }); + } + + // It may need to renegotiate. + this._emitNegotiationNeeded(); + } + + /** + * Promise based implementation for createAnswer(). + * @returns {Promise} + * @private + */ + _createAnswer(options) { // eslint-disable-line no-unused-vars + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + if (this.signalingState !== RTCSignalingState.haveRemoteOffer) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + // Create an answer. + const localDescription = this._createLocalDescription('answer'); + + // Resolve with it. + return Promise.resolve(localDescription); + } + + /** + * Creates the local RTCSessionDescription. + * @param {String} type - 'offer' / 'answer'. + * @return {RTCSessionDescription} + */ + _createLocalDescription(type) { + const sdpObject = {}; + const localIceParameters = this._iceGatherer.getLocalParameters(); + const localIceCandidates = this._iceGatherer.getLocalCandidates(); + const localDtlsParameters = this._dtlsTransport.getLocalParameters(); + const remoteDtlsParameters = this._dtlsTransport.getRemoteParameters(); + const localCapabilities = this._localCapabilities; + const localTrackInfos = this._localTrackInfos; + + // Increase SDP version if an offer. + if (type === 'offer') { + this._sdpGlobalFields.version++; + } + + // SDP global fields. + sdpObject.version = 0; + sdpObject.origin = { + address: '127.0.0.1', + ipVer: 4, + netType: 'IN', + sessionId: this._sdpGlobalFields.id, + sessionVersion: this._sdpGlobalFields.version, + username: 'jitsi-ortc-webrtc-shim' + }; + sdpObject.name = '-'; + sdpObject.timing = { + start: 0, + stop: 0 + }; + sdpObject.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + sdpObject.groups = [ + { + mids: Array.from(this._mids.keys()).join(' '), + type: 'BUNDLE' + } + ]; + sdpObject.media = []; + + // DTLS fingerprint. + sdpObject.fingerprint = { + hash: localDtlsParameters.fingerprints[0].value, + type: localDtlsParameters.fingerprints[0].algorithm + }; + + // Let's check whether there is video RTX. + let hasVideoRtx = false; + + for (const codec of localCapabilities.codecs) { + if (codec.kind === 'video' && codec.name === 'rtx') { + hasVideoRtx = true; + break; + } + } + + // Add m= sections. + for (const [ mid, kind ] of this._mids) { + addMediaSection.call(this, mid, kind); + } + + // Create a RTCSessionDescription. + const localDescription = new RTCSessionDescription({ + type, + _sdpObject: sdpObject + }); + + logger.debug('_createLocalDescription():', localDescription); + + return localDescription; + + /** + * Add a m= section. + */ + function addMediaSection(mid, kind) { + const mediaObject = {}; + + // m= line. + mediaObject.type = kind; + + switch (kind) { + case 'audio': + case 'video': + mediaObject.protocol = 'RTP/SAVPF'; + mediaObject.port = 9; + mediaObject.direction = 'sendrecv'; + break; + case 'application': + mediaObject.protocol = 'DTLS/SCTP'; + mediaObject.port = 0; // Reject m section. + mediaObject.payloads = '0'; // Just put something. + mediaObject.direction = 'inactive'; + break; + } + + // c= line. + mediaObject.connection = { + ip: '127.0.0.1', + version: 4 + }; + + // a=mid attribute. + mediaObject.mid = mid; + + // ICE. + mediaObject.iceUfrag = localIceParameters.usernameFragment; + mediaObject.icePwd = localIceParameters.password; + mediaObject.candidates = []; + + for (const candidate of localIceCandidates) { + const candidateObject = {}; + + // rtcp-mux is assumed, so component is always 1 (RTP). + candidateObject.component = 1; + candidateObject.foundation = candidate.foundation; + candidateObject.ip = candidate.ip; + candidateObject.port = candidate.port; + candidateObject.priority = candidate.priority; + candidateObject.transport + = candidate.protocol.toLowerCase(); + candidateObject.type = candidate.type; + if (candidateObject.transport === 'tcp') { + candidateObject.tcptype = candidate.tcpType; + } + + mediaObject.candidates.push(candidateObject); + } + + mediaObject.endOfCandidates = 'end-of-candidates'; + + // DTLS. + // If 'offer' always use 'actpass'. + if (type === 'offer') { + mediaObject.setup = 'actpass'; + } else { + mediaObject.setup = remoteDtlsParameters.role === 'server' + ? 'active' : 'passive'; + } + + if (kind === 'audio' || kind === 'video') { + mediaObject.rtp = []; + mediaObject.rtcpFb = []; + mediaObject.fmtp = []; + + // Array of payload types. + const payloads = []; + + // Add codecs. + for (const codec of localCapabilities.codecs) { + if (codec.kind && codec.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + payloads.push(codec.preferredPayloadType); + + const rtpObject = { + codec: codec.name, + payload: codec.preferredPayloadType, + rate: codec.clockRate + }; + + if (codec.numChannels > 1) { + rtpObject.encoding = codec.numChannels; + } + + mediaObject.rtp.push(rtpObject); + + // If codec has parameters add them into a=fmtp attributes. + if (codec.parameters) { + const paramFmtp = { + config: '', + payload: codec.preferredPayloadType + }; + + for (const name of Object.keys(codec.parameters)) { + /* eslint-disable max-depth */ + if (paramFmtp.config) { + paramFmtp.config += ';'; + } + /* eslint-enable max-depth */ + + paramFmtp.config + += `${name}=${codec.parameters[name]}`; + } + + if (paramFmtp.config) { + mediaObject.fmtp.push(paramFmtp); + } + } + + // Set RTCP feedback. + for (const fb of codec.rtcpFeedback || []) { + mediaObject.rtcpFb.push({ + payload: codec.preferredPayloadType, + subtype: fb.parameter || undefined, + type: fb.type + }); + } + } + + // If there are no codecs, set this m section as unavailable. + if (payloads.length === 0) { + mediaObject.payloads = '9'; // Just put something. + mediaObject.port = 0; + mediaObject.direction = 'inactive'; + } else { + mediaObject.payloads = payloads.join(' '); + } + + // SSRCs. + mediaObject.ssrcs = []; + mediaObject.ssrcGroups = []; + + // Add RTP sending stuff. + for (const info of localTrackInfos.values()) { + const rtpSender = info.rtpSender; + const streamId = info.stream.id; + const track = rtpSender.track; + + // Ignore if ended. + if (track.readyState === 'ended') { + continue; // eslint-disable-line no-continue + } + + if (track.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + // Set a random provisional SSRC if not set. + if (!info.ssrc) { + info.ssrc = utils.randomNumber(); + } + + // Whether RTX should be enabled. + const enableRtx = hasVideoRtx && track.kind === 'video'; + + // Set a random provisional RTX SSRC if not set. + if (enableRtx && !info.rtxSsrc) { + info.rtxSsrc = info.ssrc + 1; + } + + mediaObject.ssrcs.push({ + attribute: 'cname', + id: info.ssrc, + value: CNAME + }); + + mediaObject.ssrcs.push({ + attribute: 'msid', + id: info.ssrc, + value: `${streamId} ${track.id}` + }); + + mediaObject.ssrcs.push({ + attribute: 'mslabel', + id: info.ssrc, + value: streamId + }); + + mediaObject.ssrcs.push({ + attribute: 'label', + id: info.ssrc, + value: track.id + }); + + if (enableRtx) { + mediaObject.ssrcs.push({ + attribute: 'cname', + id: info.rtxSsrc, + value: CNAME + }); + + mediaObject.ssrcs.push({ + attribute: 'msid', + id: info.rtxSsrc, + value: `${streamId} ${track.id}` + }); + + mediaObject.ssrcs.push({ + attribute: 'mslabel', + id: info.rtxSsrc, + value: streamId + }); + + mediaObject.ssrcs.push({ + attribute: 'label', + id: info.rtxSsrc, + value: track.id + }); + + mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${info.ssrc} ${info.rtxSsrc}` + }); + } + } + + // RTP header extensions. + mediaObject.ext = []; + + for (const extension of localCapabilities.headerExtensions) { + if (extension.kind && extension.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + mediaObject.ext.push({ + value: extension.preferredId, + uri: extension.uri + }); + } + + // a=rtcp-mux attribute. + mediaObject.rtcpMux = 'rtcp-mux'; + + // a=rtcp-rsize. + mediaObject.rtcpRsize = 'rtcp-rsize'; + } + + // Add the media section. + sdpObject.media.push(mediaObject); + } + } + + /** + * Promise based implementation for createOffer(). + * @returns {Promise} + * @private + */ + _createOffer(options) { // eslint-disable-line no-unused-vars + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + if (this.signalingState !== RTCSignalingState.stable) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + // NOTE: P2P mode not yet supported, so createOffer() should never be + // called. + // return Promise.reject(new Error('createoOffer() not yet supported')); + + // HACK: Create an offer assuming this is called before any + // setRemoteDescription() and assuming that setLocalDescription() + // wont be called with this offer. + + const sdpObject = {}; + const localIceParameters = this._iceGatherer.getLocalParameters(); + const localIceCandidates = this._iceGatherer.getLocalCandidates(); + const localDtlsParameters = this._dtlsTransport.getLocalParameters(); + const localCapabilities = RTCRtpReceiver.getCapabilities(); + const localTrackInfos = this._localTrackInfos; + const mids = new Map([ ['audio', 'audio'], ['video', 'video'] ]); + + // SDP global fields. + sdpObject.version = 0; + sdpObject.origin = { + address: '127.0.0.1', + ipVer: 4, + netType: 'IN', + sessionId: this._sdpGlobalFields.id, + sessionVersion: this._sdpGlobalFields.version, + username: 'jitsi-ortc-webrtc-shim' + }; + sdpObject.name = '-'; + sdpObject.timing = { + start: 0, + stop: 0 + }; + sdpObject.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + sdpObject.groups = [ + { + mids: Array.from(mids.keys()).join(' '), + type: 'BUNDLE' + } + ]; + sdpObject.media = []; + + // DTLS fingerprint. + sdpObject.fingerprint = { + hash: localDtlsParameters.fingerprints[0].value, + type: localDtlsParameters.fingerprints[0].algorithm + }; + + // Let's check whether there is video RTX. + let hasVideoRtx = false; + + for (const codec of localCapabilities.codecs) { + if (codec.kind === 'video' && codec.name === 'rtx') { + hasVideoRtx = true; + break; + } + } + + // Add m= sections. + for (const [ mid, kind ] of mids) { + addMediaSection.call(this, mid, kind); + } + + // Create a RTCSessionDescription. + const localDescription = new RTCSessionDescription({ + type: 'offer', + _sdpObject: sdpObject + }); + + logger.debug('_createLocalDescription():', localDescription); + + // Resolve with it. + return Promise.resolve(localDescription); + + /** + * Add a m= section. + */ + function addMediaSection(mid, kind) { + const mediaObject = {}; + + // m= line. + mediaObject.type = kind; + + switch (kind) { + case 'audio': + case 'video': + mediaObject.protocol = 'RTP/SAVPF'; + mediaObject.port = 9; + mediaObject.direction = 'sendrecv'; + break; + case 'application': + mediaObject.protocol = 'DTLS/SCTP'; + mediaObject.port = 0; // Reject m section. + mediaObject.payloads = '0'; // Just put something. + mediaObject.direction = 'inactive'; + break; + } + + // c= line. + mediaObject.connection = { + ip: '127.0.0.1', + version: 4 + }; + + // a=mid attribute. + mediaObject.mid = mid; + + // ICE. + mediaObject.iceUfrag = localIceParameters.usernameFragment; + mediaObject.icePwd = localIceParameters.password; + mediaObject.candidates = []; + + for (const candidate of localIceCandidates) { + const candidateObject = {}; + + // rtcp-mux is assumed, so component is always 1 (RTP). + candidateObject.component = 1; + candidateObject.foundation = candidate.foundation; + candidateObject.ip = candidate.ip; + candidateObject.port = candidate.port; + candidateObject.priority = candidate.priority; + candidateObject.transport + = candidate.protocol.toLowerCase(); + candidateObject.type = candidate.type; + if (candidateObject.transport === 'tcp') { + candidateObject.tcptype = candidate.tcpType; + } + + mediaObject.candidates.push(candidateObject); + } + + mediaObject.endOfCandidates = 'end-of-candidates'; + + // DTLS. + // 'offer' so always use 'actpass'. + mediaObject.setup = 'actpass'; + + if (kind === 'audio' || kind === 'video') { + mediaObject.rtp = []; + mediaObject.rtcpFb = []; + mediaObject.fmtp = []; + + // Array of payload types. + const payloads = []; + + // Add codecs. + for (const codec of localCapabilities.codecs) { + if (codec.kind && codec.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + payloads.push(codec.preferredPayloadType); + + const rtpObject = { + codec: codec.name, + payload: codec.preferredPayloadType, + rate: codec.clockRate + }; + + if (codec.numChannels > 1) { + rtpObject.encoding = codec.numChannels; + } + + mediaObject.rtp.push(rtpObject); + + // If codec has parameters add them into a=fmtp attributes. + if (codec.parameters) { + const paramFmtp = { + config: '', + payload: codec.preferredPayloadType + }; + + for (const name of Object.keys(codec.parameters)) { + /* eslint-disable max-depth */ + if (paramFmtp.config) { + paramFmtp.config += ';'; + } + /* eslint-enable max-depth */ + + paramFmtp.config + += `${name}=${codec.parameters[name]}`; + } + + if (paramFmtp.config) { + mediaObject.fmtp.push(paramFmtp); + } + } + + // Set RTCP feedback. + for (const fb of codec.rtcpFeedback || []) { + mediaObject.rtcpFb.push({ + payload: codec.preferredPayloadType, + subtype: fb.parameter || undefined, + type: fb.type + }); + } + } + + // If there are no codecs, set this m section as unavailable. + if (payloads.length === 0) { + mediaObject.payloads = '9'; // Just put something. + mediaObject.port = 0; + mediaObject.direction = 'inactive'; + } else { + mediaObject.payloads = payloads.join(' '); + } + + // SSRCs. + mediaObject.ssrcs = []; + mediaObject.ssrcGroups = []; + + // Add RTP sending stuff. + for (const info of localTrackInfos.values()) { + const rtpSender = info.rtpSender; + const streamId = info.stream.id; + const track = rtpSender.track; + + // Ignore if ended. + if (track.readyState === 'ended') { + continue; // eslint-disable-line no-continue + } + + if (track.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + // Set a random provisional SSRC if not set. + if (!info.ssrc) { + info.ssrc = utils.randomNumber(); + } + + // Whether RTX should be enabled. + const enableRtx = hasVideoRtx && track.kind === 'video'; + + // Set a random provisional RTX SSRC if not set. + if (enableRtx && !info.rtxSsrc) { + info.rtxSsrc = info.ssrc + 1; + } + + mediaObject.ssrcs.push({ + attribute: 'cname', + id: info.ssrc, + value: CNAME + }); + + mediaObject.ssrcs.push({ + attribute: 'msid', + id: info.ssrc, + value: `${streamId} ${track.id}` + }); + + mediaObject.ssrcs.push({ + attribute: 'mslabel', + id: info.ssrc, + value: streamId + }); + + mediaObject.ssrcs.push({ + attribute: 'label', + id: info.ssrc, + value: track.id + }); + + if (enableRtx) { + mediaObject.ssrcs.push({ + attribute: 'cname', + id: info.rtxSsrc, + value: CNAME + }); + + mediaObject.ssrcs.push({ + attribute: 'msid', + id: info.rtxSsrc, + value: `${streamId} ${track.id}` + }); + + mediaObject.ssrcs.push({ + attribute: 'mslabel', + id: info.rtxSsrc, + value: streamId + }); + + mediaObject.ssrcs.push({ + attribute: 'label', + id: info.rtxSsrc, + value: track.id + }); + + mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${info.ssrc} ${info.rtxSsrc}` + }); + } + } + + // RTP header extensions. + mediaObject.ext = []; + + for (const extension of localCapabilities.headerExtensions) { + if (extension.kind && extension.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + mediaObject.ext.push({ + value: extension.preferredId, + uri: extension.uri + }); + } + + // a=rtcp-mux attribute. + mediaObject.rtcpMux = 'rtcp-mux'; + + // a=rtcp-rsize. + mediaObject.rtcpRsize = 'rtcp-rsize'; + } + + // Add the media section. + sdpObject.media.push(mediaObject); + } + } + + /** + * Emit 'addstream' event. + * @private + */ + _emitAddStream(stream) { + if (this._closed) { + return; + } + + logger.debug('emitting "addstream"'); + + const event = new yaeti.Event('addstream'); + + event.stream = stream; + this.dispatchEvent(event); + } + + /** + * May emit buffered ICE candidates. + * @private + */ + _emitBufferedIceCandidates() { + if (this._closed) { + return; + } + + for (const sdpCandidate of this._bufferedIceCandidates) { + if (!sdpCandidate) { + continue; // eslint-disable-line no-continue + } + + // Now we have set the MID values of the SDP O/A, so let's fill the + // sdpMIndex of the candidate. + sdpCandidate.sdpMIndex = this._mids.keys().next().value; + + logger.debug( + 'emitting buffered "icecandidate", candidate:', sdpCandidate); + + const event = new yaeti.Event('icecandidate'); + + event.candidate = sdpCandidate; + this.dispatchEvent(event); + } + + this._bufferedIceCandidates = []; + } + + /** + * May emit 'connectionstatechange' event. + * @private + */ + _emitConnectionStateChange() { + if (this._closed && this.connectionState !== 'closed') { + return; + } + + logger.debug( + 'emitting "connectionstatechange", connectionState:', + this.connectionState); + + const event = new yaeti.Event('connectionstatechange'); + + this.dispatchEvent(event); + } + + /** + * May emit 'icecandidate' event. + * @private + */ + _emitIceCandidate(candidate) { + if (this._closed) { + return; + } + + let sdpCandidate = null; + + if (candidate) { + // NOTE: We assume BUNDLE so let's just emit candidates for the + // first m= section. + const sdpMIndex = this._mids.keys().next().value; + const sdpMLineIndex = 0; + let sdpAttribute + = `candidate:${candidate.foundation} 1 ${candidate.protocol}` + + ` ${candidate.priority} ${candidate.ip} ${candidate.port}` + + ` typ ${candidate.type}`; + + if (candidate.relatedAddress) { + sdpAttribute += ` raddr ${candidate.relatedAddress}`; + } + if (candidate.relatedPort) { + sdpAttribute += ` rport ${candidate.relatedPort}`; + } + if (candidate.protocol === 'tcp') { + sdpAttribute += ` tcptype ${candidate.tcpType}`; + } + + sdpCandidate = { + candidate: sdpAttribute, + component: 1, // rtcp-mux assumed, so always 1 (RTP). + foundation: candidate.foundation, + ip: candidate.ip, + port: candidate.port, + priority: candidate.priority, + protocol: candidate.protocol, + type: candidate.type, + sdpMIndex, + sdpMLineIndex + }; + + if (candidate.protocol === 'tcp') { + sdpCandidate.tcptype = candidate.tcpType; + } + if (candidate.relatedAddress) { + sdpCandidate.relatedAddress = candidate.relatedAddress; + } + if (candidate.relatedPort) { + sdpCandidate.relatedPort = candidate.relatedPort; + } + } + + // If we don't have yet a local description, buffer the candidate. + if (this._localDescription) { + logger.debug( + 'emitting "icecandidate", candidate:', sdpCandidate); + + const event = new yaeti.Event('icecandidate'); + + event.candidate = sdpCandidate; + this.dispatchEvent(event); + } else { + logger.debug( + 'buffering gathered ICE candidate:', sdpCandidate); + + this._bufferedIceCandidates.push(sdpCandidate); + } + } + + /** + * May emit 'iceconnectionstatechange' event. + * @private + */ + _emitIceConnectionStateChange() { + if (this._closed && this.iceConnectionState !== 'closed') { + return; + } + + logger.debug( + 'emitting "iceconnectionstatechange", iceConnectionState:', + this.iceConnectionState); + + const event = new yaeti.Event('iceconnectionstatechange'); + + this.dispatchEvent(event); + } + + /** + * May emit 'negotiationneeded' event. + * @private + */ + _emitNegotiationNeeded() { + // Ignore if signalingState is not 'stable'. + if (this.signalingState !== RTCSignalingState.stable) { + return; + } + + logger.debug('emitting "negotiationneeded"'); + + const event = new yaeti.Event('negotiationneeded'); + + this.dispatchEvent(event); + } + + /** + * Emit 'removestream' event. + * @private + */ + _emitRemoveStream(stream) { + if (this._closed) { + return; + } + + logger.debug('emitting "removestream"'); + + const event = new yaeti.Event('removestream'); + + event.stream = stream; + this.dispatchEvent(event); + } + + /** + * Get RTP parameters for a RTCRtpReceiver. + * @private + * @return {RTCRtpParameters} + */ + _getParametersForRtpReceiver(kind, data) { + const ssrc = data.ssrc; + const rtxSsrc = data.rtxSsrc; + const cname = data.cname; + const localCapabilities = this._localCapabilities; + const parameters = { + codecs: [], + degradationPreference: 'balanced', + encodings: [], + headerExtensions: [], + muxId: '', + rtcp: { + cname, + compound: true, // NOTE: Implemented in Edge. + mux: true, + reducedSize: true // NOTE: Not yet implemented in Edge. + } + }; + + const codecs = []; + let codecPayloadType; + + for (const codecCapability of localCapabilities.codecs) { + if (codecCapability.kind !== kind + || codecCapability.name === 'rtx') { + continue; // eslint-disable-line no-continue + } + + codecPayloadType = codecCapability.preferredPayloadType; + codecs.push({ + clockRate: codecCapability.clockRate, + maxptime: codecCapability.maxptime, + mimeType: codecCapability.mimeType, + name: codecCapability.name, + numChannels: codecCapability.numChannels, + parameters: codecCapability.parameters, + payloadType: codecCapability.preferredPayloadType, + ptime: codecCapability.ptime, + rtcpFeedback: codecCapability.rtcpFeedback + }); + + break; + } + + if (rtxSsrc) { + for (const codecCapability of localCapabilities.codecs) { + if (codecCapability.kind !== kind + || codecCapability.name !== 'rtx') { + continue; // eslint-disable-line no-continue + } + + codecs.push({ + clockRate: codecCapability.clockRate, + mimeType: codecCapability.mimeType, + name: 'rtx', + parameters: codecCapability.parameters, + payloadType: codecCapability.preferredPayloadType, + rtcpFeedback: codecCapability.rtcpFeedback + }); + + break; + } + } + + parameters.codecs = codecs; + + const encoding = { + active: true, + codecPayloadType, + ssrc + }; + + if (rtxSsrc) { + encoding.rtx = { + ssrc: rtxSsrc + }; + } + + parameters.encodings.push(encoding); + + for (const extension of localCapabilities.headerExtensions) { + if (extension.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + parameters.headerExtensions.push({ + encrypt: extension.preferredEncrypt, + id: extension.preferredId, + uri: extension.uri + }); + } + + return parameters; + } + + /** + * Get RTP parameters for a RTCRtpSender. + * @private + * @return {RTCRtpParameters} + */ + _getParametersForRtpSender(kind, data) { + const ssrc = data.ssrc; + const rtxSsrc = data.rtxSsrc; + const cname = CNAME; + const localCapabilities = this._localCapabilities; + const parameters = { + codecs: [], + degradationPreference: 'balanced', + encodings: [], + headerExtensions: [], + muxId: '', + rtcp: { + cname, + compound: true, // NOTE: Implemented in Edge. + mux: true, + reducedSize: true // NOTE: Not yet implemented in Edge. + } + }; + + const codecs = []; + let codecPayloadType; + + for (const codecCapability of localCapabilities.codecs) { + if (codecCapability.kind !== kind + || codecCapability.name === 'rtx') { + continue; // eslint-disable-line no-continue + } + + codecPayloadType = codecCapability.preferredPayloadType; + codecs.push({ + clockRate: codecCapability.clockRate, + maxptime: codecCapability.maxptime, + mimeType: codecCapability.mimeType, + name: codecCapability.name, + numChannels: codecCapability.numChannels, + parameters: codecCapability.parameters, + payloadType: codecCapability.preferredPayloadType, + ptime: codecCapability.ptime, + rtcpFeedback: codecCapability.rtcpFeedback + }); + + break; + } + + if (rtxSsrc) { + for (const codecCapability of localCapabilities.codecs) { + if (codecCapability.kind !== kind + || codecCapability.name !== 'rtx') { + continue; // eslint-disable-line no-continue + } + + codecs.push({ + clockRate: codecCapability.clockRate, + mimeType: codecCapability.mimeType, + name: 'rtx', + parameters: codecCapability.parameters, + payloadType: codecCapability.preferredPayloadType, + rtcpFeedback: codecCapability.rtcpFeedback + }); + + break; + } + } + + parameters.codecs = codecs; + + const encoding = { + active: true, + codecPayloadType, + ssrc + }; + + if (rtxSsrc) { + encoding.rtx = { + ssrc: rtxSsrc + }; + } + + parameters.encodings.push(encoding); + + for (const extension of localCapabilities.headerExtensions) { + if (extension.kind !== kind) { + continue; // eslint-disable-line no-continue + } + + parameters.headerExtensions.push({ + encrypt: extension.preferredEncrypt, + id: extension.preferredId, + uri: extension.uri + }); + } + + return parameters; + } + + /** + * Promise based implementation for getStats(). + * @return {Promise} + * @private + */ + _getStats(selector) { // eslint-disable-line no-unused-vars + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + // TODO: TBD + return Promise.reject(new Error('getStats() not yet implemented')); + } + + /** + * Handles the local initial answer. + * @return {Promise} + * @private + */ + _handleLocalInitialAnswer(desc) { + logger.debug('_handleLocalInitialAnswer(), desc:', desc); + + const sdpObject = desc.sdpObject; + + // Update local capabilities as decided by the app. + this._localCapabilities = ortcUtils.extractCapabilities(sdpObject); + + logger.debug('local capabilities:', this._localCapabilities); + + // TODO: Should inspect the answer given by the app and update our + // sending RTP parameters if, for example, the app modified SSRC + // values. + } + + /** + * Handles a local re-answer. + * @return {Promise} + * @private + */ + _handleLocalReAnswer(desc) { + logger.debug('_handleLocalReAnswer(), desc:', desc); + + const sdpObject = desc.sdpObject; + + // Update local capabilities as decided by the app. + this._localCapabilities = ortcUtils.extractCapabilities(sdpObject); + + logger.debug('local capabilities:', this._localCapabilities); + + // TODO: Should inspect the answer given by the app and update our + // sending RTP parameters if, for example, the app modified SSRC + // values. + } + + /** + * Handles the remote initial offer. + * @return {Promise} + * @private + */ + _handleRemoteInitialOffer(desc) { + logger.debug('_handleRemoteInitialOffer(), desc:', desc); + + const sdpObject = desc.sdpObject; + + // Set MID values. + this._mids = ortcUtils.extractMids(sdpObject); + + // Get remote RTP capabilities. + const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject); + + logger.debug('remote capabilities:', remoteCapabilities); + + // Get local RTP capabilities (filter them with remote capabilities). + this._localCapabilities + = ortcUtils.getLocalCapabilities(remoteCapabilities); + + // Start ICE and DTLS. + this._startIceAndDtls(desc); + } + + /** + * Handles a remote re-offer. + * @return {Promise} + * @private + */ + _handleRemoteReOffer(desc) { + logger.debug('_handleRemoteReOffer(), desc:', desc); + + const sdpObject = desc.sdpObject; + + // Update MID values (just in case). + this._mids = ortcUtils.extractMids(sdpObject); + + // Get remote RTP capabilities (filter them with remote capabilities). + const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject); + + logger.debug('remote capabilities:', remoteCapabilities); + + // Update local RTP capabilities (just in case). + this._localCapabilities + = ortcUtils.getLocalCapabilities(remoteCapabilities); + } + + /** + * Start receiving remote media. + */ + _receiveMedia() { + logger.debug('_receiveMedia()'); + + const currentRemoteSsrcs = new Set(this._remoteTrackInfos.keys()); + const newRemoteTrackInfos + = ortcUtils.extractTrackInfos(this._remoteDescription.sdpObject); + + // Map of new remote MediaStream indexed by MediaStream.jitsiRemoteId. + const addedRemoteStreams = new Map(); + + // Map of remote MediaStream indexed by added MediaStreamTrack. + // NOTE: Just filled for already existing streams. + const addedRemoteTracks = new Map(); + + // Map of remote MediaStream indexed by removed MediaStreamTrack. + const removedRemoteTracks = new Map(); + + logger.debug( + '_receiveMedia() remote track infos:', newRemoteTrackInfos); + + // Check new tracks. + for (const [ ssrc, info ] of newRemoteTrackInfos) { + // If already handled, ignore it. + if (currentRemoteSsrcs.has(ssrc)) { + continue; // eslint-disable-line no-continue + } + + logger.debug(`_receiveMedia() new remote track, ssrc:${ssrc}`); + + // Otherwise append to the map. + this._remoteTrackInfos.set(ssrc, info); + + const kind = info.kind; + const rtxSsrc = info.rtxSsrc; + const streamRemoteId = info.streamId; + const trackRemoteId = info.trackId; + const cname = info.cname; + const isNewStream = !this._remoteStreams.has(streamRemoteId); + let stream; + + if (isNewStream) { + logger.debug( + `_receiveMedia() new remote stream, id:${streamRemoteId}`); + + // Create a new MediaStream. + stream = new MediaStream(); + + // Set custom property with the remote id. + stream.jitsiRemoteId = streamRemoteId; + + addedRemoteStreams.set(streamRemoteId, stream); + this._remoteStreams.set(streamRemoteId, stream); + } else { + stream = this._remoteStreams.get(streamRemoteId); + } + + const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); + const parameters = this._getParametersForRtpReceiver(kind, { + ssrc, + rtxSsrc, + cname + }); + + // Store the track into the info object. + // NOTE: This should not be needed, but Edge has a bug: + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12399497/ + info.track = rtpReceiver.track; + + // Set error handler. + rtpReceiver.onerror = ev => { + logger.error('rtpReceiver "error" event, event:'); + logger.error(ev); + }; + + // Fill the info with the stream and rtpReceiver. + info.stream = stream; + info.rtpReceiver = rtpReceiver; + + logger.debug( + 'calling rtpReceiver.receive(), parameters:', parameters); + + // Start receiving media. + try { + rtpReceiver.receive(parameters); + + // Get the associated MediaStreamTrack. + const track = info.track; + + // Set custom property with the remote id. + track.jitsiRemoteId = trackRemoteId; + + // TODO: TMP + logger.warn(`new remote track [stream.jitsiRemoteId:${stream.jitsiRemoteId}, track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`); + + // Add the track to the stream. + stream.addTrack(track); + + if (!addedRemoteStreams.has(streamRemoteId)) { + addedRemoteTracks.set(track, stream); + } + } catch (error) { + logger.error(`rtpReceiver.receive() failed:${error.message}`); + logger.error(error); + } + } + + // Check track removal. + for (const ssrc of currentRemoteSsrcs) { + if (newRemoteTrackInfos.has(ssrc)) { + continue; // eslint-disable-line no-continue + } + + logger.debug(`_receiveMedia() remote track removed, ssrc:${ssrc}`); + + const info = this._remoteTrackInfos.get(ssrc); + const stream = info.stream; + const track = info.track; + const rtpReceiver = info.rtpReceiver; + + // TODO: TMP + logger.warn(`remote track removed [track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`); + + try { + rtpReceiver.stop(); + } catch (error) { + logger.warn(`rtpReceiver.stop() failed:${error}`); + } + + removedRemoteTracks.set(track, stream); + stream.removeTrack(track); + this._remoteTrackInfos.delete(ssrc); + } + + // Emit MediaStream 'addtrack' for new tracks in already existing + // streams. + for (const [ track, stream ] of addedRemoteTracks) { + const event = new Event('addtrack'); + + event.track = track; + stream.dispatchEvent(event); + } + + // Emit MediaStream 'removetrack' for removed tracks. + for (const [ track, stream ] of removedRemoteTracks) { + // TODO: TMP + logger.warn(`emit "removetrack" [stream.jitsiRemoteId:${stream.jitsiRemoteId}, track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`); + + const event = new Event('removetrack'); + + event.track = track; + stream.dispatchEvent(event); + } + + // Emit RTCPeerConnection 'addstream' for new remote streams. + for (const stream of addedRemoteStreams.values()) { + // Check whether at least a track was added, otherwise ignore it. + if (stream.getTracks().length === 0) { + logger.warn( + 'ignoring new stream for which no track could be added'); + + addedRemoteStreams.delete(stream.jitsiRemoteId); + this._remoteStreams.delete(stream.jitsiRemoteId); + } else { + this._emitAddStream(stream); + } + } + + // Emit RTCPeerConnection 'removestream' for removed remote streams. + for (const [ streamRemoteId, stream ] of this._remoteStreams) { + // TODO: TMP + logger.warn(`remote stream [streamRemoteId:${streamRemoteId}, jitsiRemoteId:${stream.jitsiRemoteId}, tracks:${stream.getTracks().length}]`); + + if (stream.getTracks().length > 0) { + continue; // eslint-disable-line no-continue + } + + this._remoteStreams.delete(streamRemoteId); + this._emitRemoveStream(stream); + } + } + + /** + * Implementation for removeStream(). + * @private + */ + _removeStream(stream) { + if (this._closed) { + throw new InvalidStateError('RTCPeerConnection closed'); + } + + // Stop and remove the RTCRtpSender associated to each track. + for (const track of stream.getTracks()) { + // Ignore if track not present. + if (!this._localTrackInfos.has(track.id)) { + continue; // eslint-disable-line no-continue + } + + const rtpSender = this._localTrackInfos.get(track.id).rtpSender; + + try { + rtpSender.stop(); + } catch (error) { + logger.warn(`rtpSender.stop() failed:${error}`); + } + + // Remove from the map. + this._localTrackInfos.delete(track.id); + } + + // It may need to renegotiate. + this._emitNegotiationNeeded(); + } + + /** + * Start sending our media to the remote. + */ + _sendMedia() { + logger.debug('_sendMedia()'); + + for (const info of this._localTrackInfos.values()) { + // Ignore if already sending. + if (info.sending) { + continue; // eslint-disable-line no-continue + } + + // Update sending field. + info.sending = true; + + const rtpSender = info.rtpSender; + const ssrc = info.ssrc; + const rtxSsrc = info.rtxSsrc; + const track = rtpSender.track; + const kind = track.kind; + + const parameters = this._getParametersForRtpSender(kind, { + ssrc, + rtxSsrc + }); + + logger.debug( + 'calling rtpSender.send(), parameters:', parameters); + + // Start rsending media. + try { + rtpSender.send(parameters); + } catch (error) { + logger.error(`rtpSender.send() failed:${error.message}`); + logger.error(error); + } + } + } + + /** + * Creates the RTCDtlsTransport. + * @private + */ + _setDtlsTransport(iceTransport) { + const dtlsTransport = new RTCDtlsTransport(iceTransport); + + // NOTE: Not yet implemented by Edge. + dtlsTransport.onstatechange = () => { + logger.debug( + 'dtlsTransport "statechange" event, ' + + `state:${dtlsTransport.state}`); + + this._emitConnectionStateChange(); + }; + + // NOTE: Not standard, but implemented by Edge. + dtlsTransport.ondtlsstatechange = () => { + logger.debug( + 'dtlsTransport "dtlsstatechange" event, ' + + `state:${dtlsTransport.state}`); + + this._emitConnectionStateChange(); + }; + + dtlsTransport.onerror = ev => { + let message; + + if (ev.message) { + message = ev.message; + } else if (ev.error) { + message = ev.error.message; + } + + logger.error(`dtlsTransport "error" event, message:${message}`); + + // TODO: Edge does not set state to 'failed' on error. We should + // hack it. + + this._emitConnectionStateChange(); + }; + + this._dtlsTransport = dtlsTransport; + } + + /** + * Creates the RTCIceGatherer. + * @private + */ + _setIceGatherer(pcConfig) { + const iceGatherOptions = { + gatherPolicy: pcConfig.iceTransportPolicy || 'all', + iceServers: pcConfig.iceServers || [] + }; + const iceGatherer = new RTCIceGatherer(iceGatherOptions); + + // NOTE: Not yet implemented by Edge. + iceGatherer.onstatechange = () => { + logger.debug( + `iceGatherer "statechange" event, state:${iceGatherer.state}`); + + this._updateAndEmitIceGatheringStateChange(iceGatherer.state); + }; + + iceGatherer.onlocalcandidate = ev => { + let candidate = ev.candidate; + + // NOTE: Not yet implemented by Edge. + const complete = ev.complete; + + logger.debug( + 'iceGatherer "localcandidate" event, candidate:', candidate); + + // NOTE: Instead of null candidate or complete:true, current Edge + // signals end of gathering with an empty candidate object. + if (complete + || !candidate + || Object.keys(candidate).length === 0) { + + candidate = null; + + this._updateAndEmitIceGatheringStateChange( + RTCIceGatheringState.complete); + this._emitIceCandidate(null); + } else { + this._emitIceCandidate(candidate); + } + }; + + iceGatherer.onerror = ev => { + const errorCode = ev.errorCode; + const errorText = ev.errorText; + + logger.error( + `iceGatherer "error" event, errorCode:${errorCode}, ` + + `errorText:${errorText}`); + }; + + // NOTE: Not yet implemented by Edge, which starts gathering + // automatically. + try { + iceGatherer.gather(); + } catch (error) { + logger.warn(`iceGatherer.gather() failed:${error}`); + } + + this._iceGatherer = iceGatherer; + } + + /** + * Creates the RTCIceTransport. + * @private + */ + _setIceTransport(iceGatherer) { + const iceTransport = new RTCIceTransport(iceGatherer); + + // NOTE: Not yet implemented by Edge. + iceTransport.onstatechange = () => { + logger.debug( + 'iceTransport "statechange" event, ' + + `state:${iceTransport.state}`); + + this._emitIceConnectionStateChange(); + }; + + // NOTE: Not standard, but implemented by Edge. + iceTransport.onicestatechange = () => { + logger.debug( + 'iceTransport "icestatechange" event, ' + + `state:${iceTransport.state}`); + + if (iceTransport.state === 'completed') { + logger.debug( + 'nominated candidate pair:', + iceTransport.getNominatedCandidatePair()); + } + + this._emitIceConnectionStateChange(); + }; + + iceTransport.oncandidatepairchange = ev => { + logger.debug( + 'iceTransport "candidatepairchange" event, ' + + `pair:${ev.pair}`); + }; + + this._iceTransport = iceTransport; + } + + /** + * Promise based implementation for setLocalDescription(). + * @returns {Promise} + * @private + */ + _setLocalDescription(desc) { + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + let localDescription; + + try { + localDescription = new RTCSessionDescription(desc); + } catch (error) { + return Promise.reject(new TypeError( + `invalid RTCSessionDescriptionInit: ${error}`)); + } + + switch (desc.type) { + case 'offer': { + if (this.signalingState !== RTCSignalingState.stable) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + // NOTE: P2P mode not yet supported, so createOffer() should never + // has been called, neither setLocalDescription() with an offer. + return Promise.reject(new TypeError( + 'setLocalDescription() with type "offer" not supported')); + } + case 'answer': { + if (this.signalingState !== RTCSignalingState.haveRemoteOffer) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + const isLocalInitialAnswer = Boolean(!this._localDescription); + + return Promise.resolve() + .then(() => { + // Different handling for initial answer and re-answer. + if (isLocalInitialAnswer) { + return this._handleLocalInitialAnswer(localDescription); + } else { // eslint-disable-line no-else-return + return this._handleLocalReAnswer(localDescription); + } + }) + .then(() => { + logger.debug('setLocalDescription() succeed'); + + // Update local description. + this._localDescription = localDescription; + + // Update signaling state. + this._updateAndEmitSignalingStateChange( + RTCSignalingState.stable); + + // If initial answer, emit buffered ICE candidates. + if (isLocalInitialAnswer) { + this._emitBufferedIceCandidates(); + } + + // Send our RTP. + this._sendMedia(); + + // Receive remote RTP. + this._receiveMedia(); + }) + .catch(error => { + logger.error( + `setLocalDescription() failed: ${error.message}`); + logger.error(error); + + throw error; + }); + } + default: + return Promise.reject(new TypeError( + `unsupported description.type "${desc.type}"`)); + } + } + + /** + * Promise based implementation for setRemoteDescription(). + * @returns {Promise} + * @private + */ + _setRemoteDescription(desc) { + if (this._closed) { + return Promise.reject( + new InvalidStateError('RTCPeerConnection closed')); + } + + let remoteDescription; + + try { + remoteDescription = new RTCSessionDescription(desc); + } catch (error) { + return Promise.reject(new TypeError( + `invalid RTCSessionDescriptionInit: ${error}`)); + } + + switch (desc.type) { + case 'offer': { + if (this.signalingState !== RTCSignalingState.stable) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + const isRemoteInitialOffer = Boolean(!this._remoteDescription); + + return Promise.resolve() + .then(() => { + // Different handling for initial answer and re-answer. + if (isRemoteInitialOffer) { + return this._handleRemoteInitialOffer( + remoteDescription); + } else { // eslint-disable-line no-else-return + return this._handleRemoteReOffer(remoteDescription); + } + }) + .then(() => { + logger.debug('setRemoteDescription() succeed'); + + // Update remote description. + this._remoteDescription = remoteDescription; + + // Update signaling state. + this._updateAndEmitSignalingStateChange( + RTCSignalingState.haveRemoteOffer); + }) + .catch(error => { + logger.error(`setRemoteDescription() failed: ${error}`); + + throw error; + }); + } + case 'answer': { + if (this.signalingState !== RTCSignalingState.haveLocalOffer) { + return Promise.reject(new InvalidStateError( + `invalid signalingState "${this.signalingState}"`)); + } + + // NOTE: P2P mode not yet supported, so createOffer() should never + // has been called, neither setRemoteDescription() with an answer. + return Promise.reject(new TypeError( + 'setRemoteDescription() with type "answer" not supported')); + } + default: + return Promise.reject(new TypeError( + `unsupported description.type "${desc.type}"`)); + } + } + + /** + * Start ICE and DTLS connection procedures. + * @param {RTCSessionDescription} desc - Remote description. + */ + _startIceAndDtls(desc) { + const sdpObject = desc.sdpObject; + const remoteIceParameters + = ortcUtils.extractIceParameters(sdpObject); + const remoteIceCandidates + = ortcUtils.extractIceCandidates(sdpObject); + const remoteDtlsParameters + = ortcUtils.extractDtlsParameters(sdpObject); + + // Start the RTCIceTransport. + switch (desc.type) { + case 'offer': + this._iceTransport.start( + this._iceGatherer, remoteIceParameters, 'controlled'); + break; + case 'answer': + this._iceTransport.start( + this._iceGatherer, remoteIceParameters, 'controlling'); + break; + } + + // Add remote ICE candidates. + // NOTE: Remove candidates that Edge doesn't like. + for (const candidate of remoteIceCandidates) { + if (candidate.port === 0 || candidate.port === 9) { + continue; // eslint-disable-line no-continue + } + + this._iceTransport.addRemoteCandidate(candidate); + } + + // Also signal a 'complete' candidate as per spec. + // NOTE: It should be {complete: true} but Edge prefers {}. + // NOTE: We know that addCandidate() is never used so we need to signal + // end of candidates (otherwise the RTCIceTransport never enters the + // 'completed' state). + this._iceTransport.addRemoteCandidate({}); + + // Set desired remote DTLS role (as we receive the offer). + switch (desc.type) { + case 'offer': + remoteDtlsParameters.role = 'server'; + break; + case 'answer': + remoteDtlsParameters.role = 'client'; + break; + } + + // Start RTCDtlsTransport. + this._dtlsTransport.start(remoteDtlsParameters); + } + + /** + * May update iceGatheringState and emit 'icegatheringstatechange' event. + * @private + */ + _updateAndEmitIceGatheringStateChange(state) { + if (this._closed || state === this.iceGatheringState) { + return; + } + + this._iceGatheringState = state; + + logger.debug( + 'emitting "icegatheringstatechange", iceGatheringState:', + this.iceGatheringState); + + const event = new yaeti.Event('icegatheringstatechange'); + + this.dispatchEvent(event); + } + + /** + * May update signalingState and emit 'signalingstatechange' event. + * @private + */ + _updateAndEmitSignalingStateChange(state) { + if (state === this.signalingState) { + return; + } + + this._signalingState = state; + + logger.debug( + 'emitting "signalingstatechange", signalingState:', + this.signalingState); + + const event = new yaeti.Event('signalingstatechange'); + + this.dispatchEvent(event); + } } diff --git a/app/lib/index.jsx b/app/lib/index.jsx index 438bdf2..e4fcba6 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -1,7 +1,6 @@ 'use strict'; import browser from 'bowser'; -import webrtc from 'webrtc-adapter'; // eslint-disable-line no-unused-vars import domready from 'domready'; import UrlParse from 'url-parse'; import React from 'react'; @@ -14,20 +13,29 @@ import edgeRTCPeerConnection from './edge/RTCPeerConnection'; import edgeRTCSessionDescription from './edge/RTCSessionDescription'; import App from './components/App'; -const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z_\-]+)$'); +const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z_-]+)$'); const logger = new Logger(); injectTapEventPlugin(); logger.debug('detected browser [name:"%s", version:%s]', browser.name, browser.version); +// If Edge, use the Jitsi RTCPeerConnection shim. if (browser.msedge) { - logger.debug('EDGE detected, overriding WebRTC global classes'); + logger.debug('Edge detected, overriding RTCPeerConnection and RTCSessionDescription'); window.RTCPeerConnection = edgeRTCPeerConnection; window.RTCSessionDescription = edgeRTCSessionDescription; } +// Otherwise, do almost anything. +else +{ + window.RTCPeerConnection = + window.webkitRTCPeerConnection || + window.mozRTCPeerConnection || + window.RTCPeerConnection; +} domready(() => { diff --git a/app/lib/utils.js b/app/lib/utils.js index 06512bb..56a67eb 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -4,6 +4,8 @@ import browser from 'bowser'; import randomNumberLib from 'random-number'; import Logger from './Logger'; +global.BROWSER = browser; + const logger = new Logger('utils'); const randomNumberGenerator = randomNumberLib.generator( { @@ -42,6 +44,18 @@ export function isPlanB() return false; } +/** + * Unfortunately Edge produces rtpSender.send() to fail when receiving media + * from others and removing/adding a local track. + */ +export function canChangeResolution() +{ + if (browser.msedge) + return false; + + return true; +} + export function randomNumber() { return randomNumberGenerator(); diff --git a/app/package.json b/app/package.json index 78b085d..2b0e221 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "mediasoup-demo-app", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "mediasoup demo app", "author": "IƱaki Baz Castillo ", @@ -8,25 +8,24 @@ "main": "lib/index.jsx", "dependencies": { "babel-runtime": "^6.23.0", - "bowser": "^1.6.1", + "bowser": "^1.7.0", "classnames": "^2.2.5", - "debug": "^2.6.4", + "debug": "^2.6.8", "domready": "^1.0.8", "hark": "ibc/hark#main-with-raf", - "material-ui": "^0.18.2", + "material-ui": "^0.18.3", "prop-types": "^15.5.10", "protoo-client": "^1.1.4", "random-number": "0.0.7", "random-string": "^0.2.0", - "react": "^15.5.4", - "react-clipboard.js": "^1.0.1", - "react-dom": "^15.5.4", + "react": "^15.6.1", + "react-clipboard.js": "^1.1.2", + "react-dom": "^15.6.1", "react-notification-system": "ibc/react-notification-system#master", "react-tap-event-plugin": "^2.0.1", - "react-transition-group": "^1.1.3", + "react-transition-group": "^1.2.0", "sdp-transform": "^2.3.0", - "url-parse": "^1.1.8", - "webrtc-adapter": "^4.0.0", + "url-parse": "^1.1.9", "yaeti": "^1.0.1" }, "devDependencies": { @@ -35,16 +34,16 @@ "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babelify": "^7.3.0", - "browser-sync": "^2.18.8", - "browserify": "^14.3.0", - "del": "^2.2.2", + "browser-sync": "^2.18.12", + "browserify": "^14.4.0", + "del": "^3.0.0", "envify": "^4.0.0", - "eslint": "^3.19.0", - "eslint-plugin-import": "^2.2.0", - "eslint-plugin-react": "^7.0.1", + "eslint": "^4.0.0", + "eslint-plugin-import": "^2.3.0", + "eslint-plugin-react": "^7.1.0", "gulp": "git://github.com/gulpjs/gulp.git#4.0", "gulp-css-base64": "^1.3.4", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-header": "^1.8.8", "gulp-if": "^2.0.2", "gulp-plumber": "^1.1.0", diff --git a/server/lib/Room.js b/server/lib/Room.js index 1bec584..35bc04a 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -57,7 +57,8 @@ class Room extends EventEmitter }); }); - this._mediaRoom.on('audiolevels', (entries) => + // TODO: FIX + this._mediaRoom.on('____audiolevels', (entries) => { logger.debug('room "audiolevels" event'); diff --git a/server/package.json b/server/package.json index d827b6e..88f9390 100644 --- a/server/package.json +++ b/server/package.json @@ -8,9 +8,9 @@ "main": "lib/index.js", "dependencies": { "colors": "^1.1.2", - "debug": "^2.6.4", - "express": "^4.15.2", - "mediasoup": "^1.2.3", + "debug": "^2.6.8", + "express": "^4.15.3", + "mediasoup": "^1.2.5", "protoo-server": "^1.1.4" }, "devDependencies": { @@ -19,7 +19,7 @@ "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "gulp": "git://github.com/gulpjs/gulp.git#4.0", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-plumber": "^1.1.0" } }