@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
889 lines (887 loc) • 67.3 kB
JavaScript
import { __awaiter } from 'tslib';
import { EventEmitter } from 'events';
import Device from './device.js';
import { NotSupportedError, InvalidArgumentError } from './errors/index.js';
import Log from './log.js';
import OutputDeviceCollection from './outputdevicecollection.js';
import MediaDeviceInfoShim from './shims/mediadeviceinfo.js';
import { difference, average } from './util.js';
/**
* Aliases for audio kinds, used for labelling.
*/
const kindAliases = {
audioinput: 'Audio Input',
audiooutput: 'Audio Output',
};
/**
* Provides input and output audio-based functionality in one convenient class.
*/
class AudioHelper extends EventEmitter {
/**
* The currently set audio constraints set by setAudioConstraints(). Starts as null.
*/
get audioConstraints() { return this._audioConstraints; }
/**
* The active input device. Having no inputDevice specified by `setInputDevice()`
* will disable input selection related functionality.
*/
get inputDevice() { return this._inputDevice; }
/**
* The current input stream coming from the microphone device or
* the processed audio stream if there is an {@link AudioProcessor}.
*/
get inputStream() { return this._localProcessedStream || this._selectedInputDeviceStream; }
/**
* The processed stream if a local {@link AudioProcessor} was previously added.
* @deprecated Use {@link AudioHelper#localProcessedStream} instead.
*/
get processedStream() {
this._log.warn('AudioHelper#processedStream is deprecated. Please use AudioHelper#localProcessedStream instead.');
return this._localProcessedStream;
}
/**
* The processed stream if a local {@link AudioProcessor} was previously added.
*/
get localProcessedStream() { return this._localProcessedStream; }
/**
* The processed stream if a remote {@link AudioProcessor} was previously added.
*/
get remoteProcessedStream() { return this._remoteProcessedStream; }
/**
* @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]
*/
constructor(onActiveOutputsChanged, onActiveInputChanged, options) {
super();
/**
* 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 = {
[Device.SoundName.Disconnect]: true,
[Device.SoundName.Incoming]: true,
[Device.SoundName.Outgoing]: true,
};
/**
* 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('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 = () => {
if (!this._mediaDevices || !this._enumerateDevices) {
return Promise.reject('Enumeration not supported');
}
return this._enumerateDevices().then((devices) => {
this._updateDevices(devices.filter((d) => d.kind === 'audiooutput'), this.availableOutputDevices, this._removeLostOutput);
this._updateDevices(devices.filter((d) => d.kind === 'audioinput'), this.availableInputDevices, this._removeLostInput);
const defaultDevice = this.availableOutputDevices.get('default')
|| Array.from(this.availableOutputDevices.values())[0];
[this.speakerDevices, this.ringtoneDevices].forEach(outputDevices => {
if (!outputDevices.get().size && this.availableOutputDevices.size && this.isOutputSelectionSupported) {
outputDevices.set(defaultDevice.deviceId)
.catch((reason) => {
this._log.warn(`Unable to set audio output devices. ${reason}`);
});
}
});
});
};
/**
* Remove an input device from inputs
* @param lostDevice
* @returns Whether the device was active
*/
this._removeLostInput = (lostDevice) => {
if (!this.inputDevice || this.inputDevice.deviceId !== lostDevice.deviceId) {
return false;
}
this._destroyLocalProcessedStream();
this._replaceStream(null);
this._inputDevice = null;
this._maybeStopPollingVolume();
const 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 = (lostDevice) => {
const wasSpeakerLost = this.speakerDevices.delete(lostDevice);
const 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 || (() => 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);
const isAudioContextSupported = !!(options.AudioContext || options.audioContext);
const isEnumerationSupported = !!this._enumerateDevices;
if (options.enabledSounds) {
this._enabledSounds = options.enabledSounds;
}
const 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('ringtone', this.availableOutputDevices, onActiveOutputsChanged, this.isOutputSelectionSupported);
this.speakerDevices = new OutputDeviceCollection('speaker', this.availableOutputDevices, onActiveOutputsChanged, this.isOutputSelectionSupported);
this.addListener('newListener', (eventName) => {
if (eventName === 'inputVolume') {
this._maybeStartPollingVolume();
}
});
this.addListener('removeListener', (eventName) => {
if (eventName === 'inputVolume') {
this._maybeStopPollingVolume();
}
});
this.once('newListener', () => {
// 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((microphonePermissionStatus) => {
if (microphonePermissionStatus.state !== 'granted') {
const handleStateChange = () => {
this._updateAvailableDevices();
this._stopMicrophonePermissionListener();
};
microphonePermissionStatus.addEventListener('change', handleStateChange);
this._microphonePermissionStatus = microphonePermissionStatus;
this._onMicrophonePermissionStatusChanged = handleStateChange;
}
}).catch((reason) => this._log.warn(`Warning: unable to listen for microphone permission changes. ${reason}`));
}
else {
this._log.warn('Warning: current browser does not support permissions API.');
}
}
/**
* Destroy this AudioHelper instance
* @internal
*/
_destroy() {
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
*/
_destroyRemoteProcessedStream() {
if (this._remoteProcessor && this._remoteProcessedStream) {
this._log.info('destroying remote processed stream');
const remoteProcessedStream = this._remoteProcessedStream;
this._remoteProcessedStream.getTracks().forEach(track => 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
*/
_getInputDevicePromise() {
return this._inputDevicePromise;
}
/**
* The current AudioProcessorEventObserver instance.
* @internal
*/
_getAudioProcessorEventObserver() {
return this._audioProcessorEventObserver;
}
/**
* Route remote stream to the processor if it exists.
* @internal
*/
_maybeCreateRemoteProcessedStream(stream) {
if (this._remoteProcessor) {
this._log.info('Creating remote processed stream');
return this._remoteProcessor.createProcessedStream(stream).then((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
*/
_maybeStartPollingVolume() {
if (!this.isVolumeSupported || !this.inputStream) {
return;
}
this._updateVolumeSource();
if (this._isPollingInputVolume || !this._inputVolumeAnalyser) {
return;
}
const bufferLength = this._inputVolumeAnalyser.frequencyBinCount;
const buffer = new Uint8Array(bufferLength);
this._isPollingInputVolume = true;
const emitVolume = () => {
if (!this._isPollingInputVolume) {
return;
}
if (this._inputVolumeAnalyser) {
this._inputVolumeAnalyser.getByteFrequencyData(buffer);
const inputVolume = 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
*/
_maybeStopPollingVolume() {
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
*/
_openDefaultDeviceWithConstraints(constraints) {
this._log.info('Opening default device with constraints', constraints);
return this._getUserMedia(constraints).then((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(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
*/
_stopDefaultInputDeviceStream() {
if (this._defaultInputDeviceStream) {
this._log.info('stopping default device stream');
this._defaultInputDeviceStream.getTracks().forEach(track => track.stop());
this._defaultInputDeviceStream = null;
this._destroyLocalProcessedStream();
}
}
/**
* Unbind the listeners from mediaDevices.
* @internal
*/
_unbind() {
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
*/
_updateUserOptions(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
*/
addProcessor(processor, isRemote = false) {
this._log.debug('.addProcessor');
if (this._localProcessor && !isRemote) {
throw new NotSupportedError('Can only have one Local AudioProcessor at a time.');
}
if (this._remoteProcessor && isRemote) {
throw new NotSupportedError('Can only have one Remote AudioProcessor at a time.');
}
if (typeof processor !== 'object' || processor === null) {
throw new InvalidArgumentError('Missing AudioProcessor argument.');
}
if (typeof processor.createProcessedStream !== 'function') {
throw new InvalidArgumentError('Missing createProcessedStream() method.');
}
if (typeof processor.destroyProcessedStream !== 'function') {
throw new 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.
*/
disconnect(doEnable) {
this._log.debug('.disconnect', doEnable);
return this._maybeEnableSound(Device.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.
*/
incoming(doEnable) {
this._log.debug('.incoming', doEnable);
return this._maybeEnableSound(Device.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.
*/
outgoing(doEnable) {
this._log.debug('.outgoing', doEnable);
return this._maybeEnableSound(Device.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
*/
removeProcessor(processor, isRemote = false) {
this._log.debug('.removeProcessor');
if (typeof processor !== 'object' || processor === null) {
throw new InvalidArgumentError('Missing AudioProcessor argument.');
}
if (this._localProcessor !== processor && !isRemote) {
throw new InvalidArgumentError('Cannot remove a Local AudioProcessor that has not been previously added.');
}
if (this._remoteProcessor !== processor && isRemote) {
throw new 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.
*/
setAudioConstraints(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.
*/
setInputDevice(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.
*/
unsetAudioConstraints() {
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.
*/
unsetInputDevice() {
this._log.debug('.unsetInputDevice', this.inputDevice);
if (!this.inputDevice) {
return Promise.resolve();
}
this._destroyLocalProcessedStream();
return this._onActiveInputChanged(null).then(() => {
this._replaceStream(null);
this._inputDevice = null;
this._maybeStopPollingVolume();
});
}
/**
* Destroys the local processed stream and updates references.
*/
_destroyLocalProcessedStream() {
if (this._localProcessor && this._localProcessedStream) {
this._log.info('destroying local processed stream');
const localProcessedStream = this._localProcessedStream;
this._localProcessedStream.getTracks().forEach(track => 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
*/
_getUnknownDeviceIndex(mediaDeviceInfo) {
const id = mediaDeviceInfo.deviceId;
const kind = mediaDeviceInfo.kind;
let 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.
*/
_initializeEnumeration() {
if (!this._mediaDevices || !this._enumerateDevices) {
throw new NotSupportedError('Enumeration is not supported');
}
if (this._mediaDevices.addEventListener) {
this._mediaDevices.addEventListener('devicechange', this._updateAvailableDevices);
}
this._updateAvailableDevices().then(() => {
if (!this.isOutputSelectionSupported) {
return;
}
Promise.all([
this.speakerDevices.set('default'),
this.ringtoneDevices.set('default'),
]).catch(reason => {
this._log.warn(`Warning: Unable to set audio output devices. ${reason}`);
});
});
}
/**
* Route local stream to the processor if it exists.
*/
_maybeCreateLocalProcessedStream(stream) {
if (this._localProcessor) {
this._log.info('Creating local processed stream');
return this._localProcessor.createProcessedStream(stream).then((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
*/
_maybeEnableSound(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
*/
_replaceStream(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
*/
_restartInputStreams() {
if (this.inputDevice && this._selectedInputDeviceStream) {
this._log.info('Restarting selected input device');
return this._setInputDevice(this.inputDevice.deviceId, true);
}
if (this._defaultInputDeviceStream) {
const 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.
*/
_setInputDevice(deviceId, forceGetUserMedia) {
return __awaiter(this, void 0, void 0, function* () {
const setInputDevice = () => __awaiter(this, void 0, void 0, function* () {
yield this._beforeSetInputDevice();
if (typeof deviceId !== 'string') {
return Promise.reject(new InvalidArgumentError('Must specify the device to set'));
}
const device = this.availableInputDevices.get(deviceId);
if (!device) {
return Promise.reject(new InvalidArgumentError(`Device not found: ${deviceId}`));
}
this._log.info('Setting input device. ID: ' + deviceId);
if (this._inputDevice && this._inputDevice.deviceId === deviceId && this._selectedInputDeviceStream) {
if (!forceGetUserMedia) {
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();
const constraints = { audio: Object.assign({ deviceId: { exact: deviceId } }, this.audioConstraints) };
this._log.info('setInputDevice: getting new tracks.');
return this._getUserMedia(constraints).then((originalStream) => {
this._destroyLocalProcessedStream();
return this._maybeCreateLocalProcessedStream(originalStream).then((newStream) => {
this._log.info('setInputDevice: invoking _onActiveInputChanged.');
return this._onActiveInputChanged(newStream).then(() => {
this._replaceStream(originalStream);
this._inputDevice = device;
this._maybeStartPollingVolume();
});
});
});
});
return this._inputDevicePromise = setInputDevice().finally(() => {
this._inputDevicePromise = null;
});
});
}
/**
* Remove event listener for microphone permissions
*/
_stopMicrophonePermissionListener() {
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
*/
_stopSelectedInputDeviceStream() {
if (this._selectedInputDeviceStream) {
this._log.info('Stopping selected device stream');
this._selectedInputDeviceStream.getTracks().forEach(track => 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.
*/
_updateDevices(updatedDevices, availableDevices, removeLostDevice) {
const updatedDeviceIds = updatedDevices.map(d => d.deviceId);
const knownDeviceIds = Array.from(availableDevices.values()).map(d => d.deviceId);
const lostActiveDevices = [];
// Remove lost devices
const lostDeviceIds = difference(knownDeviceIds, updatedDeviceIds);
lostDeviceIds.forEach((lostDeviceId) => {
const lostDevice = availableDevices.get(lostDeviceId);
if (lostDevice) {
availableDevices.delete(lostDeviceId);
if (removeLostDevice(lostDevice)) {
lostActiveDevices.push(lostDevice);
}
}
});
// Add any new devices, or devices with updated labels
let deviceChanged = false;
updatedDevices.forEach(newDevice => {
const existingDevice = availableDevices.get(newDevice.deviceId);
const 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)
const defaultId = 'default';
// this.inputDevice is not null if audio.setInputDevice() was explicitly called
const isInputDeviceSet = this.inputDevice && this.inputDevice.deviceId === defaultId;
// 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()
const isDefaultDeviceSet = this._defaultInputDeviceStream && this.availableInputDevices.get(defaultId);
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(() => {
this._setInputDevice(defaultId, 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.
*/
_updateVolumeSource() {
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
*/
_wrapMediaDeviceInfo(mediaDeviceInfo) {
const options = {
deviceId: mediaDeviceInfo.deviceId,
groupId: mediaDeviceInfo.groupId,
kind: mediaDeviceInfo.kind,
label: mediaDeviceInfo.label,
};
if (!options.label) {
if (options.deviceId === 'default') {
options.label = 'Default';
}
else {
const index = this._getUnknownDeviceIndex(mediaDeviceInfo);
options.label = `Unknown ${kindAliases[options.kind]} Device ${index}`;
}
}
return new MediaDeviceInfoShim(options);
}
}
/**
* @mergeModuleWith AudioHelper
*/
(function (AudioHelper) {
})(AudioHelper || (AudioHelper = {}));
var AudioHelper$1 = AudioHelper;
export { AudioHelper$1 as default };
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXVkaW9oZWxwZXIuanMiLCJzb3VyY2VzIjpbIi4uLy4uL2xpYi90d2lsaW8vYXVkaW9oZWxwZXIudHMiXSwic291cmNlc0NvbnRlbnQiOltudWxsXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7O0FBVUE7O0FBRUc7QUFDSCxNQUFNLFdBQVcsR0FBMkI7QUFDMUMsSUFBQSxVQUFVLEVBQUUsYUFBYTtBQUN6QixJQUFBLFdBQVcsRUFBRSxjQUFjO0NBQzVCO0FBRUQ7O0FBRUc7QUFDSCxNQUFNLFdBQVksU0FBUSxZQUFZLENBQUE7QUFDcEM7O0FBRUc7SUFDSCxJQUFJLGdCQUFnQixLQUFtQyxPQUFPLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0FBWXRGOzs7QUFHRztJQUNILElBQUksV0FBVyxLQUE2QixPQUFPLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztBQUV0RTs7O0FBR0c7QUFDSCxJQUFBLElBQUksV0FBVyxHQUFBLEVBQXlCLE9BQU8sSUFBSSxDQUFDLHFCQUFxQixJQUFJLElBQUksQ0FBQywwQkFBMEIsQ0FBQyxDQUFDO0FBYzlHOzs7QUFHRztBQUNILElBQUEsSUFBSSxlQUFlLEdBQUE7QUFDakIsUUFBQSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxpR0FBaUcsQ0FBQztRQUNqSCxPQUFPLElBQUksQ0FBQyxxQkFBcUI7SUFDbkM7QUFFQTs7QUFFRztJQUNILElBQUksb0JBQW9CLEtBQXlCLE9BQU8sSUFBSSxDQUFDLHFCQUFxQixDQUFDLENBQUM7QUFFcEY7O0FBRUc7SUFDSCxJQUFJLHFCQUFxQixLQUF5QixPQUFPLElBQUksQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO0FBd0p0Rjs7Ozs7QUFLRztBQUNILElBQUEsV0FBQSxDQUFZLHNCQUE0RixFQUM1RixvQkFBbUUsRUFDbkUsT0FBNkIsRUFBQTtBQUN2QyxRQUFBLEtBQUssRUFBRTtBQXBOVDs7QUFFRztBQUNILFFBQUEsSUFBQSxDQUFBLHFCQUFxQixHQUFpQyxJQUFJ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