@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
1,083 lines (1,045 loc) • 524 kB
JavaScript
/*! @twilio/voice-sdk.js 2.17.0
The following license applies to all parts of this software except as
documented below.
Copyright (C) 2015-2024 Twilio, inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This software includes rtcpeerconnection-shim under the following (BSD 3-Clause) license.
Copyright (c) 2017 Philipp Hancke. All rights reserved.
Copyright (c) 2014, The WebRTC project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Philipp Hancke nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This software includes backoff under the following (MIT) license.
Copyright (C) 2012 Mathieu Turcotte
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This software includes loglevel under the following (MIT) license.
Copyright (c) 2013 Tim Perry
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
(function(root) {
var bundle = (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'use strict';
var call = require('./twilio/call.js');
var device = require('./twilio/device.js');
var log = require('./twilio/log.js');
var preflight = require('./twilio/preflight/preflight.js');
var index = require('./twilio/errors/index.js');
Object.defineProperty(exports, "Call", {
enumerable: true,
get: function () { return call.default; }
});
Object.defineProperty(exports, "Device", {
enumerable: true,
get: function () { return device.default; }
});
exports.Logger = log.Logger;
Object.defineProperty(exports, "PreflightTest", {
enumerable: true,
get: function () { return preflight.PreflightTest; }
});
exports.TwilioError = index;
},{"./twilio/call.js":9,"./twilio/device.js":12,"./twilio/errors/index.js":15,"./twilio/log.js":18,"./twilio/preflight/preflight.js":20}],2:[function(require,module,exports){
'use strict';
var tslib = require('tslib');
var deferred = require('./deferred.js');
/**
* Queue async operations and executes them synchronously.
*/
var AsyncQueue = /** @class */ (function () {
function AsyncQueue() {
/**
* The list of async operations in this queue
*/
this._operations = [];
}
/**
* Adds the async operation to the queue
* @param callback An async callback that returns a promise
* @returns A promise that will get resolved or rejected after executing the callback
*/
AsyncQueue.prototype.enqueue = function (callback) {
var hasPending = !!this._operations.length;
var deferred$1 = new deferred.default();
this._operations.push({ deferred: deferred$1, callback: callback });
if (!hasPending) {
this._processQueue();
}
return deferred$1.promise;
};
/**
* Start processing the queue. This executes the first item and removes it after.
* Then do the same for next items until the queue is emptied.
*/
AsyncQueue.prototype._processQueue = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var _a, deferred, callback, result, error, hasResolved, e_1;
return tslib.__generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!this._operations.length) return [3 /*break*/, 5];
_a = this._operations[0], deferred = _a.deferred, callback = _a.callback;
result = void 0;
error = void 0;
hasResolved = void 0;
_b.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
return [4 /*yield*/, callback()];
case 2:
result = _b.sent();
hasResolved = true;
return [3 /*break*/, 4];
case 3:
e_1 = _b.sent();
error = e_1;
return [3 /*break*/, 4];
case 4:
// Remove the item
this._operations.shift();
if (hasResolved) {
deferred.resolve(result);
}
else {
deferred.reject(error);
}
return [3 /*break*/, 0];
case 5: return [2 /*return*/];
}
});
});
};
return AsyncQueue;
}());
exports.AsyncQueue = AsyncQueue;
},{"./deferred.js":11,"tslib":41}],3:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var tslib = require('tslib');
var events = require('events');
var device = require('./device.js');
var index = require('./errors/index.js');
var log = require('./log.js');
var outputdevicecollection = require('./outputdevicecollection.js');
var mediadeviceinfo = require('./shims/mediadeviceinfo.js');
var util = require('./util.js');
/**
* Aliases for audio kinds, used for labelling.
*/
var kindAliases = {
audioinput: 'Audio Input',
audiooutput: 'Audio Output',
};
/**
* Provides input and output audio-based functionality in one convenient class.
*/
var AudioHelper = /** @class */ (function (_super) {
tslib.__extends(AudioHelper, _super);
/**
* @internal
* @param onActiveOutputsChanged - A callback to be called when the user changes the active output devices.
* @param onActiveInputChanged - A callback to be called when the user changes the active input device.
* @param [options]
*/
function AudioHelper(onActiveOutputsChanged, onActiveInputChanged, options) {
var _a;
var _this = _super.call(this) || this;
/**
* A Map of all audio input devices currently available to the browser by their device ID.
*/
_this.availableInputDevices = new Map();
/**
* A Map of all audio output devices currently available to the browser by their device ID.
*/
_this.availableOutputDevices = new Map();
/**
* The currently set audio constraints set by setAudioConstraints().
*/
_this._audioConstraints = null;
/**
* The audio stream of the default device.
* This is populated when _openDefaultDeviceWithConstraints is called,
* See _selectedInputDeviceStream for differences.
* TODO: Combine these two workflows (3.x?)
*/
_this._defaultInputDeviceStream = null;
/**
* Whether each sound is enabled.
*/
_this._enabledSounds = (_a = {},
_a[device.default.SoundName.Disconnect] = true,
_a[device.default.SoundName.Incoming] = true,
_a[device.default.SoundName.Outgoing] = true,
_a);
/**
* The current input device.
*/
_this._inputDevice = null;
/**
* The internal promise created when calling setInputDevice
*/
_this._inputDevicePromise = null;
/**
* Whether the {@link AudioHelper} is currently polling the input stream's volume.
*/
_this._isPollingInputVolume = false;
/**
* An instance of Logger to use.
*/
_this._log = new log.default('AudioHelper');
/**
* Internal reference to the local processed stream.
*/
_this._localProcessedStream = null;
/**
* Internal reference to the remote processed stream.
*/
_this._remoteProcessedStream = null;
/**
* The selected input stream coming from the microphone device.
* This is populated when the setInputDevice is called, meaning,
* the end user manually selected it, which is different than
* the defaultInputDeviceStream.
* TODO: Combine these two workflows (3.x?)
*/
_this._selectedInputDeviceStream = null;
/**
* A record of unknown devices (Devices without labels)
*/
_this._unknownDeviceIndexes = {
audioinput: {},
audiooutput: {},
};
/**
* Update the available input and output devices
* @internal
*/
_this._updateAvailableDevices = function () {
if (!_this._mediaDevices || !_this._enumerateDevices) {
return Promise.reject('Enumeration not supported');
}
return _this._enumerateDevices().then(function (devices) {
_this._updateDevices(devices.filter(function (d) { return d.kind === 'audiooutput'; }), _this.availableOutputDevices, _this._removeLostOutput);
_this._updateDevices(devices.filter(function (d) { return d.kind === 'audioinput'; }), _this.availableInputDevices, _this._removeLostInput);
var defaultDevice = _this.availableOutputDevices.get('default')
|| Array.from(_this.availableOutputDevices.values())[0];
[_this.speakerDevices, _this.ringtoneDevices].forEach(function (outputDevices) {
if (!outputDevices.get().size && _this.availableOutputDevices.size && _this.isOutputSelectionSupported) {
outputDevices.set(defaultDevice.deviceId)
.catch(function (reason) {
_this._log.warn("Unable to set audio output devices. ".concat(reason));
});
}
});
});
};
/**
* Remove an input device from inputs
* @param lostDevice
* @returns Whether the device was active
*/
_this._removeLostInput = function (lostDevice) {
if (!_this.inputDevice || _this.inputDevice.deviceId !== lostDevice.deviceId) {
return false;
}
_this._destroyLocalProcessedStream();
_this._replaceStream(null);
_this._inputDevice = null;
_this._maybeStopPollingVolume();
var defaultDevice = _this.availableInputDevices.get('default')
|| Array.from(_this.availableInputDevices.values())[0];
if (defaultDevice) {
_this.setInputDevice(defaultDevice.deviceId);
}
return true;
};
/**
* Remove an input device from outputs
* @param lostDevice
* @returns Whether the device was active
*/
_this._removeLostOutput = function (lostDevice) {
var wasSpeakerLost = _this.speakerDevices.delete(lostDevice);
var wasRingtoneLost = _this.ringtoneDevices.delete(lostDevice);
return wasSpeakerLost || wasRingtoneLost;
};
options = Object.assign({
AudioContext: typeof AudioContext !== 'undefined' && AudioContext,
setSinkId: typeof HTMLAudioElement !== 'undefined' && HTMLAudioElement.prototype.setSinkId,
}, options);
_this._beforeSetInputDevice = options.beforeSetInputDevice || (function () { return Promise.resolve(); });
_this._updateUserOptions(options);
_this._audioProcessorEventObserver = options.audioProcessorEventObserver;
_this._mediaDevices = options.mediaDevices || navigator.mediaDevices;
_this._onActiveInputChanged = onActiveInputChanged;
_this._enumerateDevices = typeof options.enumerateDevices === 'function'
? options.enumerateDevices
: _this._mediaDevices && _this._mediaDevices.enumerateDevices.bind(_this._mediaDevices);
var isAudioContextSupported = !!(options.AudioContext || options.audioContext);
var isEnumerationSupported = !!_this._enumerateDevices;
if (options.enabledSounds) {
_this._enabledSounds = options.enabledSounds;
}
var isSetSinkSupported = typeof options.setSinkId === 'function';
_this.isOutputSelectionSupported = isEnumerationSupported && isSetSinkSupported;
_this.isVolumeSupported = isAudioContextSupported;
if (_this.isVolumeSupported) {
_this._audioContext = options.audioContext || options.AudioContext && new options.AudioContext();
if (_this._audioContext) {
_this._inputVolumeAnalyser = _this._audioContext.createAnalyser();
_this._inputVolumeAnalyser.fftSize = 32;
_this._inputVolumeAnalyser.smoothingTimeConstant = 0.3;
}
}
_this.ringtoneDevices = new outputdevicecollection.default('ringtone', _this.availableOutputDevices, onActiveOutputsChanged, _this.isOutputSelectionSupported);
_this.speakerDevices = new outputdevicecollection.default('speaker', _this.availableOutputDevices, onActiveOutputsChanged, _this.isOutputSelectionSupported);
_this.addListener('newListener', function (eventName) {
if (eventName === 'inputVolume') {
_this._maybeStartPollingVolume();
}
});
_this.addListener('removeListener', function (eventName) {
if (eventName === 'inputVolume') {
_this._maybeStopPollingVolume();
}
});
_this.once('newListener', function () {
// NOTE (rrowland): Ideally we would only check isEnumerationSupported here, but
// in at least one browser version (Tested in FF48) enumerateDevices actually
// returns bad data for the listed devices. Instead, we check for
// isOutputSelectionSupported to avoid these quirks that may negatively affect customers.
if (!_this.isOutputSelectionSupported) {
_this._log.warn('Warning: This browser does not support audio output selection.');
}
if (!_this.isVolumeSupported) {
_this._log.warn("Warning: This browser does not support Twilio's volume indicator feature.");
}
});
if (isEnumerationSupported) {
_this._initializeEnumeration();
}
// NOTE (kchoy): Currently microphone permissions are not supported in firefox, and Safari V15 and older.
// https://github.com/mozilla/standards-positions/issues/19#issuecomment-370158947
// https://caniuse.com/permissions-api
if (navigator && navigator.permissions && typeof navigator.permissions.query === 'function') {
navigator.permissions.query({ name: 'microphone' }).then(function (microphonePermissionStatus) {
if (microphonePermissionStatus.state !== 'granted') {
var handleStateChange = function () {
_this._updateAvailableDevices();
_this._stopMicrophonePermissionListener();
};
microphonePermissionStatus.addEventListener('change', handleStateChange);
_this._microphonePermissionStatus = microphonePermissionStatus;
_this._onMicrophonePermissionStatusChanged = handleStateChange;
}
}).catch(function (reason) { return _this._log.warn("Warning: unable to listen for microphone permission changes. ".concat(reason)); });
}
else {
_this._log.warn('Warning: current browser does not support permissions API.');
}
return _this;
}
Object.defineProperty(AudioHelper.prototype, "audioConstraints", {
/**
* The currently set audio constraints set by setAudioConstraints(). Starts as null.
*/
get: function () { return this._audioConstraints; },
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "inputDevice", {
/**
* The active input device. Having no inputDevice specified by `setInputDevice()`
* will disable input selection related functionality.
*/
get: function () { return this._inputDevice; },
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "inputStream", {
/**
* The current input stream coming from the microphone device or
* the processed audio stream if there is an {@link AudioProcessor}.
*/
get: function () { return this._localProcessedStream || this._selectedInputDeviceStream; },
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "processedStream", {
/**
* The processed stream if a local {@link AudioProcessor} was previously added.
* @deprecated Use {@link AudioHelper#localProcessedStream} instead.
*/
get: function () {
this._log.warn('AudioHelper#processedStream is deprecated. Please use AudioHelper#localProcessedStream instead.');
return this._localProcessedStream;
},
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "localProcessedStream", {
/**
* The processed stream if a local {@link AudioProcessor} was previously added.
*/
get: function () { return this._localProcessedStream; },
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "remoteProcessedStream", {
/**
* The processed stream if a remote {@link AudioProcessor} was previously added.
*/
get: function () { return this._remoteProcessedStream; },
enumerable: false,
configurable: true
});
/**
* Destroy this AudioHelper instance
* @internal
*/
AudioHelper.prototype._destroy = function () {
this._stopDefaultInputDeviceStream();
this._stopSelectedInputDeviceStream();
this._destroyLocalProcessedStream();
this._destroyRemoteProcessedStream();
this._maybeStopPollingVolume();
this.removeAllListeners();
this._stopMicrophonePermissionListener();
this._unbind();
};
/**
* Destroys the remote processed stream and updates references.
* @internal
*/
AudioHelper.prototype._destroyRemoteProcessedStream = function () {
if (this._remoteProcessor && this._remoteProcessedStream) {
this._log.info('destroying remote processed stream');
var remoteProcessedStream = this._remoteProcessedStream;
this._remoteProcessedStream.getTracks().forEach(function (track) { return track.stop(); });
this._remoteProcessedStream = null;
this._remoteProcessor.destroyProcessedStream(remoteProcessedStream);
this._audioProcessorEventObserver.emit('destroy', true);
}
};
/**
* Promise to wait for the input device, if setInputDevice is called outside of the SDK.
* @internal
*/
AudioHelper.prototype._getInputDevicePromise = function () {
return this._inputDevicePromise;
};
/**
* The current AudioProcessorEventObserver instance.
* @internal
*/
AudioHelper.prototype._getAudioProcessorEventObserver = function () {
return this._audioProcessorEventObserver;
};
/**
* Route remote stream to the processor if it exists.
* @internal
*/
AudioHelper.prototype._maybeCreateRemoteProcessedStream = function (stream) {
var _this = this;
if (this._remoteProcessor) {
this._log.info('Creating remote processed stream');
return this._remoteProcessor.createProcessedStream(stream).then(function (processedStream) {
_this._remoteProcessedStream = processedStream;
_this._audioProcessorEventObserver.emit('create', true);
return _this._remoteProcessedStream;
});
}
return Promise.resolve(stream);
};
/**
* Start polling volume if it's supported and there's an input stream to poll.
* @internal
*/
AudioHelper.prototype._maybeStartPollingVolume = function () {
var _this = this;
if (!this.isVolumeSupported || !this.inputStream) {
return;
}
this._updateVolumeSource();
if (this._isPollingInputVolume || !this._inputVolumeAnalyser) {
return;
}
var bufferLength = this._inputVolumeAnalyser.frequencyBinCount;
var buffer = new Uint8Array(bufferLength);
this._isPollingInputVolume = true;
var emitVolume = function () {
if (!_this._isPollingInputVolume) {
return;
}
if (_this._inputVolumeAnalyser) {
_this._inputVolumeAnalyser.getByteFrequencyData(buffer);
var inputVolume = util.average(buffer);
_this.emit('inputVolume', inputVolume / 255);
}
requestAnimationFrame(emitVolume);
};
requestAnimationFrame(emitVolume);
};
/**
* Stop polling volume if it's currently polling and there are no listeners.
* @internal
*/
AudioHelper.prototype._maybeStopPollingVolume = function () {
if (!this.isVolumeSupported) {
return;
}
if (!this._isPollingInputVolume || (this.inputStream && this.listenerCount('inputVolume'))) {
return;
}
if (this._inputVolumeSource) {
this._inputVolumeSource.disconnect();
delete this._inputVolumeSource;
}
this._isPollingInputVolume = false;
};
/**
* Call getUserMedia with specified constraints
* @internal
*/
AudioHelper.prototype._openDefaultDeviceWithConstraints = function (constraints) {
var _this = this;
this._log.info('Opening default device with constraints', constraints);
return this._getUserMedia(constraints).then(function (stream) {
_this._log.info('Opened default device. Updating available devices.');
// Ensures deviceId's and labels are populated after the gUM call
// by calling enumerateDevices
_this._updateAvailableDevices().catch(function (error) {
// Ignore error, we don't want to break the call flow
_this._log.warn('Unable to updateAvailableDevices after gUM call', error);
});
_this._defaultInputDeviceStream = stream;
return _this._maybeCreateLocalProcessedStream(stream);
});
};
/**
* Stop the default audio stream
* @internal
*/
AudioHelper.prototype._stopDefaultInputDeviceStream = function () {
if (this._defaultInputDeviceStream) {
this._log.info('stopping default device stream');
this._defaultInputDeviceStream.getTracks().forEach(function (track) { return track.stop(); });
this._defaultInputDeviceStream = null;
this._destroyLocalProcessedStream();
}
};
/**
* Unbind the listeners from mediaDevices.
* @internal
*/
AudioHelper.prototype._unbind = function () {
var _a;
if ((_a = this._mediaDevices) === null || _a === void 0 ? void 0 : _a.removeEventListener) {
this._mediaDevices.removeEventListener('devicechange', this._updateAvailableDevices);
}
};
/**
* Update AudioHelper options that can be changed by the user
* @internal
*/
AudioHelper.prototype._updateUserOptions = function (options) {
if (typeof options.enumerateDevices === 'function') {
this._enumerateDevices = options.enumerateDevices;
}
if (typeof options.getUserMedia === 'function') {
this._getUserMedia = options.getUserMedia;
}
};
/**
* Adds an {@link AudioProcessor} object. Once added, the AudioHelper will route
* the input audio stream through the processor before sending the audio
* stream to Twilio. Only one AudioProcessor can be added at this time.
*
* See the {@link AudioProcessor} interface for an example.
*
* @param processor The AudioProcessor to add.
* @param isRemote If set to true, the processor will be applied to the remote
* audio track. Default value is false.
* @returns
*/
AudioHelper.prototype.addProcessor = function (processor, isRemote) {
if (isRemote === void 0) { isRemote = false; }
this._log.debug('.addProcessor');
if (this._localProcessor && !isRemote) {
throw new index.NotSupportedError('Can only have one Local AudioProcessor at a time.');
}
if (this._remoteProcessor && isRemote) {
throw new index.NotSupportedError('Can only have one Remote AudioProcessor at a time.');
}
if (typeof processor !== 'object' || processor === null) {
throw new index.InvalidArgumentError('Missing AudioProcessor argument.');
}
if (typeof processor.createProcessedStream !== 'function') {
throw new index.InvalidArgumentError('Missing createProcessedStream() method.');
}
if (typeof processor.destroyProcessedStream !== 'function') {
throw new index.InvalidArgumentError('Missing destroyProcessedStream() method.');
}
if (isRemote) {
this._remoteProcessor = processor;
this._audioProcessorEventObserver.emit('add', true);
return Promise.resolve();
}
else {
this._localProcessor = processor;
this._audioProcessorEventObserver.emit('add', false);
return this._restartInputStreams();
}
};
/**
* Enable or disable the disconnect sound.
* @param doEnable Passing `true` will enable the sound and `false` will disable the sound.
* Not passing this parameter will not alter the enable-status of the sound.
* @returns The enable-status of the sound.
*/
AudioHelper.prototype.disconnect = function (doEnable) {
this._log.debug('.disconnect', doEnable);
return this._maybeEnableSound(device.default.SoundName.Disconnect, doEnable);
};
/**
* Enable or disable the incoming sound.
* @param doEnable Passing `true` will enable the sound and `false` will disable the sound.
* Not passing this parameter will not alter the enable-status of the sound.
* @returns The enable-status of the sound.
*/
AudioHelper.prototype.incoming = function (doEnable) {
this._log.debug('.incoming', doEnable);
return this._maybeEnableSound(device.default.SoundName.Incoming, doEnable);
};
/**
* Enable or disable the outgoing sound.
* @param doEnable Passing `true` will enable the sound and `false` will disable the sound.
* Not passing this parameter will not alter the enable-status of the sound.
* @returns The enable-status of the sound.
*/
AudioHelper.prototype.outgoing = function (doEnable) {
this._log.debug('.outgoing', doEnable);
return this._maybeEnableSound(device.default.SoundName.Outgoing, doEnable);
};
/**
* Removes an {@link AudioProcessor}. Once removed, the AudioHelper will start using
* the audio stream from the selected input device for existing or future calls.
*
* @param processor The AudioProcessor to remove.
* @param isRemote If set to true, the processor will be removed from the remote
* audio track. Default value is false.
* @returns
*/
AudioHelper.prototype.removeProcessor = function (processor, isRemote) {
if (isRemote === void 0) { isRemote = false; }
this._log.debug('.removeProcessor');
if (typeof processor !== 'object' || processor === null) {
throw new index.InvalidArgumentError('Missing AudioProcessor argument.');
}
if (this._localProcessor !== processor && !isRemote) {
throw new index.InvalidArgumentError('Cannot remove a Local AudioProcessor that has not been previously added.');
}
if (this._remoteProcessor !== processor && isRemote) {
throw new index.InvalidArgumentError('Cannot remove a Remote AudioProcessor that has not been previously added.');
}
if (isRemote) {
this._destroyRemoteProcessedStream();
this._remoteProcessor = null;
this._audioProcessorEventObserver.emit('remove', true);
return Promise.resolve();
}
else {
this._destroyLocalProcessedStream();
this._localProcessor = null;
this._audioProcessorEventObserver.emit('remove', false);
return this._restartInputStreams();
}
};
/**
* Set the MediaTrackConstraints to be applied on every getUserMedia call for new input
* device audio. Any deviceId specified here will be ignored. Instead, device IDs should
* be specified using {@link AudioHelper#setInputDevice}. The returned Promise resolves
* when the media is successfully reacquired, or immediately if no input device is set.
* @param audioConstraints - The MediaTrackConstraints to apply.
*/
AudioHelper.prototype.setAudioConstraints = function (audioConstraints) {
this._log.debug('.setAudioConstraints', audioConstraints);
this._audioConstraints = Object.assign({}, audioConstraints);
delete this._audioConstraints.deviceId;
return this.inputDevice
? this._setInputDevice(this.inputDevice.deviceId, true)
: Promise.resolve();
};
/**
* Replace the current input device with a new device by ID.
*
* Calling `setInputDevice` sets the stream for current and future calls and
* will not release it automatically.
*
* While this behavior is not an issue, it will result in the application
* holding onto the input device, and the application may show a red
* "recording" symbol in the browser tab.
*
* To remove the red "recording" symbol, the device must be released. To
* release it, call `unsetInputDevice` after the call disconnects. Note that
* after calling `unsetInputDevice` future calls will then use the default
* input device.
*
* Consider application logic that keeps track of the user-selected device
* and call `setInputDevice` before calling `device.connect()` for outgoing
* calls and `call.accept()` for incoming calls. Furthermore, consider
* calling `unsetInputDevice` once a call is disconnected. Below is an
* example:
*
* ```ts
* import { Device } from '@twilio/voice-sdk';
* let inputDeviceId = ...;
* const device = new Device(...);
*
* async function makeOutgoingCall() {
* await device.audio.setInputDevice(inputDeviceId);
* const call = await device.connect(...);
*
* call.on('disconnect', async () => {
* inputDeviceId = ... // save the current input device id
* await device.audio.unsetInputDevice();
* });
* }
*
* async function acceptIncomingCall(incomingCall) {
* await device.audio.setInputDevice(inputDeviceId);
* await incomingCall.accept();
*
* incomingCall.on('disconnect', async () => {
* inputDeviceId = ... // save the current input device id
* await device.audio.unsetInputDevice();
* });
* }
* ```
*
* @param deviceId - An ID of a device to replace the existing
* input device with.
*/
AudioHelper.prototype.setInputDevice = function (deviceId) {
this._log.debug('.setInputDevice', deviceId);
return this._setInputDevice(deviceId, false);
};
/**
* Unset the MediaTrackConstraints to be applied on every getUserMedia call for new input
* device audio. The returned Promise resolves when the media is successfully reacquired,
* or immediately if no input device is set.
*/
AudioHelper.prototype.unsetAudioConstraints = function () {
this._log.debug('.unsetAudioConstraints');
this._audioConstraints = null;
return this.inputDevice
? this._setInputDevice(this.inputDevice.deviceId, true)
: Promise.resolve();
};
/**
* Unset the input device, stopping the tracks. This should only be called when not in a connection, and
* will not allow removal of the input device during a live call.
*/
AudioHelper.prototype.unsetInputDevice = function () {
var _this = this;
this._log.debug('.unsetInputDevice', this.inputDevice);
if (!this.inputDevice) {
return Promise.resolve();
}
this._destroyLocalProcessedStream();
return this._onActiveInputChanged(null).then(function () {
_this._replaceStream(null);
_this._inputDevice = null;
_this._maybeStopPollingVolume();
});
};
/**
* Destroys the local processed stream and updates references.
*/
AudioHelper.prototype._destroyLocalProcessedStream = function () {
if (this._localProcessor && this._localProcessedStream) {
this._log.info('destroying local processed stream');
var localProcessedStream = this._localProcessedStream;
this._localProcessedStream.getTracks().forEach(function (track) { return track.stop(); });
this._localProcessedStream = null;
this._localProcessor.destroyProcessedStream(localProcessedStream);
this._audioProcessorEventObserver.emit('destroy', false);
}
};
/**
* Get the index of an un-labeled Device.
* @param mediaDeviceInfo
* @returns The index of the passed MediaDeviceInfo
*/
AudioHelper.prototype._getUnknownDeviceIndex = function (mediaDeviceInfo) {
var id = mediaDeviceInfo.deviceId;
var kind = mediaDeviceInfo.kind;
var index = this._unknownDeviceIndexes[kind][id];
if (!index) {
index = Object.keys(this._unknownDeviceIndexes[kind]).length + 1;
this._unknownDeviceIndexes[kind][id] = index;
}
return index;
};
/**
* Initialize output device enumeration.
*/
AudioHelper.prototype._initializeEnumeration = function () {
var _this = this;
if (!this._mediaDevices || !this._enumerateDevices) {
throw new index.NotSupportedError('Enumeration is not supported');
}
if (this._mediaDevices.addEventListener) {
this._mediaDevices.addEventListener('devicechange', this._updateAvailableDevices);
}
this._updateAvailableDevices().then(function () {
if (!_this.isOutputSelectionSupported) {
return;
}
Promise.all([
_this.speakerDevices.set('default'),
_this.ringtoneDevices.set('default'),
]).catch(function (reason) {
_this._log.warn("Warning: Unable to set audio output devices. ".concat(reason));
});
});
};
/**
* Route local stream to the processor if it exists.
*/
AudioHelper.prototype._maybeCreateLocalProcessedStream = function (stream) {
var _this = this;
if (this._localProcessor) {
this._log.info('Creating local processed stream');
return this._localProcessor.createProcessedStream(stream).then(function (processedStream) {
_this._localProcessedStream = processedStream;
_this._audioProcessorEventObserver.emit('create', false);
return _this._localProcessedStream;
});
}
return Promise.resolve(stream);
};
/**
* Set whether the sound is enabled or not
* @param soundName
* @param doEnable
* @returns Whether the sound is enabled or not
*/
AudioHelper.prototype._maybeEnableSound = function (soundName, doEnable) {
if (typeof doEnable !== 'undefined') {
this._enabledSounds[soundName] = doEnable;
}
return this._enabledSounds[soundName];
};
/**
* Stop the tracks on the current input stream before replacing it with the passed stream.
* @param stream - The new stream
*/
AudioHelper.prototype._replaceStream = function (stream) {
this._log.info('Replacing with new stream.');
if (this._selectedInputDeviceStream) {
this._log.info('Old stream detected. Stopping tracks.');
this._stopSelectedInputDeviceStream();
}
this._selectedInputDeviceStream = stream;
};
/**
* Restart the active input streams
*/
AudioHelper.prototype._restartInputStreams = function () {
if (this.inputDevice && this._selectedInputDeviceStream) {
this._log.info('Restarting selected input device');
return this._setInputDevice(this.inputDevice.deviceId, true);
}
if (this._defaultInputDeviceStream) {
var defaultDevice = this.availableInputDevices.get('default')
|| Array.from(this.availableInputDevices.values())[0];
this._log.info('Restarting default input device, now becoming selected.');
return this._setInputDevice(defaultDevice.deviceId, true);
}
return Promise.resolve();
};
/**
* Replace the current input device with a new device by ID.
* @param deviceId - An ID of a device to replace the existing
* input device with.
* @param forceGetUserMedia - If true, getUserMedia will be called even if
* the specified device is already active.
*/
AudioHelper.prototype._setInputDevice = function (deviceId, forceGetUserMedia) {
return tslib.__awaiter(this, void 0, void 0, function () {
var setInputDevice;
var _this = this;
return tslib.__generator(this, function (_a) {
setInputDevice = function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var device, constraints;
var _this = this;
return tslib.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._beforeSetInputDevice()];
case 1:
_a.sent();
if (typeof deviceId !== 'string') {
return [2 /*return*/, Promise.reject(new index.InvalidArgumentError('Must specify the device to set'))];
}
device = this.availableInputDevices.get(deviceId);
if (!device) {
return [2 /*return*/, Promise.reject(new index.InvalidArgumentError("Device not found: ".concat(deviceId)))];
}
this._log.info('Setting input device. ID: ' + deviceId);
if (this._inputDevice && this._inputDevice.deviceId === deviceId && this._selectedInputDeviceStream) {
if (!forceGetUserMedia) {
return [2 /*return*/, Promise.resolve()];
}
// If the currently active track is still in readyState `live`, gUM may return the same track
// rather than returning a fresh track.
this._log.info('Same track detected on setInputDevice, stopping old tracks.');
this._stopSelectedInputDeviceStream();
}
// Release the default device in case it was created previously
this._stopDefaultInputDeviceStream();
constraints = { audio: Object.assign({ deviceId: { exact: deviceId } }, this.audioConstraints) };
this._log.info('setInputDevice: getting new tracks.');
return [2 /*return*/, this._getUserMedia(constraints).then(function (originalStream) {
_this._destroyLocalProcessedStream();
return _this._maybeCreateLocalProcessedStream(originalStream).then(function (newStream) {
_this._log.info('setInputDevice: invoking _onActiveInputChanged.');
return _this._onActiveInputChanged(newStream).then(function () {
_this._replaceStream(originalStream);
_this._inputDevice = device;
_this._maybeStartPollingVolume();
});
});
})];
}
});
}); };
return [2 /*return*/, this._inputDevicePromise = setInputDevice().finally(function () {
_this._inputDevicePromise = null;
})];
});
});
};
/**
* Remove event listener for microphone permissions
*/
AudioHelper.prototype._stopMicrophonePermissionListener = function () {
var _a;
if ((_a = this._microphonePermissionStatus) === null || _a === void 0 ? void 0 : _a.removeEventListener) {
this._microphonePermissionStatus.removeEventListener('change', this._onMicrophonePermissionStatusChanged);
}
};
/**
* Stop the selected audio stream
*/
AudioHelper.prototype._stopSelectedInputDeviceStream = function () {
if (this._selectedInputDeviceStream) {
this._log.info('Stopping selected device stream');
this._selectedInputDeviceStream.getTracks().forEach(function (track) { return track.stop(); });
}
};
/**
* Update a set of devices.
* @param updatedDevices - An updated list of available Devices
* @param availableDevices - The previous list of available Devices
* @param removeLostDevice - The method to call if a previously available Device is
* no longer available.
*/
AudioHelper.prototype._updateDevices = function (updatedDevices, availableDevices, removeLostDevice) {
var _this = this;
var updatedDeviceIds = updatedDevices.map(function (d) { return d.deviceId; });
var knownDeviceIds = Array.from(availableDevices.values()).map(function (d) { return d.deviceId; });
var lostActiveDevices = [];
// Remove lost devices
var lostDeviceIds = util.difference(knownDeviceIds, updatedDeviceIds);
lostDeviceIds.forEach(function (lostDeviceId) {
var lostDevice = availableDevices.get(lostDeviceId);
if (lostDevice) {
availableDevices.delete(lostDeviceId);
if (removeLostDevice(lostDevice)) {
lostActiveDevices.push(lostDevice);
}
}
});
// Add any new devices, or devices with updated labels
var deviceChanged = false;
updatedDevices.forEach(function (newDevice) {
var existingDevice = availableDevices.get(newDevice.deviceId);
var newMediaDeviceInfo = _this._wrapMediaDeviceInfo(newDevice);
if (!existingDevice || existingDevice.label !== newMediaDeviceInfo.label) {
availableDevices.set(newDevice.deviceId, newMediaDeviceInfo);
deviceChanged = true;
}
});
if (deviceChanged || lostDeviceIds.length) {
// Force a new gUM in case the underlying tracks of the active stream have changed. One
// reason this might happen is when `default` is selected and set to a USB device,
// then that device is unplugged or plugged back in. We can't check for the 'ended'
// event or readyState because it is asynchronous and may take upwards of 5 seconds,
// in my testing. (rrowland)
var defaultId_1 = 'default';
// this.inputDevice is not null if audio.setInputDevice() was explicitly called
var isInputDeviceSet = this.inputDevice && this.inputDevice.deviceId === defaultId_1;
// If this.inputDevice is null, and default stream is not null, it means
// the user is using the default stream and did not explicitly call audio.setInputDevice()
var isDefaultDeviceSet = this._defaultInputDeviceStream && this.availableInputDevices.get(defaultId_1);
if (isInputDeviceSet || isDefaultDeviceSet) {
this._log.warn("Calling getUserMedia after device change to ensure that the tracks of the active device (default) have not gone stale.");
// NOTE(csantos): Updating the stream in the same execution context as the devicechange event
// causes the new gUM call to fail silently. Meaning, the gUM call may succeed,
// but it won't actually update the stream. We need to update the stream