@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
1,114 lines (1,109 loc) • 123 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var tslib = require('tslib');
var events = require('events');
var loglevel = require('loglevel');
var audiohelper = require('./audiohelper.js');
var audioprocessoreventobserver = require('./audioprocessoreventobserver.js');
var call = require('./call.js');
var constants = require('./constants.js');
var dialtonePlayer = require('./dialtonePlayer.js');
var index$1 = require('./errors/index.js');
var eventpublisher = require('./eventpublisher.js');
var log = require('./log.js');
var preflight = require('./preflight/preflight.js');
var pstream = require('./pstream.js');
var regions = require('./regions.js');
var index = require('./rtc/index.js');
var getusermedia = require('./rtc/getusermedia.js');
var sid = require('./sid.js');
var sound = require('./sound.js');
var util = require('./util.js');
var generated = require('./errors/generated.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var loglevel__namespace = /*#__PURE__*/_interopNamespaceDefault(loglevel);
var REGISTRATION_INTERVAL = 30000;
var RINGTONE_PLAY_TIMEOUT = 2000;
var PUBLISHER_PRODUCT_NAME = 'twilio-js-sdk';
var INVALID_TOKEN_MESSAGE = 'Parameter "token" must be of type "string".';
/**
* Twilio Device. Allows registration for incoming calls, and placing outgoing calls.
*/
exports.default = /** @class */ (function (_super) {
tslib.__extends(Device, _super);
/**
* Construct a {@link Device} instance. The {@link Device} can be registered
* to make and listen for calls using {@link Device.register}.
* @param options
*/
function Device(token, options) {
var _a;
if (options === void 0) { options = {}; }
var _this = _super.call(this) || this;
/**
* The currently active {@link Call}, if there is one.
*/
_this._activeCall = null;
/**
* The AudioHelper instance associated with this {@link Device}.
*/
_this._audio = null;
/**
* The AudioProcessorEventObserver instance to use
*/
_this._audioProcessorEventObserver = null;
/**
* An audio input MediaStream to pass to new {@link Call} instances.
*/
_this._callInputStream = null;
/**
* An array of {@link Call}s. Though only one can be active, multiple may exist when there
* are multiple incoming, unanswered {@link Call}s.
*/
_this._calls = [];
/**
* An array of {@link Device} IDs to be used to play sounds through, to be passed to
* new {@link Call} instances.
*/
_this._callSinkIds = ['default'];
/**
* The list of chunder URIs that will be passed to PStream
*/
_this._chunderURIs = [];
/**
* Default options used by {@link Device}.
*/
_this._defaultOptions = {
allowIncomingWhileBusy: false,
closeProtection: false,
codecPreferences: [call.default.Codec.PCMU, call.default.Codec.Opus],
dscp: true,
enableImprovedSignalingErrorPrecision: false,
forceAggressiveIceNomination: false,
logLevel: loglevel__namespace.levels.ERROR,
maxCallSignalingTimeoutMs: 0,
preflight: false,
sounds: {},
tokenRefreshMs: 10000,
voiceEventSidGenerator: sid.generateVoiceEventSid,
};
/**
* The name of the edge the {@link Device} is connected to.
*/
_this._edge = null;
/**
* The name of the home region the {@link Device} is connected to.
*/
_this._home = null;
/**
* The identity associated with this Device.
*/
_this._identity = null;
/**
* An instance of Logger to use.
*/
_this._log = new log.default('Device');
/**
* The internal promise created when calling {@link Device.makeCall}.
*/
_this._makeCallPromise = null;
/**
* The options passed to {@link Device} constructor or {@link Device.updateOptions}.
*/
_this._options = {};
/**
* The preferred URI to (re)-connect signaling to.
*/
_this._preferredURI = null;
/**
* An Insights Event Publisher.
*/
_this._publisher = null;
/**
* The region the {@link Device} is connected to.
*/
_this._region = null;
/**
* A timeout ID for a setTimeout schedule to re-register the {@link Device}.
*/
_this._regTimer = null;
/**
* Boolean representing whether or not the {@link Device} was registered when
* receiving a signaling `offline`. Determines if the {@link Device} attempts
* a `re-register` once signaling is re-established when receiving a
* `connected` event from the stream.
*/
_this._shouldReRegister = false;
/**
* A Map of Sounds to play.
*/
_this._soundcache = new Map();
/**
* The current status of the {@link Device}.
*/
_this._state = Device.State.Unregistered;
/**
* A map from {@link Device.State} to {@link Device.EventName}.
*/
_this._stateEventMapping = (_a = {},
_a[Device.State.Destroyed] = Device.EventName.Destroyed,
_a[Device.State.Unregistered] = Device.EventName.Unregistered,
_a[Device.State.Registering] = Device.EventName.Registering,
_a[Device.State.Registered] = Device.EventName.Registered,
_a);
/**
* The Signaling stream.
*/
_this._stream = null;
/**
* A promise that will resolve when the Signaling stream is ready.
*/
_this._streamConnectedPromise = null;
/**
* A timeout to track when the current AccessToken will expire.
*/
_this._tokenWillExpireTimeout = null;
/**
* Create the default Insights payload
* @param call
*/
_this._createDefaultPayload = function (call) {
var payload = {
aggressive_nomination: _this._options.forceAggressiveIceNomination,
browser_extension: _this._isBrowserExtension,
dscp: !!_this._options.dscp,
ice_restart_enabled: true,
platform: index.getMediaEngine(),
sdk_version: constants.RELEASE_VERSION,
};
function setIfDefined(propertyName, value) {
if (value) {
payload[propertyName] = value;
}
}
if (call) {
var callSid = call.parameters.CallSid;
setIfDefined('call_sid', /^TJ/.test(callSid) ? undefined : callSid);
setIfDefined('temp_call_sid', call.outboundConnectionId);
setIfDefined('audio_codec', call.codec);
payload.direction = call.direction;
}
setIfDefined('gateway', _this._stream && _this._stream.gateway);
setIfDefined('region', _this._stream && _this._stream.region);
return payload;
};
/**
* Called when a 'close' event is received from the signaling stream.
*/
_this._onSignalingClose = function () {
_this._stream = null;
_this._streamConnectedPromise = null;
};
/**
* Called when a 'connected' event is received from the signaling stream.
*/
_this._onSignalingConnected = function (payload) {
var _a;
var region = regions.getRegionShortcode(payload.region);
_this._edge = payload.edge || regions.regionToEdge[region] || payload.region;
_this._region = region || payload.region;
_this._home = payload.home;
(_a = _this._publisher) === null || _a === void 0 ? void 0 : _a.setHost(regions.createEventGatewayURI(payload.home));
if (payload.token) {
_this._identity = payload.token.identity;
if (typeof payload.token.ttl === 'number' &&
typeof _this._options.tokenRefreshMs === 'number') {
var ttlMs = payload.token.ttl * 1000;
var timeoutMs = Math.max(0, ttlMs - _this._options.tokenRefreshMs);
_this._tokenWillExpireTimeout = setTimeout(function () {
_this._log.debug('#tokenWillExpire');
_this.emit('tokenWillExpire', _this);
if (_this._tokenWillExpireTimeout) {
clearTimeout(_this._tokenWillExpireTimeout);
_this._tokenWillExpireTimeout = null;
}
}, timeoutMs);
}
}
var preferredURIs = _this._getChunderws() || regions.getChunderURIs(_this._edge);
if (preferredURIs.length > 0) {
var preferredURI = preferredURIs[0];
_this._preferredURI = regions.createSignalingEndpointURL(preferredURI);
}
else {
_this._log.warn('Could not parse a preferred URI from the stream#connected event.');
}
// The signaling stream emits a `connected` event after reconnection, if the
// device was registered before this, then register again.
if (_this._shouldReRegister) {
_this.register();
}
};
/**
* Called when an 'error' event is received from the signaling stream.
*/
_this._onSignalingError = function (payload) {
if (typeof payload !== 'object') {
_this._log.warn('Invalid signaling error payload', payload);
return;
}
var originalError = payload.error, callsid = payload.callsid, voiceeventsid = payload.voiceeventsid;
// voiceeventsid is for call message events which are handled in the call object
// missing originalError shouldn't be possible but check here to fail properly
if (typeof originalError !== 'object' || !!voiceeventsid) {
_this._log.warn('Ignoring signaling error payload', { originalError: originalError, voiceeventsid: voiceeventsid });
return;
}
var call = (typeof callsid === 'string' && _this._findCall(callsid)) || undefined;
var code = originalError.code, customMessage = originalError.message;
var twilioError = originalError.twilioError;
if (typeof code === 'number') {
if (code === 31201) {
twilioError = new generated.AuthorizationErrors.AuthenticationFailed(originalError);
}
else if (code === 31204) {
twilioError = new generated.AuthorizationErrors.AccessTokenInvalid(originalError);
}
else if (code === 31205) {
// Stop trying to register presence after token expires
_this._stopRegistrationTimer();
twilioError = new generated.AuthorizationErrors.AccessTokenExpired(originalError);
}
else {
var errorConstructor = index$1.getPreciseSignalingErrorByCode(!!_this._options.enableImprovedSignalingErrorPrecision, code);
if (typeof errorConstructor !== 'undefined') {
twilioError = new errorConstructor(originalError);
}
}
}
if (!twilioError) {
_this._log.error('Unknown signaling error: ', originalError);
twilioError = new generated.GeneralErrors.UnknownError(customMessage, originalError);
}
_this._log.error('Received error: ', twilioError);
_this._log.debug('#error', originalError);
_this.emit(Device.EventName.Error, twilioError, call);
};
/**
* Called when an 'invite' event is received from the signaling stream.
*/
_this._onSignalingInvite = function (payload) { return tslib.__awaiter(_this, void 0, void 0, function () {
var wasBusy, callParameters, customParameters, call, play;
var _this = this;
var _a;
return tslib.__generator(this, function (_b) {
switch (_b.label) {
case 0:
wasBusy = !!this._activeCall;
if (wasBusy && !this._options.allowIncomingWhileBusy) {
this._log.info('Device busy; ignoring incoming invite');
return [2 /*return*/];
}
if (!payload.callsid || !payload.sdp) {
this._log.debug('#error', payload);
this.emit(Device.EventName.Error, new generated.ClientErrors.BadRequest('Malformed invite from gateway'));
return [2 /*return*/];
}
callParameters = payload.parameters || {};
callParameters.CallSid = callParameters.CallSid || payload.callsid;
customParameters = Object.assign({}, util.queryToJson(callParameters.Params));
this._makeCallPromise = this._makeCall(customParameters, {
callParameters: callParameters,
enableImprovedSignalingErrorPrecision: !!this._options.enableImprovedSignalingErrorPrecision,
offerSdp: payload.sdp,
reconnectToken: payload.reconnect,
voiceEventSidGenerator: this._options.voiceEventSidGenerator,
});
_b.label = 1;
case 1:
_b.trys.push([1, , 3, 4]);
return [4 /*yield*/, this._makeCallPromise];
case 2:
call = _b.sent();
return [3 /*break*/, 4];
case 3:
this._makeCallPromise = null;
return [7 /*endfinally*/];
case 4:
this._calls.push(call);
call.once('accept', function () {
_this._soundcache.get(Device.SoundName.Incoming).stop();
_this._publishNetworkChange();
});
play = (((_a = this._audio) === null || _a === void 0 ? void 0 : _a.incoming()) && !wasBusy)
? function () { return _this._soundcache.get(Device.SoundName.Incoming).play(); }
: function () { return Promise.resolve(); };
this._showIncomingCall(call, play);
return [2 /*return*/];
}
});
}); };
/**
* Called when an 'offline' event is received from the signaling stream.
*/
_this._onSignalingOffline = function () {
_this._log.info('Stream is offline');
_this._edge = null;
_this._region = null;
_this._shouldReRegister = _this.state !== Device.State.Unregistered;
_this._setState(Device.State.Unregistered);
};
/**
* Called when a 'ready' event is received from the signaling stream.
*/
_this._onSignalingReady = function () {
_this._log.info('Stream is ready');
_this._setState(Device.State.Registered);
};
/**
* Publish a NetworkInformation#change event to Insights if there's an active {@link Call}.
*/
_this._publishNetworkChange = function () {
if (!_this._activeCall) {
return;
}
if (_this._networkInformation) {
_this._publisher.info('network-information', 'network-change', {
connection_type: _this._networkInformation.type,
downlink: _this._networkInformation.downlink,
downlinkMax: _this._networkInformation.downlinkMax,
effective_type: _this._networkInformation.effectiveType,
rtt: _this._networkInformation.rtt,
}, _this._activeCall);
}
};
/**
* Update the input stream being used for calls so that any current call and all future calls
* will use the new input stream.
* @param inputStream
*/
_this._updateInputStream = function (inputStream) {
var call = _this._activeCall;
if (call && !inputStream) {
return Promise.reject(new index$1.InvalidStateError('Cannot unset input device while a call is in progress.'));
}
_this._callInputStream = inputStream;
return call
? call._setInputTracksFromStream(inputStream)
: Promise.resolve();
};
/**
* Update the device IDs of output devices being used to play sounds through.
* @param type - Whether to update ringtone or speaker sounds
* @param sinkIds - An array of device IDs
*/
_this._updateSinkIds = function (type, sinkIds) {
var promise = type === 'ringtone'
? _this._updateRingtoneSinkIds(sinkIds)
: _this._updateSpeakerSinkIds(sinkIds);
return promise.then(function () {
_this._publisher.info('audio', "".concat(type, "-devices-set"), {
audio_device_ids: sinkIds,
}, _this._activeCall);
}, function (error) {
_this._publisher.error('audio', "".concat(type, "-devices-set-failed"), {
audio_device_ids: sinkIds,
message: error.message,
}, _this._activeCall);
throw error;
});
};
// Setup loglevel asap to avoid missed logs
_this._setupLoglevel(options.logLevel);
_this._logOptions('constructor', options);
_this.updateToken(token);
if (util.isLegacyEdge()) {
throw new index$1.NotSupportedError('Microsoft Edge Legacy (https://support.microsoft.com/en-us/help/4533505/what-is-microsoft-edge-legacy) ' +
'is deprecated and will not be able to connect to Twilio to make or receive calls after September 1st, 2020. ' +
'Please see this documentation for a list of supported browsers ' +
'https://www.twilio.com/docs/voice/client/javascript#supported-browsers');
}
if (!Device.isSupported && options.ignoreBrowserSupport) {
if (window && window.location && window.location.protocol === 'http:') {
throw new index$1.NotSupportedError("twilio.js wasn't able to find WebRTC browser support. This is most likely because this page is served over http rather than https, which does not support WebRTC in many browsers. Please load this page over https and try again.");
}
throw new index$1.NotSupportedError("twilio.js 1.3+ SDKs require WebRTC browser support. For more information, see <https://www.twilio.com/docs/api/client/twilio-js>. If you have any questions about this announcement, please contact Twilio Support at <help@twilio.com>.");
}
var root = globalThis;
var browser = root.msBrowser || root.browser || root.chrome;
_this._isBrowserExtension = (!!browser && !!browser.runtime && !!browser.runtime.id)
|| (!!root.safari && !!root.safari.extension);
if (_this._isBrowserExtension) {
_this._log.info('Running as browser extension.');
}
if (navigator) {
var n = navigator;
_this._networkInformation = n.connection
|| n.mozConnection
|| n.webkitConnection;
}
if (_this._networkInformation && typeof _this._networkInformation.addEventListener === 'function') {
_this._networkInformation.addEventListener('change', _this._publishNetworkChange);
}
Device._getOrCreateAudioContext();
if (Device._audioContext) {
if (!Device._dialtonePlayer) {
Device._dialtonePlayer = new dialtonePlayer.default(Device._audioContext);
}
}
_this._boundDestroy = _this.destroy.bind(_this);
_this._boundConfirmClose = _this._confirmClose.bind(_this);
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('unload', _this._boundDestroy);
window.addEventListener('pagehide', _this._boundDestroy);
}
_this.updateOptions(options);
return _this;
}
Object.defineProperty(Device, "audioContext", {
/**
* The AudioContext to be used by {@link Device} instances.
* @private
*/
get: function () {
return Device._audioContext;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device, "extension", {
/**
* Which sound file extension is supported.
* @private
*/
get: function () {
// NOTE(mroberts): Node workaround.
var a = typeof document !== 'undefined'
? document.createElement('audio') : { canPlayType: false };
var canPlayMp3;
try {
canPlayMp3 = a.canPlayType && !!a.canPlayType('audio/mpeg').replace(/no/, '');
}
catch (e) {
canPlayMp3 = false;
}
var canPlayVorbis;
try {
canPlayVorbis = a.canPlayType && !!a.canPlayType('audio/ogg;codecs=\'vorbis\'').replace(/no/, '');
}
catch (e) {
canPlayVorbis = false;
}
return (canPlayVorbis && !canPlayMp3) ? 'ogg' : 'mp3';
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device, "isSupported", {
/**
* Whether or not this SDK is supported by the current browser.
*/
get: function () { return index.enabled(); },
enumerable: false,
configurable: true
});
Object.defineProperty(Device, "packageName", {
/**
* Package name of the SDK.
*/
get: function () { return constants.PACKAGE_NAME; },
enumerable: false,
configurable: true
});
/**
* Run some tests to identify issues, if any, prohibiting successful calling.
* @param token - A Twilio JWT token string
* @param options
*/
Device.runPreflight = function (token, options) {
return new preflight.PreflightTest(token, tslib.__assign({ audioContext: Device._getOrCreateAudioContext() }, options));
};
/**
* String representation of {@link Device} class.
* @private
*/
Device.toString = function () {
return '[Twilio.Device class]';
};
Object.defineProperty(Device, "version", {
/**
* Current SDK version.
*/
get: function () { return constants.RELEASE_VERSION; },
enumerable: false,
configurable: true
});
/**
* Initializes the AudioContext instance shared across the Voice SDK,
* or returns the existing instance if one has already been initialized.
*/
Device._getOrCreateAudioContext = function () {
if (!Device._audioContext) {
if (typeof AudioContext !== 'undefined') {
Device._audioContext = new AudioContext();
}
else if (typeof webkitAudioContext !== 'undefined') {
Device._audioContext = new webkitAudioContext();
}
}
return Device._audioContext;
};
Object.defineProperty(Device.prototype, "audio", {
/**
* Return the {@link AudioHelper} used by this {@link Device}.
*/
get: function () {
return this._audio;
},
enumerable: false,
configurable: true
});
/**
* Make an outgoing Call.
* @param options
*/
Device.prototype.connect = function () {
return tslib.__awaiter(this, arguments, void 0, function (options) {
var customParameters, parameters, signalingReconnectToken, connectTokenParts, isReconnect, twimlParams, callOptions, activeCall, _a;
if (options === void 0) { options = {}; }
return tslib.__generator(this, function (_b) {
switch (_b.label) {
case 0:
this._log.debug('.connect', JSON.stringify(options));
this._throwIfDestroyed();
if (this._activeCall) {
throw new index$1.InvalidStateError('A Call is already active');
}
if (options.connectToken) {
try {
connectTokenParts = JSON.parse(decodeURIComponent(atob(options.connectToken)));
customParameters = connectTokenParts.customParameters;
parameters = connectTokenParts.parameters;
signalingReconnectToken = connectTokenParts.signalingReconnectToken;
}
catch (_c) {
throw new index$1.InvalidArgumentError('Cannot parse connectToken');
}
if (!parameters || !parameters.CallSid || !signalingReconnectToken) {
throw new index$1.InvalidArgumentError('Invalid connectToken');
}
}
isReconnect = false;
twimlParams = {};
callOptions = {
enableImprovedSignalingErrorPrecision: !!this._options.enableImprovedSignalingErrorPrecision,
rtcConfiguration: options.rtcConfiguration,
voiceEventSidGenerator: this._options.voiceEventSidGenerator,
};
if (signalingReconnectToken && parameters) {
isReconnect = true;
callOptions.callParameters = parameters;
callOptions.reconnectCallSid = parameters.CallSid;
callOptions.reconnectToken = signalingReconnectToken;
twimlParams = customParameters || twimlParams;
}
else {
twimlParams = options.params || twimlParams;
}
this._makeCallPromise = this._makeCall(twimlParams, callOptions, isReconnect);
_b.label = 1;
case 1:
_b.trys.push([1, , 3, 4]);
_a = this;
return [4 /*yield*/, this._makeCallPromise];
case 2:
activeCall = _a._activeCall = _b.sent();
return [3 /*break*/, 4];
case 3:
this._makeCallPromise = null;
return [7 /*endfinally*/];
case 4:
// Make sure any incoming calls are ignored
this._calls.splice(0).forEach(function (call) { return call.ignore(); });
// Stop the incoming sound if it's playing
this._soundcache.get(Device.SoundName.Incoming).stop();
activeCall.accept({ rtcConstraints: options.rtcConstraints });
this._publishNetworkChange();
return [2 /*return*/, activeCall];
}
});
});
};
Object.defineProperty(Device.prototype, "calls", {
/**
* Return the calls that this {@link Device} is maintaining.
*/
get: function () {
return this._calls;
},
enumerable: false,
configurable: true
});
/**
* Destroy the {@link Device}, freeing references to be garbage collected.
*/
Device.prototype.destroy = function () {
var _a;
this._log.debug('.destroy');
this._log.debug('Rejecting any incoming calls');
var calls = this._calls.slice(0);
calls.forEach(function (call) { return call.reject(); });
this.disconnectAll();
this._stopRegistrationTimer();
this._destroyStream();
this._destroyAudioHelper();
(_a = this._audioProcessorEventObserver) === null || _a === void 0 ? void 0 : _a.destroy();
this._destroyPublisher();
if (this._networkInformation && typeof this._networkInformation.removeEventListener === 'function') {
this._networkInformation.removeEventListener('change', this._publishNetworkChange);
}
if (typeof window !== 'undefined' && window.removeEventListener) {
window.removeEventListener('beforeunload', this._boundConfirmClose);
window.removeEventListener('unload', this._boundDestroy);
window.removeEventListener('pagehide', this._boundDestroy);
}
this._setState(Device.State.Destroyed);
events.EventEmitter.prototype.removeAllListeners.call(this);
};
/**
* Disconnect all {@link Call}s.
*/
Device.prototype.disconnectAll = function () {
this._log.debug('.disconnectAll');
var calls = this._calls.splice(0);
calls.forEach(function (call) { return call.disconnect(); });
if (this._activeCall) {
this._activeCall.disconnect();
}
};
Object.defineProperty(Device.prototype, "edge", {
/**
* Returns the {@link Edge} value the {@link Device} is currently connected
* to. The value will be `null` when the {@link Device} is offline.
*/
get: function () {
return this._edge;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device.prototype, "home", {
/**
* Returns the home value the {@link Device} is currently connected
* to. The value will be `null` when the {@link Device} is offline.
*/
get: function () {
return this._home;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device.prototype, "identity", {
/**
* Returns the identity associated with the {@link Device} for incoming calls. Only
* populated when registered.
*/
get: function () {
return this._identity;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device.prototype, "isBusy", {
/**
* Whether the Device is currently on an active Call.
*/
get: function () {
return !!this._activeCall;
},
enumerable: false,
configurable: true
});
/**
* Register the `Device` to the Twilio backend, allowing it to receive calls.
*/
Device.prototype.register = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_a) {
switch (_a.label) {
case 0:
this._log.debug('.register');
if (this.state !== Device.State.Unregistered) {
throw new index$1.InvalidStateError("Attempt to register when device is in state \"".concat(this.state, "\". ") +
"Must be \"".concat(Device.State.Unregistered, "\"."));
}
this._shouldReRegister = false;
this._setState(Device.State.Registering);
return [4 /*yield*/, (this._streamConnectedPromise || this._setupStream())];
case 1:
_a.sent();
return [4 /*yield*/, this._sendPresence(true)];
case 2:
_a.sent();
return [4 /*yield*/, util.promisifyEvents(this, Device.State.Registered, Device.State.Unregistered)];
case 3:
_a.sent();
return [2 /*return*/];
}
});
});
};
Object.defineProperty(Device.prototype, "state", {
/**
* Get the state of this {@link Device} instance
*/
get: function () {
return this._state;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Device.prototype, "token", {
/**
* Get the token used by this {@link Device}.
*/
get: function () {
return this._token;
},
enumerable: false,
configurable: true
});
/**
* String representation of {@link Device} instance.
* @private
*/
Device.prototype.toString = function () {
return '[Twilio.Device instance]';
};
/**
* Unregister the `Device` to the Twilio backend, disallowing it to receive
* calls.
*/
Device.prototype.unregister = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var stream, streamOfflinePromise;
return tslib.__generator(this, function (_a) {
switch (_a.label) {
case 0:
this._log.debug('.unregister');
if (this.state !== Device.State.Registered) {
throw new index$1.InvalidStateError("Attempt to unregister when device is in state \"".concat(this.state, "\". ") +
"Must be \"".concat(Device.State.Registered, "\"."));
}
this._shouldReRegister = false;
return [4 /*yield*/, this._streamConnectedPromise];
case 1:
stream = _a.sent();
streamOfflinePromise = new Promise(function (resolve) {
stream.on('offline', resolve);
});
return [4 /*yield*/, this._sendPresence(false)];
case 2:
_a.sent();
return [4 /*yield*/, streamOfflinePromise];
case 3:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* Set the options used within the {@link Device}.
* @param options
*/
Device.prototype.updateOptions = function (options) {
if (options === void 0) { options = {}; }
this._logOptions('updateOptions', options);
if (this.state === Device.State.Destroyed) {
throw new index$1.InvalidStateError("Attempt to \"updateOptions\" when device is in state \"".concat(this.state, "\"."));
}
this._options = tslib.__assign(tslib.__assign(tslib.__assign({}, this._defaultOptions), this._options), options);
var originalChunderURIs = new Set(this._chunderURIs);
var newChunderURIs = this._chunderURIs = (this._getChunderws() || regions.getChunderURIs(this._options.edge)).map(regions.createSignalingEndpointURL);
var hasChunderURIsChanged = originalChunderURIs.size !== newChunderURIs.length;
if (!hasChunderURIsChanged) {
for (var _i = 0, newChunderURIs_1 = newChunderURIs; _i < newChunderURIs_1.length; _i++) {
var uri = newChunderURIs_1[_i];
if (!originalChunderURIs.has(uri)) {
hasChunderURIsChanged = true;
break;
}
}
}
if (this.isBusy && hasChunderURIsChanged) {
throw new index$1.InvalidStateError('Cannot change Edge while on an active Call');
}
this._setupLoglevel(this._options.logLevel);
for (var _a = 0, _b = Object.keys(Device._defaultSounds); _a < _b.length; _a++) {
var name_1 = _b[_a];
var soundDef = Device._defaultSounds[name_1];
var defaultUrl = "".concat(constants.SOUNDS_BASE_URL, "/").concat(soundDef.filename, ".").concat(Device.extension)
+ "?cache=".concat(constants.RELEASE_VERSION);
var soundUrl = this._options.sounds && this._options.sounds[name_1] || defaultUrl;
var sound$1 = new (this._options.Sound || sound.default)(name_1, soundUrl, {
audioContext: this._options.disableAudioContextSounds ? null : Device.audioContext,
maxDuration: soundDef.maxDuration,
shouldLoop: soundDef.shouldLoop,
});
this._soundcache.set(name_1, sound$1);
}
this._setupAudioHelper();
this._setupPublisher();
if (hasChunderURIsChanged && this._streamConnectedPromise) {
this._setupStream();
}
// Setup close protection and make sure we clean up ongoing calls on unload.
if (typeof window !== 'undefined' &&
typeof window.addEventListener === 'function' &&
this._options.closeProtection) {
window.removeEventListener('beforeunload', this._boundConfirmClose);
window.addEventListener('beforeunload', this._boundConfirmClose);
}
};
/**
* Update the token used by this {@link Device} to connect to Twilio.
* It is recommended to call this API after [[Device.tokenWillExpireEvent]] is emitted,
* and before or after a call to prevent a potential ~1s audio loss during the update process.
* @param token
*/
Device.prototype.updateToken = function (token) {
this._log.debug('.updateToken');
if (this.state === Device.State.Destroyed) {
throw new index$1.InvalidStateError("Attempt to \"updateToken\" when device is in state \"".concat(this.state, "\"."));
}
if (typeof token !== 'string') {
throw new index$1.InvalidArgumentError(INVALID_TOKEN_MESSAGE);
}
this._token = token;
if (this._stream) {
this._stream.setToken(this._token);
}
if (this._publisher) {
this._publisher.setToken(this._token);
}
};
/**
* Called on window's beforeunload event if closeProtection is enabled,
* preventing users from accidentally navigating away from an active call.
* @param event
*/
Device.prototype._confirmClose = function (event) {
if (!this._activeCall) {
return '';
}
var closeProtection = this._options.closeProtection || false;
var confirmationMsg = typeof closeProtection !== 'string'
? 'A call is currently in-progress. Leaving or reloading this page will end the call.'
: closeProtection;
(event || window.event).returnValue = confirmationMsg;
return confirmationMsg;
};
/**
* Destroy the AudioHelper.
*/
Device.prototype._destroyAudioHelper = function () {
if (!this._audio) {
return;
}
this._audio._destroy();
this._audio = null;
};
/**
* Destroy the publisher.
*/
Device.prototype._destroyPublisher = function () {
// Attempt to destroy non-existent publisher.
if (!this._publisher) {
return;
}
this._publisher = null;
};
/**
* Destroy the connection to the signaling server.
*/
Device.prototype._destroyStream = function () {
if (this._stream) {
this._stream.removeListener('close', this._onSignalingClose);
this._stream.removeListener('connected', this._onSignalingConnected);
this._stream.removeListener('error', this._onSignalingError);
this._stream.removeListener('invite', this._onSignalingInvite);
this._stream.removeListener('offline', this._onSignalingOffline);
this._stream.removeListener('ready', this._onSignalingReady);
this._stream.destroy();
this._stream = null;
}
this._onSignalingOffline();
this._streamConnectedPromise = null;
};
/**
* Find a {@link Call} by its CallSid.
* @param callSid
*/
Device.prototype._findCall = function (callSid) {
return this._calls.find(function (call) { return call.parameters.CallSid === callSid
|| call.outboundConnectionId === callSid; }) || null;
};
/**
* Get chunderws array from the chunderw param
*/
Device.prototype._getChunderws = function () {
return typeof this._options.chunderw === 'string' ? [this._options.chunderw]
: Array.isArray(this._options.chunderw) ? this._options.chunderw : null;
};
/**
* Utility function to log device options
*/
Device.prototype._logOptions = function (caller, options) {
if (options === void 0) { options = {}; }
// Selectively log options that users can modify.
// Also, convert user overrides.
// This prevents potential app crash when calling JSON.stringify
// and when sending log strings remotely
var userOptions = [
'allowIncomingWhileBusy',
'appName',
'appVersion',
'closeProtection',
'codecPreferences',
'disableAudioContextSounds',
'dscp',
'edge',
'enableImprovedSignalingErrorPrecision',
'forceAggressiveIceNomination',
'logLevel',
'maxAverageBitrate',
'maxCallSignalingTimeoutMs',
'sounds',
'tokenRefreshMs',
];
var userOptionOverrides = [
'RTCPeerConnection',
'enumerateDevices',
'getUserMedia',
'MediaStream',
];
if (typeof options === 'object') {
var toLog_1 = tslib.__assign({}, options);
Object.keys(toLog_1).forEach(function (key) {
if (!userOptions.includes(key) && !userOptionOverrides.includes(key)) {
delete toLog_1[key];
}
if (userOptionOverrides.includes(key)) {
toLog_1[key] = true;
}
});
this._log.debug(".".concat(caller), JSON.stringify(toLog_1));
}
};
/**
* Create a new {@link Call}.
* @param twimlParams - A flat object containing key:value pairs to be sent to the TwiML app.
* @param options - Options to be used to instantiate the {@link Call}.
*/
Device.prototype._makeCall = function (twimlParams_1, options_1) {
return tslib.__awaiter(this, arguments, void 0, function (twimlParams, options, isReconnect) {
var inputDevicePromise, config, maybeUnsetPreferredUri, call$1;
var _a;
var _this = this;
var _b;
if (isReconnect === void 0) { isReconnect = false; }
return tslib.__generator(this, function (_c) {
switch (_c.label) {
case 0:
inputDevicePromise = (_b = this._audio) === null || _b === void 0 ? void 0 : _b._getInputDevicePromise();
if (!inputDevicePromise) return [3 /*break*/, 2];
this._log.debug('inputDevicePromise detected, waiting...');
return [4 /*yield*/, inputDevicePromise];
case 1:
_c.sent();
this._log.debug('inputDevicePromise resolved');
_c.label = 2;
case 2:
_a = {
audioHelper: this._audio,
onIgnore: function () {
_this._soundcache.get(Device.SoundName.Incoming).stop();
}
};
return [4 /*yield*/, (this._streamConnectedPromise || this._setupStream())];
case 3:
config = (_a.pstream = _c.sent(),
_a.publisher = this._publisher,
_a.soundcache = this._soundcache,
_a);
options = Object.assign({
MediaStream: this._options.MediaStream,
RTCPeerConnection: this._options.RTCPeerConnection,
beforeAccept: function (currentCall) {
if (!_this._activeCall || _this._activeCall === currentCall) {
return;
}
_this._activeCall.disconnect();
_this._removeCall(_this._activeCall);
},
codecPreferences: this._options.codecPreferences,
customSounds: this._options.sounds,
dialtonePlayer: Device._dialtonePlayer,
dscp: this._options.dscp,
// TODO(csantos): Remove forceAggressiveIceNomination option in 3.x
forceAggressiveIceNomination: this._options.forceAggressiveIceNomination,
getInputStream: function () { return _this._options.fileInputStream || _this._callInputStream; },
getSinkIds: function () { return _this._callSinkIds; },
maxAverageBitrate: this._options.maxAverageBitrate,
preflight: this._options.preflight,
rtcConstraints: this._options.rtcConstraints,
shouldPlayDisconnect: function () { var _a; return (_a = _this._audio) === null || _a === void 0 ? void 0 : _a.disconnect(); },
twimlParams: twimlParams,
voiceEventSidGenerator: this._options.voiceEventSidGenerator,
}, options);
maybeUnsetPreferredUri = function () {
if (!_this._stream) {
_this._log.warn('UnsetPreferredUri called without a stream');
return;
}
if (_this._activeCall === null && _this._calls.length === 0) {
_this._stream.updatePreferredURI(null);
}
};
call$1 = new (this._options.Call || call.default)(config, options);
this._publisher.info('settings', 'init', {
MediaStream: !!this._options.MediaStream,
RTCPeerConnection: !!this._options.RTCPeerConnection,
enumerateDevices: !!this._options.enumerateDevices,
getUserMedia: !!this._options.getUserMedia,
}, call$1);
call$1.once('accept', function () {
var _a, _b, _c, _d, _e;
_this._stream.updatePreferredURI(_this._preferredURI);
_this._removeCall(call$1);
_this._activeCall = call$1;
if (_this._audio) {
_this._audio._maybeStartPollingVolume();
}
if (call$1.direction === call.default.CallDirection.Outgoing && ((_a = _this._audio) === null || _a === void 0 ? void 0 : _a.outgoing()) && !isReconnect) {
_this._soundcache.get(Device.SoundName.Outgoing).play();
}
var data = { edge: _this._edge || _this._region };
if (_this._options.edge) {
data['selected_edge'] = Array.isArray(_this._options.edge)
? _this._options.edge
: [_this._options.edge];
}
_this._publisher.info('settings', 'edge', data, call$1);
if ((_b = _this._audio) === null || _b