@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
897 lines • 70 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __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;
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 in a different
// execution context (setTimeout) to properly update the stream.
setTimeout(function () {
_this._setInputDevice(defaultId_1, true);
}, 0);
}
this._log.debug('#deviceChange', lostActiveDevices);
this.emit('deviceChange', lostActiveDevices);
}
};
/**
* Disconnect the old input volume source, and create and connect a new one with the current
* input stream.
*/
AudioHelper.prototype._updateVolumeSource = function () {
if (!this.inputStream || !this._audioContext || !this._inputVolumeAnalyser) {
return;
}
if (this._inputVolumeSource) {
this._inputVolumeSource.disconnect();
}
try {
this._inputVolumeSource = this._audioContext.createMediaStreamSource(this.inputStream);
this._inputVolumeSource.connect(this._inputVolumeAnalyser);
}
catch (ex) {
this._log.warn('Unable to update volume source', ex);
delete this._inputVolumeSource;
}
};
/**
* Convert a MediaDeviceInfo to a IMediaDeviceInfoShim.
* @param mediaDeviceInfo - The info to convert
* @returns The converted shim
*/
AudioHelper.prototype._wrapMediaDeviceInfo = function (mediaDeviceInfo) {
var options = {
deviceId: mediaDeviceInfo.deviceId,
groupId: mediaDeviceInfo.groupId,
kind: mediaDeviceInfo.kind,
label: mediaDeviceInfo.label,
};
if (!options.label) {
if (options.deviceId === 'default') {
options.label = 'Default';
}
else {
var index = this._getUnknownDeviceIndex(mediaDeviceInfo);
options.label = "Unknown ".concat(kindAliases[options.kind], " Device ").concat(index);
}
}
return new mediadeviceinfo_1.default(options);
};
return AudioHelper;
}(events_1.EventEmitter));
/**
* @mergeModuleWith AudioHelper
*/
(function (AudioHelper) {
})(AudioHelper || (AudioHelper = {}));
exports.default = AudioHelper;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXVkaW9oZWxwZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9saWIvdHdpbGlvL2F1ZGlvaGVscGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsaUNBQXNDO0FBR3RDLG1DQUE4QjtBQUM5QixtQ0FBbUU7QUFDbkUsNkJBQXdCO0FBQ3hCLG1FQUE4RDtBQUM5RCwyREFBMEQ7QUFDMUQsK0JBQXdEO0FBRXhEOztHQUVHO0FBQ0gsSUFBTSxXQUFXLEdBQTJCO0lBQzFDLFVBQVUsRUFBRSxhQUFhO0lBQ3pCLFdBQVcsRUFBRSxjQUFjO0NBQzVCLENBQUM7QUFFRjs7R0FFRztBQUNIO0lBQTBCLCtCQUFZO0lBeUxwQzs7Ozs7T0FLRztJQUNILHFCQUFZLHNCQUE0RixFQUM1RixvQkFBbUUsRUFDbkUsT0FBNkI7O1FBQ3ZDLFlBQUEsTUFBSyxXQUFFLFNBQUM7UUE1TFY7O1dBRUc7UUFDSCwyQkFBcUIsR0FBaUMsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUVoRTs7V0FFRztRQUNILDRCQUFzQixHQUFpQyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBK0NqRTs7V0FFRztRQUNLLHVCQUFpQixHQUFpQyxJQUFJLENBQUM7UUFpQi9EOzs7OztXQUtHO1FBQ0ssK0JBQXlCLEdBQXVCLElBQUksQ0FBQztRQUU3RDs7V0FFRztRQUNLLG9CQUFjO1lBQ3BCLEdBQUMsZ0JBQU0sQ0FBQyxTQUFTLENBQUMsVUFBVSxJQUFHLElBQUk7WUFDbkMsR0FBQyxnQkFBTSxDQUFDLFNBQVMsQ0FBQyxRQUFRLElBQUcsSUFBSTtZQUNqQyxHQUFDLGdCQUFNLENBQUMsU0FBUyxDQUFDLFFBQVEsSUFBRyxJQUFJO2dCQUNqQztRQVlGOztXQUVHO1FBQ0ssa0JBQVksR0FBMkIsSUFBSSxDQUFDO1FBRXBEOztXQUVHO1FBQ0sseUJBQW1CLEdBQXlCLElBQUksQ0FBQztR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