@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
1,039 lines (1,013 loc) • 523 kB
JavaScript
/*! @twilio/voice-sdk.js 2.15.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";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwilioError = exports.Logger = exports.PreflightTest = exports.Device = exports.Call = void 0;
var call_1 = require("./twilio/call");
exports.Call = call_1.default;
var device_1 = require("./twilio/device");
exports.Device = device_1.default;
var log_1 = require("./twilio/log");
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return log_1.Logger; } });
var preflight_1 = require("./twilio/preflight/preflight");
Object.defineProperty(exports, "PreflightTest", { enumerable: true, get: function () { return preflight_1.PreflightTest; } });
// TODO: Consider refactoring this export (VBLOCKS-4589)
var TwilioError = require("./twilio/errors");
exports.TwilioError = TwilioError;
},{"./twilio/call":9,"./twilio/device":12,"./twilio/errors":15,"./twilio/log":18,"./twilio/preflight/preflight":20}],2:[function(require,module,exports){
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncQueue = void 0;
var deferred_1 = require("./deferred");
/**
* 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 = new deferred_1.default();
this._operations.push({ deferred: deferred, callback: callback });
if (!hasPending) {
this._processQueue();
}
return deferred.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 __awaiter(this, void 0, void 0, function () {
var _a, deferred, callback, result, error, hasResolved, e_1;
return __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":11}],3:[function(require,module,exports){
"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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var events_1 = require("events");
var device_1 = require("./device");
var errors_1 = require("./errors");
var log_1 = require("./log");
var outputdevicecollection_1 = require("./outputdevicecollection");
var mediadeviceinfo_1 = require("./shims/mediadeviceinfo");
var util_1 = require("./util");
/**
* 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) {
__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_1.default.SoundName.Disconnect] = true,
_a[device_1.default.SoundName.Incoming] = true,
_a[device_1.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_1.default('AudioHelper');
/**
* Internal reference to the processed stream
*/
_this._processedStream = 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._destroyProcessedStream();
_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_1.default('ringtone', _this.availableOutputDevices, onActiveOutputsChanged, _this.isOutputSelectionSupported);
_this.speakerDevices = new outputdevicecollection_1.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._processedStream || this._selectedInputDeviceStream; },
enumerable: false,
configurable: true
});
Object.defineProperty(AudioHelper.prototype, "processedStream", {
/**
* The processed stream if an {@link AudioProcessor} was previously added.
*/
get: function () { return this._processedStream; },
enumerable: false,
configurable: true
});
/**
* Destroy this AudioHelper instance
* @internal
*/
AudioHelper.prototype._destroy = function () {
this._stopDefaultInputDeviceStream();
this._stopSelectedInputDeviceStream();
this._destroyProcessedStream();
this._maybeStopPollingVolume();
this.removeAllListeners();
this._stopMicrophonePermissionListener();
this._unbind();
};
/**
* Promise to wait for the input device, if setInputDevice is called outside of the SDK
* @internal
*/
AudioHelper.prototype._getInputDevicePromise = function () {
return this._inputDevicePromise;
};
/**
* 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 = (0, util_1.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._maybeCreateProcessedStream(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._destroyProcessedStream();
}
};
/**
* 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.
* @returns
*/
AudioHelper.prototype.addProcessor = function (processor) {
this._log.debug('.addProcessor');
if (this._processor) {
throw new errors_1.NotSupportedError('Adding multiple AudioProcessors is not supported at this time.');
}
if (typeof processor !== 'object' || processor === null) {
throw new errors_1.InvalidArgumentError('Missing AudioProcessor argument.');
}
if (typeof processor.createProcessedStream !== 'function') {
throw new errors_1.InvalidArgumentError('Missing createProcessedStream() method.');
}
if (typeof processor.destroyProcessedStream !== 'function') {
throw new errors_1.InvalidArgumentError('Missing destroyProcessedStream() method.');
}
this._processor = processor;
this._audioProcessorEventObserver.emit('add');
return this._restartStreams();
};
/**
* 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_1.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_1.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_1.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.
* @returns
*/
AudioHelper.prototype.removeProcessor = function (processor) {
this._log.debug('.removeProcessor');
if (typeof processor !== 'object' || processor === null) {
throw new errors_1.InvalidArgumentError('Missing AudioProcessor argument.');
}
if (this._processor !== processor) {
throw new errors_1.InvalidArgumentError('Cannot remove an AudioProcessor that has not been previously added.');
}
this._destroyProcessedStream();
this._processor = null;
this._audioProcessorEventObserver.emit('remove');
return this._restartStreams();
};
/**
* 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._destroyProcessedStream();
return this._onActiveInputChanged(null).then(function () {
_this._replaceStream(null);
_this._inputDevice = null;
_this._maybeStopPollingVolume();
});
};
/**
* Destroys processed stream and update references
*/
AudioHelper.prototype._destroyProcessedStream = function () {
if (this._processor && this._processedStream) {
this._log.info('destroying processed stream');
var processedStream = this._processedStream;
this._processedStream.getTracks().forEach(function (track) { return track.stop(); });
this._processedStream = null;
this._processor.destroyProcessedStream(processedStream);
this._audioProcessorEventObserver.emit('destroy');
}
};
/**
* 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 errors_1.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 input stream to the processor if it exists
*/
AudioHelper.prototype._maybeCreateProcessedStream = function (stream) {
var _this = this;
if (this._processor) {
this._log.info('Creating processed stream');
return this._processor.createProcessedStream(stream).then(function (processedStream) {
_this._processedStream = processedStream;
_this._audioProcessorEventObserver.emit('create');
return _this._processedStream;
});
}
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 streams
*/
AudioHelper.prototype._restartStreams = 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 __awaiter(this, void 0, void 0, function () {
var setInputDevice;
var _this = this;
return __generator(this, function (_a) {
setInputDevice = function () { return __awaiter(_this, void 0, void 0, function () {
var device, constraints;
var _this = this;
return __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 errors_1.InvalidArgumentError('Must specify the device to set'))];
}
device = this.availableInputDevices.get(deviceId);
if (!device) {
return [2 /*return*/, Promise.reject(new errors_1.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._destroyProcessedStream();
return _this._maybeCreateProcessedStream(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 = (0, util_1.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;
u