twilio-video
Version:
Twilio Video JavaScript Library
1,136 lines (1,134 loc) • 83.6 kB
JavaScript
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
var DefaultBackoff = require('../../util/backoff');
var _a = require('../../webrtc'), DefaultRTCIceCandidate = _a.RTCIceCandidate, DefaultRTCPeerConnection = _a.RTCPeerConnection, DefaultRTCSessionDescription = _a.RTCSessionDescription, getStatistics = _a.getStats;
var util = require('../../webrtc/util');
var _b = require('../../util/constants'), DEFAULT_ICE_GATHERING_TIMEOUT_MS = _b.DEFAULT_ICE_GATHERING_TIMEOUT_MS, DEFAULT_LOG_LEVEL = _b.DEFAULT_LOG_LEVEL, DEFAULT_SESSION_TIMEOUT_SEC = _b.DEFAULT_SESSION_TIMEOUT_SEC, iceRestartBackoffConfig = _b.iceRestartBackoffConfig;
var _c = require('../../util/sdp'), addOrRewriteNewTrackIds = _c.addOrRewriteNewTrackIds, addOrRewriteTrackIds = _c.addOrRewriteTrackIds, createCodecMapForMediaSection = _c.createCodecMapForMediaSection, disableRtx = _c.disableRtx, enableDtxForOpus = _c.enableDtxForOpus, filterLocalCodecs = _c.filterLocalCodecs, getMediaSections = _c.getMediaSections, removeSSRCAttributes = _c.removeSSRCAttributes, revertSimulcast = _c.revertSimulcast, setCodecPreferences = _c.setCodecPreferences, setSimulcast = _c.setSimulcast;
var DefaultTimeout = require('../../util/timeout');
var _d = require('../../util/twilio-video-errors'), MediaClientLocalDescFailedError = _d.MediaClientLocalDescFailedError, MediaClientRemoteDescFailedError = _d.MediaClientRemoteDescFailedError;
var _e = require('../../util'), buildLogLevels = _e.buildLogLevels, getPlatform = _e.getPlatform, isChromeScreenShareTrack = _e.isChromeScreenShareTrack, oncePerTick = _e.oncePerTick, defer = _e.defer;
var IceBox = require('./icebox');
var DefaultIceConnectionMonitor = require('./iceconnectionmonitor.js');
var DataTrackReceiver = require('../../data/receiver');
var MediaTrackReceiver = require('../../media/track/receiver');
var StateMachine = require('../../statemachine');
var Log = require('../../util/log');
var TrackMatcher = require('../../util/sdp/trackmatcher');
var workaroundIssue8329 = require('../../util/sdp/issue8329');
var guess = util.guessBrowser();
var platform = getPlatform();
var isAndroid = /android/.test(platform);
var isChrome = guess === 'chrome';
var isFirefox = guess === 'firefox';
var isSafari = guess === 'safari';
var nInstances = 0;
/*
PeerConnectionV2 States
-----------------------
+------+ +--------+
| | | |
| open |--->| closed |
| | | |
+------+ +--------+
| ^ ^
| | |
| | |
v | |
+----------+ |
| | |
| updating |------+
| |
+----------+
*/
var states = {
open: [
'closed',
'updating'
],
updating: [
'closed',
'open'
],
closed: []
};
/**
* @extends StateMachine
* @property {id}
* @emits PeerConnectionV2#connectionStateChanged
* @emits PeerConnectionV2#iceConnectionStateChanged
* @emits PeerConnectionV2#candidates
* @emits PeerConnectionV2#description
*/
var PeerConnectionV2 = /** @class */ (function (_super) {
__extends(PeerConnectionV2, _super);
/**
* Construct a {@link PeerConnectionV2}.
* @param {string} id
* @param {EncodingParametersImpl} encodingParameters
* @param {PreferredCodecs} preferredCodecs
* @param {object} [options]
*/
function PeerConnectionV2(id, encodingParameters, preferredCodecs, options) {
var _this = _super.call(this, 'open', states) || this;
options = Object.assign({
enableDscp: false,
dummyAudioMediaStreamTrack: null,
isChromeScreenShareTrack: isChromeScreenShareTrack,
iceServers: [],
logLevel: DEFAULT_LOG_LEVEL,
offerOptions: {},
revertSimulcast: revertSimulcast,
sessionTimeout: DEFAULT_SESSION_TIMEOUT_SEC * 1000,
setCodecPreferences: setCodecPreferences,
setSimulcast: setSimulcast,
Backoff: DefaultBackoff,
IceConnectionMonitor: DefaultIceConnectionMonitor,
RTCIceCandidate: DefaultRTCIceCandidate,
RTCPeerConnection: DefaultRTCPeerConnection,
RTCSessionDescription: DefaultRTCSessionDescription,
Timeout: DefaultTimeout
}, options);
// NOTE(lrivas): We intentionally include the options object for backward compatibility.
// The ConnectOptions 'iceServers' and 'iceTransportPolicy' are part of the 'RTCConfiguration' interface and should be passed
// as 'rtcConfiguration.iceServers' and 'rtcConfiguration.iceTransportPolicy' respectively.
var configuration = getConfiguration(Object.assign({}, options, options.rtcConfiguration));
var logLevels = buildLogLevels(options.logLevel);
var RTCPeerConnection = options.RTCPeerConnection;
if (options.enableDscp === true) {
options.chromeSpecificConstraints = options.chromeSpecificConstraints || {};
options.chromeSpecificConstraints.optional = options.chromeSpecificConstraints.optional || [];
options.chromeSpecificConstraints.optional.push({ googDscp: true });
}
var log = options.log ? options.log.createLog('webrtc', _this) : new Log('webrtc', _this, logLevels, options.loggerName);
var peerConnection = new RTCPeerConnection(configuration, options.chromeSpecificConstraints);
if (options.dummyAudioMediaStreamTrack) {
peerConnection.addTrack(options.dummyAudioMediaStreamTrack);
}
Object.defineProperties(_this, {
_appliedTrackIdsToAttributes: {
value: new Map(),
writable: true
},
_dataChannels: {
value: new Map()
},
_dataTrackReceivers: {
value: new Set()
},
_descriptionRevision: {
writable: true,
value: 0
},
_didGenerateLocalCandidates: {
writable: true,
value: false
},
_enableDscp: {
value: options.enableDscp
},
_encodingParameters: {
value: encodingParameters
},
_isChromeScreenShareTrack: {
value: options.isChromeScreenShareTrack,
},
_iceGatheringFailed: {
value: false,
writable: true
},
_iceGatheringTimeout: {
value: new options.Timeout(function () { return _this._handleIceGatheringTimeout(); }, DEFAULT_ICE_GATHERING_TIMEOUT_MS, false)
},
_iceRestartBackoff: {
// eslint-disable-next-line new-cap
value: new options.Backoff(iceRestartBackoffConfig)
},
_instanceId: {
value: ++nInstances
},
_isIceConnectionInactive: {
writable: true,
value: false
},
_isIceLite: {
writable: true,
value: false
},
_isIceRestartBackoffInProgress: {
writable: true,
value: false
},
_isRestartingIce: {
writable: true,
value: false
},
_lastIceConnectionState: {
writable: true,
value: null
},
_lastStableDescriptionRevision: {
writable: true,
value: 0
},
_localCandidates: {
writable: true,
value: []
},
_localCodecs: {
value: new Set()
},
_localCandidatesRevision: {
writable: true,
value: 1
},
_localDescriptionWithoutSimulcast: {
writable: true,
value: null
},
_localDescription: {
writable: true,
value: null
},
_localUfrag: {
writable: true,
value: null
},
_log: {
value: log
},
_eventObserver: {
value: options.eventObserver
},
_remoteCodecMaps: {
value: new Map()
},
_rtpSenders: {
value: new Map()
},
_rtpNewSenders: {
value: new Set()
},
_iceConnectionMonitor: {
value: new options.IceConnectionMonitor(peerConnection)
},
_mediaTrackReceivers: {
value: new Set()
},
_needsAnswer: {
writable: true,
value: false
},
_negotiationRole: {
writable: true,
value: null
},
_offerOptions: {
writable: true,
value: options.offerOptions
},
_onEncodingParametersChanged: {
value: oncePerTick(function () {
if (!_this._needsAnswer) {
updateEncodingParameters(_this);
}
})
},
_peerConnection: {
value: peerConnection
},
_preferredAudioCodecs: {
value: preferredCodecs.audio
},
_preferredVideoCodecs: {
value: preferredCodecs.video
},
_shouldApplyDtx: {
value: preferredCodecs.audio.every(function (_a) {
var codec = _a.codec;
return codec !== 'opus';
})
|| preferredCodecs.audio.some(function (_a) {
var codec = _a.codec, dtx = _a.dtx;
return codec === 'opus' && dtx;
})
},
_queuedDescription: {
writable: true,
value: null
},
_iceReconnectTimeout: {
value: new options.Timeout(function () {
log.debug('ICE reconnect timed out');
_this.close();
}, options.sessionTimeout, false)
},
_recycledTransceivers: {
value: {
audio: [],
video: []
}
},
_replaceTrackPromises: {
value: new Map()
},
_remoteCandidates: {
writable: true,
value: new IceBox()
},
_setCodecPreferences: {
// NOTE(mmalavalli): Re-ordering payload types in order to make sure a non-H264
// preferred codec is selected does not work on Android Firefox due to this behavior:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1683258. So, we work around this by
// not applying any non-H264 preferred video codec.
value: isFirefox && isAndroid && preferredCodecs.video[0] && preferredCodecs.video[0].codec.toLowerCase() !== 'h264'
? function (sdp) { return sdp; }
: options.setCodecPreferences
},
_setSimulcast: {
value: options.setSimulcast
},
_revertSimulcast: {
value: options.revertSimulcast
},
_RTCIceCandidate: {
value: options.RTCIceCandidate
},
_RTCPeerConnection: {
value: options.RTCPeerConnection
},
_RTCSessionDescription: {
value: options.RTCSessionDescription
},
_shouldOffer: {
writable: true,
value: false
},
_shouldRestartIce: {
writable: true,
value: false
},
_trackIdsToAttributes: {
value: new Map(),
writable: true
},
_trackMatcher: {
writable: true,
value: null
},
_mediaTrackSenderToPublisherHints: {
value: new Map()
},
id: {
enumerable: true,
value: id
}
});
encodingParameters.on('changed', _this._onEncodingParametersChanged);
peerConnection.addEventListener('connectionstatechange', _this._handleConnectionStateChange.bind(_this));
peerConnection.addEventListener('datachannel', _this._handleDataChannelEvent.bind(_this));
peerConnection.addEventListener('icecandidate', _this._handleIceCandidateEvent.bind(_this));
peerConnection.addEventListener('iceconnectionstatechange', _this._handleIceConnectionStateChange.bind(_this));
peerConnection.addEventListener('icegatheringstatechange', _this._handleIceGatheringStateChange.bind(_this));
peerConnection.addEventListener('signalingstatechange', _this._handleSignalingStateChange.bind(_this));
peerConnection.addEventListener('track', _this._handleTrackEvent.bind(_this));
var self = _this;
_this.on('stateChanged', function stateChanged(state) {
if (state !== 'closed') {
return;
}
self.removeListener('stateChanged', stateChanged);
self._dataChannels.forEach(function (dataChannel, dataTrackSender) {
self.removeDataTrackSender(dataTrackSender);
});
});
return _this;
}
PeerConnectionV2.prototype.toString = function () {
return "[PeerConnectionV2 #" + this._instanceId + ": " + this.id + "]";
};
PeerConnectionV2.prototype.setEffectiveAdaptiveSimulcast = function (effectiveAdaptiveSimulcast) {
this._log.debug('Setting setEffectiveAdaptiveSimulcast: ', effectiveAdaptiveSimulcast);
// clear adaptive simulcast from codec preferences if it was set.
this._preferredVideoCodecs.forEach(function (cs) {
if ('adaptiveSimulcast' in cs) {
cs.adaptiveSimulcast = effectiveAdaptiveSimulcast;
}
});
};
Object.defineProperty(PeerConnectionV2.prototype, "_shouldApplySimulcast", {
get: function () {
if (!isChrome && !isSafari) {
return false;
}
// adaptiveSimulcast is set to false after connected message is received if other party does not support it.
var simulcast = this._preferredVideoCodecs.some(function (cs) {
return cs.codec.toLowerCase() === 'vp8' && cs.simulcast && cs.adaptiveSimulcast !== false;
});
return simulcast;
},
enumerable: false,
configurable: true
});
Object.defineProperty(PeerConnectionV2.prototype, "connectionState", {
/**
* The {@link PeerConnectionV2}'s underlying RTCPeerConnection's RTCPeerConnectionState
* if supported by the browser, its RTCIceConnectionState otherwise.
* @property {RTCPeerConnectionState}
*/
get: function () {
return this.iceConnectionState === 'failed'
? 'failed' : (this._peerConnection.connectionState || this.iceConnectionState);
},
enumerable: false,
configurable: true
});
Object.defineProperty(PeerConnectionV2.prototype, "iceConnectionState", {
/**
* The {@link PeerConnectionV2}'s underlying RTCPeerConnection's
* RTCIceConnectionState.
* @property {RTCIceConnectionState}
*/
get: function () {
return ((this._isIceConnectionInactive && this._peerConnection.iceConnectionState === 'disconnected') || this._iceGatheringFailed)
? 'failed' : this._peerConnection.iceConnectionState;
},
enumerable: false,
configurable: true
});
Object.defineProperty(PeerConnectionV2.prototype, "isApplicationSectionNegotiated", {
/**
* Whether the {@link PeerConnectionV2} has negotiated or is in the process
* of negotiating the application m= section.
* @returns {boolean}
*/
get: function () {
if (this._peerConnection.signalingState !== 'closed') {
// accessing .localDescription in 'closed' state causes it throw exceptions.
return this._peerConnection.localDescription
? getMediaSections(this._peerConnection.localDescription.sdp, 'application').length > 0
: false;
}
return true;
},
enumerable: false,
configurable: true
});
Object.defineProperty(PeerConnectionV2.prototype, "_isAdaptiveSimulcastEnabled", {
/**
* Whether adaptive simulcast is enabled.
* @returns {boolean}
*/
get: function () {
var adaptiveSimulcastEntry = this._preferredVideoCodecs.find(function (cs) { return 'adaptiveSimulcast' in cs; });
return adaptiveSimulcastEntry && adaptiveSimulcastEntry.adaptiveSimulcast === true;
},
enumerable: false,
configurable: true
});
/**
* @param {MediaStreamTrack} track
* @param {Array<RTCRtpEncodingParameters>} encodings
* @param {boolean} trackReplaced
* @returns {boolean} true if encodings were updated.
*/
PeerConnectionV2.prototype._maybeUpdateEncodings = function (track, encodings, trackReplaced) {
if (trackReplaced === void 0) { trackReplaced = false; }
if (track.kind !== 'video' || track.readyState === 'ended') {
return false;
}
// NOTE(mmalavalli): There is no guarantee that CanvasCaptureMediaStreamTracks will always have "width" and "height"
// in their settings. So, we don't update the encodings if they are not present.
// Chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1367082
var _a = track.getSettings(), height = _a.height, width = _a.width;
if (typeof height !== 'number' || typeof width !== 'number') {
return false;
}
// Note(mpatwardhan): always configure encodings for safari.
// for chrome only when adaptive simulcast enabled.
var browser = util.guessBrowser();
if (browser === 'safari' || (browser === 'chrome' && this._isAdaptiveSimulcastEnabled)) {
this._updateEncodings(track, encodings, trackReplaced);
return true;
}
return false;
};
/**
* Configures with default encodings depending on track type and resolution.
* Default configuration sets some encodings to disabled, and for others set scaleResolutionDownBy
* values. When trackReplaced is set to true, it will clear 'active' for any encodings that
* needs to be enabled.
* @param {MediaStreamTrack} track
* @param {Array<RTCRtpEncodingParameters>} encodings
* @param {boolean} trackReplaced
*/
PeerConnectionV2.prototype._updateEncodings = function (track, encodings, trackReplaced) {
if (this._isChromeScreenShareTrack(track)) {
var screenShareActiveLayerConfig_1 = [
{ scaleResolutionDownBy: 1 },
{ scaleResolutionDownBy: 1 }
];
encodings.forEach(function (encoding, i) {
var activeLayerConfig = screenShareActiveLayerConfig_1[i];
if (activeLayerConfig) {
encoding.scaleResolutionDownBy = activeLayerConfig.scaleResolutionDownBy;
if (trackReplaced) {
delete encoding.active;
}
}
else {
encoding.active = false;
delete encoding.scaleResolutionDownBy;
}
});
}
else {
var _a = track.getSettings(), width = _a.width, height = _a.height;
// NOTE(mpatwardhan): for non-screen share tracks
// enable layers depending on track resolutions
var pixelsToMaxActiveLayers = [
{ pixels: 960 * 540, maxActiveLayers: 3 },
{ pixels: 480 * 270, maxActiveLayers: 2 },
{ pixels: 0, maxActiveLayers: 1 }
];
var trackPixels_1 = width * height;
var activeLayersInfo = pixelsToMaxActiveLayers.find(function (layer) { return trackPixels_1 >= layer.pixels; });
var activeLayers_1 = Math.min(encodings.length, activeLayersInfo.maxActiveLayers);
encodings.forEach(function (encoding, i) {
var enabled = i < activeLayers_1;
if (enabled) {
encoding.scaleResolutionDownBy = 1 << (activeLayers_1 - i - 1);
if (trackReplaced) {
encoding.active = true;
}
}
else {
encoding.active = false;
delete encoding.scaleResolutionDownBy;
}
});
}
this._log.debug('_updateEncodings:', encodings.map(function (_a, i) {
var active = _a.active, scaleResolutionDownBy = _a.scaleResolutionDownBy;
return "[" + i + ": " + active + ", " + (scaleResolutionDownBy || 0) + "]";
}).join(', '));
};
/**
* Add an ICE candidate to the {@link PeerConnectionV2}.
* @private
* @param {object} candidate
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._addIceCandidate = function (candidate) {
var _this = this;
return Promise.resolve().then(function () {
candidate = new _this._RTCIceCandidate(candidate);
return _this._peerConnection.addIceCandidate(candidate);
}).catch(function (error) {
// NOTE(mmalavalli): Firefox 68+ now generates an RTCIceCandidate with an
// empty candidate string to signal end-of-candidates, followed by a null
// candidate. As of now, Chrome and Safari reject this RTCIceCandidate. Since
// this does not affect the media connection between Firefox 68+ and Chrome/Safari
// in Peer-to-Peer Rooms, we suppress the Error and log a warning message.
//
// Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=978582
//
_this._log.warn("Failed to add RTCIceCandidate " + (candidate ? "\"" + candidate.candidate + "\"" : 'null') + ": "
+ error.message);
});
};
/**
* Add ICE candidates to the {@link PeerConnectionV2}.
* @private
* @param {Array<object>} candidates
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._addIceCandidates = function (candidates) {
return Promise.all(candidates.map(this._addIceCandidate, this)).then(function () { });
};
/**
* Add a new RTCRtpTransceiver or update an existing RTCRtpTransceiver for the
* given MediaStreamTrack.
* @private
* @param {MediaStreamTrack} track
* @returns {RTCRtpTransceiver}
*/
PeerConnectionV2.prototype._addOrUpdateTransceiver = function (track) {
var _this = this;
var transceiver = takeRecycledTransceiver(this, track.kind);
if (transceiver && transceiver.sender) {
var oldTrackId = transceiver.sender.track ? transceiver.sender.track.id : null;
if (oldTrackId) {
this._log.warn("Reusing transceiver: " + transceiver.mid + "] " + oldTrackId + " => " + track.id);
}
// NOTE(mpatwardhan):remember this transceiver while we replace track.
// we recycle transceivers that are not in use after 'negotiationCompleted', but we want to prevent
// this one from getting recycled while replaceTrack is pending.
this._replaceTrackPromises.set(transceiver, transceiver.sender.replaceTrack(track).then(function () {
transceiver.direction = 'sendrecv';
}, function () {
// Do nothing.
}).finally(function () {
_this._replaceTrackPromises.delete(transceiver);
}));
return transceiver;
}
// TODO(lrivas): Review with Charlie, the second argument is required by VDI environment
return this._peerConnection.addTransceiver(track, {});
};
/**
* Check the {@link IceBox}.
* @private
* @param {RTCSessionDescriptionInit} description
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._checkIceBox = function (description) {
var ufrag = getUfrag(description);
if (!ufrag) {
return Promise.resolve();
}
var candidates = this._remoteCandidates.setUfrag(ufrag);
return this._addIceCandidates(candidates);
};
/**
* Create an answer and set it on the {@link PeerConnectionV2}.
* @private
* @param {RTCSessionDescriptionInit} offer
* @returns {Promise<boolean>}
*/
PeerConnectionV2.prototype._answer = function (offer) {
var _this = this;
return Promise.resolve().then(function () {
if (!_this._negotiationRole) {
_this._negotiationRole = 'answerer';
}
return _this._setRemoteDescription(offer);
}).catch(function () {
throw new MediaClientRemoteDescFailedError();
}).then(function () {
return _this._peerConnection.createAnswer();
}).then(function (answer) {
if (isFirefox) {
// NOTE(mmalavalli): We work around Chromium bug 1106157 by disabling
// RTX in Firefox 79+. For more details about the bug, please go here:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1106157
answer = new _this._RTCSessionDescription({
sdp: disableRtx(answer.sdp),
type: answer.type
});
}
else {
answer = workaroundIssue8329(answer);
}
// NOTE(mpatwardhan): Upcoming chrome versions are going to remove ssrc attributes
// mslabel and label. See this bug https://bugs.chromium.org/p/webrtc/issues/detail?id=7110
// and PSA: https://groups.google.com/forum/#!searchin/discuss-webrtc/PSA%7Csort:date/discuss-webrtc/jcZO-Wj0Wus/k2XvPCvoAwAJ
// We are not referencing those attributes, but this changes goes ahead and removes them to see if it works.
// this also helps reduce bytes on wires
var updatedSdp = removeSSRCAttributes(answer.sdp, ['mslabel', 'label']);
if (_this._shouldApplySimulcast) {
var sdpWithoutSimulcast = updatedSdp;
updatedSdp = _this._setSimulcast(sdpWithoutSimulcast, _this._trackIdsToAttributes);
// NOTE(syerrapragada): VMS does not support H264 simulcast. So,
// unset simulcast for sections in local offer where corresponding
// sections in answer doesn't have vp8 as preferred codec and reapply offer.
updatedSdp = _this._revertSimulcast(updatedSdp, sdpWithoutSimulcast, offer.sdp);
}
// NOTE(mmalavalli): Work around Chromium bug 1074421.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1074421
updatedSdp = updatedSdp.replace(/42e015/g, '42e01f');
return _this._setLocalDescription({
type: answer.type,
sdp: updatedSdp
});
}).then(function () {
return _this._checkIceBox(offer);
}).then(function () {
return _this._queuedDescription
&& _this._updateDescription(_this._queuedDescription);
}).then(function () {
_this._queuedDescription = null;
return _this._maybeReoffer(_this._peerConnection.localDescription);
}).catch(function (error) {
var errorToThrow = error instanceof MediaClientRemoteDescFailedError ? error : new MediaClientLocalDescFailedError();
_this._publishMediaWarning({
message: 'Failed to _answer',
code: errorToThrow.code,
error: error
});
throw errorToThrow;
});
};
/**
* Close the underlying RTCPeerConnection. Returns false if the
* RTCPeerConnection was already closed.
* @private
* @returns {boolean}
*/
PeerConnectionV2.prototype._close = function () {
this._iceConnectionMonitor.stop();
if (this._peerConnection.signalingState !== 'closed') {
this._peerConnection.close();
this.preempt('closed');
this._encodingParameters.removeListener('changed', this._onEncodingParametersChanged);
return true;
}
return false;
};
/**
* Handle a "connectionstatechange" event.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._handleConnectionStateChange = function () {
this.emit('connectionStateChanged');
};
/**
* Handle a "datachannel" event.
* @private
* @param {RTCDataChannelEvent} event
* @returns {void}
*/
PeerConnectionV2.prototype._handleDataChannelEvent = function (event) {
var _this = this;
var dataChannel = event.channel;
var dataTrackReceiver = new DataTrackReceiver(dataChannel);
this._dataTrackReceivers.add(dataTrackReceiver);
dataChannel.addEventListener('close', function () {
_this._dataTrackReceivers.delete(dataTrackReceiver);
});
this.emit('trackAdded', dataTrackReceiver);
};
/**
* Handle a glare scenario on the {@link PeerConnectionV2}.
* @private
* @param {RTCSessionDescriptionInit} offer
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._handleGlare = function (offer) {
var _this = this;
this._log.debug('Glare detected; rolling back');
if (this._isRestartingIce) {
this._log.debug('An ICE restart was in progress; we\'ll need to restart ICE again after rolling back');
this._isRestartingIce = false;
this._shouldRestartIce = true;
}
return Promise.resolve().then(function () {
_this._trackIdsToAttributes = new Map(_this._appliedTrackIdsToAttributes);
return _this._setLocalDescription({ type: 'rollback' });
}).then(function () {
_this._needsAnswer = false;
return _this._answer(offer);
}).then(function (didReoffer) {
return didReoffer ? Promise.resolve() : _this._offer();
});
};
PeerConnectionV2.prototype._publishMediaWarning = function (_a) {
var message = _a.message, code = _a.code, error = _a.error, sdp = _a.sdp;
this._eventObserver.emit('event', { level: 'warning', name: 'error', group: 'media', payload: {
message: message,
code: code,
context: JSON.stringify({ error: error.message, sdp: sdp })
} });
};
/**
* Handle an ICE candidate event.
* @private
* @param {Event} event
* @returns {void}
*/
PeerConnectionV2.prototype._handleIceCandidateEvent = function (event) {
if (event.candidate) {
this._log.debug('Clearing ICE gathering timeout');
this._didGenerateLocalCandidates = true;
this._iceGatheringTimeout.clear();
this._localCandidates.push(event.candidate);
}
var peerConnectionState = {
ice: {
candidates: this._isIceLite ? [] : this._localCandidates.slice(),
ufrag: this._localUfrag
},
id: this.id
};
if (!event.candidate) {
peerConnectionState.ice.complete = true;
}
if (!(this._isIceLite && event.candidate)) {
peerConnectionState.ice.revision = this._localCandidatesRevision++;
this.emit('candidates', peerConnectionState);
}
};
/**
* Handle an ICE connection state change event.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._handleIceConnectionStateChange = function () {
var _this = this;
var iceConnectionState = this._peerConnection.iceConnectionState;
var isIceConnectedOrComplete = ['connected', 'completed'].includes(iceConnectionState);
var log = this._log;
log.debug("ICE connection state is \"" + iceConnectionState + "\"");
if (isIceConnectedOrComplete) {
this._iceReconnectTimeout.clear();
this._iceRestartBackoff.reset();
}
if (this._lastIceConnectionState !== 'failed' && iceConnectionState === 'failed' && !this._shouldRestartIce && !this._isRestartingIce) {
// Case 1: Transition to "failed".
log.warn('ICE failed');
this._initiateIceRestartBackoff();
}
else if (['disconnected', 'failed'].includes(this._lastIceConnectionState) && isIceConnectedOrComplete) {
// Case 2: Transition from "disconnected" or "failed".
log.debug('ICE reconnected');
}
// start monitor media when connected, and continue to monitor while state is complete-disconnected-connected.
if (iceConnectionState === 'connected') {
this._isIceConnectionInactive = false;
this._iceConnectionMonitor.start(function () {
// note: iceConnection monitor waits for iceConnectionState=disconnected for
// detecting inactivity. Its possible that it may know about disconnected before _handleIceConnectionStateChange
_this._iceConnectionMonitor.stop();
if (!_this._shouldRestartIce && !_this._isRestartingIce) {
log.warn('ICE Connection Monitor detected inactivity');
_this._isIceConnectionInactive = true;
_this._initiateIceRestartBackoff();
_this.emit('iceConnectionStateChanged');
_this.emit('connectionStateChanged');
}
});
}
else if (!['disconnected', 'completed'].includes(iceConnectionState)) { // don't stop monitoring for disconnected or completed.
this._iceConnectionMonitor.stop();
this._isIceConnectionInactive = false;
}
this._lastIceConnectionState = iceConnectionState;
this.emit('iceConnectionStateChanged');
};
/**
* Handle ICE gathering timeout.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._handleIceGatheringTimeout = function () {
this._log.warn('ICE failed to gather any local candidates');
this._iceGatheringFailed = true;
this._initiateIceRestartBackoff();
this.emit('iceConnectionStateChanged');
this.emit('connectionStateChanged');
};
/**
* Handle an ICE gathering state change event.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._handleIceGatheringStateChange = function () {
var iceGatheringState = this._peerConnection.iceGatheringState;
var log = this._log;
log.debug("ICE gathering state is \"" + iceGatheringState + "\"");
// NOTE(mmalavalli): Start the ICE gathering timeout only if the RTCPeerConnection
// has started gathering candidates for the first time since the initial offer/answer
// or an offer/answer with ICE restart.
var _a = this._iceGatheringTimeout, delay = _a.delay, isSet = _a.isSet;
if (iceGatheringState === 'gathering' && !this._didGenerateLocalCandidates && !isSet) {
log.debug("Starting ICE gathering timeout: " + delay);
this._iceGatheringFailed = false;
this._iceGatheringTimeout.start();
}
};
/**
* Handle a signaling state change event.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._handleSignalingStateChange = function () {
if (this._peerConnection.signalingState === 'stable') {
this._appliedTrackIdsToAttributes = new Map(this._trackIdsToAttributes);
}
};
/**
* Handle a track event.
* @private
* @param {RTCTrackEvent} event
* @returns {void}
*/
PeerConnectionV2.prototype._handleTrackEvent = function (event) {
var _this = this;
var sdp = this._peerConnection.remoteDescription
? this._peerConnection.remoteDescription.sdp
: null;
this._trackMatcher = this._trackMatcher || new TrackMatcher();
this._trackMatcher.update(sdp);
var mediaStreamTrack = event.track;
var signaledTrackId = this._trackMatcher.match(event) || mediaStreamTrack.id;
var mediaTrackReceiver = new MediaTrackReceiver(signaledTrackId, mediaStreamTrack);
// NOTE(mmalavalli): "ended" is not fired on the remote MediaStreamTrack when
// the remote peer removes a track. So, when this MediaStreamTrack is re-used
// for a different track due to the remote peer calling RTCRtpSender.replaceTrack(),
// we delete the previous MediaTrackReceiver that owned this MediaStreamTrack
// before adding the new MediaTrackReceiver.
this._mediaTrackReceivers.forEach(function (trackReceiver) {
if (trackReceiver.track.id === mediaTrackReceiver.track.id) {
_this._mediaTrackReceivers.delete(trackReceiver);
}
});
this._mediaTrackReceivers.add(mediaTrackReceiver);
if (mediaStreamTrack.addEventListener) {
mediaStreamTrack.addEventListener('ended', function () { return _this._mediaTrackReceivers.delete(mediaTrackReceiver); });
}
else {
mediaStreamTrack.onended = function () { return _this._mediaTrackReceivers.delete(mediaTrackReceiver); };
}
this.emit('trackAdded', mediaTrackReceiver);
};
/**
* Initiate ICE Restart.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._initiateIceRestart = function () {
if (this._peerConnection.signalingState === 'closed') {
return;
}
var log = this._log;
log.warn('Attempting to restart ICE');
this._didGenerateLocalCandidates = false;
this._isIceRestartBackoffInProgress = false;
this._shouldRestartIce = true;
var _a = this._iceReconnectTimeout, delay = _a.delay, isSet = _a.isSet;
if (!isSet) {
log.debug("Starting ICE reconnect timeout: " + delay);
this._iceReconnectTimeout.start();
}
this.offer().catch(function (ex) {
log.error("offer failed in _initiateIceRestart with: " + ex.message);
});
};
/**
* Schedule an ICE Restart.
* @private
* @returns {void}
*/
PeerConnectionV2.prototype._initiateIceRestartBackoff = function () {
var _this = this;
if (this._peerConnection.signalingState === 'closed' || this._isIceRestartBackoffInProgress) {
return;
}
this._log.warn('An ICE restart has been scheduled');
this._isIceRestartBackoffInProgress = true;
this._iceRestartBackoff.backoff(function () { return _this._initiateIceRestart(); });
};
/**
* Conditionally re-offer.
* @private
* @param {?RTCSessionDescriptionInit} localDescription
* @returns {Promise<boolean>}
*/
PeerConnectionV2.prototype._maybeReoffer = function (localDescription) {
var shouldReoffer = this._shouldOffer;
if (localDescription && localDescription.sdp) {
// NOTE(mmalavalli): If the local RTCSessionDescription has fewer audio and/or
// video send* m= lines than the corresponding RTCRtpSenders with non-null
// MediaStreamTracks, it means that the newly added RTCRtpSenders require
// renegotiation.
var senders_1 = this._peerConnection.getSenders().filter(function (sender) { return sender.track; });
shouldReoffer = ['audio', 'video'].reduce(function (shouldOffer, kind) {
var mediaSections = getMediaSections(localDescription.sdp, kind, '(sendrecv|sendonly)');
var sendersOfKind = senders_1.filter(isSenderOfKind.bind(null, kind));
return shouldOffer || (mediaSections.length < sendersOfKind.length);
}, shouldReoffer);
// NOTE(mroberts): We also need to re-offer if we have a DataTrack to share
// but no m= application section.
var hasDataTrack = this._dataChannels.size > 0;
var hasApplicationMediaSection = getMediaSections(localDescription.sdp, 'application').length > 0;
var needsApplicationMediaSection = hasDataTrack && !hasApplicationMediaSection;
shouldReoffer = shouldReoffer || needsApplicationMediaSection;
}
var promise = shouldReoffer ? this._offer() : Promise.resolve();
return promise.then(function () { return shouldReoffer; });
};
/**
* Create an offer and set it on the {@link PeerConnectionV2}.
* @private
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._offer = function () {
var _this = this;
var offerOptions = Object.assign({}, this._offerOptions);
this._needsAnswer = true;
if (this._shouldRestartIce) {
this._shouldRestartIce = false;
this._isRestartingIce = true;
offerOptions.iceRestart = true;
}
return Promise.all(this._replaceTrackPromises.values()).then(function () {
return _this._peerConnection.createOffer(offerOptions);
}).catch(function (error) {
var errorToThrow = new MediaClientLocalDescFailedError();
_this._publishMediaWarning({
message: 'Failed to create offer',
code: errorToThrow.code,
error: error
});
throw errorToThrow;
}).then(function (offer) {
if (isFirefox) {
// NOTE(mmalavalli): We work around Chromium bug 1106157 by disabling
// RTX in Firefox 79+. For more details about the bug, please go here:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1106157
offer = new _this._RTCSessionDescription({
sdp: disableRtx(offer.sdp),
type: offer.type
});
}
else {
offer = workaroundIssue8329(offer);
}
// NOTE(mpatwardhan): upcoming chrome versions are going to remove ssrc attributes
// mslabel and label. See this bug https://bugs.chromium.org/p/webrtc/issues/detail?id=7110
// and PSA: https://groups.google.com/forum/#!searchin/discuss-webrtc/PSA%7Csort:date/discuss-webrtc/jcZO-Wj0Wus/k2XvPCvoAwAJ
// Looks like we are not referencing those attributes, but this changes goes ahead and removes them to see if it works.
// this also helps reduce bytes on wires
var sdp = removeSSRCAttributes(offer.sdp, ['mslabel', 'label']);
sdp = _this._peerConnection.remoteDescription
? filterLocalCodecs(sdp, _this._peerConnection.remoteDescription.sdp)
: sdp;
var updatedSdp = _this._setCodecPreferences(sdp, _this._preferredAudioCodecs, _this._preferredVideoCodecs);
_this._shouldOffer = false;
if (!_this._negotiationRole) {
_this._negotiationRole = 'offerer';
}
if (_this._shouldApplySimulcast) {
_this._localDescriptionWithoutSimulcast = {
type: 'offer',
sdp: updatedSdp
};
updatedSdp = _this._setSimulcast(updatedSdp, _this._trackIdsToAttributes);
}
return _this._setLocalDescription({
type: 'offer',
sdp: updatedSdp
});
});
};
/**
* Get the MediaTrackSender ID of the given MediaStreamTrack ID.
* Since a MediaTrackSender's underlying MediaStreamTrack can be
* replaced, the corresponding IDs can mismatch.
* @private
* @param {Track.ID} id
* @returns {Track.ID}
*/
PeerConnectionV2.prototype._getMediaTrackSenderId = function (trackId) {
var mediaTrackSender = Array.from(this._rtpSenders.keys()).find(function (_a) {
var id = _a.track.id;
return id === trackId;
});
return mediaTrackSender ? mediaTrackSender.id : trackId;
};
/**
* Add or rewrite local MediaStreamTrack IDs in the given RTCSessionDescription.
* @private
* @param {RTCSessionDescription} description
* @return {RTCSessionDescription}
*/
PeerConnectionV2.prototype._addOrRewriteLocalTrackIds = function (description) {
var _this = this;
var transceivers = this._peerConnection.getTransceivers();
var activeTransceivers = transceivers.filter(function (_a) {
var sender = _a.sender, stopped = _a.stopped;
return !stopped && sender && sender.track;
});
// NOTE(mmalavalli): There is no guarantee that MediaStreamTrack IDs will be present in
// SDPs, and even if they are, there is no guarantee that they will be the same as the
// actual MediaStreamTrack IDs. So, we add or re-write the actual MediaStreamTrack IDs
// to the assigned m= sections here.
var assignedTransceivers = activeTransceivers.filter(function (_a) {
var mid = _a.mid;
return mid;
});
var midsToTrackIds = new Map(assignedTransceivers.map(function (_a) {
var mid = _a.mid, sender = _a.sender;
return [mid, _this._getMediaTrackSenderId(sender.track.id)];
}));
var sdp1 = addOrRewriteTrackIds(description.sdp, midsToTrackIds);
// NOTE(mmalavalli): Chrome and Safari do not apply the offer until they get an answer.
// So, we add or re-write the actual MediaStreamTrack IDs to the unassigned m= sections here.
var unassignedTransceivers = activeTransceivers.filter(function (_a) {
var mid = _a.mid;
return !mid;
});
var newTrackIdsByKind = new Map(['audio', 'video'].map(function (kind) { return [
kind,
unassignedTransceivers.filter(function (_a) {
var sender = _a.sender;
return sender.track.kind === kind;
}).map(function (_a) {
var sender = _a.sender;
return _this._getMediaTrackSenderId(sender.track.id);
})
]; }));
var sdp2 = addOrRewriteNewTrackIds(sdp1, midsToTrackIds, newTrackIdsByKind);
return new this._RTCSessionDescription({
sdp: sdp2,
type: description.type
});
};
/**
* Rollback and apply the given offer.
* @private
* @param {RTCSessionDescriptionInit} offer
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._rollbackAndApplyOffer = function (offer) {
var _this = this;
return this._setLocalDescription({ type: 'rollback' }).then(function () { return _this._setLocalDescription(offer); });
};
/**
* Set a local description on the {@link PeerConnectionV2}.
* @private
* @param {RTCSessionDescription|RTCSessionDescriptionInit} description
* @returns {Promise<void>}
*/
PeerConnectionV2.prototype._setLocalDescription = function (description) {
var _this = this;
if (description.type !== 'rollback' && this._shouldApplyDtx) {
description = new this._RTCSessionDescription({
sdp: enableDtxForOpus(description.sdp),
type: description.t