@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
1,037 lines (1,033 loc) • 87 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var index = require('../errors/index.js');
var log = require('../log.js');
var util = require('../util.js');
var rtcpc = require('./rtcpc.js');
var sdp = require('./sdp.js');
var generated = require('../errors/generated.js');
// @ts-nocheck
var ICE_GATHERING_TIMEOUT = 15000;
var ICE_GATHERING_FAIL_NONE = 'none';
var ICE_GATHERING_FAIL_TIMEOUT = 'timeout';
var INITIAL_ICE_CONNECTION_STATE = 'new';
var VOLUME_INTERVAL_MS = 50;
/**
* @typedef {Object} PeerConnection
* @param audioHelper
* @param pstream
* @param options
* @return {PeerConnection}
* @constructor
*/
function PeerConnection(audioHelper, pstream, options) {
var _this = this;
if (!audioHelper || !pstream) {
throw new index.InvalidArgumentError('Audiohelper, and pstream are required arguments');
}
if (!(this instanceof PeerConnection)) {
return new PeerConnection(audioHelper, pstream, options);
}
this._log = new log.default('PeerConnection');
function noop() {
this._log.warn('Unexpected noop call in peerconnection');
}
this.onaudio = noop;
this.onopen = noop;
this.onerror = noop;
this.onclose = noop;
this.ondisconnected = noop;
this.onfailed = noop;
this.onconnected = noop;
this.onreconnected = noop;
this.onsignalingstatechange = noop;
this.ondtlstransportstatechange = noop;
this.onicegatheringfailure = noop;
this.onicegatheringstatechange = noop;
this.oniceconnectionstatechange = noop;
this.onpcconnectionstatechange = noop;
this.onicecandidate = noop;
this.onselectedcandidatepairchange = noop;
this.onvolume = noop;
this.version = null;
this.pstream = pstream;
this.stream = null;
this.sinkIds = new Set(['default']);
this.outputs = new Map();
this.status = 'connecting';
this.callSid = null;
this.isMuted = false;
var AudioContext = typeof window !== 'undefined'
&& (window.AudioContext || window.webkitAudioContext);
this._isSinkSupported = !!AudioContext &&
typeof HTMLAudioElement !== 'undefined' && HTMLAudioElement.prototype.setSinkId;
// NOTE(mmalavalli): Since each Connection creates its own AudioContext,
// after 6 instances an exception is thrown. Refer https://www.w3.org/2011/audio/track/issues/3.
// In order to get around it, we are re-using the Device's AudioContext.
this._audioContext = AudioContext && audioHelper._audioContext;
this._audioHelper = audioHelper;
this._audioProcessorEventObserver = audioHelper._getAudioProcessorEventObserver();
this._hasIceCandidates = false;
this._hasIceGatheringFailures = false;
this._iceGatheringTimeoutId = null;
this._masterAudio = null;
this._masterAudioDeviceId = null;
this._mediaStreamSource = null;
this._dtmfSender = null;
this._dtmfSenderUnsupported = false;
this._callEvents = [];
this._nextTimeToPublish = Date.now();
this._onAnswerOrRinging = noop;
this._onHangup = noop;
this._remoteStream = null;
this._shouldManageStream = true;
this._iceState = INITIAL_ICE_CONNECTION_STATE;
this.options = options = options || {};
this.navigator = options.navigator
|| (typeof navigator !== 'undefined' ? navigator : null);
this.util = options.util || util;
this.codecPreferences = options.codecPreferences;
this._onAudioProcessorAdded = function (isRemote) {
_this._handleAudioProcessorEvent(isRemote, true);
};
this._onAudioProcessorRemoved = function (isRemote) {
_this._handleAudioProcessorEvent(isRemote, false);
};
this._audioProcessorEventObserver.on('add', this._onAudioProcessorAdded);
this._audioProcessorEventObserver.on('remove', this._onAudioProcessorRemoved);
return this;
}
PeerConnection.prototype.uri = function () {
return this._uri;
};
/**
* Open the underlying RTCPeerConnection with a MediaStream obtained by
* passed constraints. The resulting MediaStream is created internally
* and will therefore be managed and destroyed internally.
* @param {MediaStreamConstraints} constraints
*/
PeerConnection.prototype.openDefaultDeviceWithConstraints = function (constraints) {
return this._audioHelper._openDefaultDeviceWithConstraints(constraints)
.then(this._setInputTracksFromStream.bind(this, false));
};
/**
* Replace the existing input audio tracks with the audio tracks from the
* passed input audio stream. We re-use the existing stream because
* the AnalyzerNode is bound to the stream.
* @param {MediaStream} stream
*/
PeerConnection.prototype.setInputTracksFromStream = function (stream) {
var self = this;
return this._setInputTracksFromStream(true, stream).then(function () {
self._shouldManageStream = false;
});
};
PeerConnection.prototype._createAnalyser = function (audioContext, options) {
options = Object.assign({
fftSize: 32,
smoothingTimeConstant: 0.3,
}, options);
var analyser = audioContext.createAnalyser();
// tslint:disable-next-line
for (var field in options) {
analyser[field] = options[field];
}
return analyser;
};
PeerConnection.prototype._setVolumeHandler = function (handler) {
this.onvolume = handler;
};
PeerConnection.prototype._startPollingVolume = function () {
if (!this._audioContext || !this.stream || !this._remoteStream) {
return;
}
var audioContext = this._audioContext;
var inputAnalyser = this._inputAnalyser = this._createAnalyser(audioContext);
var inputBufferLength = inputAnalyser.frequencyBinCount;
var inputDataArray = new Uint8Array(inputBufferLength);
this._inputAnalyser2 = this._createAnalyser(audioContext, {
maxDecibels: 0,
minDecibels: -127,
smoothingTimeConstant: 0,
});
var outputAnalyser = this._outputAnalyser = this._createAnalyser(audioContext);
var outputBufferLength = outputAnalyser.frequencyBinCount;
var outputDataArray = new Uint8Array(outputBufferLength);
this._outputAnalyser2 = this._createAnalyser(audioContext, {
maxDecibels: 0,
minDecibels: -127,
smoothingTimeConstant: 0,
});
this._updateInputStreamSource(this.stream);
this._updateOutputStreamSource(this._remoteStream);
var self = this;
setTimeout(function emitVolume() {
if (!self._audioContext) {
return;
}
else if (self.status === 'closed') {
self._inputAnalyser.disconnect();
self._outputAnalyser.disconnect();
self._inputAnalyser2.disconnect();
self._outputAnalyser2.disconnect();
return;
}
self._inputAnalyser.getByteFrequencyData(inputDataArray);
var inputVolume = self.util.average(inputDataArray);
self._inputAnalyser2.getByteFrequencyData(inputDataArray);
var inputVolume2 = self.util.average(inputDataArray);
self._outputAnalyser.getByteFrequencyData(outputDataArray);
var outputVolume = self.util.average(outputDataArray);
self._outputAnalyser2.getByteFrequencyData(outputDataArray);
var outputVolume2 = self.util.average(outputDataArray);
self.onvolume(inputVolume / 255, outputVolume / 255, inputVolume2, outputVolume2);
setTimeout(emitVolume, VOLUME_INTERVAL_MS);
}, VOLUME_INTERVAL_MS);
};
PeerConnection.prototype._stopStream = function _stopStream() {
// We shouldn't stop the tracks if they were not created inside
// this PeerConnection.
if (!this._shouldManageStream) {
return;
}
this._audioHelper._stopDefaultInputDeviceStream();
};
/**
* Update the stream source with the new input audio stream.
* @param {MediaStream} stream
* @private
*/
PeerConnection.prototype._updateInputStreamSource = function (stream) {
if (this._inputStreamSource) {
this._inputStreamSource.disconnect();
}
try {
this._inputStreamSource = this._audioContext.createMediaStreamSource(stream);
this._inputStreamSource.connect(this._inputAnalyser);
this._inputStreamSource.connect(this._inputAnalyser2);
}
catch (ex) {
this._log.warn('Unable to update input MediaStreamSource', ex);
this._inputStreamSource = null;
}
};
/**
* Update the stream source with the new ouput audio stream.
* @param {MediaStream} stream
* @private
*/
PeerConnection.prototype._updateOutputStreamSource = function (stream) {
if (this._outputStreamSource) {
this._outputStreamSource.disconnect();
}
try {
this._outputStreamSource = this._audioContext.createMediaStreamSource(stream);
this._outputStreamSource.connect(this._outputAnalyser);
this._outputStreamSource.connect(this._outputAnalyser2);
}
catch (ex) {
this._log.warn('Unable to update output MediaStreamSource', ex);
this._outputStreamSource = null;
}
};
/**
* Replace the tracks of the current stream with new tracks. We do this rather than replacing the
* whole stream because AnalyzerNodes are bound to a stream.
* @param {Boolean} shouldClone - Whether the stream should be cloned if it is the first
* stream, or set directly. As a rule of thumb, streams that are passed in externally may have
* their lifecycle managed externally, and should be cloned so that we do not tear it or its tracks
* down when the call ends. Streams that we create internally (inside PeerConnection) should be set
* directly so that when the call ends it is disposed of.
* @param {MediaStream} newStream - The new stream to copy the tracks over from.
* @private
*/
PeerConnection.prototype._setInputTracksFromStream = function (shouldClone, newStream) {
var _this = this;
if (!newStream) {
return Promise.reject(new index.InvalidArgumentError('Can not set input stream to null while in a call'));
}
if (!newStream.getAudioTracks().length) {
return Promise.reject(new index.InvalidArgumentError('Supplied input stream has no audio tracks'));
}
var localStream = this.stream;
var getStreamPromise = function () {
// Apply mute settings to new input track
_this.mute(_this.isMuted);
return Promise.resolve(_this.stream);
};
if (!localStream) {
// We can't use MediaStream.clone() here because it stopped copying over tracks
// as of Chrome 61. https://bugs.chromium.org/p/chromium/issues/detail?id=770908
this.stream = shouldClone ? cloneStream(newStream, this.options.MediaStream) : newStream;
}
else {
// If the call was started with gUM, and we are now replacing that track with an
// external stream's tracks, we should stop the old managed track.
if (this._shouldManageStream) {
this._stopStream();
}
if (!this._sender) {
this._sender = this.version.pc.getSenders()[0];
}
return this._sender.replaceTrack(newStream.getAudioTracks()[0]).then(function () {
_this._updateInputStreamSource(newStream);
_this.stream = shouldClone ? cloneStream(newStream, _this.options.MediaStream) : newStream;
return getStreamPromise();
});
}
return getStreamPromise();
};
PeerConnection.prototype._onInputDevicesChanged = function () {
if (!this.stream) {
return;
}
// If all of our active tracks are ended, then our active input was lost
var activeInputWasLost = this.stream.getAudioTracks().every(function (track) { return track.readyState === 'ended'; });
// We only want to act if we manage the stream in PeerConnection (It was created
// here, rather than passed in.)
if (activeInputWasLost && this._shouldManageStream) {
this.openDefaultDeviceWithConstraints({ audio: true });
}
};
PeerConnection.prototype._onIceGatheringFailure = function (type) {
this._hasIceGatheringFailures = true;
this.onicegatheringfailure(type);
};
PeerConnection.prototype._onMediaConnectionStateChange = function (newState) {
var previousState = this._iceState;
if (previousState === newState
|| (newState !== 'connected'
&& newState !== 'disconnected'
&& newState !== 'failed')) {
return;
}
this._iceState = newState;
var message;
switch (newState) {
case 'connected':
if (previousState === 'disconnected' || previousState === 'failed') {
message = 'ICE liveliness check succeeded. Connection with Twilio restored';
this._log.info(message);
this.onreconnected(message);
}
else {
message = 'Media connection established.';
this._log.info(message);
this.onconnected(message);
}
this._stopIceGatheringTimeout();
this._hasIceGatheringFailures = false;
break;
case 'disconnected':
message = 'ICE liveliness check failed. May be having trouble connecting to Twilio';
this._log.warn(message);
this.ondisconnected(message);
break;
case 'failed':
message = 'Connection with Twilio was interrupted.';
this._log.warn(message);
this.onfailed(message);
break;
}
};
PeerConnection.prototype._setSinkIds = function (sinkIds) {
if (!this._isSinkSupported) {
return Promise.reject(new index.NotSupportedError('Audio output selection is not supported by this browser'));
}
this.sinkIds = new Set(sinkIds.forEach ? sinkIds : [sinkIds]);
return this.version
? this._updateAudioOutputs()
: Promise.resolve();
};
/**
* Start timeout for ICE Gathering
*/
PeerConnection.prototype._startIceGatheringTimeout = function startIceGatheringTimeout() {
var _this = this;
this._stopIceGatheringTimeout();
this._iceGatheringTimeoutId = setTimeout(function () {
_this._onIceGatheringFailure(ICE_GATHERING_FAIL_TIMEOUT);
}, ICE_GATHERING_TIMEOUT);
};
/**
* Stop timeout for ICE Gathering
*/
PeerConnection.prototype._stopIceGatheringTimeout = function stopIceGatheringTimeout() {
clearInterval(this._iceGatheringTimeoutId);
};
PeerConnection.prototype._updateAudioOutputs = function updateAudioOutputs() {
var addedOutputIds = Array.from(this.sinkIds).filter(function (id) {
return !this.outputs.has(id);
}, this);
var removedOutputIds = Array.from(this.outputs.keys()).filter(function (id) {
return !this.sinkIds.has(id);
}, this);
var self = this;
var createOutputPromises = addedOutputIds.map(this._createAudioOutput, this);
return Promise.all(createOutputPromises).then(function () { return Promise.all(removedOutputIds.map(self._removeAudioOutput, self)); });
};
PeerConnection.prototype._createAudio = function createAudio(arr) {
var audio = new Audio(arr);
this.onaudio(audio);
return audio;
};
PeerConnection.prototype._createAudioOutput = function createAudioOutput(id) {
var _this = this;
var dest = null;
if (this._mediaStreamSource) {
dest = this._audioContext.createMediaStreamDestination();
this._mediaStreamSource.connect(dest);
}
var audio = this._createAudio();
setAudioSource(audio, dest && dest.stream ? dest.stream : this.pcStream, this._audioHelper)
.catch(function () { return _this._log.error('Error attaching stream to element (_createAudioOutput).'); });
var self = this;
return audio.setSinkId(id).then(function () { return audio.play(); }).then(function () {
self.outputs.set(id, {
audio: audio,
dest: dest,
});
});
};
PeerConnection.prototype._removeAudioOutputs = function removeAudioOutputs() {
if (this._masterAudio && typeof this._masterAudioDeviceId !== 'undefined') {
this._disableOutput(this, this._masterAudioDeviceId);
this.outputs.delete(this._masterAudioDeviceId);
this._masterAudioDeviceId = null;
// Release the audio resources before deleting the audio
if (!this._masterAudio.paused) {
this._masterAudio.pause();
}
if (typeof this._masterAudio.srcObject !== 'undefined') {
this._masterAudio.srcObject = null;
}
else {
this._masterAudio.src = '';
}
this._masterAudio = null;
}
return Array.from(this.outputs.keys()).map(this._removeAudioOutput, this);
};
PeerConnection.prototype._disableOutput = function disableOutput(pc, id) {
var output = pc.outputs.get(id);
if (!output) {
return;
}
if (output.audio) {
output.audio.pause();
output.audio.src = '';
}
if (output.dest) {
output.dest.disconnect();
}
};
/**
* Disable a non-master output, and update the master output to assume its state. This
* is called when the device ID assigned to the master output has been removed from
* active devices. We can not simply remove the master audio output, so we must
* instead reassign it.
* @private
* @param {PeerConnection} pc
* @param {string} masterId - The current device ID assigned to the master audio element.
*/
PeerConnection.prototype._reassignMasterOutput = function reassignMasterOutput(pc, masterId) {
var masterOutput = pc.outputs.get(masterId);
pc.outputs.delete(masterId);
var self = this;
var activeDeviceId = Array.from(pc.outputs.keys())[0];
// The audio device key could also be '' on Chrome if no media device permissions are allowed
var idToReplace = typeof activeDeviceId === 'string' ? activeDeviceId : 'default';
return masterOutput.audio.setSinkId(idToReplace).then(function () {
self._disableOutput(pc, idToReplace);
pc.outputs.set(idToReplace, masterOutput);
pc._masterAudioDeviceId = idToReplace;
}).catch(function rollback() {
pc.outputs.set(masterId, masterOutput);
self._log.info('Could not reassign master output. Attempted to roll back.');
});
};
PeerConnection.prototype._removeAudioOutput = function removeAudioOutput(id) {
if (this._masterAudioDeviceId === id) {
return this._reassignMasterOutput(this, id);
}
this._disableOutput(this, id);
this.outputs.delete(id);
return Promise.resolve();
};
/**
* Use an AudioContext to potentially split our audio output stream to multiple
* audio devices. This is only available to browsers with AudioContext and
* HTMLAudioElement.setSinkId() available. We save the source stream in
* _masterAudio, and use it for one of the active audio devices. We keep
* track of its ID because we must replace it if we lose its initial device.
*/
PeerConnection.prototype._onAddTrack = function onAddTrack(pc, stream) {
var audio = pc._masterAudio = this._createAudio();
setAudioSource(audio, stream, this._audioHelper)
.then(function () { return audio.play(); })
.catch(function () { return pc._log.error('Error attaching stream to element (_onAddTrack).'); });
// Assign the initial master audio element to a random active output device
var activeDeviceId = Array.from(pc.outputs.keys())[0];
// The audio device key could also be '' on Chrome if no media device permissions are allowed
var deviceId = typeof activeDeviceId === 'string' ? activeDeviceId : 'default';
pc._masterAudioDeviceId = deviceId;
pc.outputs.set(deviceId, { audio: audio });
try {
pc._mediaStreamSource = pc._audioContext.createMediaStreamSource(stream);
}
catch (ex) {
this._log.warn('Unable to create a MediaStreamSource from onAddTrack', ex);
this._mediaStreamSource = null;
}
pc.pcStream = stream;
pc._updateAudioOutputs();
};
/**
* Use a single audio element to play the audio output stream. This does not
* support multiple output devices, and is a fallback for when AudioContext
* and/or HTMLAudioElement.setSinkId() is not available to the client.
*/
PeerConnection.prototype._fallbackOnAddTrack = function fallbackOnAddTrack(pc, stream) {
var audio = document && document.createElement('audio');
setAudioSource(audio, stream, this._audioHelper)
.then(function () { return audio.play(); })
.catch(function () { return pc._log.error('Error attaching stream to element (_fallbackOnAddTrack).'); });
pc.outputs.set('default', { audio: audio });
};
PeerConnection.prototype._setEncodingParameters = function (enableDscp) {
if (!enableDscp
|| !this._sender
|| typeof this._sender.getParameters !== 'function'
|| typeof this._sender.setParameters !== 'function') {
return;
}
var params = this._sender.getParameters();
if (!params.priority && !(params.encodings && params.encodings.length)) {
return;
}
// This is how MDN's RTPSenderParameters defines priority
params.priority = 'high';
// And this is how it's currently implemented in Chrome M72+
if (params.encodings && params.encodings.length) {
params.encodings.forEach(function (encoding) {
encoding.priority = 'high';
encoding.networkPriority = 'high';
});
}
this._sender.setParameters(params);
};
PeerConnection.prototype._setupPeerConnection = function (rtcConfiguration) {
var _this = this;
var self = this;
var version = new (this.options.rtcpcFactory || rtcpc.default)({ RTCPeerConnection: this.options.RTCPeerConnection });
version.create(rtcConfiguration);
addStream(version.pc, this.stream);
var supportedCodecs = RTCRtpReceiver.getCapabilities('audio').codecs;
this._log.debug('sorting codecs', supportedCodecs, this.codecPreferences);
var sortedCodecs = util.sortByMimeTypes(supportedCodecs, this.codecPreferences);
var transceiver = version.pc.getTransceivers()[0];
this._log.debug('setting sorted codecs', sortedCodecs);
transceiver.setCodecPreferences(sortedCodecs);
var eventName = 'ontrack' in version.pc
? 'ontrack' : 'onaddstream';
version.pc[eventName] = function (event) {
var stream = self._remoteStream = event.stream || event.streams[0];
if (typeof version.pc.getSenders === 'function') {
_this._sender = version.pc.getSenders()[0];
}
if (self._isSinkSupported) {
self._onAddTrack(self, stream);
}
else {
self._fallbackOnAddTrack(self, stream);
}
self._startPollingVolume();
};
return version;
};
PeerConnection.prototype._maybeSetIceAggressiveNomination = function (sdp$1) {
return this.options.forceAggressiveIceNomination ? sdp.setIceAggressiveNomination(sdp$1) : sdp$1;
};
PeerConnection.prototype._setupChannel = function () {
var _this = this;
var pc = this.version.pc;
// Chrome 25 supports onopen
this.version.pc.onopen = function () {
_this.status = 'open';
_this.onopen();
};
// Chrome 26 doesn't support onopen so must detect state change
this.version.pc.onstatechange = function () {
if (_this.version.pc && _this.version.pc.readyState === 'stable') {
_this.status = 'open';
_this.onopen();
}
};
// Chrome 27 changed onstatechange to onsignalingstatechange
this.version.pc.onsignalingstatechange = function () {
var state = pc.signalingState;
_this._log.info("signalingState is \"".concat(state, "\""));
if (_this.version.pc && _this.version.pc.signalingState === 'stable') {
_this.status = 'open';
_this.onopen();
}
_this.onsignalingstatechange(pc.signalingState);
};
// Chrome 72+
pc.onconnectionstatechange = function (event) {
var state = pc.connectionState;
if (!state && event && event.target) {
// VDI environment
var targetPc = event.target;
state = targetPc.connectionState || targetPc.connectionState_;
_this._log.info("pc.connectionState not detected. Using target PC. State=".concat(state));
}
if (!state) {
_this._log.warn("onconnectionstatechange detected but state is \"".concat(state, "\""));
}
else {
_this._log.info("pc.connectionState is \"".concat(state, "\""));
}
_this.onpcconnectionstatechange(state);
_this._onMediaConnectionStateChange(state);
};
pc.onicecandidate = function (event) {
var candidate = event.candidate;
if (candidate) {
_this._hasIceCandidates = true;
_this.onicecandidate(candidate);
_this._setupRTCIceTransportListener();
}
_this._log.info("ICE Candidate: ".concat(JSON.stringify(candidate)));
};
pc.onicegatheringstatechange = function () {
var state = pc.iceGatheringState;
if (state === 'gathering') {
_this._startIceGatheringTimeout();
}
else if (state === 'complete') {
_this._stopIceGatheringTimeout();
// Fail if no candidates found
if (!_this._hasIceCandidates) {
_this._onIceGatheringFailure(ICE_GATHERING_FAIL_NONE);
}
// There was a failure mid-gathering phase. We want to start our timer and issue
// an ice restart if we don't get connected after our timeout
if (_this._hasIceCandidates && _this._hasIceGatheringFailures) {
_this._startIceGatheringTimeout();
}
}
_this._log.info("pc.iceGatheringState is \"".concat(pc.iceGatheringState, "\""));
_this.onicegatheringstatechange(state);
};
pc.oniceconnectionstatechange = function () {
_this._log.info("pc.iceConnectionState is \"".concat(pc.iceConnectionState, "\""));
_this.oniceconnectionstatechange(pc.iceConnectionState);
_this._onMediaConnectionStateChange(pc.iceConnectionState);
};
};
PeerConnection.prototype._initializeMediaStream = function (rtcConfiguration) {
// if mediastream already open then do nothing
if (this.status === 'open') {
return false;
}
if (this.pstream.status === 'disconnected') {
this.onerror({ info: {
code: 31000,
message: 'Cannot establish connection. Client is disconnected',
twilioError: new generated.SignalingErrors.ConnectionDisconnected(),
} });
this.close();
return false;
}
this.version = this._setupPeerConnection(rtcConfiguration);
this._setupChannel();
return true;
};
/**
* Remove reconnection-related listeners
* @private
*/
PeerConnection.prototype._removeReconnectionListeners = function () {
if (this.pstream) {
this.pstream.removeListener('answer', this._onAnswerOrRinging);
this.pstream.removeListener('hangup', this._onHangup);
}
};
/**
* Setup a listener for RTCDtlsTransport to capture state changes events
* @private
*/
PeerConnection.prototype._setupRTCDtlsTransportListener = function () {
var _this = this;
var dtlsTransport = this.getRTCDtlsTransport();
if (!dtlsTransport || dtlsTransport.onstatechange) {
return;
}
var handler = function () {
_this._log.info("dtlsTransportState is \"".concat(dtlsTransport.state, "\""));
_this.ondtlstransportstatechange(dtlsTransport.state);
};
// Publish initial state
handler();
dtlsTransport.onstatechange = handler;
};
/**
* Setup a listener for RTCIceTransport to capture selected candidate pair changes
* @private
*/
PeerConnection.prototype._setupRTCIceTransportListener = function () {
var _this = this;
var iceTransport = this._getRTCIceTransport();
if (!iceTransport || iceTransport.onselectedcandidatepairchange) {
return;
}
iceTransport.onselectedcandidatepairchange = function () {
return _this.onselectedcandidatepairchange(iceTransport.getSelectedCandidatePair());
};
};
/**
* Restarts ICE for the current connection
* ICE Restart failures are ignored. Retries are managed in Connection
* @private
*/
PeerConnection.prototype.iceRestart = function () {
var _this = this;
this._log.info('Attempting to restart ICE...');
this._hasIceCandidates = false;
this.version.createOffer(this.options.maxAverageBitrate, { iceRestart: true }).then(function () {
_this._removeReconnectionListeners();
_this._onAnswerOrRinging = function (payload) {
_this._removeReconnectionListeners();
if (!payload.sdp || _this.version.pc.signalingState !== 'have-local-offer') {
var message = 'Invalid state or param during ICE Restart:'
+ "hasSdp:".concat(!!payload.sdp, ", signalingState:").concat(_this.version.pc.signalingState);
_this._log.warn(message);
return;
}
var sdp = _this._maybeSetIceAggressiveNomination(payload.sdp);
_this._answerSdp = sdp;
if (_this.status !== 'closed') {
_this.version.processAnswer(_this.codecPreferences, sdp, null, function (err) {
var message = err && err.message ? err.message : err;
_this._log.error("Failed to process answer during ICE Restart. Error: ".concat(message));
});
}
};
_this._onHangup = function () {
_this._log.info('Received hangup during ICE Restart');
_this._removeReconnectionListeners();
};
_this.pstream.on('answer', _this._onAnswerOrRinging);
_this.pstream.on('hangup', _this._onHangup);
_this.pstream.reinvite(_this.version.getSDP(), _this.callSid);
}).catch(function (err) {
var message = err && err.message ? err.message : err;
_this._log.error("Failed to createOffer during ICE Restart. Error: ".concat(message));
// CreateOffer failures doesn't transition ice state to failed
// We need trigger it so it can be picked up by retries
_this.onfailed(message);
});
};
PeerConnection.prototype.makeOutgoingCall = function (params, signalingReconnectToken, callsid, rtcConfiguration, onMediaStarted) {
var _this = this;
if (!this._initializeMediaStream(rtcConfiguration)) {
return;
}
var self = this;
this.callSid = callsid;
function onAnswerSuccess() {
if (self.options) {
self._setEncodingParameters(self.options.dscp);
}
onMediaStarted(self.version.pc);
}
function onAnswerError(err) {
var errMsg = err.message || err;
self.onerror({ info: {
code: 31000,
message: "Error processing answer: ".concat(errMsg),
twilioError: new generated.MediaErrors.ClientRemoteDescFailed(),
} });
}
this._onAnswerOrRinging = function (payload) {
if (!payload.sdp) {
return;
}
var sdp = _this._maybeSetIceAggressiveNomination(payload.sdp);
self._answerSdp = sdp;
if (self.status !== 'closed') {
self.version.processAnswer(_this.codecPreferences, sdp, onAnswerSuccess, onAnswerError);
}
self.pstream.removeListener('answer', self._onAnswerOrRinging);
self.pstream.removeListener('ringing', self._onAnswerOrRinging);
};
this.pstream.on('answer', this._onAnswerOrRinging);
this.pstream.on('ringing', this._onAnswerOrRinging);
function onOfferSuccess() {
if (self.status !== 'closed') {
if (signalingReconnectToken) {
self.pstream.reconnect(self.version.getSDP(), self.callSid, signalingReconnectToken);
}
else {
self.pstream.invite(self.version.getSDP(), self.callSid, params);
}
self._setupRTCDtlsTransportListener();
}
}
function onOfferError(err) {
var errMsg = err.message || err;
self.onerror({ info: {
code: 31000,
message: "Error creating the offer: ".concat(errMsg),
twilioError: new generated.MediaErrors.ClientLocalDescFailed(),
} });
}
this.version.createOffer(this.options.maxAverageBitrate, { audio: true }, onOfferSuccess, onOfferError);
};
PeerConnection.prototype.answerIncomingCall = function (callSid, sdp, rtcConfiguration, onMediaStarted) {
if (!this._initializeMediaStream(rtcConfiguration)) {
return;
}
sdp = this._maybeSetIceAggressiveNomination(sdp);
this._answerSdp = sdp.replace(/^a=setup:actpass$/gm, 'a=setup:passive');
this.callSid = callSid;
var self = this;
function onAnswerSuccess() {
if (self.status !== 'closed') {
self.pstream.answer(self.version.getSDP(), callSid);
if (self.options) {
self._setEncodingParameters(self.options.dscp);
}
onMediaStarted(self.version.pc);
self._setupRTCDtlsTransportListener();
}
}
function onAnswerError(err) {
var errMsg = err.message || err;
self.onerror({ info: {
code: 31000,
message: "Error creating the answer: ".concat(errMsg),
twilioError: new generated.MediaErrors.ClientRemoteDescFailed(),
} });
}
this.version.processSDP(this.options.maxAverageBitrate, this.codecPreferences, sdp, { audio: true }, onAnswerSuccess, onAnswerError);
};
PeerConnection.prototype.close = function () {
if (this.version && this.version.pc) {
if (this.version.pc.signalingState !== 'closed') {
this.version.pc.close();
}
this.version.pc = null;
}
if (this.stream) {
this.mute(false);
this._stopStream();
}
this.stream = null;
this._removeReconnectionListeners();
this._stopIceGatheringTimeout();
this._audioHelper._destroyRemoteProcessedStream();
this._audioProcessorEventObserver.removeListener('add', this._onAudioProcessorAdded);
this._audioProcessorEventObserver.removeListener('remove', this._onAudioProcessorRemoved);
Promise.all(this._removeAudioOutputs()).catch(function () {
// We don't need to alert about failures here.
});
if (this._mediaStreamSource) {
this._mediaStreamSource.disconnect();
}
if (this._inputAnalyser) {
this._inputAnalyser.disconnect();
}
if (this._outputAnalyser) {
this._outputAnalyser.disconnect();
}
if (this._inputAnalyser2) {
this._inputAnalyser2.disconnect();
}
if (this._outputAnalyser2) {
this._outputAnalyser2.disconnect();
}
this.status = 'closed';
this.onclose();
};
PeerConnection.prototype.reject = function (callSid) {
this.callSid = callSid;
};
PeerConnection.prototype.ignore = function (callSid) {
this.callSid = callSid;
};
/**
* Mute or unmute input audio. If the stream is not yet present, the setting
* is saved and applied to future streams/tracks.
* @params {boolean} shouldMute - Whether the input audio should
* be muted or unmuted.
*/
PeerConnection.prototype.mute = function (shouldMute) {
this.isMuted = shouldMute;
if (!this.stream) {
return;
}
if (this._sender && this._sender.track) {
this._sender.track.enabled = !shouldMute;
}
else {
var audioTracks = typeof this.stream.getAudioTracks === 'function'
? this.stream.getAudioTracks()
: this.stream.audioTracks;
audioTracks.forEach(function (track) {
track.enabled = !shouldMute;
});
}
};
/**
* Get or create an RTCDTMFSender for the first local audio MediaStreamTrack
* we can get from the RTCPeerConnection. Return null if unsupported.
* @instance
* @returns ?RTCDTMFSender
*/
PeerConnection.prototype.getOrCreateDTMFSender = function getOrCreateDTMFSender() {
if (this._dtmfSender || this._dtmfSenderUnsupported) {
return this._dtmfSender || null;
}
var self = this;
var pc = this.version.pc;
if (!pc) {
this._log.warn('No RTCPeerConnection available to call createDTMFSender on');
return null;
}
if (typeof pc.getSenders === 'function' && (typeof RTCDTMFSender === 'function' || typeof RTCDtmfSender === 'function')) {
var chosenSender = pc.getSenders().find(function (sender) { return sender.dtmf; });
if (chosenSender) {
this._log.info('Using RTCRtpSender#dtmf');
this._dtmfSender = chosenSender.dtmf;
return this._dtmfSender;
}
}
if (typeof pc.createDTMFSender === 'function' && typeof pc.getLocalStreams === 'function') {
var track = pc.getLocalStreams().map(function (stream) {
var tracks = self._getAudioTracks(stream);
return tracks && tracks[0];
})[0];
if (!track) {
this._log.warn('No local audio MediaStreamTrack available on the RTCPeerConnection to pass to createDTMFSender');
return null;
}
this._log.info('Creating RTCDTMFSender');
this._dtmfSender = pc.createDTMFSender(track);
return this._dtmfSender;
}
this._log.info('RTCPeerConnection does not support RTCDTMFSender');
this._dtmfSenderUnsupported = true;
return null;
};
/**
* Get the RTCDtlTransport object from the PeerConnection
* @returns RTCDtlTransport
*/
PeerConnection.prototype.getRTCDtlsTransport = function getRTCDtlsTransport() {
var sender = this.version && this.version.pc
&& typeof this.version.pc.getSenders === 'function'
&& this.version.pc.getSenders()[0];
return sender && sender.transport || null;
};
PeerConnection.prototype._canStopMediaStreamTrack = function () { return typeof MediaStreamTrack.prototype.stop === 'function'; };
PeerConnection.prototype._getAudioTracks = function (stream) { return typeof stream.getAudioTracks === 'function' ?
stream.getAudioTracks() : stream.audioTracks; };
/**
* Get the RTCIceTransport object from the PeerConnection
* @returns RTCIceTransport
*/
PeerConnection.prototype._getRTCIceTransport = function _getRTCIceTransport() {
var dtlsTransport = this.getRTCDtlsTransport();
return dtlsTransport && dtlsTransport.iceTransport || null;
};
// Is PeerConnection.protocol used outside of our SDK? We should remove this if not.
PeerConnection.protocol = ((function () { return rtcpc.default.test() ? new rtcpc.default() : null; }))();
PeerConnection.prototype._handleAudioProcessorEvent = function (isRemote, isAddProcessor) {
var _this = this;
if (!isRemote || !this._remoteStream) {
return;
}
var audio = null;
if (this._masterAudio) {
this._log.info('Setting audio source for master audio.');
audio = this._masterAudio;
}
else {
this._log.info('No master audio. Setting audio source for default audio output.');
audio = this.outputs.get('default').audio;
}
setAudioSource(audio, this._remoteStream, this._audioHelper)
.then(function () {
var successLog = isAddProcessor
? 'Successfully updated audio source with processed stream'
: 'Successfully reverted audio source to original stream';
_this._log.info(successLog);
// If the audio was paused, resume playback
if (audio.paused) {
_this._log.info('Resuming audio playback');
audio.play();
}
})
.catch(function () {
var errorLog = isAddProcessor
? 'Failed to update audio source'
: 'Failed to revert audio source';
_this._log.error(errorLog);
});
};
function addStream(pc, stream) {
if (typeof pc.addTrack === 'function') {
stream.getAudioTracks().forEach(function (track) {
// The second parameters, stream, should not be necessary per the latest editor's
// draft, but FF requires it. https://bugzilla.mozilla.org/show_bug.cgi?id=1231414
pc.addTrack(track, stream);
});
}
else {
pc.addStream(stream);
}
}
function cloneStream(oldStream, _MediaStream) {
var newStream;
if (_MediaStream) {
newStream = new _MediaStream();
}
else if (typeof MediaStream !== 'undefined') {
newStream = new MediaStream();
}
else {
newStream = new webkitMediaStream();
}
oldStream.getAudioTracks().forEach(newStream.addTrack, newStream);
return newStream;
}
/**
* Sets the source of an HTMLAudioElement to the specified MediaStream and
* applies a remote audio processor if available
* @param {HTMLAudioElement} audio
* @param {MediaStream} stream
* @returns {Promise} Fulfilled if the audio source was set successfully
*/
function setAudioSource(audio, stream, audioHelper) {
return audioHelper._maybeCreateRemoteProcessedStream(stream).then(function (maybeProcessedStream) {
if (typeof audio.srcObject !== 'undefined') {
audio.srcObject = maybeProcessedStream;
}
else if (typeof audio.mozSrcObject !== 'undefined') {
audio.mozSrcObject = maybeProcessedStream;
}
else if (typeof audio.src !== 'undefined') {
var _window = audio.options.window || window;
audio.src = (_window.URL || _window.webkitURL).createObjectURL(maybeProcessedStream);
}
else {
return Promise.reject();
}
return Promise.resolve();
});
}
PeerConnection.enabled = rtcpc.default.test();
exports.default = PeerConnection;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVlcmNvbm5lY3Rpb24uanMiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL2xpYi90d2lsaW8vcnRjL3BlZXJjb25uZWN0aW9uLnRzIl0sInNvdXJjZXNDb250ZW50IjpbbnVsbF0sIm5hbWVzIjpbIkludmFsaWRBcmd1bWVudEVycm9yIiwiTG9nIiwiTm90U3VwcG9ydGVkRXJyb3IiLCJSVENQQyIsInV0aWwuc29ydEJ5TWltZVR5cGVzIiwic2RwIiwic2V0SWNlQWdncmVzc2l2ZU5vbWluYXRpb24iLCJTaWduYWxpbmdFcnJvcnMiLCJNZWRpYUVycm9ycyJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7QUFBQTtBQVlBLElBQU0scUJBQXFCLEdBQUcsS0FBSztBQUNuQyxJQUFNLHVCQUF1QixHQUFHLE1BQU07QUFDdEMsSUFBTSwwQkFBMEIsR0FBRyxTQUFTO0FBQzVDLElBQU0sNEJBQTRCLEdBQUcsS0FBSztBQUMxQyxJQUFNLGtCQUFrQixHQUFHLEVBQUU7QUFFN0I7Ozs7Ozs7QUFPRztBQUNILFNBQVMsY0FBYyxDQUFDLFdBQVcsRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFBO0lBQXJELElBQUEsS0FBQSxHQUFBLElBQUE7QUFDRSxJQUFBLElBQUksQ0FBQyxXQUFXLElBQUksQ0FBQyxPQUFPLEVBQUU7QUFDNUIsUUFBQSxNQUFNLElBQUlBLDBCQUFvQixDQUFDLGlEQUFpRCxDQUFDO0lBQ25GO0FBRUEsSUFBQSxJQUFJLEVBQUUsSUFBSSxZQUFZLGNBQWMsQ0FBQyxFQUFFO1FBQ3JDLE9BQU8sSUFBSSxjQUFjLENBQUMsV0FBVyxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQUM7SUFDMUQ7SUFFQSxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUlDLFdBQUcsQ0FBQyxnQkFBZ0IsQ0FBQztBQUVyQyxJQUFBLFNBQVMsSUFBSSxHQUFBO0FBQ1gsUUFBQSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyx3Q0FBd0MsQ0FBQztJQUMxRDtBQUNBLElBQUEsSUFBSSxDQUFDLE9B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