@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
1,110 lines (1,109 loc) • 122 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 __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
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 loglevel_1 = require("loglevel");
var audiohelper_1 = require("./audiohelper");
var audioprocessoreventobserver_1 = require("./audioprocessoreventobserver");
var call_1 = require("./call");
var C = require("./constants");
var dialtonePlayer_1 = require("./dialtonePlayer");
var errors_1 = require("./errors");
var eventpublisher_1 = require("./eventpublisher");
var log_1 = require("./log");
var preflight_1 = require("./preflight/preflight");
var pstream_1 = require("./pstream");
var regions_1 = require("./regions");
var rtc = require("./rtc");
var getusermedia_1 = require("./rtc/getusermedia");
var sid_1 = require("./sid");
var sound_1 = require("./sound");
var util_1 = require("./util");
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.
*/
var Device = /** @class */ (function (_super) {
__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_1.default.Codec.PCMU, call_1.default.Codec.Opus],
dscp: true,
enableImprovedSignalingErrorPrecision: false,
forceAggressiveIceNomination: false,
logLevel: loglevel_1.levels.ERROR,
maxCallSignalingTimeoutMs: 0,
preflight: false,
sounds: {},
tokenRefreshMs: 10000,
voiceEventSidGenerator: sid_1.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_1.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: rtc.getMediaEngine(),
sdk_version: C.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 = (0, regions_1.getRegionShortcode)(payload.region);
_this._edge = payload.edge || regions_1.regionToEdge[region] || payload.region;
_this._region = region || payload.region;
_this._home = payload.home;
(_a = _this._publisher) === null || _a === void 0 ? void 0 : _a.setHost((0, regions_1.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() || (0, regions_1.getChunderURIs)(_this._edge);
if (preferredURIs.length > 0) {
var preferredURI = preferredURIs[0];
_this._preferredURI = (0, regions_1.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 errors_1.AuthorizationErrors.AuthenticationFailed(originalError);
}
else if (code === 31204) {
twilioError = new errors_1.AuthorizationErrors.AccessTokenInvalid(originalError);
}
else if (code === 31205) {
// Stop trying to register presence after token expires
_this._stopRegistrationTimer();
twilioError = new errors_1.AuthorizationErrors.AccessTokenExpired(originalError);
}
else {
var errorConstructor = (0, errors_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 errors_1.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 __awaiter(_this, void 0, void 0, function () {
var wasBusy, callParameters, customParameters, call, play;
var _this = this;
var _a;
return __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 errors_1.ClientErrors.BadRequest('Malformed invite from gateway'));
return [2 /*return*/];
}
callParameters = payload.parameters || {};
callParameters.CallSid = callParameters.CallSid || payload.callsid;
customParameters = Object.assign({}, (0, util_1.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 errors_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 ((0, util_1.isLegacyEdge)()) {
throw new errors_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 errors_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 errors_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_1.default(Device._audioContext);
}
}
if (typeof Device._isUnifiedPlanDefault === 'undefined') {
Device._isUnifiedPlanDefault = typeof window !== 'undefined'
&& typeof RTCPeerConnection !== 'undefined'
&& typeof RTCRtpTransceiver !== 'undefined'
? (0, util_1.isUnifiedPlanDefault)(window, window.navigator, RTCPeerConnection, RTCRtpTransceiver)
: false;
}
_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 rtc.enabled(); },
enumerable: false,
configurable: true
});
Object.defineProperty(Device, "packageName", {
/**
* Package name of the SDK.
*/
get: function () { return C.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_1.PreflightTest(token, __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 C.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 __awaiter(this, arguments, void 0, function (options) {
var customParameters, parameters, signalingReconnectToken, connectTokenParts, isReconnect, twimlParams, callOptions, activeCall, _a;
if (options === void 0) { options = {}; }
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this._log.debug('.connect', JSON.stringify(options));
this._throwIfDestroyed();
if (this._activeCall) {
throw new errors_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 errors_1.InvalidArgumentError('Cannot parse connectToken');
}
if (!parameters || !parameters.CallSid || !signalingReconnectToken) {
throw new errors_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_1.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 __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this._log.debug('.register');
if (this.state !== Device.State.Unregistered) {
throw new errors_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*/, (0, util_1.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 __awaiter(this, void 0, void 0, function () {
var stream, streamOfflinePromise;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this._log.debug('.unregister');
if (this.state !== Device.State.Registered) {
throw new errors_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 errors_1.InvalidStateError("Attempt to \"updateOptions\" when device is in state \"".concat(this.state, "\"."));
}
this._options = __assign(__assign(__assign({}, this._defaultOptions), this._options), options);
var originalChunderURIs = new Set(this._chunderURIs);
var newChunderURIs = this._chunderURIs = (this._getChunderws() || (0, regions_1.getChunderURIs)(this._options.edge)).map(regions_1.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 errors_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(C.SOUNDS_BASE_URL, "/").concat(soundDef.filename, ".").concat(Device.extension)
+ "?cache=".concat(C.RELEASE_VERSION);
var soundUrl = this._options.sounds && this._options.sounds[name_1] || defaultUrl;
var sound = new (this._options.Sound || sound_1.default)(name_1, soundUrl, {
audioContext: this._options.disableAudioContextSounds ? null : Device.audioContext,
maxDuration: soundDef.maxDuration,
shouldLoop: soundDef.shouldLoop,
});
this._soundcache.set(name_1, sound);
}
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 errors_1.InvalidStateError("Attempt to \"updateToken\" when device is in state \"".concat(this.state, "\"."));
}
if (typeof token !== 'string') {
throw new errors_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 = __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 __awaiter(this, arguments, void 0, function (twimlParams, options, isReconnect) {
var inputDevicePromise, config, maybeUnsetPreferredUri, call;
var _a;
var _this = this;
var _b;
if (isReconnect === void 0) { isReconnect = false; }
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
if (typeof Device._isUnifiedPlanDefault === 'undefined') {
throw new errors_1.InvalidStateError('Device has not been initialized.');
}
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,
isUnifiedPlanDefault: Device._isUnifiedPlanDefault,
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;