vendor: 4,171 bytes, lines 1-136
1(function () { 2 'use strict'; 3 4 function _mergeNamespaces(n, m) { 5 m.forEach(function (e) { 6 e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { 7 if (k !== 'default' && !(k in n)) { 8 var d = Object.getOwnPropertyDescriptor(e, k); 9 Object.defineProperty(n, k, d.get ? d : { 10 enumerable: true, 11 get: function () { return e[k]; } 12 }); 13 } 14 }); 15 }); 16 return Object.freeze(n); 17 } 18 19 function getDefaultExportFromCjs (x) { 20 return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 21 } 22 23 var events = {exports: {}}; 24 25 var hasRequiredEvents; 26 27 function requireEvents () { 28 if (hasRequiredEvents) return events.exports; 29 hasRequiredEvents = 1; 30 31 var R = typeof Reflect === 'object' ? Reflect : null; 32 var ReflectApply = R && typeof R.apply === 'function' 33 ? R.apply 34 : function ReflectApply(target, receiver, args) { 35 return Function.prototype.apply.call(target, receiver, args); 36 }; 37 38 var ReflectOwnKeys; 39 if (R && typeof R.ownKeys === 'function') { 40 ReflectOwnKeys = R.ownKeys; 41 } else if (Object.getOwnPropertySymbols) { 42 ReflectOwnKeys = function ReflectOwnKeys(target) { 43 return Object.getOwnPropertyNames(target) 44 .concat(Object.getOwnPropertySymbols(target)); 45 }; 46 } else { 47 ReflectOwnKeys = function ReflectOwnKeys(target) { 48 return Object.getOwnPropertyNames(target); 49 }; 50 } 51 52 function ProcessEmitWarning(warning) { 53 if (console && console.warn) console.warn(warning); 54 } 55 56 var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { 57 return value !== value; 58 }; 59 60 function EventEmitter() { 61 EventEmitter.init.call(this); 62 } 63 events.exports = EventEmitter; 64 events.exports.once = once; 65 66 // Backwards-compat with node 0.10.x 67 EventEmitter.EventEmitter = EventEmitter; 68 69 EventEmitter.prototype._events = undefined; 70 EventEmitter.prototype._eventsCount = 0; 71 EventEmitter.prototype._maxListeners = undefined; 72 73 // By default EventEmitters will print a warning if more than 10 listeners are 74 // added to it. This is a useful default which helps finding memory leaks. 75 var defaultMaxListeners = 10; 76 77 function checkListener(listener) { 78 if (typeof listener !== 'function') { 79 throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); 80 } 81 } 82 83 Object.defineProperty(EventEmitter, 'defaultMaxListeners', { 84 enumerable: true, 85 get: function() { 86 return defaultMaxListeners; 87 }, 88 set: function(arg) { 89 if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { 90 throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); 91 } 92 defaultMaxListeners = arg; 93 } 94 }); 95 96 EventEmitter.init = function() { 97 98 if (this._events === undefined || 99 this._events === Object.getPrototypeOf(this)._events) { 100 this._events = Object.create(null); 101 this._eventsCount = 0; 102 } 103 104 this._maxListeners = this._maxListeners || undefined; 105 }; 106 107 // Obviously not all Emitters should be limited to 10. This function allows 108 // that to be increased. Set to zero for unlimited. 109 EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 110 if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { 111 throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); 112 } 113 this._maxListeners = n; 114 return this; 115 }; 116 117 function _getMaxListeners(that) { 118 if (that._maxListeners === undefined) 119 return EventEmitter.defaultMaxListeners; 120 return that._maxListeners; 121 } 122 123 EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 124 return _getMaxListeners(this); 125 }; 126 127 EventEmitter.prototype.emit = function emit(type) { 128 var args = []; 129 for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); 130 var doError = (type === 'error'); 131 132 var events = this._events; 133 if (events !== undefined) 134 doError = (doError && events.error === undefined); 135 else if (!doError) 136 return false;
vendor: 5,112 bytes, lines 137-295
137 138 // If there is no 'error' event listener then throw. 139 if (doError) { 140 var er; 141 if (args.length > 0) 142 er = args[0]; 143 if (er instanceof Error) { 144 // Note: The comments on the `throw` lines are intentional, they show 145 // up in Node's output if this results in an unhandled exception. 146 throw er; // Unhandled 'error' event 147 } 148 // At least give some kind of context to the user 149 var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); 150 err.context = er; 151 throw err; // Unhandled 'error' event 152 } 153 154 var handler = events[type]; 155 156 if (handler === undefined) 157 return false; 158 159 if (typeof handler === 'function') { 160 ReflectApply(handler, this, args); 161 } else { 162 var len = handler.length; 163 var listeners = arrayClone(handler, len); 164 for (var i = 0; i < len; ++i) 165 ReflectApply(listeners[i], this, args); 166 } 167 168 return true; 169 }; 170 171 function _addListener(target, type, listener, prepend) { 172 var m; 173 var events; 174 var existing; 175 176 checkListener(listener); 177 178 events = target._events; 179 if (events === undefined) { 180 events = target._events = Object.create(null); 181 target._eventsCount = 0; 182 } else { 183 // To avoid recursion in the case that type === "newListener"! Before 184 // adding it to the listeners, first emit "newListener". 185 if (events.newListener !== undefined) { 186 target.emit('newListener', type, 187 listener.listener ? listener.listener : listener); 188 189 // Re-assign `events` because a newListener handler could have caused the 190 // this._events to be assigned to a new object 191 events = target._events; 192 } 193 existing = events[type]; 194 } 195 196 if (existing === undefined) { 197 // Optimize the case of one listener. Don't need the extra array object. 198 existing = events[type] = listener; 199 ++target._eventsCount; 200 } else { 201 if (typeof existing === 'function') { 202 // Adding the second element, need to change to array. 203 existing = events[type] = 204 prepend ? [listener, existing] : [existing, listener]; 205 // If we've already got an array, just append. 206 } else if (prepend) { 207 existing.unshift(listener); 208 } else { 209 existing.push(listener); 210 } 211 212 // Check for listener leak 213 m = _getMaxListeners(target); 214 if (m > 0 && existing.length > m && !existing.warned) { 215 existing.warned = true; 216 // No error code for this since it is a Warning 217 // eslint-disable-next-line no-restricted-syntax 218 var w = new Error('Possible EventEmitter memory leak detected. ' + 219 existing.length + ' ' + String(type) + ' listeners ' + 220 'added. Use emitter.setMaxListeners() to ' + 221 'increase limit'); 222 w.name = 'MaxListenersExceededWarning'; 223 w.emitter = target; 224 w.type = type; 225 w.count = existing.length; 226 ProcessEmitWarning(w); 227 } 228 } 229 230 return target; 231 } 232 233 EventEmitter.prototype.addListener = function addListener(type, listener) { 234 return _addListener(this, type, listener, false); 235 }; 236 237 EventEmitter.prototype.on = EventEmitter.prototype.addListener; 238 239 EventEmitter.prototype.prependListener = 240 function prependListener(type, listener) { 241 return _addListener(this, type, listener, true); 242 }; 243 244 function onceWrapper() { 245 if (!this.fired) { 246 this.target.removeListener(this.type, this.wrapFn); 247 this.fired = true; 248 if (arguments.length === 0) 249 return this.listener.call(this.target); 250 return this.listener.apply(this.target, arguments); 251 } 252 } 253 254 function _onceWrap(target, type, listener) { 255 var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; 256 var wrapped = onceWrapper.bind(state); 257 wrapped.listener = listener; 258 state.wrapFn = wrapped; 259 return wrapped; 260 } 261 262 EventEmitter.prototype.once = function once(type, listener) { 263 checkListener(listener); 264 this.on(type, _onceWrap(this, type, listener)); 265 return this; 266 }; 267 268 EventEmitter.prototype.prependOnceListener = 269 function prependOnceListener(type, listener) { 270 checkListener(listener); 271 this.prependListener(type, _onceWrap(this, type, listener)); 272 return this; 273 }; 274 275 // Emits a 'removeListener' event if and only if the listener was removed. 276 EventEmitter.prototype.removeListener = 277 function removeListener(type, listener) { 278 var list, events, position, i, originalListener; 279 280 checkListener(listener); 281 282 events = this._events; 283 if (events === undefined) 284 return this; 285 286 list = events[type]; 287 if (list === undefined) 288 return this; 289 290 if (list === listener || list.listener === listener) { 291 if (--this._eventsCount === 0) 292 this._events = Object.create(null); 293 else { 294 delete events[type]; 295 if (events.removeListener)
vendor: 7,439 bytes, lines 296-538
296 this.emit('removeListener', type, list.listener || listener); 297 } 298 } else if (typeof list !== 'function') { 299 position = -1; 300 301 for (i = list.length - 1; i >= 0; i--) { 302 if (list[i] === listener || list[i].listener === listener) { 303 originalListener = list[i].listener; 304 position = i; 305 break; 306 } 307 } 308 309 if (position < 0) 310 return this; 311 312 if (position === 0) 313 list.shift(); 314 else { 315 spliceOne(list, position); 316 } 317 318 if (list.length === 1) 319 events[type] = list[0]; 320 321 if (events.removeListener !== undefined) 322 this.emit('removeListener', type, originalListener || listener); 323 } 324 325 return this; 326 }; 327 328 EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 329 330 EventEmitter.prototype.removeAllListeners = 331 function removeAllListeners(type) { 332 var listeners, events, i; 333 334 events = this._events; 335 if (events === undefined) 336 return this; 337 338 // not listening for removeListener, no need to emit 339 if (events.removeListener === undefined) { 340 if (arguments.length === 0) { 341 this._events = Object.create(null); 342 this._eventsCount = 0; 343 } else if (events[type] !== undefined) { 344 if (--this._eventsCount === 0) 345 this._events = Object.create(null); 346 else 347 delete events[type]; 348 } 349 return this; 350 } 351 352 // emit removeListener for all listeners on all events 353 if (arguments.length === 0) { 354 var keys = Object.keys(events); 355 var key; 356 for (i = 0; i < keys.length; ++i) { 357 key = keys[i]; 358 if (key === 'removeListener') continue; 359 this.removeAllListeners(key); 360 } 361 this.removeAllListeners('removeListener'); 362 this._events = Object.create(null); 363 this._eventsCount = 0; 364 return this; 365 } 366 367 listeners = events[type]; 368 369 if (typeof listeners === 'function') { 370 this.removeListener(type, listeners); 371 } else if (listeners !== undefined) { 372 // LIFO order 373 for (i = listeners.length - 1; i >= 0; i--) { 374 this.removeListener(type, listeners[i]); 375 } 376 } 377 378 return this; 379 }; 380 381 function _listeners(target, type, unwrap) { 382 var events = target._events; 383 384 if (events === undefined) 385 return []; 386 387 var evlistener = events[type]; 388 if (evlistener === undefined) 389 return []; 390 391 if (typeof evlistener === 'function') 392 return unwrap ? [evlistener.listener || evlistener] : [evlistener]; 393 394 return unwrap ? 395 unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); 396 } 397 398 EventEmitter.prototype.listeners = function listeners(type) { 399 return _listeners(this, type, true); 400 }; 401 402 EventEmitter.prototype.rawListeners = function rawListeners(type) { 403 return _listeners(this, type, false); 404 }; 405 406 EventEmitter.listenerCount = function(emitter, type) { 407 if (typeof emitter.listenerCount === 'function') { 408 return emitter.listenerCount(type); 409 } else { 410 return listenerCount.call(emitter, type); 411 } 412 }; 413 414 EventEmitter.prototype.listenerCount = listenerCount; 415 function listenerCount(type) { 416 var events = this._events; 417 418 if (events !== undefined) { 419 var evlistener = events[type]; 420 421 if (typeof evlistener === 'function') { 422 return 1; 423 } else if (evlistener !== undefined) { 424 return evlistener.length; 425 } 426 } 427 428 return 0; 429 } 430 431 EventEmitter.prototype.eventNames = function eventNames() { 432 return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; 433 }; 434 435 function arrayClone(arr, n) { 436 var copy = new Array(n); 437 for (var i = 0; i < n; ++i) 438 copy[i] = arr[i]; 439 return copy; 440 } 441 442 function spliceOne(list, index) { 443 for (; index + 1 < list.length; index++) 444 list[index] = list[index + 1]; 445 list.pop(); 446 } 447 448 function unwrapListeners(arr) { 449 var ret = new Array(arr.length); 450 for (var i = 0; i < ret.length; ++i) { 451 ret[i] = arr[i].listener || arr[i]; 452 } 453 return ret; 454 } 455 456 function once(emitter, name) { 457 return new Promise(function (resolve, reject) { 458 function errorListener(err) { 459 emitter.removeListener(name, resolver); 460 reject(err); 461 } 462 463 function resolver() { 464 if (typeof emitter.removeListener === 'function') { 465 emitter.removeListener('error', errorListener); 466 } 467 resolve([].slice.call(arguments)); 468 } 469 eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); 470 if (name !== 'error') { 471 addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); 472 } 473 }); 474 } 475 476 function addErrorHandlerIfEventEmitter(emitter, handler, flags) { 477 if (typeof emitter.on === 'function') { 478 eventTargetAgnosticAddListener(emitter, 'error', handler, flags); 479 } 480 } 481 482 function eventTargetAgnosticAddListener(emitter, name, listener, flags) { 483 if (typeof emitter.on === 'function') { 484 if (flags.once) { 485 emitter.once(name, listener); 486 } else { 487 emitter.on(name, listener); 488 } 489 } else if (typeof emitter.addEventListener === 'function') { 490 // EventTarget does not have `error` event semantics like Node 491 // EventEmitters, we do not listen for `error` events here. 492 emitter.addEventListener(name, function wrapListener(arg) { 493 // IE does not have builtin `{ once: true }` support so we 494 // have to do it manually. 495 if (flags.once) { 496 emitter.removeEventListener(name, wrapListener); 497 } 498 listener(arg); 499 }); 500 } else { 501 throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); 502 } 503 } 504 return events.exports; 505 } 506 507 var eventsExports = requireEvents(); 508 509 // @ts-nocheck 510 // NOTE (csantos): This file was taken directly from twilio-video and has been renamed from JS to TS only. 511 // It needs to be re-written as part of the overall updating of the files to TS. 512 class Backoff extends eventsExports.EventEmitter { 513 /** 514 * Construct a {@link Backoff}. 515 * @param {object} options 516 * @property {number} min - Initial timeout in milliseconds [100] 517 * @property {number} max - Max timeout [10000] 518 * @property {boolean} jitter - Apply jitter [0] 519 * @property {number} factor - Multiplication factor for Backoff operation [2] 520 */ 521 constructor(options) { 522 super(); 523 Object.defineProperties(this, { 524 _attempts: { 525 value: 0, 526 writable: true, 527 }, 528 _duration: { 529 enumerable: false, 530 get() { 531 let ms = this._min * Math.pow(this._factor, this._attempts); 532 if (this._jitter) { 533 const rand = Math.random(); 534 const deviation = Math.floor(rand * this._jitter * ms); 535 // tslint:disable-next-line 536 ms = (Math.floor(rand * 10) & 1) === 0 ? ms - deviation : ms + deviation; 537 } 538 // tslint:disable-next-line
539 return Math.min(ms, this._max) | 0; 540 }, 541 }, 542 _factor: { value: options.factor || 2 }, 543 _jitter: { value: options.jitter > 0 && options.jitter <= 1 ? options.jitter : 0 }, 544 _max: { value: options.max || 10000 }, 545 _min: { value: options.min || 100 }, 546 _timeoutID: { 547 value: null, 548 writable: true, 549 }, 550 }); 551 } 552 backoff() { 553 const duration = this._duration; 554 if (this._timeoutID) { 555 clearTimeout(this._timeoutID); 556 this._timeoutID = null; 557 } 558 this.emit('backoff', this._attempts, duration); 559 this._timeoutID = setTimeout(() => { 560 this.emit('ready', this._attempts, duration); 561 this._attempts++; 562 }, duration); 563 } 564 reset() { 565 this._attempts = 0; 566 if (this._timeoutID) { 567 clearTimeout(this._timeoutID); 568 this._timeoutID = null; 569 } 570 } 571 } 572 573 /****************************************************************************** 574 Copyright (c) Microsoft Corporation. 575 576 Permission to use, copy, modify, and/or distribute this software for any 577 purpose with or without fee is hereby granted. 578 579 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 580 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 581 AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 582 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 583 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 584 OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 585 PERFORMANCE OF THIS SOFTWARE. 586 ***************************************************************************** */ 587 /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ 588 589 590 function __awaiter(thisArg, _arguments, P, generator) { 591 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 592 return new (P || (P = Promise))(function (resolve, reject) { 593 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 594 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 595 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 596 step((generator = generator.apply(thisArg, _arguments || [])).next()); 597 }); 598 } 599 600 typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { 601 var e = new Error(message); 602 return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; 603 }; 604 605 var loglevel$3 = {exports: {}}; 606 607 /* 608 * loglevel - https://github.com/pimterry/loglevel 609 * 610 * Copyright (c) 2013 Tim Perry 611 * Licensed under the MIT license. 612 */ 613 var loglevel$2 = loglevel$3.exports; 614 615 var hasRequiredLoglevel; 616 617 function requireLoglevel () { 618 if (hasRequiredLoglevel) return loglevel$3.exports; 619 hasRequiredLoglevel = 1; 620 (function (module) { 621 (function (root, definition) { 622 if (module.exports) { 623 module.exports = definition(); 624 } else { 625 root.log = definition(); 626 } 627 }(loglevel$2, function () { 628 629 // Slightly dubious tricks to cut down minimized file size 630 var noop = function() {}; 631 var undefinedType = "undefined"; 632 var isIE = (typeof window !== undefinedType) && (typeof window.navigator !== undefinedType) && ( 633 /Trident\/|MSIE /.test(window.navigator.userAgent) 634 ); 635 636 var logMethods = [ 637 "trace", 638 "debug", 639 "info", 640 "warn", 641 "error" 642 ]; 643 644 var _loggersByName = {}; 645 var defaultLogger = null; 646 647 // Cross-browser bind equivalent that works at least back to IE6 648 function bindMethod(obj, methodName) { 649 var method = obj[methodName]; 650 if (typeof method.bind === 'function') { 651 return method.bind(obj); 652 } else { 653 try { 654 return Function.prototype.bind.call(method, obj); 655 } catch (e) { 656 // Missing bind shim or IE8 + Modernizr, fallback to wrapping 657 return function() { 658 return Function.prototype.apply.apply(method, [obj, arguments]); 659 }; 660 } 661 } 662 } 663 664 // Trace() doesn't print the message in IE, so for that case we need to wrap it 665 function traceForIE() { 666 if (console.log) { 667 if (console.log.apply) { 668 console.log.apply(console, arguments); 669 } else { 670 // In old IE, native console methods themselves don't have apply(). 671 Function.prototype.apply.apply(console.log, [console, arguments]); 672 } 673 } 674 if (console.trace) console.trace(); 675 } 676 677 // Build the best logging method possible for this env 678 // Wherever possible we want to bind, not wrap, to preserve stack traces 679 function realMethod(methodName) { 680 if (methodName === 'debug') { 681 methodName = 'log'; 682 } 683 684 if (typeof console === undefinedType) { 685 return false; // No method possible, for now - fixed later by enableLoggingWhenConsoleArrives 686 } else if (methodName === 'trace' && isIE) { 687 return traceForIE; 688 } else if (console[methodName] !== undefined) { 689 return bindMethod(console, methodName); 690 } else if (console.log !== undefined) { 691 return bindMethod(console, 'log'); 692 } else { 693 return noop; 694 } 695 } 696 697 // These private functions always need `this` to be set properly 698 699 function replaceLoggingMethods() { 700 /*jshint validthis:true */ 701 var level = this.getLevel(); 702 703 // Replace the actual methods. 704 for (var i = 0; i < logMethods.length; i++) { 705 var methodName = logMethods[i]; 706 this[methodName] = (i < level) ? 707 noop : 708 this.methodFactory(methodName, level, this.name); 709 } 710 711 // Define log.log as an alias for log.debug 712 this.log = this.debug; 713 714 // Return any important warnings. 715 if (typeof console === undefinedType && level < this.levels.SILENT) { 716 return "No console available for logging"; 717 } 718 } 719 720 // In old IE versions, the console isn't present until you first open it. 721 // We build realMethod() replacements here that regenerate logging methods 722 function enableLoggingWhenConsoleArrives(methodName) { 723 return function () { 724 if (typeof console !== undefinedType) { 725 replaceLoggingMethods.call(this); 726 this[methodName].apply(this, arguments); 727 } 728 }; 729 } 730 731 // By default, we use closely bound real methods wherever possible, and 732 // otherwise we wait for a console to appear, and then try again. 733 function defaultMethodFactory(methodName, _level, _loggerName) { 734 /*jshint validthis:true */ 735 return realMethod(methodName) || 736 enableLoggingWhenConsoleArrives.apply(this, arguments); 737 } 738 739 function Logger(name, factory) { 740 // Private instance variables. 741 var self = this; 742 /** 743 * The level inherited from a parent logger (or a global default). We 744 * cache this here rather than delegating to the parent so that it stays 745 * in sync with the actual logging methods that we have installed (the 746 * parent could change levels but we might not have rebuilt the loggers 747 * in this child yet). 748 * @type {number} 749 */ 750 var inheritedLevel; 751 /** 752 * The default level for this logger, if any. If set, this overrides 753 * `inheritedLevel`. 754 * @type {number|null} 755 */ 756 var defaultLevel; 757 /** 758 * A user-specific level for this logger. If set, this overrides 759 * `defaultLevel`. 760 * @type {number|null} 761 */ 762 var userLevel; 763 764 var storageKey = "loglevel"; 765 if (typeof name === "string") { 766 storageKey += ":" + name; 767 } else if (typeof name === "symbol") { 768 storageKey = undefined; 769 } 770 771 function persistLevelIfPossible(levelNum) { 772 var levelName = (logMethods[levelNum] || 'silent').toUpperCase(); 773 774 if (typeof window === undefinedType || !storageKey) return; 775 776 // Use localStorage if available 777 try { 778 window.localStorage[storageKey] = levelName; 779 return; 780 } catch (ignore) {} 781 782 // Use session cookie as fallback 783 try { 784 window.document.cookie = 785 encodeURIComponent(storageKey) + "=" + levelName + ";"; 786 } catch (ignore) {} 787 } 788 789 function getPersistedLevel() { 790 var storedLevel; 791 792 if (typeof window === undefinedType || !storageKey) return; 793 794 try { 795 storedLevel = window.localStorage[storageKey]; 796 } catch (ignore) {} 797 798 // Fallback to cookies if local storage gives us nothing 799 if (typeof storedLevel === undefinedType) { 800 try { 801 var cookie = window.document.cookie; 802 var cookieName = encodeURIComponent(storageKey); 803 var location = cookie.indexOf(cookieName + "="); 804 if (location !== -1) { 805 storedLevel = /^([^;]+)/.exec( 806 cookie.slice(location + cookieName.length + 1) 807 )[1]; 808 } 809 } catch (ignore) {} 810 } 811
vendor: 5,319 bytes, lines 812-983
812 // If the stored level is not valid, treat it as if nothing was stored. 813 if (self.levels[storedLevel] === undefined) { 814 storedLevel = undefined; 815 } 816 817 return storedLevel; 818 } 819 820 function clearPersistedLevel() { 821 if (typeof window === undefinedType || !storageKey) return; 822 823 // Use localStorage if available 824 try { 825 window.localStorage.removeItem(storageKey); 826 } catch (ignore) {} 827 828 // Use session cookie as fallback 829 try { 830 window.document.cookie = 831 encodeURIComponent(storageKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; 832 } catch (ignore) {} 833 } 834 835 function normalizeLevel(input) { 836 var level = input; 837 if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) { 838 level = self.levels[level.toUpperCase()]; 839 } 840 if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) { 841 return level; 842 } else { 843 throw new TypeError("log.setLevel() called with invalid level: " + input); 844 } 845 } 846 847 /* 848 * 849 * Public logger API - see https://github.com/pimterry/loglevel for details 850 * 851 */ 852 853 self.name = name; 854 855 self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, 856 "ERROR": 4, "SILENT": 5}; 857 858 self.methodFactory = factory || defaultMethodFactory; 859 860 self.getLevel = function () { 861 if (userLevel != null) { 862 return userLevel; 863 } else if (defaultLevel != null) { 864 return defaultLevel; 865 } else { 866 return inheritedLevel; 867 } 868 }; 869 870 self.setLevel = function (level, persist) { 871 userLevel = normalizeLevel(level); 872 if (persist !== false) { // defaults to true 873 persistLevelIfPossible(userLevel); 874 } 875 876 // NOTE: in v2, this should call rebuild(), which updates children. 877 return replaceLoggingMethods.call(self); 878 }; 879 880 self.setDefaultLevel = function (level) { 881 defaultLevel = normalizeLevel(level); 882 if (!getPersistedLevel()) { 883 self.setLevel(level, false); 884 } 885 }; 886 887 self.resetLevel = function () { 888 userLevel = null; 889 clearPersistedLevel(); 890 replaceLoggingMethods.call(self); 891 }; 892 893 self.enableAll = function(persist) { 894 self.setLevel(self.levels.TRACE, persist); 895 }; 896 897 self.disableAll = function(persist) { 898 self.setLevel(self.levels.SILENT, persist); 899 }; 900 901 self.rebuild = function () { 902 if (defaultLogger !== self) { 903 inheritedLevel = normalizeLevel(defaultLogger.getLevel()); 904 } 905 replaceLoggingMethods.call(self); 906 907 if (defaultLogger === self) { 908 for (var childName in _loggersByName) { 909 _loggersByName[childName].rebuild(); 910 } 911 } 912 }; 913 914 // Initialize all the internal levels. 915 inheritedLevel = normalizeLevel( 916 defaultLogger ? defaultLogger.getLevel() : "WARN" 917 ); 918 var initialLevel = getPersistedLevel(); 919 if (initialLevel != null) { 920 userLevel = normalizeLevel(initialLevel); 921 } 922 replaceLoggingMethods.call(self); 923 } 924 925 /* 926 * 927 * Top-level API 928 * 929 */ 930 931 defaultLogger = new Logger(); 932 933 defaultLogger.getLogger = function getLogger(name) { 934 if ((typeof name !== "symbol" && typeof name !== "string") || name === "") { 935 throw new TypeError("You must supply a name when creating a logger."); 936 } 937 938 var logger = _loggersByName[name]; 939 if (!logger) { 940 logger = _loggersByName[name] = new Logger( 941 name, 942 defaultLogger.methodFactory 943 ); 944 } 945 return logger; 946 }; 947 948 // Grab the current global log variable in case of overwrite 949 var _log = (typeof window !== undefinedType) ? window.log : undefined; 950 defaultLogger.noConflict = function() { 951 if (typeof window !== undefinedType && 952 window.log === defaultLogger) { 953 window.log = _log; 954 } 955 956 return defaultLogger; 957 }; 958 959 defaultLogger.getLoggers = function getLoggers() { 960 return _loggersByName; 961 }; 962 963 // ES6 default export, for compatibility 964 defaultLogger['default'] = defaultLogger; 965 966 return defaultLogger; 967 })); 968 } (loglevel$3)); 969 return loglevel$3.exports; 970 } 971 972 var loglevelExports = requireLoglevel(); 973 var loglevel = /*@__PURE__*/getDefaultExportFromCjs(loglevelExports); 974 975 var loglevel$1 = /*#__PURE__*/_mergeNamespaces({ 976 __proto__: null, 977 default: loglevel 978 }, [loglevelExports]); 979 980 /** 981 * Base class for all possible errors that the library can receive from the 982 * Twilio backend. 983 */
984 class TwilioError extends Error { 985 /** 986 * @internal 987 */ 988 constructor(messageOrError, error) { 989 super(); 990 Object.setPrototypeOf(this, TwilioError.prototype); 991 const message = typeof messageOrError === 'string' 992 ? messageOrError 993 : this.explanation; 994 const originalError = typeof messageOrError === 'object' 995 ? messageOrError 996 : error; 997 this.message = `${this.name} (${this.code}): ${message}`; 998 this.originalError = originalError; 999 } 1000 } 1001 1002 /* tslint:disable max-classes-per-file max-line-length */ 1003 /** 1004 * This is a generated file. Any modifications here will be overwritten. See scripts/errors.js. 1005 */ 1006 var AuthorizationErrors; 1007 (function (AuthorizationErrors) { 1008 /** 1009 * Error received from the Twilio backend. 1010 */ 1011 class AccessTokenInvalid extends TwilioError { 1012 /** 1013 * @internal 1014 */ 1015 constructor(messageOrError, error) { 1016 super(messageOrError, error); 1017 this.causes = []; 1018 this.code = 20101; 1019 this.description = 'Invalid access token'; 1020 this.explanation = 'Twilio was unable to validate your Access Token'; 1021 this.name = 'AccessTokenInvalid'; 1022 this.solutions = []; 1023 Object.setPrototypeOf(this, AuthorizationErrors.AccessTokenInvalid.prototype); 1024 const message = typeof messageOrError === 'string' 1025 ? messageOrError 1026 : this.explanation; 1027 const originalError = typeof messageOrError === 'object' 1028 ? messageOrError 1029 : error; 1030 this.message = `${this.name} (${this.code}): ${message}`; 1031 this.originalError = originalError; 1032 } 1033 } 1034 AuthorizationErrors.AccessTokenInvalid = AccessTokenInvalid; 1035 /** 1036 * Error received from the Twilio backend. 1037 */ 1038 class AccessTokenExpired extends TwilioError { 1039 /** 1040 * @internal 1041 */ 1042 constructor(messageOrError, error) { 1043 super(messageOrError, error); 1044 this.causes = []; 1045 this.code = 20104; 1046 this.description = 'Access token expired or expiration date invalid'; 1047 this.explanation = 'The Access Token provided to the Twilio API has expired, the expiration time specified in the token was invalid, or the expiration time specified was too far in the future'; 1048 this.name = 'AccessTokenExpired'; 1049 this.solutions = []; 1050 Object.setPrototypeOf(this, AuthorizationErrors.AccessTokenExpired.prototype); 1051 const message = typeof messageOrError === 'string' 1052 ? messageOrError 1053 : this.explanation; 1054 const originalError = typeof messageOrError === 'object' 1055 ? messageOrError 1056 : error; 1057 this.message = `${this.name} (${this.code}): ${message}`; 1058 this.originalError = originalError; 1059 } 1060 } 1061 AuthorizationErrors.AccessTokenExpired = AccessTokenExpired; 1062 /** 1063 * Error received from the Twilio backend. 1064 */ 1065 class AuthenticationFailed extends TwilioError { 1066 /** 1067 * @internal 1068 */ 1069 constructor(messageOrError, error) { 1070 super(messageOrError, error); 1071 this.causes = []; 1072 this.code = 20151; 1073 this.description = 'Authentication Failed'; 1074 this.explanation = 'The Authentication with the provided JWT failed'; 1075 this.name = 'AuthenticationFailed'; 1076 this.solutions = []; 1077 Object.setPrototypeOf(this, AuthorizationErrors.AuthenticationFailed.prototype); 1078 const message = typeof messageOrError === 'string' 1079 ? messageOrError 1080 : this.explanation; 1081 const originalError = typeof messageOrError === 'object' 1082 ? messageOrError 1083 : error; 1084 this.message = `${this.name} (${this.code}): ${message}`; 1085 this.originalError = originalError; 1086 } 1087 } 1088 AuthorizationErrors.AuthenticationFailed = AuthenticationFailed; 1089 })(AuthorizationErrors || (AuthorizationErrors = {})); 1090 var SignatureValidationErrors; 1091 (function (SignatureValidationErrors) { 1092 /** 1093 * Error received from the Twilio backend. 1094 */ 1095 class AccessTokenSignatureValidationFailed extends TwilioError { 1096 /** 1097 * @internal 1098 */ 1099 constructor(messageOrError, error) { 1100 super(messageOrError, error); 1101 this.causes = [
1102 'The access token has an invalid Account SID, API Key, or API Key Secret.', 1103 ]; 1104 this.code = 31202; 1105 this.description = 'Signature validation failed.'; 1106 this.explanation = 'The provided access token failed signature validation.'; 1107 this.name = 'AccessTokenSignatureValidationFailed'; 1108 this.solutions = [ 1109 'Ensure the Account SID, API Key, and API Key Secret are valid when generating your access token.', 1110 ]; 1111 Object.setPrototypeOf(this, SignatureValidationErrors.AccessTokenSignatureValidationFailed.prototype); 1112 const message = typeof messageOrError === 'string' 1113 ? messageOrError 1114 : this.explanation; 1115 const originalError = typeof messageOrError === 'object' 1116 ? messageOrError 1117 : error; 1118 this.message = `${this.name} (${this.code}): ${message}`; 1119 this.originalError = originalError; 1120 } 1121 } 1122 SignatureValidationErrors.AccessTokenSignatureValidationFailed = AccessTokenSignatureValidationFailed; 1123 })(SignatureValidationErrors || (SignatureValidationErrors = {})); 1124 var ClientErrors; 1125 (function (ClientErrors) { 1126 /** 1127 * Error received from the Twilio backend. 1128 */ 1129 class BadRequest extends TwilioError { 1130 /** 1131 * @internal 1132 */ 1133 constructor(messageOrError, error) { 1134 super(messageOrError, error); 1135 this.causes = []; 1136 this.code = 31400; 1137 this.description = 'Bad Request (HTTP/SIP)'; 1138 this.explanation = 'The request could not be understood due to malformed syntax.'; 1139 this.name = 'BadRequest'; 1140 this.solutions = []; 1141 Object.setPrototypeOf(this, ClientErrors.BadRequest.prototype); 1142 const message = typeof messageOrError === 'string' 1143 ? messageOrError 1144 : this.explanation; 1145 const originalError = typeof messageOrError === 'object' 1146 ? messageOrError 1147 : error; 1148 this.message = `${this.name} (${this.code}): ${message}`; 1149 this.originalError = originalError; 1150 } 1151 } 1152 ClientErrors.BadRequest = BadRequest; 1153 /** 1154 * Error received from the Twilio backend. 1155 */ 1156 class NotFound extends TwilioError { 1157 /** 1158 * @internal 1159 */ 1160 constructor(messageOrError, error) { 1161 super(messageOrError, error); 1162 this.causes = [ 1163 'The outbound call was made to an invalid phone number.', 1164 'The TwiML application sid is missing a Voice URL.', 1165 ]; 1166 this.code = 31404; 1167 this.description = 'Not Found (HTTP/SIP)'; 1168 this.explanation = 'The server has not found anything matching the request.'; 1169 this.name = 'NotFound'; 1170 this.solutions = [ 1171 'Ensure the phone number dialed is valid.', 1172 'Ensure the TwiML application is configured correctly with a Voice URL link.', 1173 ]; 1174 Object.setPrototypeOf(this, ClientErrors.NotFound.prototype); 1175 const message = typeof messageOrError === 'string' 1176 ? messageOrError 1177 : this.explanation; 1178 const originalError = typeof messageOrError === 'object' 1179 ? messageOrError 1180 : error; 1181 this.message = `${this.name} (${this.code}): ${message}`; 1182 this.originalError = originalError; 1183 } 1184 } 1185 ClientErrors.NotFound = NotFound; 1186 /** 1187 * Error received from the Twilio backend. 1188 */ 1189 class TemporarilyUnavailable extends TwilioError { 1190 /** 1191 * @internal 1192 */ 1193 constructor(messageOrError, error) { 1194 super(messageOrError, error); 1195 this.causes = []; 1196 this.code = 31480; 1197 this.description = 'Temporarily Unavailable (SIP)'; 1198 this.explanation = 'The callee is currently unavailable.'; 1199 this.name = 'TemporarilyUnavailable'; 1200 this.solutions = []; 1201 Object.setPrototypeOf(this, ClientErrors.TemporarilyUnavailable.prototype); 1202 const message = typeof messageOrError === 'string' 1203 ? messageOrError 1204 : this.explanation; 1205 const originalError = typeof messageOrError === 'object' 1206 ? messageOrError 1207 : error; 1208 this.message = `${this.name} (${this.code}): ${message}`; 1209 this.originalError = originalError; 1210 } 1211 } 1212 ClientErrors.TemporarilyUnavailable = TemporarilyUnavailable; 1213 /** 1214 * Error received from the Twilio backend. 1215 */ 1216 class BusyHere extends TwilioError { 1217 /** 1218 * @internal 1219 */ 1220 constructor(messageOrError, error) { 1221 super(messageOrError, error); 1222 this.causes = []; 1223 this.code = 31486; 1224 this.description = 'Busy Here (SIP)'; 1225 this.explanation = 'The callee is busy.'; 1226 this.name = 'BusyHere'; 1227 this.solutions = []; 1228 Object.setPrototypeOf(this, ClientErrors.BusyHere.prototype); 1229 const message = typeof messageOrError === 'string' 1230 ? messageOrError 1231 : this.explanation; 1232 const originalError = typeof messageOrError === 'object' 1233 ? messageOrError 1234 : error; 1235 this.message = `${this.name} (${this.code}): ${message}`; 1236 this.originalError = originalError; 1237 } 1238 } 1239 ClientErrors.BusyHere = BusyHere; 1240 })(ClientErrors || (ClientErrors = {})); 1241 var SIPServerErrors; 1242 (function (SIPServerErrors) { 1243 /** 1244 * Error received from the Twilio backend. 1245 */ 1246 class Decline extends TwilioError { 1247 /** 1248 * @internal 1249 */ 1250 constructor(messageOrError, error) { 1251 super(messageOrError, error); 1252 this.causes = []; 1253 this.code = 31603; 1254 this.description = 'Decline (SIP)'; 1255 this.explanation = 'The callee does not wish to participate in the call.'; 1256 this.name = 'Decline'; 1257 this.solutions = []; 1258 Object.setPrototypeOf(this, SIPServerErrors.Decline.prototype); 1259 const message = typeof messageOrError === 'string' 1260 ? messageOrError 1261 : this.explanation; 1262 const originalError = typeof messageOrError === 'object' 1263 ? messageOrError 1264 : error; 1265 this.message = `${this.name} (${this.code}): ${message}`; 1266 this.originalError = originalError; 1267 } 1268 } 1269 SIPServerErrors.Decline = Decline; 1270 })(SIPServerErrors || (SIPServerErrors = {})); 1271 var GeneralErrors; 1272 (function (GeneralErrors) { 1273 /** 1274 * Error received from the Twilio backend. 1275 */ 1276 class UnknownError extends TwilioError { 1277 /** 1278 * @internal 1279 */ 1280 constructor(messageOrError, error) { 1281 super(messageOrError, error); 1282 this.causes = []; 1283 this.code = 31000; 1284 this.description = 'Unknown Error'; 1285 this.explanation = 'An unknown error has occurred. See error details for more information.'; 1286 this.name = 'UnknownError'; 1287 this.solutions = []; 1288 Object.setPrototypeOf(this, GeneralErrors.UnknownError.prototype); 1289 const message = typeof messageOrError === 'string' 1290 ? messageOrError 1291 : this.explanation; 1292 const originalError = typeof messageOrError === 'object' 1293 ? messageOrError 1294 : error; 1295 this.message = `${this.name} (${this.code}): ${message}`; 1296 this.originalError = originalError; 1297 } 1298 } 1299 GeneralErrors.UnknownError = UnknownError; 1300 /** 1301 * Error received from the Twilio backend. 1302 */ 1303 class ApplicationNotFoundError extends TwilioError { 1304 /** 1305 * @internal 1306 */ 1307 constructor(messageOrError, error) { 1308 super(messageOrError, error); 1309 this.causes = []; 1310 this.code = 31001; 1311 this.description = 'Application Not Found'; 1312 this.explanation = ''; 1313 this.name = 'ApplicationNotFoundError'; 1314 this.solutions = []; 1315 Object.setPrototypeOf(this, GeneralErrors.ApplicationNotFoundError.prototype); 1316 const message = typeof messageOrError === 'string' 1317 ? messageOrError 1318 : this.explanation; 1319 const originalError = typeof messageOrError === 'object' 1320 ? messageOrError 1321 : error; 1322 this.message = `${this.name} (${this.code}): ${message}`; 1323 this.originalError = originalError; 1324 } 1325 } 1326 GeneralErrors.ApplicationNotFoundError = ApplicationNotFoundError; 1327 /** 1328 * Error received from the Twilio backend. 1329 */ 1330 class ConnectionDeclinedError extends TwilioError { 1331 /** 1332 * @internal 1333 */ 1334 constructor(messageOrError, error) { 1335 super(messageOrError, error); 1336 this.causes = []; 1337 this.code = 31002; 1338 this.description = 'Connection Declined'; 1339 this.explanation = ''; 1340 this.name = 'ConnectionDeclinedError'; 1341 this.solutions = []; 1342 Object.setPrototypeOf(this, GeneralErrors.ConnectionDeclinedError.prototype); 1343 const message = typeof messageOrError === 'string' 1344 ? messageOrError 1345 : this.explanation; 1346 const originalError = typeof messageOrError === 'object' 1347 ? messageOrError 1348 : error; 1349 this.message = `${this.name} (${this.code}): ${message}`; 1350 this.originalError = originalError; 1351 } 1352 } 1353 GeneralErrors.ConnectionDeclinedError = ConnectionDeclinedError; 1354 /** 1355 * Error received from the Twilio backend. 1356 */ 1357 class ConnectionTimeoutError extends TwilioError { 1358 /** 1359 * @internal 1360 */ 1361 constructor(messageOrError, error) { 1362 super(messageOrError, error); 1363 this.causes = []; 1364 this.code = 31003; 1365 this.description = 'Connection Timeout'; 1366 this.explanation = 'The server could not produce a response within a suitable amount of time.'; 1367 this.name = 'ConnectionTimeoutError'; 1368 this.solutions = []; 1369 Object.setPrototypeOf(this, GeneralErrors.ConnectionTimeoutError.prototype); 1370 const message = typeof messageOrError === 'string' 1371 ? messageOrError 1372 : this.explanation; 1373 const originalError = typeof messageOrError === 'object' 1374 ? messageOrError 1375 : error; 1376 this.message = `${this.name} (${this.code}): ${message}`; 1377 this.originalError = originalError; 1378 } 1379 } 1380 GeneralErrors.ConnectionTimeoutError = ConnectionTimeoutError; 1381 /** 1382 * Error received from the Twilio backend. 1383 */ 1384 class ConnectionError extends TwilioError { 1385 /** 1386 * @internal 1387 */ 1388 constructor(messageOrError, error) { 1389 super(messageOrError, error); 1390 this.causes = []; 1391 this.code = 31005; 1392 this.description = 'Connection error'; 1393 this.explanation = 'A connection error occurred during the call'; 1394 this.name = 'ConnectionError'; 1395 this.solutions = []; 1396 Object.setPrototypeOf(this, GeneralErrors.ConnectionError.prototype); 1397 const message = typeof messageOrError === 'string' 1398 ? messageOrError 1399 : this.explanation; 1400 const originalError = typeof messageOrError === 'object' 1401 ? messageOrError 1402 : error; 1403 this.message = `${this.name} (${this.code}): ${message}`; 1404 this.originalError = originalError; 1405 } 1406 } 1407 GeneralErrors.ConnectionError = ConnectionError; 1408 /** 1409 * Error received from the Twilio backend. 1410 */ 1411 class CallCancelledError extends TwilioError { 1412 /** 1413 * @internal 1414 */ 1415 constructor(messageOrError, error) { 1416 super(messageOrError, error); 1417 this.causes = [ 1418 'The incoming call was cancelled because it was not answered in time or it was accepted/rejected by another application instance registered with the same identity.', 1419 ]; 1420 this.code = 31008; 1421 this.description = 'Call cancelled'; 1422 this.explanation = 'Unable to answer because the call has ended'; 1423 this.name = 'CallCancelledError'; 1424 this.solutions = []; 1425 Object.setPrototypeOf(this, GeneralErrors.CallCancelledError.prototype); 1426 const message = typeof messageOrError === 'string' 1427 ? messageOrError 1428 : this.explanation; 1429 const originalError = typeof messageOrError === 'object' 1430 ? messageOrError 1431 : error; 1432 this.message = `${this.name} (${this.code}): ${message}`; 1433 this.originalError = originalError; 1434 } 1435 } 1436 GeneralErrors.CallCancelledError = CallCancelledError; 1437 /** 1438 * Error received from the Twilio backend. 1439 */ 1440 class TransportError extends TwilioError { 1441 /** 1442 * @internal 1443 */ 1444 constructor(messageOrError, error) { 1445 super(messageOrError, error); 1446 this.causes = []; 1447 this.code = 31009; 1448 this.description = 'Transport error'; 1449 this.explanation = 'No transport available to send or receive messages'; 1450 this.name = 'TransportError'; 1451 this.solutions = []; 1452 Object.setPrototypeOf(this, GeneralErrors.TransportError.prototype); 1453 const message = typeof messageOrError === 'string' 1454 ? messageOrError 1455 : this.explanation; 1456 const originalError = typeof messageOrError === 'object' 1457 ? messageOrError 1458 : error; 1459 this.message = `${this.name} (${this.code}): ${message}`; 1460 this.originalError = originalError; 1461 } 1462 } 1463 GeneralErrors.TransportError = TransportError; 1464 })(GeneralErrors || (GeneralErrors = {})); 1465 var MalformedRequestErrors; 1466 (function (MalformedRequestErrors) { 1467 /** 1468 * Error received from the Twilio backend. 1469 */ 1470 class MalformedRequestError extends TwilioError { 1471 /** 1472 * @internal 1473 */ 1474 constructor(messageOrError, error) { 1475 super(messageOrError, error); 1476 this.causes = [ 1477 'Invalid content or MessageType passed to sendMessage method.', 1478 ]; 1479 this.code = 31100; 1480 this.description = 'The request had malformed syntax.';
1481 this.explanation = 'The request could not be understood due to malformed syntax.'; 1482 this.name = 'MalformedRequestError'; 1483 this.solutions = [ 1484 'Ensure content and MessageType passed to sendMessage method are valid.', 1485 ]; 1486 Object.setPrototypeOf(this, MalformedRequestErrors.MalformedRequestError.prototype); 1487 const message = typeof messageOrError === 'string' 1488 ? messageOrError 1489 : this.explanation; 1490 const originalError = typeof messageOrError === 'object' 1491 ? messageOrError 1492 : error; 1493 this.message = `${this.name} (${this.code}): ${message}`; 1494 this.originalError = originalError; 1495 } 1496 } 1497 MalformedRequestErrors.MalformedRequestError = MalformedRequestError; 1498 /** 1499 * Error received from the Twilio backend. 1500 */ 1501 class MissingParameterArrayError extends TwilioError { 1502 /** 1503 * @internal 1504 */ 1505 constructor(messageOrError, error) { 1506 super(messageOrError, error); 1507 this.causes = []; 1508 this.code = 31101; 1509 this.description = 'Missing parameter array in request'; 1510 this.explanation = ''; 1511 this.name = 'MissingParameterArrayError'; 1512 this.solutions = []; 1513 Object.setPrototypeOf(this, MalformedRequestErrors.MissingParameterArrayError.prototype); 1514 const message = typeof messageOrError === 'string' 1515 ? messageOrError 1516 : this.explanation; 1517 const originalError = typeof messageOrError === 'object' 1518 ? messageOrError 1519 : error; 1520 this.message = `${this.name} (${this.code}): ${message}`; 1521 this.originalError = originalError; 1522 } 1523 } 1524 MalformedRequestErrors.MissingParameterArrayError = MissingParameterArrayError; 1525 /** 1526 * Error received from the Twilio backend. 1527 */ 1528 class AuthorizationTokenMissingError extends TwilioError { 1529 /** 1530 * @internal 1531 */ 1532 constructor(messageOrError, error) { 1533 super(messageOrError, error); 1534 this.causes = []; 1535 this.code = 31102; 1536 this.description = 'Authorization token missing in request.'; 1537 this.explanation = ''; 1538 this.name = 'AuthorizationTokenMissingError'; 1539 this.solutions = []; 1540 Object.setPrototypeOf(this, MalformedRequestErrors.AuthorizationTokenMissingError.prototype); 1541 const message = typeof messageOrError === 'string' 1542 ? messageOrError 1543 : this.explanation; 1544 const originalError = typeof messageOrError === 'object' 1545 ? messageOrError 1546 : error; 1547 this.message = `${this.name} (${this.code}): ${message}`; 1548 this.originalError = originalError; 1549 } 1550 } 1551 MalformedRequestErrors.AuthorizationTokenMissingError = AuthorizationTokenMissingError; 1552 /** 1553 * Error received from the Twilio backend. 1554 */ 1555 class MaxParameterLengthExceededError extends TwilioError { 1556 /** 1557 * @internal 1558 */ 1559 constructor(messageOrError, error) { 1560 super(messageOrError, error); 1561 this.causes = []; 1562 this.code = 31103; 1563 this.description = 'Maximum parameter length has been exceeded.'; 1564 this.explanation = 'Length of parameters cannot exceed MAX_PARAM_LENGTH.'; 1565 this.name = 'MaxParameterLengthExceededError'; 1566 this.solutions = []; 1567 Object.setPrototypeOf(this, MalformedRequestErrors.MaxParameterLengthExceededError.prototype); 1568 const message = typeof messageOrError === 'string' 1569 ? messageOrError 1570 : this.explanation; 1571 const originalError = typeof messageOrError === 'object' 1572 ? messageOrError 1573 : error; 1574 this.message = `${this.name} (${this.code}): ${message}`; 1575 this.originalError = originalError; 1576 } 1577 } 1578 MalformedRequestErrors.MaxParameterLengthExceededError = MaxParameterLengthExceededError; 1579 /** 1580 * Error received from the Twilio backend. 1581 */ 1582 class InvalidBridgeTokenError extends TwilioError { 1583 /** 1584 * @internal 1585 */ 1586 constructor(messageOrError, error) { 1587 super(messageOrError, error); 1588 this.causes = []; 1589 this.code = 31104; 1590 this.description = 'Invalid bridge token'; 1591 this.explanation = ''; 1592 this.name = 'InvalidBridgeTokenError'; 1593 this.solutions = []; 1594 Object.setPrototypeOf(this, MalformedRequestErrors.InvalidBridgeTokenError.prototype); 1595 const message = typeof messageOrError === 'string' 1596 ? messageOrError 1597 : this.explanation; 1598 const originalError = typeof messageOrError === 'object' 1599 ? messageOrError 1600 : error; 1601 this.message = `${this.name} (${this.code}): ${message}`; 1602 this.originalError = originalError; 1603 } 1604 } 1605 MalformedRequestErrors.InvalidBridgeTokenError = InvalidBridgeTokenError; 1606 /** 1607 * Error received from the Twilio backend. 1608 */ 1609 class InvalidClientNameError extends TwilioError { 1610 /** 1611 * @internal 1612 */ 1613 constructor(messageOrError, error) { 1614 super(messageOrError, error); 1615 this.causes = [
1616 'Client name contains invalid characters.', 1617 ]; 1618 this.code = 31105; 1619 this.description = 'Invalid client name'; 1620 this.explanation = 'Client name should not contain control, space, delims, or unwise characters.'; 1621 this.name = 'InvalidClientNameError'; 1622 this.solutions = [ 1623 'Make sure that client name does not contain any of the invalid characters.', 1624 ]; 1625 Object.setPrototypeOf(this, MalformedRequestErrors.InvalidClientNameError.prototype); 1626 const message = typeof messageOrError === 'string' 1627 ? messageOrError 1628 : this.explanation; 1629 const originalError = typeof messageOrError === 'object' 1630 ? messageOrError 1631 : error; 1632 this.message = `${this.name} (${this.code}): ${message}`; 1633 this.originalError = originalError; 1634 } 1635 } 1636 MalformedRequestErrors.InvalidClientNameError = InvalidClientNameError; 1637 /** 1638 * Error received from the Twilio backend. 1639 */ 1640 class ReconnectParameterInvalidError extends TwilioError { 1641 /** 1642 * @internal 1643 */ 1644 constructor(messageOrError, error) { 1645 super(messageOrError, error); 1646 this.causes = []; 1647 this.code = 31107; 1648 this.description = 'The reconnect parameter is invalid'; 1649 this.explanation = ''; 1650 this.name = 'ReconnectParameterInvalidError'; 1651 this.solutions = []; 1652 Object.setPrototypeOf(this, MalformedRequestErrors.ReconnectParameterInvalidError.prototype); 1653 const message = typeof messageOrError === 'string' 1654 ? messageOrError 1655 : this.explanation; 1656 const originalError = typeof messageOrError === 'object' 1657 ? messageOrError 1658 : error; 1659 this.message = `${this.name} (${this.code}): ${message}`; 1660 this.originalError = originalError; 1661 } 1662 } 1663 MalformedRequestErrors.ReconnectParameterInvalidError = ReconnectParameterInvalidError; 1664 })(MalformedRequestErrors || (MalformedRequestErrors = {})); 1665 (function (AuthorizationErrors) { 1666 /** 1667 * Error received from the Twilio backend. 1668 */ 1669 class AuthorizationError extends TwilioError { 1670 /** 1671 * @internal 1672 */ 1673 constructor(messageOrError, error) { 1674 super(messageOrError, error); 1675 this.causes = []; 1676 this.code = 31201; 1677 this.description = 'Authorization error'; 1678 this.explanation = 'The request requires user authentication. The server understood the request, but is refusing to fulfill it.'; 1679 this.name = 'AuthorizationError'; 1680 this.solutions = []; 1681 Object.setPrototypeOf(this, AuthorizationErrors.AuthorizationError.prototype); 1682 const message = typeof messageOrError === 'string' 1683 ? messageOrError 1684 : this.explanation; 1685 const originalError = typeof messageOrError === 'object' 1686 ? messageOrError 1687 : error; 1688 this.message = `${this.name} (${this.code}): ${message}`; 1689 this.originalError = originalError; 1690 } 1691 } 1692 AuthorizationErrors.AuthorizationError = AuthorizationError; 1693 /** 1694 * Error received from the Twilio backend. 1695 */ 1696 class NoValidAccountError extends TwilioError { 1697 /** 1698 * @internal 1699 */ 1700 constructor(messageOrError, error) { 1701 super(messageOrError, error); 1702 this.causes = []; 1703 this.code = 31203; 1704 this.description = 'No valid account'; 1705 this.explanation = ''; 1706 this.name = 'NoValidAccountError'; 1707 this.solutions = []; 1708 Object.setPrototypeOf(this, AuthorizationErrors.NoValidAccountError.prototype); 1709 const message = typeof messageOrError === 'string' 1710 ? messageOrError 1711 : this.explanation; 1712 const originalError = typeof messageOrError === 'object' 1713 ? messageOrError 1714 : error; 1715 this.message = `${this.name} (${this.code}): ${message}`; 1716 this.originalError = originalError; 1717 } 1718 } 1719 AuthorizationErrors.NoValidAccountError = NoValidAccountError; 1720 /** 1721 * Error received from the Twilio backend. 1722 */ 1723 class InvalidJWTTokenError extends TwilioError { 1724 /** 1725 * @internal 1726 */ 1727 constructor(messageOrError, error) { 1728 super(messageOrError, error); 1729 this.causes = []; 1730 this.code = 31204; 1731 this.description = 'Invalid JWT token'; 1732 this.explanation = ''; 1733 this.name = 'InvalidJWTTokenError'; 1734 this.solutions = []; 1735 Object.setPrototypeOf(this, AuthorizationErrors.InvalidJWTTokenError.prototype); 1736 const message = typeof messageOrError === 'string' 1737 ? messageOrError 1738 : this.explanation; 1739 const originalError = typeof messageOrError === 'object' 1740 ? messageOrError 1741 : error; 1742 this.message = `${this.name} (${this.code}): ${message}`; 1743 this.originalError = originalError; 1744 } 1745 } 1746 AuthorizationErrors.InvalidJWTTokenError = InvalidJWTTokenError; 1747 /** 1748 * Error received from the Twilio backend. 1749 */ 1750 class JWTTokenExpiredError extends TwilioError { 1751 /** 1752 * @internal 1753 */ 1754 constructor(messageOrError, error) { 1755 super(messageOrError, error); 1756 this.causes = []; 1757 this.code = 31205; 1758 this.description = 'JWT token expired'; 1759 this.explanation = ''; 1760 this.name = 'JWTTokenExpiredError'; 1761 this.solutions = []; 1762 Object.setPrototypeOf(this, AuthorizationErrors.JWTTokenExpiredError.prototype); 1763 const message = typeof messageOrError === 'string' 1764 ? messageOrError 1765 : this.explanation; 1766 const originalError = typeof messageOrError === 'object' 1767 ? messageOrError 1768 : error; 1769 this.message = `${this.name} (${this.code}): ${message}`; 1770 this.originalError = originalError; 1771 } 1772 } 1773 AuthorizationErrors.JWTTokenExpiredError = JWTTokenExpiredError; 1774 /** 1775 * Error received from the Twilio backend. 1776 */ 1777 class RateExceededError extends TwilioError { 1778 /** 1779 * @internal 1780 */ 1781 constructor(messageOrError, error) { 1782 super(messageOrError, error); 1783 this.causes = [ 1784 'Rate limit exceeded.', 1785 ]; 1786 this.code = 31206; 1787 this.description = 'Rate exceeded authorized limit.'; 1788 this.explanation = 'The request performed exceeds the authorized limit.'; 1789 this.name = 'RateExceededError'; 1790 this.solutions = [ 1791 'Ensure message send rate does not exceed authorized limits.', 1792 ]; 1793 Object.setPrototypeOf(this, AuthorizationErrors.RateExceededError.prototype); 1794 const message = typeof messageOrError === 'string' 1795 ? messageOrError 1796 : this.explanation; 1797 const originalError = typeof messageOrError === 'object' 1798 ? messageOrError 1799 : error; 1800 this.message = `${this.name} (${this.code}): ${message}`; 1801 this.originalError = originalError; 1802 } 1803 } 1804 AuthorizationErrors.RateExceededError = RateExceededError; 1805 /** 1806 * Error received from the Twilio backend. 1807 */ 1808 class JWTTokenExpirationTooLongError extends TwilioError { 1809 /** 1810 * @internal 1811 */ 1812 constructor(messageOrError, error) { 1813 super(messageOrError, error); 1814 this.causes = []; 1815 this.code = 31207; 1816 this.description = 'JWT token expiration too long'; 1817 this.explanation = ''; 1818 this.name = 'JWTTokenExpirationTooLongError'; 1819 this.solutions = []; 1820 Object.setPrototypeOf(this, AuthorizationErrors.JWTTokenExpirationTooLongError.prototype); 1821 const message = typeof messageOrError === 'string' 1822 ? messageOrError 1823 : this.explanation; 1824 const originalError = typeof messageOrError === 'object' 1825 ? messageOrError 1826 : error; 1827 this.message = `${this.name} (${this.code}): ${message}`; 1828 this.originalError = originalError; 1829 } 1830 } 1831 AuthorizationErrors.JWTTokenExpirationTooLongError = JWTTokenExpirationTooLongError; 1832 /** 1833 * Error received from the Twilio backend. 1834 */ 1835 class ReconnectAttemptError extends TwilioError { 1836 /** 1837 * @internal 1838 */ 1839 constructor(messageOrError, error) { 1840 super(messageOrError, error); 1841 this.causes = []; 1842 this.code = 31209; 1843 this.description = 'Reconnect attempt is not authorized.'; 1844 this.explanation = ''; 1845 this.name = 'ReconnectAttemptError'; 1846 this.solutions = []; 1847 Object.setPrototypeOf(this, AuthorizationErrors.ReconnectAttemptError.prototype); 1848 const message = typeof messageOrError === 'string' 1849 ? messageOrError 1850 : this.explanation; 1851 const originalError = typeof messageOrError === 'object' 1852 ? messageOrError 1853 : error; 1854 this.message = `${this.name} (${this.code}): ${message}`; 1855 this.originalError = originalError; 1856 } 1857 } 1858 AuthorizationErrors.ReconnectAttemptError = ReconnectAttemptError; 1859 /** 1860 * Error received from the Twilio backend. 1861 */ 1862 class CallMessageEventTypeInvalidError extends TwilioError { 1863 /** 1864 * @internal 1865 */ 1866 constructor(messageOrError, error) { 1867 super(messageOrError, error); 1868 this.causes = [
1869 'The Call Message Event Type is invalid and is not understood by Twilio Voice.', 1870 ]; 1871 this.code = 31210; 1872 this.description = 'Call Message Event Type is invalid.'; 1873 this.explanation = 'The Call Message Event Type is invalid and is not understood by Twilio Voice.'; 1874 this.name = 'CallMessageEventTypeInvalidError'; 1875 this.solutions = [ 1876 'Ensure the Call Message Event Type is Valid and understood by Twilio Voice and try again.', 1877 ]; 1878 Object.setPrototypeOf(this, AuthorizationErrors.CallMessageEventTypeInvalidError.prototype); 1879 const message = typeof messageOrError === 'string' 1880 ? messageOrError 1881 : this.explanation; 1882 const originalError = typeof messageOrError === 'object' 1883 ? messageOrError 1884 : error; 1885 this.message = `${this.name} (${this.code}): ${message}`; 1886 this.originalError = originalError; 1887 } 1888 } 1889 AuthorizationErrors.CallMessageEventTypeInvalidError = CallMessageEventTypeInvalidError; 1890 /** 1891 * Error received from the Twilio backend. 1892 */ 1893 class PayloadSizeExceededError extends TwilioError { 1894 /** 1895 * @internal 1896 */ 1897 constructor(messageOrError, error) { 1898 super(messageOrError, error); 1899 this.causes = [ 1900 'The payload size of Call Message Event exceeds the authorized limit.', 1901 ]; 1902 this.code = 31212; 1903 this.description = 'Call Message Event Payload size exceeded authorized limit.'; 1904 this.explanation = 'The request performed to send a Call Message Event exceeds the payload size authorized limit'; 1905 this.name = 'PayloadSizeExceededError'; 1906 this.solutions = [ 1907 'Reduce payload size of Call Message Event to be within the authorized limit and try again.', 1908 ]; 1909 Object.setPrototypeOf(this, AuthorizationErrors.PayloadSizeExceededError.prototype); 1910 const message = typeof messageOrError === 'string' 1911 ? messageOrError 1912 : this.explanation; 1913 const originalError = typeof messageOrError === 'object' 1914 ? messageOrError 1915 : error; 1916 this.message = `${this.name} (${this.code}): ${message}`; 1917 this.originalError = originalError; 1918 } 1919 } 1920 AuthorizationErrors.PayloadSizeExceededError = PayloadSizeExceededError; 1921 })(AuthorizationErrors || (AuthorizationErrors = {})); 1922 var UserMediaErrors; 1923 (function (UserMediaErrors) { 1924 /** 1925 * Error received from the Twilio backend. 1926 */ 1927 class PermissionDeniedError extends TwilioError { 1928 /** 1929 * @internal 1930 */ 1931 constructor(messageOrError, error) { 1932 super(messageOrError, error); 1933 this.causes = [ 1934 'The user denied the getUserMedia request.', 1935 'The browser denied the getUserMedia request.', 1936 ]; 1937 this.code = 31401; 1938 this.description = 'UserMedia Permission Denied Error'; 1939 this.explanation = 'The browser or end-user denied permissions to user media. Therefore we were unable to acquire input audio.'; 1940 this.name = 'PermissionDeniedError'; 1941 this.solutions = [ 1942 'The user should accept the request next time prompted. If the browser saved the deny, the user should change that permission in their browser.', 1943 'The user should to verify that the browser has permission to access the microphone at this address.', 1944 ]; 1945 Object.setPrototypeOf(this, UserMediaErrors.PermissionDeniedError.prototype); 1946 const message = typeof messageOrError === 'string' 1947 ? messageOrError 1948 : this.explanation; 1949 const originalError = typeof messageOrError === 'object' 1950 ? messageOrError 1951 : error; 1952 this.message = `${this.name} (${this.code}): ${message}`; 1953 this.originalError = originalError; 1954 } 1955 } 1956 UserMediaErrors.PermissionDeniedError = PermissionDeniedError; 1957 /** 1958 * Error received from the Twilio backend. 1959 */ 1960 class AcquisitionFailedError extends TwilioError { 1961 /** 1962 * @internal 1963 */ 1964 constructor(messageOrError, error) { 1965 super(messageOrError, error); 1966 this.causes = [
1967 'NotFoundError - The deviceID specified was not found.', 1968 'The getUserMedia constraints were overconstrained and no devices matched.', 1969 ]; 1970 this.code = 31402; 1971 this.description = 'UserMedia Acquisition Failed Error'; 1972 this.explanation = 'The browser and end-user allowed permissions, however getting the media failed. Usually this is due to bad constraints, but can sometimes fail due to browser, OS or hardware issues.'; 1973 this.name = 'AcquisitionFailedError'; 1974 this.solutions = [ 1975 'Ensure the deviceID being specified exists.', 1976 'Try acquiring media with fewer constraints.', 1977 ]; 1978 Object.setPrototypeOf(this, UserMediaErrors.AcquisitionFailedError.prototype); 1979 const message = typeof messageOrError === 'string' 1980 ? messageOrError 1981 : this.explanation; 1982 const originalError = typeof messageOrError === 'object' 1983 ? messageOrError 1984 : error; 1985 this.message = `${this.name} (${this.code}): ${message}`; 1986 this.originalError = originalError; 1987 } 1988 } 1989 UserMediaErrors.AcquisitionFailedError = AcquisitionFailedError; 1990 })(UserMediaErrors || (UserMediaErrors = {})); 1991 var SignalingErrors; 1992 (function (SignalingErrors) { 1993 /** 1994 * Error received from the Twilio backend. 1995 */ 1996 class ConnectionError extends TwilioError { 1997 /** 1998 * @internal 1999 */ 2000 constructor(messageOrError, error) { 2001 super(messageOrError, error); 2002 this.causes = []; 2003 this.code = 53000; 2004 this.description = 'Signaling connection error'; 2005 this.explanation = 'Raised whenever a signaling connection error occurs that is not covered by a more specific error code.'; 2006 this.name = 'ConnectionError'; 2007 this.solutions = []; 2008 Object.setPrototypeOf(this, SignalingErrors.ConnectionError.prototype); 2009 const message = typeof messageOrError === 'string' 2010 ? messageOrError 2011 : this.explanation; 2012 const originalError = typeof messageOrError === 'object' 2013 ? messageOrError 2014 : error; 2015 this.message = `${this.name} (${this.code}): ${message}`; 2016 this.originalError = originalError; 2017 } 2018 } 2019 SignalingErrors.ConnectionError = ConnectionError; 2020 /** 2021 * Error received from the Twilio backend. 2022 */ 2023 class ConnectionDisconnected extends TwilioError { 2024 /** 2025 * @internal 2026 */ 2027 constructor(messageOrError, error) { 2028 super(messageOrError, error); 2029 this.causes = [ 2030 'The device running your application lost its Internet connection.', 2031 ]; 2032 this.code = 53001; 2033 this.description = 'Signaling connection disconnected'; 2034 this.explanation = 'Raised whenever the signaling connection is unexpectedly disconnected.'; 2035 this.name = 'ConnectionDisconnected'; 2036 this.solutions = [ 2037 'Ensure the device running your application has access to a stable Internet connection.', 2038 ]; 2039 Object.setPrototypeOf(this, SignalingErrors.ConnectionDisconnected.prototype); 2040 const message = typeof messageOrError === 'string' 2041 ? messageOrError 2042 : this.explanation; 2043 const originalError = typeof messageOrError === 'object' 2044 ? messageOrError 2045 : error; 2046 this.message = `${this.name} (${this.code}): ${message}`; 2047 this.originalError = originalError; 2048 } 2049 } 2050 SignalingErrors.ConnectionDisconnected = ConnectionDisconnected; 2051 })(SignalingErrors || (SignalingErrors = {})); 2052 var MediaErrors; 2053 (function (MediaErrors) { 2054 /** 2055 * Error received from the Twilio backend. 2056 */ 2057 class ClientLocalDescFailed extends TwilioError { 2058 /** 2059 * @internal 2060 */ 2061 constructor(messageOrError, error) { 2062 super(messageOrError, error); 2063 this.causes = [ 2064 'The Client may not be using a supported WebRTC implementation.', 2065 'The Client may not have the necessary resources to create or apply a new media description.', 2066 ]; 2067 this.code = 53400; 2068 this.description = 'Client is unable to create or apply a local media description'; 2069 this.explanation = 'Raised whenever a Client is unable to create or apply a local media description.'; 2070 this.name = 'ClientLocalDescFailed'; 2071 this.solutions = [ 2072 'If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation.', 2073 ]; 2074 Object.setPrototypeOf(this, MediaErrors.ClientLocalDescFailed.prototype); 2075 const message = typeof messageOrError === 'string' 2076 ? messageOrError 2077 : this.explanation; 2078 const originalError = typeof messageOrError === 'object' 2079 ? messageOrError 2080 : error; 2081 this.message = `${this.name} (${this.code}): ${message}`; 2082 this.originalError = originalError; 2083 } 2084 } 2085 MediaErrors.ClientLocalDescFailed = ClientLocalDescFailed; 2086 /** 2087 * Error received from the Twilio backend. 2088 */ 2089 class ClientRemoteDescFailed extends TwilioError { 2090 /** 2091 * @internal 2092 */ 2093 constructor(messageOrError, error) { 2094 super(messageOrError, error); 2095 this.causes = [ 2096 'The Client may not be using a supported WebRTC implementation.', 2097 'The Client may be connecting peer-to-peer with another Participant that is not using a supported WebRTC implementation.', 2098 'The Client may not have the necessary resources to apply a new media description.', 2099 ]; 2100 this.code = 53402; 2101 this.description = 'Client is unable to apply a remote media description'; 2102 this.explanation = 'Raised whenever the Client receives a remote media description but is unable to apply it.'; 2103 this.name = 'ClientRemoteDescFailed'; 2104 this.solutions = [ 2105 'If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation.', 2106 ]; 2107 Object.setPrototypeOf(this, MediaErrors.ClientRemoteDescFailed.prototype); 2108 const message = typeof messageOrError === 'string' 2109 ? messageOrError 2110 : this.explanation; 2111 const originalError = typeof messageOrError === 'object' 2112 ? messageOrError 2113 : error; 2114 this.message = `${this.name} (${this.code}): ${message}`; 2115 this.originalError = originalError; 2116 } 2117 } 2118 MediaErrors.ClientRemoteDescFailed = ClientRemoteDescFailed; 2119 /** 2120 * Error received from the Twilio backend. 2121 */ 2122 class ConnectionError extends TwilioError { 2123 /** 2124 * @internal 2125 */ 2126 constructor(messageOrError, error) { 2127 super(messageOrError, error); 2128 this.causes = [ 2129 'The Client was unable to establish a media connection.', 2130 'A media connection which was active failed liveliness checks.', 2131 ]; 2132 this.code = 53405; 2133 this.description = 'Media connection failed'; 2134 this.explanation = 'Raised by the Client or Server whenever a media connection fails.'; 2135 this.name = 'ConnectionError'; 2136 this.solutions = [ 2137 'If the problem persists, try connecting to another region.', 2138 'Check your Client\'s network connectivity.', 2139 'If you\'ve provided custom ICE Servers then ensure that the URLs and credentials are valid.', 2140 ]; 2141 Object.setPrototypeOf(this, MediaErrors.ConnectionError.prototype); 2142 const message = typeof messageOrError === 'string' 2143 ? messageOrError 2144 : this.explanation; 2145 const originalError = typeof messageOrError === 'object' 2146 ? messageOrError 2147 : error; 2148 this.message = `${this.name} (${this.code}): ${message}`; 2149 this.originalError = originalError; 2150 } 2151 } 2152 MediaErrors.ConnectionError = ConnectionError; 2153 })(MediaErrors || (MediaErrors = {})); 2154 /** 2155 * @private 2156 */ 2157 const errorsByCode = new Map([ 2158 [20101, AuthorizationErrors.AccessTokenInvalid], 2159 [20104, AuthorizationErrors.AccessTokenExpired], 2160 [20151, AuthorizationErrors.AuthenticationFailed], 2161 [31202, SignatureValidationErrors.AccessTokenSignatureValidationFailed], 2162 [31400, ClientErrors.BadRequest], 2163 [31404, ClientErrors.NotFound], 2164 [31480, ClientErrors.TemporarilyUnavailable], 2165 [31486, ClientErrors.BusyHere], 2166 [31603, SIPServerErrors.Decline], 2167 [31000, GeneralErrors.UnknownError], 2168 [31001, GeneralErrors.ApplicationNotFoundError], 2169 [31002, GeneralErrors.ConnectionDeclinedError], 2170 [31003, GeneralErrors.ConnectionTimeoutError], 2171 [31005, GeneralErrors.ConnectionError], 2172 [31008, GeneralErrors.CallCancelledError], 2173 [31009, GeneralErrors.TransportError], 2174 [31100, MalformedRequestErrors.MalformedRequestError], 2175 [31101, MalformedRequestErrors.MissingParameterArrayError], 2176 [31102, MalformedRequestErrors.AuthorizationTokenMissingError], 2177 [31103, MalformedRequestErrors.MaxParameterLengthExceededError], 2178 [31104, MalformedRequestErrors.InvalidBridgeTokenError], 2179 [31105, MalformedRequestErrors.InvalidClientNameError], 2180 [31107, MalformedRequestErrors.ReconnectParameterInvalidError], 2181 [31201, AuthorizationErrors.AuthorizationError], 2182 [31203, AuthorizationErrors.NoValidAccountError], 2183 [31204, AuthorizationErrors.InvalidJWTTokenError], 2184 [31205, AuthorizationErrors.JWTTokenExpiredError], 2185 [31206, AuthorizationErrors.RateExceededError], 2186 [31207, AuthorizationErrors.JWTTokenExpirationTooLongError], 2187 [31209, AuthorizationErrors.ReconnectAttemptError], 2188 [31210, AuthorizationErrors.CallMessageEventTypeInvalidError], 2189 [31212, AuthorizationErrors.PayloadSizeExceededError], 2190 [31401, UserMediaErrors.PermissionDeniedError], 2191 [31402, UserMediaErrors.AcquisitionFailedError], 2192 [53000, SignalingErrors.ConnectionError], 2193 [53001, SignalingErrors.ConnectionDisconnected], 2194 [53400, MediaErrors.ClientLocalDescFailed], 2195 [53402, MediaErrors.ClientRemoteDescFailed], 2196 [53405, MediaErrors.ConnectionError], 2197 ]); 2198 Object.freeze(errorsByCode); 2199
2200 /* tslint:disable max-classes-per-file */ 2201 // TODO: Consider refactoring this export (VBLOCKS-4589) 2202 /** 2203 * NOTE(mhuynh): Replacing generic error codes with new (more specific) codes, 2204 * is a breaking change. If an error code is found in this set, we only perform 2205 * the transformation if the feature flag is enabled. 2206 * 2207 * With every major version bump, such that we are allowed to introduce breaking 2208 * changes as per semver specification, this array should be cleared. 2209 * 2210 * TODO: [VBLOCKS-2295] Remove this in 3.x 2211 */ 2212 const PRECISE_SIGNALING_ERROR_CODES = new Set([ 2213 /** 2214 * 310XX Errors 2215 */ 2216 31001, 2217 31002, 2218 31003, 2219 /** 2220 * 311XX Errors 2221 */ 2222 31101, 2223 31102, 2224 31103, 2225 31104, 2226 31105, 2227 31107, 2228 /** 2229 * 312XX Errors 2230 */ 2231 31201, 2232 31202, 2233 31203, 2234 31204, 2235 31205, 2236 31207, 2237 /** 2238 * 314XX Errors 2239 */ 2240 31404, 2241 31480, 2242 31486, 2243 /** 2244 * 316XX Errors 2245 */ 2246 31603, 2247 ]); 2248 /** 2249 * Get an error constructor using the [[PRECISE_SIGNALING_ERROR_CODES]] set. 2250 * @internal 2251 * @param enableImprovedSignalingErrorPrecision - A boolean representing the 2252 * optional flag whether or not to use more precise error codes. 2253 * @param errorCode - The error code. 2254 * @returns This function returns `undefined` if the passed error code does not 2255 * correlate to an error or should not be constructed with a more precise error 2256 * constructor. A sub-class of {@link TwilioError} if the code does correlate to 2257 * an error. 2258 */ 2259 function getPreciseSignalingErrorByCode(enableImprovedSignalingErrorPrecision, errorCode) { 2260 if (typeof errorCode !== 'number') { 2261 return; 2262 } 2263 if (!hasErrorByCode(errorCode)) { 2264 return; 2265 } 2266 const shouldTransform = enableImprovedSignalingErrorPrecision 2267 ? true 2268 : !PRECISE_SIGNALING_ERROR_CODES.has(errorCode); 2269 if (!shouldTransform) { 2270 return; 2271 } 2272 return getErrorByCode(errorCode); 2273 } 2274 /** 2275 * The error that is thrown when an invalid argument is passed to a library API. 2276 */ 2277 class InvalidArgumentError extends Error { 2278 /** 2279 * @internal 2280 */ 2281 constructor(message) { 2282 super(message); 2283 this.name = 'InvalidArgumentError'; 2284 } 2285 } 2286 /** 2287 * The error that is thrown when the library has entered an invalid state. 2288 */ 2289 class InvalidStateError extends Error { 2290 /** 2291 * @internal 2292 */ 2293 constructor(message) { 2294 super(message); 2295 this.name = 'InvalidStateError'; 2296 } 2297 } 2298 /** 2299 * The error that is thrown when an attempt is made to use an API that is not 2300 * supported on the current platform. 2301 */ 2302 class NotSupportedError extends Error { 2303 /** 2304 * @internal 2305 */ 2306 constructor(message) { 2307 super(message); 2308 this.name = 'NotSupportedError'; 2309 } 2310 } 2311 /** 2312 * This should only be used to look up error codes returned by a server 2313 * using the same repo of error codes. 2314 * @internal 2315 */ 2316 function getErrorByCode(code) { 2317 const error = errorsByCode.get(code); 2318 if (!error) { 2319 throw new InvalidArgumentError(`Error code ${code} not found`); 2320 } 2321 return error; 2322 } 2323 /** 2324 * This should only be used to look up error codes returned by a server 2325 * using the same repo of error codes. 2326 * @internal 2327 */ 2328 function hasErrorByCode(code) { 2329 return errorsByCode.has(code); 2330 } 2331 2332 /** 2333 * This file is generated on build. To make changes, see /templates/constants.ts 2334 */ 2335 const PACKAGE_NAME = '@twilio/voice-sdk'; 2336 const RELEASE_VERSION = '2.16.0'; 2337 const SOUNDS_BASE_URL = 'https://sdk.twilio.com/js/client/sounds/releases/1.0.0'; 2338 const COWBELL_AUDIO_URL = `${SOUNDS_BASE_URL}/cowbell.mp3?cache=${RELEASE_VERSION}`; 2339 const ECHO_TEST_DURATION = 20000; 2340 2341 /** 2342 * {@link Log} provides logging features throughout the sdk using loglevel module 2343 * See https://github.com/pimterry/loglevel for documentation 2344 * @private 2345 */ 2346 class Log { 2347 /** 2348 * Return the `loglevel` instance maintained internally. 2349 * @param [options] - Optional settings 2350 * @returns The `loglevel` instance. 2351 */ 2352 static getLogLevelInstance(options) { 2353 if (!Log.loglevelInstance) { 2354 try { 2355 Log.loglevelInstance = (options && options.LogLevelModule ? options.LogLevelModule : loglevel$1).getLogger(PACKAGE_NAME); 2356 } 2357 catch (_a) { 2358 // tslint:disable-next-line 2359 console.warn('Cannot create custom logger'); 2360 Log.loglevelInstance = console; 2361 } 2362 } 2363 return Log.loglevelInstance; 2364 } 2365 /** 2366 * @constructor 2367 * @param [tag] - tag name for the logs 2368 * @param [options] - Optional settings 2369 */ 2370 constructor(tag, options) { 2371 this._log = Log.getLogLevelInstance(options); 2372 this._prefix = `[TwilioVoice][${tag}]`; 2373 } 2374 /** 2375 * Log a debug message 2376 * @param args - Any number of arguments to be passed to loglevel.debug 2377 */ 2378 debug(...args) { 2379 this._log.debug(this._prefix, ...args); 2380 } 2381 /** 2382 * Log an error message 2383 * @param args - Any number of arguments to be passed to loglevel.error 2384 */ 2385 error(...args) { 2386 this._log.error(this._prefix, ...args); 2387 } 2388 /** 2389 * Log an info message 2390 * @param args - Any number of arguments to be passed to loglevel.info 2391 */ 2392 info(...args) { 2393 this._log.info(this._prefix, ...args); 2394 } 2395 /** 2396 * Set a default log level to disable all logging below the given level 2397 */ 2398 setDefaultLevel(level) { 2399 if (this._log.setDefaultLevel) { 2400 this._log.setDefaultLevel(level); 2401 } 2402 else { 2403 // tslint:disable-next-line 2404 console.warn('Logger cannot setDefaultLevel'); 2405 } 2406 } 2407 /** 2408 * Log a warning message 2409 * @param args - Any number of arguments to be passed to loglevel.warn 2410 */ 2411 warn(...args) { 2412 this._log.warn(this._prefix, ...args); 2413 } 2414 } 2415 /** 2416 * Log levels 2417 */ 2418 Log.levels = loglevelExports.levels; 2419 Log.getLogLevelInstance(); 2420 2421 const DEFAULT_TEST_SOUND_URL = `${SOUNDS_BASE_URL}/outgoing.mp3`; 2422 /** 2423 * A smart collection containing a Set of active output devices. 2424 */ 2425 class OutputDeviceCollection { 2426 /** 2427 * @internal 2428 */ 2429 constructor(_name, _availableDevices, _beforeChange, _isSupported) { 2430 this._name = _name; 2431 this._availableDevices = _availableDevices; 2432 this._beforeChange = _beforeChange; 2433 this._isSupported = _isSupported; 2434 /** 2435 * The currently active output devices. 2436 */ 2437 this._activeDevices = new Set(); 2438 /** 2439 * An instance of Logger to use. 2440 */ 2441 this._log = new Log('OutputDeviceCollection'); 2442 } 2443 /** 2444 * Delete a device from the collection. If no devices remain, the 'default' 2445 * device will be added as the sole device. If no `default` device exists, 2446 * the first available device will be used. 2447 * @param device - The device to delete from the collection 2448 * @returns whether the device was present before it was deleted 2449 */
2450 delete(device) { 2451 this._log.debug('.delete', device); 2452 const wasDeleted = !!(this._activeDevices.delete(device)); 2453 const defaultDevice = this._availableDevices.get('default') 2454 || Array.from(this._availableDevices.values())[0]; 2455 if (!this._activeDevices.size && defaultDevice) { 2456 this._activeDevices.add(defaultDevice); 2457 } 2458 // Call _beforeChange so that the implementation can react when a device is 2459 // removed or lost. 2460 const deviceIds = Array.from(this._activeDevices.values()).map(deviceInfo => deviceInfo.deviceId); 2461 this._beforeChange(this._name, deviceIds); 2462 return !!wasDeleted; 2463 } 2464 /** 2465 * Get the current set of devices. 2466 */ 2467 get() { 2468 return this._activeDevices; 2469 } 2470 /** 2471 * Replace the current set of devices with a new set of devices. 2472 * @param deviceIdOrIds - An ID or array of IDs of devices to replace the existing devices with. 2473 * @returns Rejects if this feature is not supported, any of the supplied IDs are not found, 2474 * or no IDs are passed. 2475 */ 2476 set(deviceIdOrIds) { 2477 this._log.debug('.set', deviceIdOrIds); 2478 if (!this._isSupported) { 2479 return Promise.reject(new NotSupportedError('This browser does not support audio output selection')); 2480 } 2481 const deviceIds = Array.isArray(deviceIdOrIds) ? deviceIdOrIds : [deviceIdOrIds]; 2482 if (!deviceIds.length) { 2483 return Promise.reject(new InvalidArgumentError('Must specify at least one device to set')); 2484 } 2485 const missingIds = []; 2486 const devices = deviceIds.map((id) => { 2487 const device = this._availableDevices.get(id); 2488 if (!device) { 2489 missingIds.push(id); 2490 } 2491 return device; 2492 }); 2493 if (missingIds.length) { 2494 return Promise.reject(new InvalidArgumentError(`Devices not found: ${missingIds.join(', ')}`)); 2495 } 2496 return new Promise(resolve => { 2497 resolve(this._beforeChange(this._name, deviceIds)); 2498 }).then(() => { 2499 this._activeDevices.clear(); 2500 devices.forEach(this._activeDevices.add, this._activeDevices); 2501 }); 2502 } 2503 /** 2504 * Test the devices by playing audio through them. 2505 * @param [soundUrl] - An optional URL. If none is specified, we will 2506 * play a default test tone. 2507 * @returns Resolves with the result of the underlying HTMLAudioElements' play() calls. 2508 */ 2509 test(soundUrl = DEFAULT_TEST_SOUND_URL) { 2510 if (!this._isSupported) { 2511 return Promise.reject(new NotSupportedError('This browser does not support audio output selection')); 2512 } 2513 if (!this._activeDevices.size) { 2514 return Promise.reject(new InvalidStateError('No active output devices to test')); 2515 } 2516 return Promise.all(Array.from(this._activeDevices).map((device) => { 2517 let el; 2518 // (rrowland) We need to wait for the oncanplay event because of a regression introduced 2519 // in Chrome M72: https://bugs.chromium.org/p/chromium/issues/detail?id=930876 2520 return new Promise((resolve) => { 2521 el = new Audio(soundUrl); 2522 el.oncanplay = resolve; 2523 }).then(() => el.setSinkId(device.deviceId).then(() => el.play())); 2524 })); 2525 } 2526 } 2527 2528 // @ts-nocheck 2529 class MediaDeviceInfoShim { 2530 constructor(options) { 2531 Object.defineProperties(this, { 2532 deviceId: { get() { return options.deviceId; } }, 2533 groupId: { get() { return options.groupId; } }, 2534 kind: { get() { return options.kind; } }, 2535 label: { get() { return options.label; } }, 2536 }); 2537 } 2538 } 2539 2540 // @ts-nocheck 2541 /** 2542 * Exception class. 2543 * @class 2544 * @name Exception 2545 * @exports Exception as Twilio.Exception 2546 * @memberOf Twilio 2547 * @param {string} message The exception message 2548 */ 2549 function TwilioException(message) {
2550 if (!(this instanceof TwilioException)) { 2551 return new TwilioException(message); 2552 } 2553 this.message = message; 2554 } 2555 /** 2556 * Returns the exception message. 2557 * 2558 * @return {string} The exception message. 2559 */ 2560 TwilioException.prototype.toString = function () { 2561 return `Twilio.Exception: ${this.message}`; 2562 }; 2563 function average(values) { 2564 return values && values.length ? values.reduce((t, v) => t + v) / values.length : 0; 2565 } 2566 function difference(lefts, rights, getKey) { 2567 getKey = getKey || (a => a); 2568 const rightKeys = new Set(rights.map(getKey)); 2569 return lefts.filter(left => !rightKeys.has(getKey(left))); 2570 } 2571 function isElectron(navigator) { 2572 return !!navigator.userAgent.match('Electron'); 2573 } 2574 function isChrome(window, navigator) { 2575 const isCriOS = !!navigator.userAgent.match('CriOS'); 2576 const isHeadlessChrome = !!navigator.userAgent.match('HeadlessChrome'); 2577 const isGoogle = typeof window.chrome !== 'undefined' 2578 && navigator.vendor === 'Google Inc.' 2579 && navigator.userAgent.indexOf('OPR') === -1 2580 && navigator.userAgent.indexOf('Edge') === -1; 2581 return isCriOS || isElectron(navigator) || isGoogle || isHeadlessChrome; 2582 } 2583 function isFirefox(navigator) { 2584 navigator = navigator || (typeof window === 'undefined' 2585 ? global.navigator : window.navigator); 2586 return !!(navigator) && typeof navigator.userAgent === 'string' 2587 && /firefox|fxios/i.test(navigator.userAgent); 2588 } 2589 /** 2590 * Chromium-based Edge has a user-agent of "Edg/" where legacy Edge has a 2591 * user-agent of "Edge/". 2592 */ 2593 function isLegacyEdge(navigator) { 2594 navigator = navigator || (typeof window === 'undefined' 2595 ? global.navigator : window.navigator); 2596 return !!(navigator) && typeof navigator.userAgent === 'string' 2597 && /edge\/\d+/i.test(navigator.userAgent); 2598 } 2599 function isSafari(navigator) { 2600 return !!(navigator.vendor) && navigator.vendor.indexOf('Apple') !== -1 2601 && navigator.userAgent 2602 && navigator.userAgent.indexOf('CriOS') === -1 2603 && navigator.userAgent.indexOf('FxiOS') === -1; 2604 } 2605 function queryToJson(params) { 2606 if (!params) { 2607 return ''; 2608 } 2609 return params.split('&').reduce((output, pair) => { 2610 const parts = pair.split('='); 2611 const key = parts[0]; 2612 const value = decodeURIComponent((parts[1] || '').replace(/\+/g, '%20')); 2613 if (key) { 2614 output[key] = value; 2615 } 2616 return output; 2617 }, {}); 2618 } 2619 /** 2620 * Map a list to an array of arrays, and return the flattened result. 2621 * @param {Array<*>|Set<*>|Map<*>} list 2622 * @param {function(*): Array<*>} [mapFn] 2623 * @returns Array<*> 2624 */ 2625 function flatMap(list, mapFn) { 2626 const listArray = list instanceof Map || list instanceof Set 2627 ? Array.from(list.values()) 2628 : list; 2629 mapFn = mapFn || (item => item); 2630 return listArray.reduce((flattened, item) => { 2631 const mapped = mapFn(item); 2632 return flattened.concat(mapped); 2633 }, []); 2634 } 2635 /** 2636 * Converts an EventEmitter's events into a promise and automatically 2637 * cleans up handlers once the promise is resolved or rejected. 2638 */ 2639 function promisifyEvents(emitter, resolveEventName, rejectEventName) { 2640 return new Promise((resolve, reject) => { 2641 function resolveHandler() { 2642 emitter.removeListener(rejectEventName, rejectHandler); 2643 resolve(); 2644 } 2645 function rejectHandler() { 2646 emitter.removeListener(resolveEventName, resolveHandler); 2647 reject(); 2648 } 2649 emitter.once(resolveEventName, resolveHandler); 2650 emitter.once(rejectEventName, rejectHandler); 2651 }); 2652 } 2653 function sortByMimeTypes(codecs, preferredOrder) { 2654 const preferredCodecs = preferredOrder.map(codec => 'audio/' + codec.toLowerCase()); 2655 return codecs.sort((a, b) => { 2656 const indexA = preferredCodecs.indexOf(a.mimeType.toLowerCase()); 2657 const indexB = preferredCodecs.indexOf(b.mimeType.toLowerCase()); 2658 const orderA = indexA >= 0 ? indexA : Number.MAX_VALUE;
2659 const orderB = indexB >= 0 ? indexB : Number.MAX_VALUE; 2660 return orderA - orderB; 2661 }); 2662 } 2663 const Exception = TwilioException; 2664 2665 var util = /*#__PURE__*/Object.freeze({ 2666 __proto__: null, 2667 Exception: Exception, 2668 average: average, 2669 difference: difference, 2670 flatMap: flatMap, 2671 isChrome: isChrome, 2672 isElectron: isElectron, 2673 isFirefox: isFirefox, 2674 isLegacyEdge: isLegacyEdge, 2675 isSafari: isSafari, 2676 promisifyEvents: promisifyEvents, 2677 queryToJson: queryToJson, 2678 sortByMimeTypes: sortByMimeTypes 2679 }); 2680 2681 /** 2682 * Aliases for audio kinds, used for labelling. 2683 */ 2684 const kindAliases = { 2685 audioinput: 'Audio Input', 2686 audiooutput: 'Audio Output', 2687 }; 2688 /** 2689 * Provides input and output audio-based functionality in one convenient class. 2690 */ 2691 class AudioHelper extends eventsExports.EventEmitter { 2692 /** 2693 * The currently set audio constraints set by setAudioConstraints(). Starts as null. 2694 */ 2695 get audioConstraints() { return this._audioConstraints; } 2696 /** 2697 * The active input device. Having no inputDevice specified by `setInputDevice()` 2698 * will disable input selection related functionality. 2699 */ 2700 get inputDevice() { return this._inputDevice; } 2701 /** 2702 * The current input stream coming from the microphone device or 2703 * the processed audio stream if there is an {@link AudioProcessor}. 2704 */ 2705 get inputStream() { return this._processedStream || this._selectedInputDeviceStream; } 2706 /** 2707 * The processed stream if an {@link AudioProcessor} was previously added. 2708 */ 2709 get processedStream() { return this._processedStream; } 2710 /** 2711 * @internal 2712 * @param onActiveOutputsChanged - A callback to be called when the user changes the active output devices. 2713 * @param onActiveInputChanged - A callback to be called when the user changes the active input device. 2714 * @param [options] 2715 */ 2716 constructor(onActiveOutputsChanged, onActiveInputChanged, options) { 2717 super(); 2718 /** 2719 * A Map of all audio input devices currently available to the browser by their device ID. 2720 */ 2721 this.availableInputDevices = new Map(); 2722 /** 2723 * A Map of all audio output devices currently available to the browser by their device ID. 2724 */ 2725 this.availableOutputDevices = new Map(); 2726 /** 2727 * The currently set audio constraints set by setAudioConstraints(). 2728 */ 2729 this._audioConstraints = null; 2730 /** 2731 * The audio stream of the default device. 2732 * This is populated when _openDefaultDeviceWithConstraints is called, 2733 * See _selectedInputDeviceStream for differences. 2734 * TODO: Combine these two workflows (3.x?) 2735 */ 2736 this._defaultInputDeviceStream = null; 2737 /** 2738 * Whether each sound is enabled. 2739 */ 2740 this._enabledSounds = { 2741 [Device.SoundName.Disconnect]: true, 2742 [Device.SoundName.Incoming]: true, 2743 [Device.SoundName.Outgoing]: true, 2744 }; 2745 /** 2746 * The current input device. 2747 */ 2748 this._inputDevice = null; 2749 /** 2750 * The internal promise created when calling setInputDevice 2751 */ 2752 this._inputDevicePromise = null; 2753 /** 2754 * Whether the {@link AudioHelper} is currently polling the input stream's volume. 2755 */ 2756 this._isPollingInputVolume = false; 2757 /** 2758 * An instance of Logger to use. 2759 */ 2760 this._log = new Log('AudioHelper'); 2761 /** 2762 * Internal reference to the processed stream 2763 */ 2764 this._processedStream = null; 2765 /** 2766 * The selected input stream coming from the microphone device. 2767 * This is populated when the setInputDevice is called, meaning, 2768 * the end user manually selected it, which is different than 2769 * the defaultInputDeviceStream. 2770 * TODO: Combine these two workflows (3.x?) 2771 */ 2772 this._selectedInputDeviceStream = null; 2773 /** 2774 * A record of unknown devices (Devices without labels) 2775 */
2776 this._unknownDeviceIndexes = { 2777 audioinput: {}, 2778 audiooutput: {}, 2779 }; 2780 /** 2781 * Update the available input and output devices 2782 * @internal 2783 */ 2784 this._updateAvailableDevices = () => { 2785 if (!this._mediaDevices || !this._enumerateDevices) { 2786 return Promise.reject('Enumeration not supported'); 2787 } 2788 return this._enumerateDevices().then((devices) => { 2789 this._updateDevices(devices.filter((d) => d.kind === 'audiooutput'), this.availableOutputDevices, this._removeLostOutput); 2790 this._updateDevices(devices.filter((d) => d.kind === 'audioinput'), this.availableInputDevices, this._removeLostInput); 2791 const defaultDevice = this.availableOutputDevices.get('default') 2792 || Array.from(this.availableOutputDevices.values())[0]; 2793 [this.speakerDevices, this.ringtoneDevices].forEach(outputDevices => { 2794 if (!outputDevices.get().size && this.availableOutputDevices.size && this.isOutputSelectionSupported) { 2795 outputDevices.set(defaultDevice.deviceId) 2796 .catch((reason) => { 2797 this._log.warn(`Unable to set audio output devices. ${reason}`); 2798 }); 2799 } 2800 }); 2801 }); 2802 }; 2803 /** 2804 * Remove an input device from inputs 2805 * @param lostDevice 2806 * @returns Whether the device was active 2807 */ 2808 this._removeLostInput = (lostDevice) => { 2809 if (!this.inputDevice || this.inputDevice.deviceId !== lostDevice.deviceId) { 2810 return false; 2811 } 2812 this._destroyProcessedStream(); 2813 this._replaceStream(null); 2814 this._inputDevice = null; 2815 this._maybeStopPollingVolume(); 2816 const defaultDevice = this.availableInputDevices.get('default') 2817 || Array.from(this.availableInputDevices.values())[0]; 2818 if (defaultDevice) { 2819 this.setInputDevice(defaultDevice.deviceId); 2820 } 2821 return true; 2822 }; 2823 /** 2824 * Remove an input device from outputs 2825 * @param lostDevice 2826 * @returns Whether the device was active 2827 */ 2828 this._removeLostOutput = (lostDevice) => { 2829 const wasSpeakerLost = this.speakerDevices.delete(lostDevice); 2830 const wasRingtoneLost = this.ringtoneDevices.delete(lostDevice); 2831 return wasSpeakerLost || wasRingtoneLost; 2832 }; 2833 options = Object.assign({ 2834 AudioContext: typeof AudioContext !== 'undefined' && AudioContext, 2835 setSinkId: typeof HTMLAudioElement !== 'undefined' && HTMLAudioElement.prototype.setSinkId, 2836 }, options); 2837 this._beforeSetInputDevice = options.beforeSetInputDevice || (() => Promise.resolve()); 2838 this._updateUserOptions(options); 2839 this._audioProcessorEventObserver = options.audioProcessorEventObserver; 2840 this._mediaDevices = options.mediaDevices || navigator.mediaDevices; 2841 this._onActiveInputChanged = onActiveInputChanged; 2842 this._enumerateDevices = typeof options.enumerateDevices === 'function' 2843 ? options.enumerateDevices 2844 : this._mediaDevices && this._mediaDevices.enumerateDevices.bind(this._mediaDevices); 2845 const isAudioContextSupported = !!(options.AudioContext || options.audioContext); 2846 const isEnumerationSupported = !!this._enumerateDevices; 2847 if (options.enabledSounds) { 2848 this._enabledSounds = options.enabledSounds; 2849 } 2850 const isSetSinkSupported = typeof options.setSinkId === 'function'; 2851 this.isOutputSelectionSupported = isEnumerationSupported && isSetSinkSupported; 2852 this.isVolumeSupported = isAudioContextSupported; 2853 if (this.isVolumeSupported) { 2854 this._audioContext = options.audioContext || options.AudioContext && new options.AudioContext(); 2855 if (this._audioContext) { 2856 this._inputVolumeAnalyser = this._audioContext.createAnalyser(); 2857 this._inputVolumeAnalyser.fftSize = 32; 2858 this._inputVolumeAnalyser.smoothingTimeConstant = 0.3; 2859 } 2860 } 2861 this.ringtoneDevices = new OutputDeviceCollection('ringtone', this.availableOutputDevices, onActiveOutputsChanged, this.isOutputSelectionSupported); 2862 this.speakerDevices = new OutputDeviceCollection('speaker', this.availableOutputDevices, onActiveOutputsChanged, this.isOutputSelectionSupported); 2863 this.addListener('newListener', (eventName) => { 2864 if (eventName === 'inputVolume') { 2865 this._maybeStartPollingVolume(); 2866 } 2867 }); 2868 this.addListener('removeListener', (eventName) => { 2869 if (eventName === 'inputVolume') { 2870 this._maybeStopPollingVolume(); 2871 } 2872 }); 2873 this.once('newListener', () => { 2874 // NOTE (rrowland): Ideally we would only check isEnumerationSupported here, but 2875 // in at least one browser version (Tested in FF48) enumerateDevices actually 2876 // returns bad data for the listed devices. Instead, we check for 2877 // isOutputSelectionSupported to avoid these quirks that may negatively affect customers. 2878 if (!this.isOutputSelectionSupported) { 2879 this._log.warn('Warning: This browser does not support audio output selection.'); 2880 } 2881 if (!this.isVolumeSupported) { 2882 this._log.warn(`Warning: This browser does not support Twilio's volume indicator feature.`); 2883 } 2884 }); 2885 if (isEnumerationSupported) { 2886 this._initializeEnumeration(); 2887 } 2888 // NOTE (kchoy): Currently microphone permissions are not supported in firefox, and Safari V15 and older. 2889 // https://github.com/mozilla/standards-positions/issues/19#issuecomment-370158947 2890 // https://caniuse.com/permissions-api 2891 if (navigator && navigator.permissions && typeof navigator.permissions.query === 'function') { 2892 navigator.permissions.query({ name: 'microphone' }).then((microphonePermissionStatus) => { 2893 if (microphonePermissionStatus.state !== 'granted') { 2894 const handleStateChange = () => { 2895 this._updateAvailableDevices(); 2896 this._stopMicrophonePermissionListener(); 2897 }; 2898 microphonePermissionStatus.addEventListener('change', handleStateChange); 2899 this._microphonePermissionStatus = microphonePermissionStatus; 2900 this._onMicrophonePermissionStatusChanged = handleStateChange; 2901 } 2902 }).catch((reason) => this._log.warn(`Warning: unable to listen for microphone permission changes. ${reason}`)); 2903 } 2904 else { 2905 this._log.warn('Warning: current browser does not support permissions API.'); 2906 } 2907 } 2908 /** 2909 * Destroy this AudioHelper instance 2910 * @internal 2911 */ 2912 _destroy() { 2913 this._stopDefaultInputDeviceStream(); 2914 this._stopSelectedInputDeviceStream(); 2915 this._destroyProcessedStream(); 2916 this._maybeStopPollingVolume(); 2917 this.removeAllListeners(); 2918 this._stopMicrophonePermissionListener(); 2919 this._unbind(); 2920 } 2921 /** 2922 * Promise to wait for the input device, if setInputDevice is called outside of the SDK 2923 * @internal 2924 */ 2925 _getInputDevicePromise() { 2926 return this._inputDevicePromise; 2927 } 2928 /** 2929 * Start polling volume if it's supported and there's an input stream to poll. 2930 * @internal 2931 */ 2932 _maybeStartPollingVolume() { 2933 if (!this.isVolumeSupported || !this.inputStream) { 2934 return; 2935 } 2936 this._updateVolumeSource(); 2937 if (this._isPollingInputVolume || !this._inputVolumeAnalyser) { 2938 return; 2939 } 2940 const bufferLength = this._inputVolumeAnalyser.frequencyBinCount; 2941 const buffer = new Uint8Array(bufferLength); 2942 this._isPollingInputVolume = true; 2943 const emitVolume = () => { 2944 if (!this._isPollingInputVolume) { 2945 return; 2946 } 2947 if (this._inputVolumeAnalyser) { 2948 this._inputVolumeAnalyser.getByteFrequencyData(buffer); 2949 const inputVolume = average(buffer);
2950 this.emit('inputVolume', inputVolume / 255); 2951 } 2952 requestAnimationFrame(emitVolume); 2953 }; 2954 requestAnimationFrame(emitVolume); 2955 } 2956 /** 2957 * Stop polling volume if it's currently polling and there are no listeners. 2958 * @internal 2959 */ 2960 _maybeStopPollingVolume() { 2961 if (!this.isVolumeSupported) { 2962 return; 2963 } 2964 if (!this._isPollingInputVolume || (this.inputStream && this.listenerCount('inputVolume'))) { 2965 return; 2966 } 2967 if (this._inputVolumeSource) { 2968 this._inputVolumeSource.disconnect(); 2969 delete this._inputVolumeSource; 2970 } 2971 this._isPollingInputVolume = false; 2972 } 2973 /** 2974 * Call getUserMedia with specified constraints 2975 * @internal 2976 */ 2977 _openDefaultDeviceWithConstraints(constraints) { 2978 this._log.info('Opening default device with constraints', constraints); 2979 return this._getUserMedia(constraints).then((stream) => { 2980 this._log.info('Opened default device. Updating available devices.'); 2981 // Ensures deviceId's and labels are populated after the gUM call 2982 // by calling enumerateDevices 2983 this._updateAvailableDevices().catch(error => { 2984 // Ignore error, we don't want to break the call flow 2985 this._log.warn('Unable to updateAvailableDevices after gUM call', error); 2986 }); 2987 this._defaultInputDeviceStream = stream; 2988 return this._maybeCreateProcessedStream(stream); 2989 }); 2990 } 2991 /** 2992 * Stop the default audio stream 2993 * @internal 2994 */ 2995 _stopDefaultInputDeviceStream() { 2996 if (this._defaultInputDeviceStream) { 2997 this._log.info('stopping default device stream'); 2998 this._defaultInputDeviceStream.getTracks().forEach(track => track.stop()); 2999 this._defaultInputDeviceStream = null; 3000 this._destroyProcessedStream(); 3001 } 3002 } 3003 /** 3004 * Unbind the listeners from mediaDevices. 3005 * @internal 3006 */ 3007 _unbind() { 3008 var _a; 3009 if ((_a = this._mediaDevices) === null || _a === void 0 ? void 0 : _a.removeEventListener) { 3010 this._mediaDevices.removeEventListener('devicechange', this._updateAvailableDevices); 3011 } 3012 } 3013 /** 3014 * Update AudioHelper options that can be changed by the user 3015 * @internal 3016 */ 3017 _updateUserOptions(options) { 3018 if (typeof options.enumerateDevices === 'function') { 3019 this._enumerateDevices = options.enumerateDevices; 3020 } 3021 if (typeof options.getUserMedia === 'function') { 3022 this._getUserMedia = options.getUserMedia; 3023 } 3024 } 3025 /** 3026 * Adds an {@link AudioProcessor} object. Once added, the AudioHelper will route 3027 * the input audio stream through the processor before sending the audio 3028 * stream to Twilio. Only one AudioProcessor can be added at this time. 3029 * 3030 * See the {@link AudioProcessor} interface for an example. 3031 * 3032 * @param processor The AudioProcessor to add. 3033 * @returns 3034 */ 3035 addProcessor(processor) { 3036 this._log.debug('.addProcessor'); 3037 if (this._processor) { 3038 throw new NotSupportedError('Adding multiple AudioProcessors is not supported at this time.'); 3039 } 3040 if (typeof processor !== 'object' || processor === null) { 3041 throw new InvalidArgumentError('Missing AudioProcessor argument.'); 3042 } 3043 if (typeof processor.createProcessedStream !== 'function') { 3044 throw new InvalidArgumentError('Missing createProcessedStream() method.'); 3045 } 3046 if (typeof processor.destroyProcessedStream !== 'function') { 3047 throw new InvalidArgumentError('Missing destroyProcessedStream() method.'); 3048 } 3049 this._processor = processor; 3050 this._audioProcessorEventObserver.emit('add'); 3051 return this._restartStreams(); 3052 } 3053 /** 3054 * Enable or disable the disconnect sound. 3055 * @param doEnable Passing `true` will enable the sound and `false` will disable the sound. 3056 * Not passing this parameter will not alter the enable-status of the sound. 3057 * @returns The enable-status of the sound. 3058 */ 3059 disconnect(doEnable) { 3060 this._log.debug('.disconnect', doEnable); 3061 return this._maybeEnableSound(Device.SoundName.Disconnect, doEnable); 3062 } 3063 /** 3064 * Enable or disable the incoming sound. 3065 * @param doEnable Passing `true` will enable the sound and `false` will disable the sound. 3066 * Not passing this parameter will not alter the enable-status of the sound. 3067 * @returns The enable-status of the sound. 3068 */ 3069 incoming(doEnable) { 3070 this._log.debug('.incoming', doEnable); 3071 return this._maybeEnableSound(Device.SoundName.Incoming, doEnable); 3072 } 3073 /** 3074 * Enable or disable the outgoing sound. 3075 * @param doEnable Passing `true` will enable the sound and `false` will disable the sound. 3076 * Not passing this parameter will not alter the enable-status of the sound. 3077 * @returns The enable-status of the sound. 3078 */ 3079 outgoing(doEnable) { 3080 this._log.debug('.outgoing', doEnable); 3081 return this._maybeEnableSound(Device.SoundName.Outgoing, doEnable); 3082 } 3083 /** 3084 * Removes an {@link AudioProcessor}. Once removed, the AudioHelper will start using 3085 * the audio stream from the selected input device for existing or future calls. 3086 * 3087 * @param processor The AudioProcessor to remove. 3088 * @returns 3089 */ 3090 removeProcessor(processor) { 3091 this._log.debug('.removeProcessor'); 3092 if (typeof processor !== 'object' || processor === null) { 3093 throw new InvalidArgumentError('Missing AudioProcessor argument.'); 3094 } 3095 if (this._processor !== processor) { 3096 throw new InvalidArgumentError('Cannot remove an AudioProcessor that has not been previously added.'); 3097 } 3098 this._destroyProcessedStream(); 3099 this._processor = null; 3100 this._audioProcessorEventObserver.emit('remove'); 3101 return this._restartStreams(); 3102 } 3103 /** 3104 * Set the MediaTrackConstraints to be applied on every getUserMedia call for new input 3105 * device audio. Any deviceId specified here will be ignored. Instead, device IDs should 3106 * be specified using {@link AudioHelper#setInputDevice}. The returned Promise resolves 3107 * when the media is successfully reacquired, or immediately if no input device is set. 3108 * @param audioConstraints - The MediaTrackConstraints to apply. 3109 */ 3110 setAudioConstraints(audioConstraints) { 3111 this._log.debug('.setAudioConstraints', audioConstraints); 3112 this._audioConstraints = Object.assign({}, audioConstraints); 3113 delete this._audioConstraints.deviceId; 3114 return this.inputDevice 3115 ? this._setInputDevice(this.inputDevice.deviceId, true) 3116 : Promise.resolve(); 3117 } 3118 /** 3119 * Replace the current input device with a new device by ID. 3120 * 3121 * Calling `setInputDevice` sets the stream for current and future calls and 3122 * will not release it automatically. 3123 * 3124 * While this behavior is not an issue, it will result in the application 3125 * holding onto the input device, and the application may show a red 3126 * "recording" symbol in the browser tab. 3127 *
3128 * To remove the red "recording" symbol, the device must be released. To 3129 * release it, call `unsetInputDevice` after the call disconnects. Note that 3130 * after calling `unsetInputDevice` future calls will then use the default 3131 * input device. 3132 * 3133 * Consider application logic that keeps track of the user-selected device 3134 * and call `setInputDevice` before calling `device.connect()` for outgoing 3135 * calls and `call.accept()` for incoming calls. Furthermore, consider 3136 * calling `unsetInputDevice` once a call is disconnected. Below is an 3137 * example: 3138 * 3139 * ```ts 3140 * import { Device } from '@twilio/voice-sdk'; 3141 * let inputDeviceId = ...; 3142 * const device = new Device(...); 3143 * 3144 * async function makeOutgoingCall() { 3145 * await device.audio.setInputDevice(inputDeviceId); 3146 * const call = await device.connect(...); 3147 * 3148 * call.on('disconnect', async () => { 3149 * inputDeviceId = ... // save the current input device id 3150 * await device.audio.unsetInputDevice(); 3151 * }); 3152 * } 3153 * 3154 * async function acceptIncomingCall(incomingCall) { 3155 * await device.audio.setInputDevice(inputDeviceId); 3156 * await incomingCall.accept(); 3157 * 3158 * incomingCall.on('disconnect', async () => { 3159 * inputDeviceId = ... // save the current input device id 3160 * await device.audio.unsetInputDevice(); 3161 * }); 3162 * } 3163 * ``` 3164 * 3165 * @param deviceId - An ID of a device to replace the existing 3166 * input device with. 3167 */ 3168 setInputDevice(deviceId) { 3169 this._log.debug('.setInputDevice', deviceId); 3170 return this._setInputDevice(deviceId, false); 3171 } 3172 /** 3173 * Unset the MediaTrackConstraints to be applied on every getUserMedia call for new input 3174 * device audio. The returned Promise resolves when the media is successfully reacquired, 3175 * or immediately if no input device is set. 3176 */ 3177 unsetAudioConstraints() { 3178 this._log.debug('.unsetAudioConstraints'); 3179 this._audioConstraints = null; 3180 return this.inputDevice 3181 ? this._setInputDevice(this.inputDevice.deviceId, true) 3182 : Promise.resolve(); 3183 } 3184 /** 3185 * Unset the input device, stopping the tracks. This should only be called when not in a connection, and 3186 * will not allow removal of the input device during a live call. 3187 */ 3188 unsetInputDevice() { 3189 this._log.debug('.unsetInputDevice', this.inputDevice); 3190 if (!this.inputDevice) { 3191 return Promise.resolve(); 3192 } 3193 this._destroyProcessedStream(); 3194 return this._onActiveInputChanged(null).then(() => { 3195 this._replaceStream(null); 3196 this._inputDevice = null; 3197 this._maybeStopPollingVolume(); 3198 }); 3199 } 3200 /** 3201 * Destroys processed stream and update references 3202 */ 3203 _destroyProcessedStream() { 3204 if (this._processor && this._processedStream) { 3205 this._log.info('destroying processed stream'); 3206 const processedStream = this._processedStream; 3207 this._processedStream.getTracks().forEach(track => track.stop()); 3208 this._processedStream = null; 3209 this._processor.destroyProcessedStream(processedStream); 3210 this._audioProcessorEventObserver.emit('destroy'); 3211 } 3212 } 3213 /** 3214 * Get the index of an un-labeled Device. 3215 * @param mediaDeviceInfo 3216 * @returns The index of the passed MediaDeviceInfo 3217 */ 3218 _getUnknownDeviceIndex(mediaDeviceInfo) { 3219 const id = mediaDeviceInfo.deviceId; 3220 const kind = mediaDeviceInfo.kind; 3221 let index = this._unknownDeviceIndexes[kind][id]; 3222 if (!index) { 3223 index = Object.keys(this._unknownDeviceIndexes[kind]).length + 1; 3224 this._unknownDeviceIndexes[kind][id] = index; 3225 } 3226 return index; 3227 } 3228 /** 3229 * Initialize output device enumeration. 3230 */ 3231 _initializeEnumeration() { 3232 if (!this._mediaDevices || !this._enumerateDevices) { 3233 throw new NotSupportedError('Enumeration is not supported'); 3234 } 3235 if (this._mediaDevices.addEventListener) { 3236 this._mediaDevices.addEventListener('devicechange', this._updateAvailableDevices); 3237 } 3238 this._updateAvailableDevices().then(() => { 3239 if (!this.isOutputSelectionSupported) { 3240 return; 3241 } 3242 Promise.all([ 3243 this.speakerDevices.set('default'), 3244 this.ringtoneDevices.set('default'), 3245 ]).catch(reason => { 3246 this._log.warn(`Warning: Unable to set audio output devices. ${reason}`); 3247 }); 3248 }); 3249 } 3250 /** 3251 * Route input stream to the processor if it exists 3252 */ 3253 _maybeCreateProcessedStream(stream) { 3254 if (this._processor) { 3255 this._log.info('Creating processed stream'); 3256 return this._processor.createProcessedStream(stream).then((processedStream)
3256=> { 3257 this._processedStream = processedStream; 3258 this._audioProcessorEventObserver.emit('create'); 3259 return this._processedStream; 3260 }); 3261 } 3262 return Promise.resolve(stream); 3263 } 3264 /** 3265 * Set whether the sound is enabled or not 3266 * @param soundName 3267 * @param doEnable 3268 * @returns Whether the sound is enabled or not 3269 */ 3270 _maybeEnableSound(soundName, doEnable) { 3271 if (typeof doEnable !== 'undefined') { 3272 this._enabledSounds[soundName] = doEnable; 3273 } 3274 return this._enabledSounds[soundName]; 3275 } 3276 /** 3277 * Stop the tracks on the current input stream before replacing it with the passed stream. 3278 * @param stream - The new stream 3279 */ 3280 _replaceStream(stream) { 3281 this._log.info('Replacing with new stream.'); 3282 if (this._selectedInputDeviceStream) { 3283 this._log.info('Old stream detected. Stopping tracks.'); 3284 this._stopSelectedInputDeviceStream(); 3285 } 3286 this._selectedInputDeviceStream = stream; 3287 } 3288 /** 3289 * Restart the active streams 3290 */ 3291 _restartStreams() { 3292 if (this.inputDevice && this._selectedInputDeviceStream) { 3293 this._log.info('Restarting selected input device'); 3294 return this._setInputDevice(this.inputDevice.deviceId, true); 3295 } 3296 if (this._defaultInputDeviceStream) { 3297 const defaultDevice = this.availableInputDevices.get('default') 3298 || Array.from(this.availableInputDevices.values())[0]; 3299 this._log.info('Restarting default input device, now becoming selected.'); 3300 return this._setInputDevice(defaultDevice.deviceId, true); 3301 } 3302 return Promise.resolve(); 3303 } 3304 /** 3305 * Replace the current input device with a new device by ID. 3306 * @param deviceId - An ID of a device to replace the existing 3307 * input device with. 3308 * @param forceGetUserMedia - If true, getUserMedia will be called even if 3309 * the specified device is already active. 3310 */ 3311 _setInputDevice(deviceId, forceGetUserMedia) { 3312 return __awaiter(this, void 0, void 0, function* () { 3313 const setInputDevice = () => __awaiter(this, void 0, void 0, function* () { 3314 yield this._beforeSetInputDevice(); 3315 if (typeof deviceId !== 'string') { 3316 return Promise.reject(new InvalidArgumentError('Must specify the device to set')); 3317 } 3318 const device = this.availableInputDevices.get(deviceId); 3319 if (!device) { 3320 return Promise.reject(new InvalidArgumentError(`Device not found: ${deviceId}`)); 3321 } 3322 this._log.info('Setting input device. ID: ' + deviceId); 3323 if (this._inputDevice && this._inputDevice.deviceId === deviceId && this._selectedInputDeviceStream) { 3324 if (!forceGetUserMedia) { 3325 return Promise.resolve(); 3326 } 3327 // If the currently active track is still in readyState `live`, gUM may return the same track 3328 // rather than returning a fresh track. 3329 this._log.info('Same track detected on setInputDevice, stopping old tracks.'); 3330 this._stopSelectedInputDeviceStream(); 3331 } 3332 // Release the default device in case it was created previously 3333 this._stopDefaultInputDeviceStream(); 3334 const constraints = { audio: Object.assign({ deviceId: { exact: deviceId } }, this.audioConstraints) }; 3335 this._log.info('setInputDevice: getting new tracks.'); 3336 return this._getUserMedia(constraints).then((originalStream) => { 3337 this._destroyProcessedStream(); 3338 return this._maybeCreateProcessedStream(originalStream).then((newStream) => { 3339 this._log.info('setInputDevice: invoking _onActiveInputChanged.'); 3340 return this._onActiveInputChanged(newStream).then(() => { 3341 this._replaceStream(originalStream); 3342 this._inputDevice = device; 3343 this._maybeStartPollingVolume(); 3344 }); 3345 }); 3346 }); 3347 }); 3348 return this._inputDevicePromise = setInputDevice().finally(() => { 3349 this._inputDevicePromise = null; 3350 }); 3351 }); 3352 } 3353 /** 3354 * Remove event listener for microphone permissions 3355 */ 3356 _stopMicrophonePermissionListener() { 3357 var _a; 3358 if ((_a = this._microphonePermissionStatus) === null || _a === void 0 ? void 0 : _a.removeEventListener) { 3359 this._microphonePermissionStatus.removeEventListener('change', this._onMicrophonePermissionStatusChanged); 3360 } 3361 } 3362 /** 3363 * Stop the selected audio stream 3364 */ 3365 _stopSelectedInputDeviceStream() { 3366 if (this._selectedInputDeviceStream) { 3367 this._log.info('Stopping selected device stream');
3368 this._selectedInputDeviceStream.getTracks().forEach(track => track.stop()); 3369 } 3370 } 3371 /** 3372 * Update a set of devices. 3373 * @param updatedDevices - An updated list of available Devices 3374 * @param availableDevices - The previous list of available Devices 3375 * @param removeLostDevice - The method to call if a previously available Device is 3376 * no longer available. 3377 */ 3378 _updateDevices(updatedDevices, availableDevices, removeLostDevice) { 3379 const updatedDeviceIds = updatedDevices.map(d => d.deviceId); 3380 const knownDeviceIds = Array.from(availableDevices.values()).map(d => d.deviceId); 3381 const lostActiveDevices = []; 3382 // Remove lost devices 3383 const lostDeviceIds = difference(knownDeviceIds, updatedDeviceIds); 3384 lostDeviceIds.forEach((lostDeviceId) => { 3385 const lostDevice = availableDevices.get(lostDeviceId); 3386 if (lostDevice) { 3387 availableDevices.delete(lostDeviceId); 3388 if (removeLostDevice(lostDevice)) { 3389 lostActiveDevices.push(lostDevice); 3390 } 3391 } 3392 }); 3393 // Add any new devices, or devices with updated labels 3394 let deviceChanged = false; 3395 updatedDevices.forEach(newDevice => { 3396 const existingDevice = availableDevices.get(newDevice.deviceId); 3397 const newMediaDeviceInfo = this._wrapMediaDeviceInfo(newDevice); 3398 if (!existingDevice || existingDevice.label !== newMediaDeviceInfo.label) { 3399 availableDevices.set(newDevice.deviceId, newMediaDeviceInfo); 3400 deviceChanged = true; 3401 } 3402 }); 3403 if (deviceChanged || lostDeviceIds.length) { 3404 // Force a new gUM in case the underlying tracks of the active stream have changed. One 3405 // reason this might happen is when `default` is selected and set to a USB device, 3406 // then that device is unplugged or plugged back in. We can't check for the 'ended' 3407 // event or readyState because it is asynchronous and may take upwards of 5 seconds, 3408 // in my testing. (rrowland) 3409 const defaultId = 'default'; 3410 // this.inputDevice is not null if audio.setInputDevice() was explicitly called 3411 const isInputDeviceSet = this.inputDevice && this.inputDevice.deviceId === defaultId; 3412 // If this.inputDevice is null, and default stream is not null, it means 3413 // the user is using the default stream and did not explicitly call audio.setInputDevice() 3414 const isDefaultDeviceSet = this._defaultInputDeviceStream && this.availableInputDevices.get(defaultId); 3415 if (isInputDeviceSet || isDefaultDeviceSet) { 3416 this._log.warn(`Calling getUserMedia after device change to ensure that the \ 3417 tracks of the active device (default) have not gone stale.`); 3418 // NOTE(csantos): Updating the stream in the same execution context as the devicechange event 3419 // causes the new gUM call to fail silently. Meaning, the gUM call may succeed, 3420 // but it won't actually update the stream. We need to update the stream in a different 3421 // execution context (setTimeout) to properly update the stream. 3422 setTimeout(() => { 3423 this._setInputDevice(defaultId, true); 3424 }, 0); 3425 } 3426 this._log.debug('#deviceChange', lostActiveDevices); 3427 this.emit('deviceChange', lostActiveDevices); 3428 } 3429 } 3430 /** 3431 * Disconnect the old input volume source, and create and connect a new one with the current 3432 * input stream. 3433 */ 3434 _updateVolumeSource() { 3435 if (!this.inputStream || !this._audioContext || !this._inputVolumeAnalyser) { 3436 return; 3437 } 3438 if (this._inputVolumeSource) { 3439 this._inputVolumeSource.disconnect(); 3440 } 3441 try { 3442 this._inputVolumeSource = this._audioContext.createMediaStreamSource(this.inputStream); 3443 this._inputVolumeSource.connect(this._inputVolumeAnalyser); 3444 } 3445 catch (ex) { 3446 this._log.warn('Unable to update volume source', ex); 3447 delete this._inputVolumeSource; 3448 } 3449 } 3450 /** 3451 * Convert a MediaDeviceInfo to a IMediaDeviceInfoShim. 3452 * @param mediaDeviceInfo - The info to convert 3453 * @returns The converted shim 3454 */ 3455 _wrapMediaDeviceInfo(mediaDeviceInfo) { 3456 const options = { 3457 deviceId: mediaDeviceInfo.deviceId, 3458 groupId: mediaDeviceInfo.groupId, 3459 kind: mediaDeviceInfo.kind, 3460 label: mediaDeviceInfo.label, 3461 }; 3462 if (!options.label) { 3463 if (options.deviceId === 'default') { 3464 options.label = 'Default'; 3465 } 3466 else { 3467 const index = this._getUnknownDeviceIndex(mediaDeviceInfo); 3468 options.label = `Unknown ${kindAliases[options.kind]} Device ${index}`; 3469 } 3470 } 3471 return new MediaDeviceInfoShim(options); 3472 } 3473 } 3474 /** 3475 * @mergeModuleWith AudioHelper 3476 */ 3477 (function (AudioHelper) { 3478 })(AudioHelper || (AudioHelper = {})); 3479 var AudioHelper$1 = AudioHelper; 3480 3481 /** 3482 * AudioProcessorEventObserver observes {@link AudioProcessor} 3483 * related operations and re-emits them as generic events. 3484 * @internal 3485 */ 3486 class AudioProcessorEventObserver extends eventsExports.EventEmitter { 3487 constructor() { 3488 super(); 3489 this._log = new Log('AudioProcessorEventObserver'); 3490 this._log.info('Creating AudioProcessorEventObserver instance'); 3491 this.on('enabled', () => this._reEmitEvent('enabled')); 3492 this.on('add', () => this._reEmitEvent('add')); 3493 this.on('remove', () => this._reEmitEvent('remove')); 3494 this.on('create', () => this._reEmitEvent('create-processed-stream')); 3495 this.on('destroy', () => this._reEmitEvent('destroy-processed-stream')); 3496 } 3497 destroy() { 3498 this.removeAllListeners(); 3499 } 3500 _reEmitEvent(name) { 3501 this._log.info(`AudioProcessor:${name}`);
3502 this.emit('event', { name, group: 'audio-processor' }); 3503 } 3504 } 3505 3506 /** 3507 * A Map of DTMF Sound Names to their mock frequency pairs. 3508 */ 3509 const bandFrequencies = { 3510 dtmf0: [1360, 960], 3511 dtmf1: [1230, 720], 3512 dtmf2: [1360, 720], 3513 dtmf3: [1480, 720], 3514 dtmf4: [1230, 790], 3515 dtmf5: [1360, 790], 3516 dtmf6: [1480, 790], 3517 dtmf7: [1230, 870], 3518 dtmf8: [1360, 870], 3519 dtmf9: [1480, 870], 3520 dtmfh: [1480, 960], 3521 dtmfs: [1230, 960], 3522 }; 3523 class DialtonePlayer { 3524 constructor(_context) { 3525 this._context = _context; 3526 /** 3527 * Gain nodes, reducing the frequency. 3528 */ 3529 this._gainNodes = []; 3530 this._gainNodes = [ 3531 this._context.createGain(), 3532 this._context.createGain(), 3533 ]; 3534 this._gainNodes.forEach((gainNode) => { 3535 gainNode.connect(this._context.destination); 3536 gainNode.gain.value = 0.1; 3537 this._gainNodes.push(gainNode); 3538 }); 3539 } 3540 cleanup() { 3541 this._gainNodes.forEach((gainNode) => { 3542 gainNode.disconnect(); 3543 }); 3544 } 3545 /** 3546 * Play the dual frequency tone for the passed DTMF name. 3547 * @param sound 3548 */ 3549 play(sound) { 3550 const frequencies = bandFrequencies[sound]; 3551 if (!frequencies) { 3552 throw new InvalidArgumentError('Invalid DTMF sound name'); 3553 } 3554 const oscillators = [ 3555 this._context.createOscillator(), 3556 this._context.createOscillator(), 3557 ]; 3558 oscillators.forEach((oscillator, i) => { 3559 oscillator.type = 'sine'; 3560 oscillator.frequency.value = frequencies[i]; 3561 oscillator.connect(this._gainNodes[i]); 3562 oscillator.start(); 3563 oscillator.stop(this._context.currentTime + 0.1); 3564 oscillator.addEventListener('ended', () => oscillator.disconnect()); 3565 }); 3566 } 3567 } 3568 3569 // @ts-nocheck 3570 function request(method, params, callback) { 3571 const body = JSON.stringify(params.body || {}); 3572 const headers = new Headers(); 3573 params.headers = params.headers || []; 3574 Object.entries(params.headers).forEach(([headerName, headerBody]) => headers.append(headerName, headerBody)); 3575 fetch(params.url, { body, headers, method }) 3576 .then(response => response.text(), callback) 3577 .then(responseText => callback(null, responseText), callback); 3578 } 3579 /** 3580 * Use XMLHttpRequest to get a network resource. 3581 * @param {String} method - HTTP Method 3582 * @param {Object} params - Request parameters 3583 * @param {String} params.url - URL of the resource 3584 * @param {Array} params.headers - An array of headers to pass [{ headerName : headerBody }] 3585 * @param {Object} params.body - A JSON body to send to the resource 3586 * @returns {response} 3587 */ 3588 const Request = request; 3589 /** 3590 * Sugar function for request('GET', params, callback); 3591 * @param {Object} params - Request parameters 3592 * @param {Request~get} callback - The callback that handles the response. 3593 */ 3594 Request.get = function get(params, callback) { 3595 return new this('GET', params, callback); 3596 }; 3597 /** 3598 * Sugar function for request('POST', params, callback); 3599 * @param {Object} params - Request parameters 3600 * @param {Request~post} callback - The callback that handles the response. 3601 */ 3602 Request.post = function post(params, callback) { 3603 return new this('POST', params, callback); 3604 }; 3605 3606 // @ts-nocheck 3607 /** 3608 * Builds Endpoint Analytics (EA) event payloads and sends them to 3609 * the EA server. 3610 * @constructor 3611 * @param {String} productName - Name of the product publishing events. 3612 * @param {String} token - The JWT token to use to authenticate with 3613 * the EA server. 3614 * @param {EventPublisher.Options} options 3615 * @property {Boolean} isEnabled - Whether or not this publisher is publishing 3616 * to the server. Currently ignores the request altogether, in the future this 3617 * may store them in case publishing is re-enabled later. Defaults to true. 3618 */ 3619 /** 3620 * @typedef {Object} EventPublisher.Options 3621 * @property {Object} [metadata=undefined] - A publisher_metadata object to send 3622 * with each payload. 3623 * @property {String} [host='eventgw.twilio.com'] - The host address of the EA 3624 * server to publish to. 3625 * @property {Object|Function} [defaultPayload] - A default payload to extend 3626 * when creating and sending event payloads. Also takes a function that 3627 * should return an object representing the default payload. This is 3628 * useful for fields that should always be present when they are 3629 * available, but are not always available. 3630 */ 3631 class EventPublisher extends eventsExports.EventEmitter { 3632 constructor(productName, token, options) { 3633 super(); 3634 if (!(this instanceof EventPublisher)) { 3635 return new EventPublisher(productName, token, options); 3636 } 3637 // Apply default options 3638 options = Object.assign({ defaultPayload() { return {}; } }, options); 3639 let defaultPayload = options.defaultPayload; 3640 if (typeof defaultPayload !== 'function') { 3641 defaultPayload = () => Object.assign({}, options.defaultPayload); 3642 } 3643 let isEnabled = true; 3644 const metadata = Object.assign({ app_name: undefined, app_version: undefined }, options.metadata); 3645 Object.defineProperties(this, { 3646 _defaultPayload: { value: defaultPayload }, 3647 _host: { value: options.host, writable: true }, 3648 _isEnabled: { 3649 get() { return isEnabled; }, 3650 set(_isEnabled) { isEnabled = _isEnabled; }, 3651 }, 3652 _log: { value: new Log('EventPublisher') }, 3653 _request: { value: options.request || Request, writable: true }, 3654 _token: { value: token, writable: true }, 3655 isEnabled: { 3656 enumerable: true, 3657 get() { return isEnabled; }, 3658 }, 3659 metadata: { 3660 enumerable: true, 3661 get() { return metadata; }, 3662 }, 3663 productName: { enumerable: true, value: productName }, 3664 token: { 3665 enumerable: true, 3666 get() { return this._token; }, 3667 }, 3668 }); 3669 } 3670 } 3671 /** 3672 * Post to an EA server. 3673 * @private 3674 * @param {String} endpointName - Endpoint to post the event to 3675 * @param {String} level - ['debug', 'info', 'warning', 'error'] 3676 * @param {String} group - The name of the group the event belongs to. 3677 * @param {String} name - The designated event name. 3678 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3679 * onto the default payload object, if one exists. 3680 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3681 * @param {?Boolean} [force=false] - Whether or not to send this even if 3682 * publishing is disabled. 3683 * @returns {Promise}
3683 Fulfilled if the HTTP response is 20x. 3684 */ 3685 EventPublisher.prototype._post = function _post(endpointName, level, group, name, payload, connection, force) { 3686 if ((!this.isEnabled && !force) || !this._host) { 3687 this._log.debug('Publishing cancelled', JSON.stringify({ isEnabled: this.isEnabled, force, host: this._host })); 3688 return Promise.resolve(); 3689 } 3690 if (!connection || ((!connection.parameters || !connection.parameters.CallSid) && !connection.outboundConnectionId)) { 3691 if (!connection) { 3692 this._log.debug('Publishing cancelled. Missing connection object'); 3693 } 3694 else { 3695 this._log.debug('Publishing cancelled. Missing connection info', JSON.stringify({ 3696 outboundConnectionId: connection.outboundConnectionId, parameters: connection.parameters, 3697 })); 3698 } 3699 return Promise.resolve(); 3700 } 3701 const event = { 3702 group, 3703 level: level.toUpperCase(), 3704 name, 3705 payload: (payload && payload.forEach) ? 3706 payload.slice(0) : Object.assign(this._defaultPayload(connection), payload), 3707 payload_type: 'application/json', 3708 private: false, 3709 publisher: this.productName, 3710 timestamp: (new Date()).toISOString(), 3711 }; 3712 if (this.metadata) { 3713 event.publisher_metadata = this.metadata; 3714 } 3715 if (endpointName === 'EndpointEvents') { 3716 this._log.debug('Publishing insights', JSON.stringify({ endpointName, event, force, host: this._host })); 3717 } 3718 const requestParams = { 3719 body: event, 3720 headers: { 3721 'Content-Type': 'application/json', 3722 'X-Twilio-Token': this.token, 3723 }, 3724 url: `https://${this._host}/v4/${endpointName}`, 3725 }; 3726 return new Promise((resolve, reject) => { 3727 this._request.post(requestParams, err => { 3728 if (err) { 3729 this.emit('error', err); 3730 reject(err); 3731 } 3732 else { 3733 resolve(); 3734 } 3735 }); 3736 }).catch(e => { 3737 this._log.error(`Unable to post ${group} ${name} event to Insights. Received error: ${e}`); 3738 }); 3739 }; 3740 /** 3741 * Post an event to the EA server. Use this method when the level 3742 * is dynamic. Otherwise, it's better practice to use the sugar 3743 * methods named for the specific level. 3744 * @param {String} level - ['debug', 'info', 'warning', 'error'] 3745 * @param {String} group - The name of the group the event belongs to. 3746 * @param {String} name - The designated event name. 3747 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3748 * onto the default payload object, if one exists. 3749 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3750 * @returns {Promise}
3750 Fulfilled if the HTTP response is 20x. 3751 */ 3752 EventPublisher.prototype.post = function post(level, group, name, payload, connection, force) { 3753 return this._post('EndpointEvents', level, group, name, payload, connection, force); 3754 }; 3755 /** 3756 * Post a debug-level event to the EA server. 3757 * @param {String} group - The name of the group the event belongs to. 3758 * @param {String} name - The designated event name. 3759 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3760 * onto the default payload object, if one exists. 3761 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3762 * @returns {Promise} Fulfilled if the HTTP response is 20x. 3763 */ 3764 EventPublisher.prototype.debug = function debug(group, name, payload, connection) { 3765 return this.post('debug', group, name, payload, connection); 3766 }; 3767 /** 3768 * Post an info-level event to the EA server. 3769 * @param {String} group - The name of the group the event belongs to. 3770 * @param {String} name - The designated event name. 3771 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3772 * onto the default payload object, if one exists. 3773 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3774 * @returns {Promise}
3774 Fulfilled if the HTTP response is 20x. 3775 */ 3776 EventPublisher.prototype.info = function info(group, name, payload, connection) { 3777 return this.post('info', group, name, payload, connection); 3778 }; 3779 /** 3780 * Post a warning-level event to the EA server. 3781 * @param {String} group - The name of the group the event belongs to. 3782 * @param {String} name - The designated event name. 3783 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3784 * onto the default payload object, if one exists. 3785 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3786 * @returns {Promise} Fulfilled if the HTTP response is 20x. 3787 */ 3788 EventPublisher.prototype.warn = function warn(group, name, payload, connection) { 3789 return this.post('warning', group, name, payload, connection); 3790 }; 3791 /** 3792 * Post an error-level event to the EA server. 3793 * @param {String} group - The name of the group the event belongs to. 3794 * @param {String} name - The designated event name. 3795 * @param {?Object} [payload=null] - The payload to pass. This will be extended 3796 * onto the default payload object, if one exists. 3797 * @param {?Connection} [connection=null] - The {@link Connection} which is posting this payload. 3798 * @returns {Promise}
3798 Fulfilled if the HTTP response is 20x. 3799 */ 3800 EventPublisher.prototype.error = function error(group, name, payload, connection) { 3801 return this.post('error', group, name, payload, connection); 3802 }; 3803 /** 3804 * Post a metrics event to the EA server. 3805 * @param {String} group - The name of the group the event belongs to. 3806 * @param {String} name - The designated event name. 3807 * @param {Array<Object>} metrics - The metrics to post. 3808 * @param {?Object} [customFields] - Custom fields to append to each payload. 3809 * @returns {Promise} Fulfilled if the HTTP response is 20x. 3810 */ 3811 EventPublisher.prototype.postMetrics = function postMetrics(group, name, metrics, customFields, connection) { 3812 return new Promise(resolve => { 3813 const samples = metrics 3814 .map(formatMetric) 3815 .map(sample => Object.assign(sample, customFields)); 3816 resolve(this._post('EndpointMetrics', 'info', group, name, samples, connection)); 3817 }); 3818 }; 3819 /** 3820 * Update the host address of the insights server to publish to. 3821 * @param {String} host - The new host address of the insights server. 3822 */ 3823 EventPublisher.prototype.setHost = function setHost(host) { 3824 this._host = host; 3825 }; 3826 /** 3827 * Update the token to use to authenticate requests. 3828 * @param {string} token 3829 * @returns {void} 3830 */ 3831 EventPublisher.prototype.setToken = function setToken(token) { 3832 this._token = token; 3833 }; 3834 /** 3835 * Enable the publishing of events. 3836 */ 3837 EventPublisher.prototype.enable = function enable() { 3838 this._isEnabled = true; 3839 }; 3840 /** 3841 * Disable the publishing of events. 3842 */ 3843 EventPublisher.prototype.disable = function disable() { 3844 this._isEnabled = false; 3845 }; 3846 function formatMetric(sample) { 3847 return { 3848 audio_codec: sample.codecName, 3849 audio_level_in: sample.audioInputLevel, 3850 audio_level_out: sample.audioOutputLevel, 3851 bytes_received: sample.bytesReceived, 3852 bytes_sent: sample.bytesSent, 3853 call_volume_input: sample.inputVolume, 3854 call_volume_output: sample.outputVolume, 3855 jitter: sample.jitter, 3856 mos: sample.mos && (Math.round(sample.mos * 100) / 100), 3857 packets_lost: sample.packetsLost, 3858 packets_lost_fraction: sample.packetsLostFraction && 3859 (Math.round(sample.packetsLostFraction * 100) / 100), 3860 packets_received: sample.packetsReceived, 3861 rtt: sample.rtt, 3862 timestamp: (new Date(sample.timestamp)).toISOString(), 3863 total_bytes_received: sample.totals.bytesReceived, 3864 total_bytes_sent: sample.totals.bytesSent, 3865 total_packets_lost: sample.totals.packetsLost, 3866 total_packets_received: sample.totals.packetsReceived, 3867 total_packets_sent: sample.totals.packetsSent, 3868 }; 3869 } 3870 3871 // @ts-nocheck 3872 /** 3873 * This file was imported from another project. If making changes to this file, please don't 3874 * make them here. Make them on the linked repo below, then copy back: 3875 * https://code.hq.twilio.com/client/MockRTCStatsReport 3876 */ 3877 // The legacy max volume, which is the positive half of a signed short integer. 3878 const OLD_MAX_VOLUME = 32767; 3879 const NativeRTCStatsReport = typeof window !== 'undefined' 3880 ? window.RTCStatsReport : undefined; 3881 /** 3882 * Create a MockRTCStatsReport wrapper around a Map of RTCStats objects. If RTCStatsReport is available 3883 * natively, it will be inherited so that instanceof checks pass. 3884 * @constructor 3885 * @extends RTCStatsReport 3886 * @param {Map<string, RTCStats>} statsMap - A Map of RTCStats objects to wrap 3887 * with a MockRTCStatsReport object. 3888 */ 3889 function MockRTCStatsReport(statsMap) { 3890 if (!(this instanceof MockRTCStatsReport)) { 3891 return new MockRTCStatsReport(statsMap); 3892 } 3893 const self = this; 3894 Object.defineProperties(this, { 3895 _map: { value: statsMap }, 3896 size: { 3897 enumerable: true, 3898 get() { 3899 return self._map.size; 3900 }, 3901 }, 3902 }); 3903 this[Symbol.iterator] = statsMap[Symbol.iterator]; 3904 } 3905 // If RTCStatsReport is available natively, inherit it. Keep our constructor. 3906 if (NativeRTCStatsReport) { 3907 MockRTCStatsReport.prototype = Object.create(NativeRTCStatsReport.prototype); 3908 MockRTCStatsReport.prototype.constructor = MockRTCStatsReport; 3909 } 3910 // Map the Map-like read methods to the underlying Map 3911 ['entries', 'forEach', 'get', 'has', 'keys', 'values'].forEach(key => { 3912 MockRTCStatsReport.prototype[key] = function (...args) { 3913 return this._map[key](...args); 3914 }; 3915 }); 3916 /** 3917 * Convert an array of RTCStats objects into a mock RTCStatsReport object. 3918 * @param {Array<RTCStats>} 3919 * @return {MockRTCStatsReport} 3920 */ 3921 MockRTCStatsReport.fromArray = function fromArray(array) { 3922 return new MockRTCStatsReport(array.reduce((map, rtcStats) => { 3923 map.set(rtcStats.id, rtcStats); 3924 return map; 3925 }, new Map())); 3926 }; 3927 /** 3928 * Convert a legacy RTCStatsResponse object into a mock RTCStatsReport object. 3929 * @param {RTCStatsResponse} statsResponse - An RTCStatsResponse object returned by the 3930 * legacy getStats(callback) method in Chrome. 3931 * @return {MockRTCStatsReport} A mock RTCStatsReport object. 3932 */ 3933 MockRTCStatsReport.fromRTCStatsResponse = function fromRTCStatsResponse(statsResponse) { 3934 let activeCandidatePairId; 3935 const transportIds = new Map(); 3936 const statsMap = statsResponse.result().reduce((map, report) => { 3937 const id = report.id; 3938 switch (report.type) { 3939 case 'googCertificate': 3940 map.set(id, createRTCCertificateStats(report)); 3941 break; 3942 case 'datachannel': 3943 map.set(id, createRTCDataChannelStats(report)); 3944 break; 3945 case 'googCandidatePair': 3946 if (getBoolean(report, 'googActiveConnection')) { 3947 activeCandidatePairId = id; 3948 } 3949 map.set(id, createRTCIceCandidatePairStats(report)); 3950 break; 3951 case 'localcandidate': 3952 map.set(id, createRTCIceCandidateStats(report, false)); 3953 break;
3954 case 'remotecandidate': 3955 map.set(id, createRTCIceCandidateStats(report, true)); 3956 break; 3957 case 'ssrc': 3958 if (isPresent(report, 'packetsReceived')) { 3959 map.set(`rtp-${id}`, createRTCInboundRTPStreamStats(report)); 3960 } 3961 else { 3962 map.set(`rtp-${id}`, createRTCOutboundRTPStreamStats(report)); 3963 } 3964 map.set(`track-${id}`, createRTCMediaStreamTrackStats(report)); 3965 map.set(`codec-${id}`, createRTCCodecStats(report)); 3966 break; 3967 case 'googComponent': 3968 const transportReport = createRTCTransportStats(report); 3969 transportIds.set(transportReport.selectedCandidatePairId, id); 3970 map.set(id, createRTCTransportStats(report)); 3971 break; 3972 } 3973 return map; 3974 }, new Map()); 3975 if (activeCandidatePairId) { 3976 const activeTransportId = transportIds.get(activeCandidatePairId); 3977 if (activeTransportId) { 3978 statsMap.get(activeTransportId).dtlsState = 'connected'; 3979 } 3980 } 3981 return new MockRTCStatsReport(statsMap); 3982 }; 3983 /** 3984 * @param {RTCLegacyStatsReport} report 3985 * @returns {RTCTransportStats} 3986 */ 3987 function createRTCTransportStats(report) { 3988 return { 3989 bytesReceived: undefined, 3990 bytesSent: undefined, 3991 dtlsState: undefined, 3992 id: report.id, 3993 localCertificateId: report.stat('localCertificateId'), 3994 remoteCertificateId: report.stat('remoteCertificateId'), 3995 rtcpTransportStatsId: undefined, 3996 selectedCandidatePairId: report.stat('selectedCandidatePairId'), 3997 timestamp: Date.parse(report.timestamp), 3998 type: 'transport', 3999 }; 4000 } 4001 /** 4002 * @param {RTCLegacyStatsReport} report 4003 * @returns {RTCCodecStats} 4004 */ 4005 function createRTCCodecStats(report) { 4006 return { 4007 channels: undefined, 4008 clockRate: undefined, 4009 id: report.id, 4010 implementation: undefined, 4011 mimeType: `${report.stat('mediaType')}/${report.stat('googCodecName')}`, 4012 payloadType: undefined, 4013 sdpFmtpLine: undefined, 4014 timestamp: Date.parse(report.timestamp), 4015 type: 'codec', 4016 }; 4017 } 4018 /** 4019 * @param {RTCLegacyStatsReport} report 4020 * @returns {RTCMediaStreamTrackStats} 4021 */ 4022 function createRTCMediaStreamTrackStats(report) { 4023 return { 4024 audioLevel: isPresent(report, 'audioOutputLevel') 4025 ? getInt(report, 'audioOutputLevel') / OLD_MAX_VOLUME 4026 : (getInt(report, 'audioInputLevel') || 0) / OLD_MAX_VOLUME, 4027 detached: undefined, 4028 echoReturnLoss: getFloat(report, 'googEchoCancellationReturnLoss'), 4029 echoReturnLossEnhancement: getFloat(report, 'googEchoCancellationReturnLossEnhancement'), 4030 ended: undefined, 4031 frameHeight: isPresent(report, 'googFrameHeightReceived') 4032 ? getInt(report, 'googFrameHeightReceived') 4033 : getInt(report, 'googFrameHeightSent'), 4034 frameWidth: isPresent(report, 'googFrameWidthReceived') 4035 ? getInt(report, 'googFrameWidthReceived') 4036 : getInt(report, 'googFrameWidthSent'), 4037 framesCorrupted: undefined, 4038 framesDecoded: getInt(report, 'framesDecoded'), 4039 framesDropped: undefined, 4040 framesPerSecond: undefined, 4041 framesReceived: undefined, 4042 framesSent: getInt(report, 'framesEncoded'), 4043 fullFramesLost: undefined, 4044 id: report.id, 4045 kind: report.stat('mediaType'), 4046 partialFramesLost: undefined, 4047 remoteSource: undefined, 4048 ssrcIds: undefined, 4049 timestamp: Date.parse(report.timestamp), 4050 trackIdentifier: report.stat('googTrackId'), 4051 type: 'track', 4052 }; 4053 } 4054 /** 4055 * @param {RTCLegacyStatsReport} report 4056 * @param {boolean}
4056 isInbound - Whether to create an inbound stats object, or outbound. 4057 * @returns {RTCRTPStreamStats} 4058 */ 4059 function createRTCRTPStreamStats(report, isInbound) { 4060 return { 4061 associateStatsId: undefined, 4062 codecId: `codec-${report.id}`, 4063 firCount: isInbound 4064 ? getInt(report, 'googFirsSent') 4065 : undefined, 4066 id: report.id, 4067 isRemote: undefined, 4068 mediaType: report.stat('mediaType'), 4069 nackCount: isInbound 4070 ? getInt(report, 'googNacksSent') 4071 : getInt(report, 'googNacksReceived'), 4072 pliCount: isInbound 4073 ? getInt(report, 'googPlisSent') 4074 : getInt(report, 'googPlisReceived'), 4075 qpSum: getInt(report, 'qpSum'), 4076 sliCount: undefined, 4077 ssrc: report.stat('ssrc'), 4078 timestamp: Date.parse(report.timestamp), 4079 trackId: `track-${report.id}`, 4080 transportId: report.stat('transportId'), 4081 }; 4082 } 4083 /** 4084 * @param {RTCLegacyStatsReport} report 4085 * @returns {RTCInboundRTPStreamStats} 4086 */ 4087 function createRTCInboundRTPStreamStats(report) { 4088 const rtp = createRTCRTPStreamStats(report, true); 4089 Object.assign(rtp, { 4090 burstDiscardCount: undefined, 4091 burstDiscardRate: undefined, 4092 burstLossCount: undefined, 4093 burstLossRate: undefined, 4094 burstPacketsDiscarded: undefined, 4095 burstPacketsLost: undefined, 4096 bytesReceived: getInt(report, 'bytesReceived'), 4097 fractionLost: undefined, 4098 framesDecoded: getInt(report, 'framesDecoded'), 4099 gapDiscardRate: undefined, 4100 gapLossRate: undefined, 4101 jitter: convertMsToSeconds(report.stat('googJitterReceived')), 4102 packetsDiscarded: undefined, 4103 packetsLost: getInt(report, 'packetsLost'), 4104 packetsReceived: getInt(report, 'packetsReceived'), 4105 packetsRepaired: undefined, 4106 roundTripTime: convertMsToSeconds(report.stat('googRtt')), 4107 type: 'inbound-rtp', 4108 }); 4109 return rtp; 4110 } 4111 /** 4112 * @param {RTCLegacyStatsReport} report 4113 * @returns {RTCOutboundRTPStreamStats} 4114 */ 4115 function createRTCOutboundRTPStreamStats(report) { 4116 const rtp = createRTCRTPStreamStats(report, false); 4117 Object.assign(rtp, { 4118 bytesSent: getInt(report, 'bytesSent'), 4119 framesEncoded: getInt(report, 'framesEncoded'), 4120 packetsSent: getInt(report, 'packetsSent'), 4121 remoteTimestamp: undefined, 4122 targetBitrate: undefined, 4123 type: 'outbound-rtp', 4124 }); 4125 return rtp; 4126 } 4127 /** 4128 * @param {RTCLegacyStatsReport} report 4129 * @param {boolean} isRemote - Whether to create for a remote candidate, or local candidate. 4130 * @returns {RTCIceCandidateStats} 4131 */ 4132 function createRTCIceCandidateStats(report, isRemote) { 4133 return { 4134 candidateType: translateCandidateType(report.stat('candidateType')), 4135 deleted: undefined, 4136 id: report.id, 4137 ip: report.stat('ipAddress'), 4138 isRemote, 4139 port: getInt(report, 'portNumber'), 4140 priority: getFloat(report, 'priority'), 4141 protocol: report.stat('transport'), 4142 relayProtocol: undefined, 4143 timestamp: Date.parse(report.timestamp), 4144 transportId: undefined, 4145 type: isRemote 4146 ? 'remote-candidate' 4147 : 'local-candidate', 4148 url: undefined, 4149 }; 4150 } 4151 /** 4152 * @param {RTCLegacyStatsReport} report 4153 * @returns {RTCIceCandidatePairStats} 4154 */ 4155 function createRTCIceCandidatePairStats(report) { 4156 return { 4157 availableIncomingBitrate: undefined, 4158 availableOutgoingBitrate: undefined, 4159 bytesReceived: getInt(report, 'bytesReceived'), 4160 bytesSent: getInt(report, 'bytesSent'), 4161 consentRequestsSent: getInt(report, 'consentRequestsSent'), 4162 currentRoundTripTime: convertMsToSeconds(report.stat('googRtt')), 4163 id: report.id, 4164 lastPacketReceivedTimestamp: undefined, 4165 lastPacketSentTimestamp: undefined, 4166 localCandidateId: report.stat('localCandidateId'), 4167 nominated: undefined, 4168 priority: undefined, 4169 readable: undefined, 4170 remoteCandidateId: report.stat('remoteCandidateId'), 4171 requestsReceived: getInt(report, 'requestsReceived'), 4172 requestsSent: getInt(report, 'requestsSent'), 4173 responsesReceived: getInt(report, 'responsesReceived'), 4174 responsesSent: getInt(report, 'responsesSent'), 4175 retransmissionsReceived: undefined, 4176 retransmissionsSent: undefined, 4177 state: undefined, 4178 timestamp: Date.parse(report.timestamp), 4179 totalRoundTripTime: undefined, 4180 transportId: report.stat('googChannelId'), 4181 type: 'candidate-pair', 4182 writable: getBoolean(report, 'googWritable'), 4183 }; 4184 } 4185 /** 4186 * @param {RTCLegacyStatsReport} report 4187 * @returns {RTCIceCertificateStats} 4188 */ 4189 function createRTCCertificateStats(report) { 4190 return { 4191 base64Certificate: report.stat('googDerBase64'), 4192 fingerprint: report.stat('googFingerprint'), 4193 fingerprintAlgorithm: report.stat('googFingerprintAlgorithm'), 4194 id: report.id, 4195 issuerCertificateId: report.stat('googIssuerId'), 4196 timestamp: Date.parse(report.timestamp), 4197 type: 'certificate', 4198 }; 4199 } 4200 /** 4201 * @param {RTCLegacyStatsReport} report 4202 * @returns {RTCDataChannelStats} 4203 */ 4204 function createRTCDataChannelStats(report) { 4205 return { 4206 bytesReceived: undefined, 4207 bytesSent: undefined, 4208 datachannelid: report.stat('datachannelid'), 4209 id: report.id, 4210 label: report.stat('label'), 4211 messagesReceived: undefined, 4212 messagesSent: undefined, 4213 protocol: report.stat('protocol'), 4214 state: report.stat('state'), 4215 timestamp: Date.parse(report.timestamp), 4216 transportId: report.stat('transportId'), 4217 type: 'data-channel', 4218 }; 4219 } 4220 /** 4221 * @param {number} inMs - A time in milliseconds 4222 * @returns {number} The time in seconds 4223 */ 4224 function convertMsToSeconds(inMs) { 4225 return isNaN(inMs) || inMs === '' 4226 ? undefined 4227 : parseInt(inMs, 10) / 1000; 4228 } 4229 /** 4230 * @param {string} type - A type in the legacy format 4231 * @returns {string} The type adjusted to new standards for known naming changes 4232 */ 4233 function translateCandidateType(type) { 4234 switch (type) { 4235 case 'peerreflexive': 4236 return 'prflx'; 4237 case 'serverreflexive': 4238 return 'srflx'; 4239 case 'host': 4240 case 'relay': 4241 default: 4242 return type; 4243 } 4244 } 4245 function getInt(report, statName) { 4246 const stat = report.stat(statName); 4247 return isPresent(report, statName) 4248 ? parseInt(stat, 10) 4249 : undefined; 4250 } 4251 function getFloat(report, statName) { 4252 const stat = report.stat(statName); 4253 return isPresent(report, statName) 4254 ? parseFloat(stat) 4255 : undefined; 4256 } 4257 function getBoolean(report, statName) { 4258 const stat = report.stat(statName); 4259 return isPresent(report, statName) 4260 ? (stat === 'true' || stat === true) 4261 : undefined; 4262 } 4263 function isPresent(report, statName) { 4264 const stat = report.stat(statName); 4265 return typeof stat !== 'undefined' && stat !== ''; 4266 } 4267 4268 // @ts-nocheck 4269 // tslint:disable no-empty 4270 const ERROR_PEER_CONNECTION_NULL = 'PeerConnection is null';
4271 const ERROR_WEB_RTC_UNSUPPORTED = 'WebRTC statistics are unsupported'; 4272 /** 4273 * Helper function to find a specific stat from a report. 4274 * Some environment provide the stats report as a map (regular browsers) 4275 * but some provide stats report as an array (citrix vdi) 4276 * @private 4277 */ 4278 function findStatById(report, id) { 4279 if (typeof report.get === 'function') { 4280 return report.get(id); 4281 } 4282 return report.find(s => s.id === id); 4283 } 4284 /** 4285 * Generate WebRTC statistics report for the given {@link PeerConnection} 4286 * @param {PeerConnection} peerConnection - Target connection. 4287 * @return {Promise<RTCStatsReport>} WebRTC RTCStatsReport object 4288 */ 4289 function getRTCStatsReport(peerConnection) { 4290 if (!peerConnection) { 4291 return Promise.reject(new InvalidArgumentError(ERROR_PEER_CONNECTION_NULL)); 4292 } 4293 if (typeof peerConnection.getStats !== 'function') { 4294 return Promise.reject(new NotSupportedError(ERROR_WEB_RTC_UNSUPPORTED)); 4295 } 4296 let promise; 4297 try { 4298 promise = peerConnection.getStats(); 4299 } 4300 catch (e) { 4301 promise = new Promise(resolve => peerConnection.getStats(resolve)).then(MockRTCStatsReport.fromRTCStatsResponse); 4302 } 4303 return promise; 4304 } 4305 /** 4306 * @typedef {Object} StatsOptions 4307 * Used for testing to inject and extract methods. 4308 * @property {function} [createRTCSample] - Method for parsing an RTCStatsReport 4309 */ 4310 /** 4311 * Collects any WebRTC statistics for the given {@link PeerConnection} 4312 * @param {PeerConnection} peerConnection - Target connection. 4313 * @param {StatsOptions} options - List of custom options. 4314 * @return {Promise<RTCSample>} Universally-formatted version of RTC stats. 4315 */ 4316 function getRTCStats(peerConnection, options) { 4317 options = Object.assign({ createRTCSample }, options); 4318 return getRTCStatsReport(peerConnection).then(options.createRTCSample); 4319 } 4320 /** 4321 * Generate WebRTC stats report containing relevant information about ICE candidates for the given {@link PeerConnection} 4322 * @param {PeerConnection} peerConnection - Target connection. 4323 * @return {Promise<RTCIceCandidateStatsReport>} RTCIceCandidateStatsReport object 4324 */ 4325 function getRTCIceCandidateStatsReport(peerConnection) { 4326 return getRTCStatsReport(peerConnection).then((report) => { 4327 // Find the relevant information needed to determine selected candidates later 4328 const { candidatePairs, localCandidates, remoteCandidates, transport, } = Array.from(report.values()).reduce((rval, stat) => { 4329 ['candidatePairs', 'localCandidates', 'remoteCandidates'].forEach((prop) => { 4330 if (!rval[prop]) { 4331 rval[prop] = []; 4332 } 4333 }); 4334 switch (stat.type) { 4335 case 'candidate-pair': 4336 rval.candidatePairs.push(stat); 4337 break; 4338 case 'local-candidate': 4339 rval.localCandidates.push(stat); 4340 break; 4341 case 'remote-candidate': 4342 rval.remoteCandidates.push(stat); 4343 break; 4344 case 'transport': 4345 // This transport is the one being used if selectedCandidatePairId is populated 4346 if (stat.selectedCandidatePairId) { 4347 rval.transport = stat; 4348 } 4349 break; 4350 } 4351 return rval; 4352 }, {}); 4353 // This is a report containing information about the selected candidates, such as IDs 4354 // This is coming from WebRTC stats directly and doesn't contain the actual ICE Candidates info 4355 const selectedCandidatePairReport = candidatePairs.find(pair => 4356 // Firefox 4357 pair.selected || 4358 // Spec-compliant way 4359 (transport && pair.id === transport.selectedCandidatePairId)); 4360 let selectedIceCandidatePairStats; 4361 if (selectedCandidatePairReport) { 4362 selectedIceCandidatePairStats = { 4363 localCandidate: localCandidates.find(candidate => candidate.id === selectedCandidatePairReport.localCandidateId), 4364 remoteCandidate: remoteCandidates.find(candidate => candidate.id === selectedCandidatePairReport.remoteCandidateId), 4365 }; 4366 } 4367 // Build the return object 4368 return { 4369 iceCandidateStats: [...localCandidates, ...remoteCandidates], 4370 selectedIceCandidatePairStats, 4371 }; 4372 }); 4373 } 4374 /** 4375 * @typedef {Object} RTCSample - A sample containing relevant WebRTC stats information. 4376 * @property {Number} [timestamp] 4377 * @property {String} [codecName] - MimeType name of the codec being used by the outbound audio stream 4378 * @property {Number} [rtt] - Round trip time 4379 * @property {Number} [jitter] 4380 * @property {Number} [packetsSent] 4381 * @property {Number} [packetsLost] 4382 * @property {Number} [packetsReceived] 4383 * @property {Number} [bytesReceived] 4384 * @property {Number} [bytesSent] 4385 * @property {Number} [localAddress] 4386 * @property {Number} [remoteAddress] 4387 */ 4388 function RTCSample() { } 4389 /** 4390 * Create an RTCSample object from an RTCStatsReport 4391 * @private 4392 * @param {RTCStatsReport} statsReport 4393 * @returns {RTCSample} 4394 */ 4395 function createRTCSample(statsReport) {
4396 let activeTransportId = null; 4397 const sample = new RTCSample(); 4398 let fallbackTimestamp; 4399 Array.from(statsReport.values()).forEach(stats => { 4400 // Skip isRemote tracks which will be phased out completely and break in FF66. 4401 if (stats.isRemote) { 4402 return; 4403 } 4404 // Firefox hack -- Older firefox doesn't have dashes in type names 4405 const type = stats.type.replace('-', ''); 4406 fallbackTimestamp = fallbackTimestamp || stats.timestamp; 4407 // (rrowland) As I understand it, this is supposed to come in on remote-inbound-rtp but it's 4408 // currently coming in on remote-outbound-rtp, so I'm leaving this outside the switch until 4409 // the appropriate place to look is cleared up. 4410 if (stats.remoteId) { 4411 const remote = findStatById(statsReport, stats.remoteId); 4412 if (remote && remote.roundTripTime) { 4413 sample.rtt = remote.roundTripTime * 1000; 4414 } 4415 } 4416 switch (type) { 4417 case 'inboundrtp': 4418 sample.timestamp = sample.timestamp || stats.timestamp; 4419 sample.jitter = stats.jitter * 1000; 4420 sample.packetsLost = stats.packetsLost; 4421 sample.packetsReceived = stats.packetsReceived; 4422 sample.bytesReceived = stats.bytesReceived; 4423 break; 4424 case 'outboundrtp': 4425 sample.timestamp = stats.timestamp; 4426 sample.packetsSent = stats.packetsSent; 4427 sample.bytesSent = stats.bytesSent; 4428 if (stats.codecId) { 4429 const codec = findStatById(statsReport, stats.codecId); 4430 sample.codecName = codec 4431 ? codec.mimeType && codec.mimeType.match(/(.*\/)?(.*)/)[2] 4432 : stats.codecId; 4433 } 4434 break; 4435 case 'transport': 4436 activeTransportId = stats.id; 4437 break; 4438 } 4439 }); 4440 if (!sample.timestamp) { 4441 sample.timestamp = fallbackTimestamp; 4442 } 4443 const activeTransport = findStatById(statsReport, activeTransportId);
4444 if (!activeTransport) { 4445 return sample; 4446 } 4447 const selectedCandidatePair = findStatById(statsReport, activeTransport.selectedCandidatePairId); 4448 if (!selectedCandidatePair) { 4449 return sample; 4450 } 4451 const localCandidate = findStatById(statsReport, selectedCandidatePair.localCandidateId); 4452 const remoteCandidate = findStatById(statsReport, selectedCandidatePair.remoteCandidateId); 4453 if (!sample.rtt) { 4454 sample.rtt = selectedCandidatePair && 4455 (selectedCandidatePair.currentRoundTripTime * 1000); 4456 } 4457 Object.assign(sample, { 4458 // ip is deprecated. use address first then ip if on older versions of browser 4459 localAddress: localCandidate && (localCandidate.address || localCandidate.ip), 4460 remoteAddress: remoteCandidate && (remoteCandidate.address || remoteCandidate.ip), 4461 }); 4462 return sample; 4463 } 4464 4465 /** 4466 * Runs some tests to identify issues, if any, prohibiting successful calling. 4467 */ 4468 class PreflightTest extends eventsExports.EventEmitter { 4469 /** 4470 * Construct a {@link PreflightTest} instance. 4471 * @param token - A Twilio JWT token string. 4472 * @param options 4473 */ 4474 constructor(token, options) { 4475 super(); 4476 /** 4477 * Whether this test has already logged an insights-connection-warning. 4478 */ 4479 this._hasInsightsErrored = false; 4480 /** 4481 * An instance of Logger to use. 4482 */ 4483 this._log = new Log('PreflightTest'); 4484 /** 4485 * Network related timing measurements for this test 4486 */ 4487 this._networkTiming = {}; 4488 /** 4489 * The options passed to {@link PreflightTest} constructor 4490 */ 4491 this._options = { 4492 codecPreferences: [Call.Codec.PCMU, Call.Codec.Opus], 4493 edge: 'roaming', 4494 fakeMicInput: false, 4495 logLevel: 'error', 4496 signalingTimeoutMs: 10000, 4497 }; 4498 /** 4499 * Current status of this test 4500 */ 4501 this._status = PreflightTest.Status.Connecting; 4502 Object.assign(this._options, options); 4503 this._samples = []; 4504 this._warnings = []; 4505 this._startTime = Date.now(); 4506 this._initDevice(token, Object.assign(Object.assign({}, this._options), { fileInputStream: this._options.fakeMicInput ? 4507 this._getStreamFromFile() : undefined })); 4508 // Device sets the loglevel so start logging after initializing the device. 4509 // Then selectively log options that users can modify. 4510 const userOptions = [ 4511 'codecPreferences', 4512 'edge', 4513 'fakeMicInput', 4514 'logLevel', 4515 'signalingTimeoutMs', 4516 ]; 4517 const userOptionOverrides = [ 4518 'audioContext', 4519 'deviceFactory', 4520 'fileInputStream', 4521 'getRTCIceCandidateStatsReport', 4522 'iceServers', 4523 'rtcConfiguration', 4524 ]; 4525 if (typeof options === 'object') { 4526 const toLog = Object.assign({}, options); 4527 Object.keys(toLog).forEach((key) => { 4528 if (!userOptions.includes(key) && !userOptionOverrides.includes(key)) { 4529 delete toLog[key]; 4530 } 4531 if (userOptionOverrides.includes(key)) { 4532 toLog[key] = true; 4533 } 4534 }); 4535 this._log.debug('.constructor', JSON.stringify(toLog)); 4536 } 4537 } 4538 /** 4539 * Stops the current test and raises a failed event. 4540 */ 4541 stop() { 4542 this._log.debug('.stop'); 4543 const error = new GeneralErrors.CallCancelledError(); 4544 if (this._device) { 4545 this._device.once(Device.EventName.Unregistered, () => this._onFailed(error)); 4546 this._device.destroy(); 4547 } 4548 else { 4549 this._onFailed(error); 4550 } 4551 } 4552 /** 4553 * Emit a {PreflightTest.Warning} 4554 */ 4555 _emitWarning(name, description, rtcWarning) { 4556 const warning = { name, description }; 4557 if (rtcWarning) { 4558 warning.rtcWarning = rtcWarning; 4559 } 4560 this._warnings.push(warning); 4561 this._log.debug(`#${PreflightTest.Events.Warning}`, JSON.stringify(warning));
4562 this.emit(PreflightTest.Events.Warning, warning); 4563 } 4564 /** 4565 * Returns call quality base on the RTC Stats 4566 */ 4567 _getCallQuality(mos) { 4568 if (mos > 4.2) { 4569 return PreflightTest.CallQuality.Excellent; 4570 } 4571 else if (mos >= 4.1 && mos <= 4.2) { 4572 return PreflightTest.CallQuality.Great; 4573 } 4574 else if (mos >= 3.7 && mos <= 4) { 4575 return PreflightTest.CallQuality.Good; 4576 } 4577 else if (mos >= 3.1 && mos <= 3.6) { 4578 return PreflightTest.CallQuality.Fair; 4579 } 4580 else { 4581 return PreflightTest.CallQuality.Degraded; 4582 } 4583 } 4584 /** 4585 * Returns the report for this test. 4586 */ 4587 _getReport() { 4588 var _a, _b, _c; 4589 const stats = this._getRTCStats(); 4590 const testTiming = { start: this._startTime }; 4591 if (this._endTime) { 4592 testTiming.end = this._endTime; 4593 testTiming.duration = this._endTime - this._startTime; 4594 } 4595 const report = { 4596 callSid: this._callSid, 4597 edge: this._edge, 4598 iceCandidateStats: (_b = (_a = this._rtcIceCandidateStatsReport) === null || _a === void 0 ? void 0 : _a.iceCandidateStats) !== null && _b !== void 0 ? _b : [], 4599 networkTiming: this._networkTiming, 4600 samples: this._samples, 4601 selectedEdge: this._options.edge, 4602 stats, 4603 testTiming, 4604 totals: this._getRTCSampleTotals(), 4605 warnings: this._warnings, 4606 }; 4607 const selectedIceCandidatePairStats = (_c = this._rtcIceCandidateStatsReport) === null || _c === void 0 ? void 0 : _c.selectedIceCandidatePairStats; 4608 if (selectedIceCandidatePairStats) { 4609 report.selectedIceCandidatePairStats = selectedIceCandidatePairStats; 4610 report.isTurnRequired = selectedIceCandidatePairStats.localCandidate.candidateType === 'relay' 4611 || selectedIceCandidatePairStats.remoteCandidate.candidateType === 'relay'; 4612 } 4613 if (stats) { 4614 report.callQuality = this._getCallQuality(stats.mos.average); 4615 } 4616 return report; 4617 } 4618 /** 4619 * Returns RTC stats totals for this test 4620 */ 4621 _getRTCSampleTotals() { 4622 if (!this._latestSample) { 4623 return; 4624 } 4625 return Object.assign({}, this._latestSample.totals); 4626 } 4627 /** 4628 * Returns RTC related stats captured during the test call 4629 */ 4630 _getRTCStats() { 4631 const firstMosSampleIdx = this._samples.findIndex(sample => typeof sample.mos === 'number' && sample.mos > 0); 4632 const samples = firstMosSampleIdx >= 0 4633 ? this._samples.slice(firstMosSampleIdx) 4634 : []; 4635 if (!samples || !samples.length) { 4636 return; 4637 } 4638 return ['jitter', 'mos', 'rtt'].reduce((statObj, stat) => { 4639 const values = samples.map(s => s[stat]); 4640 return Object.assign(Object.assign({}, statObj), { [stat]: { 4641 average: Number((values.reduce((total, value) => total + value) / values.length).toPrecision(5)), 4642 max: Math.max(...values), 4643 min: Math.min(...values), 4644 } }); 4645 }, {}); 4646 } 4647 /** 4648 * Returns a MediaStream from a media file 4649 */ 4650 _getStreamFromFile() { 4651 const audioContext = this._options.audioContext; 4652 if (!audioContext) { 4653 throw new NotSupportedError('Cannot fake input audio stream: AudioContext is not supported by this browser.'); 4654 } 4655 const audioEl = new Audio(COWBELL_AUDIO_URL); 4656 audioEl.addEventListener('canplaythrough', () => audioEl.play()); 4657 if (typeof audioEl.setAttribute === 'function') { 4658 audioEl.setAttribute('crossorigin', 'anonymous'); 4659 } 4660 const src = audioContext.createMediaElementSource(audioEl); 4661 const dest = audioContext.createMediaStreamDestination(); 4662 src.connect(dest); 4663 return dest.stream; 4664 } 4665 /** 4666 * Initialize the device 4667 */ 4668 _initDevice(token, options) { 4669 try { 4670 this._device = new (options.deviceFactory || Device)(token, { 4671 chunderw: options.chunderw, 4672 codecPreferences: options.codecPreferences, 4673 edge: options.edge, 4674 eventgw: options.eventgw, 4675 fileInputStream: options.fileInputStream, 4676 logLevel: options.logLevel, 4677 preflight: true, 4678 }); 4679 this._device.once(Device.EventName.Registered, () => { 4680 this._onDeviceRegistered(); 4681 }); 4682 this._device.once(Device.EventName.Error, (error) => { 4683 this._onDeviceError(error); 4684 }); 4685 this._device.register(); 4686 } 4687 catch (error) { 4688 // We want to return before failing so the consumer can capture the event 4689 setTimeout(() => { 4690 this._onFailed(error); 4691 }); 4692 return; 4693 } 4694 this._signalingTimeoutTimer = setTimeout(() => { 4695 this._onDeviceError(new SignalingErrors.ConnectionError('WebSocket Connection Timeout')); 4696 }, options.signalingTimeoutMs); 4697 } 4698 /** 4699 * Called on {@link Device} error event 4700 * @param error 4701 */ 4702 _onDeviceError(error) { 4703 this._device.destroy(); 4704 this._onFailed(error); 4705 } 4706 /** 4707 * Called on {@link Device} ready event 4708 */ 4709 _onDeviceRegistered() { 4710 return __awaiter(this, void 0, void 0, function* () { 4711 clearTimeout(this._echoTimer); 4712 clearTimeout(this._signalingTimeoutTimer); 4713 this._call = yield this._device.connect({ 4714 rtcConfiguration: this._options.rtcConfiguration, 4715 }); 4716 this._networkTiming.signaling = { start: Date.now() }; 4717 this._setupCallHandlers(this._call); 4718 this._edge = this._device.edge || undefined; 4719 if (this._options.fakeMicInput) { 4720 this._echoTimer = setTimeout(() => this._device.disconnectAll(), ECHO_TEST_DURATION); 4721 const audio = this._device.audio; 4722 if (audio) { 4723 audio.disconnect(false); 4724 audio.outgoing(false); 4725 } 4726 } 4727 this._call.once('disconnect', () => { 4728 this._device.once(Device.EventName.Unregistered, () => this._onUnregistered()); 4729 this._device.destroy(); 4730 }); 4731 const publisher = this._call['_publisher']; 4732 publisher.on('error', () => { 4733 if (!this._hasInsightsErrored) { 4734 this._emitWarning('insights-connection-error', 'Received an error when attempting to connect to Insights gateway'); 4735 } 4736 this._hasInsightsErrored = true; 4737 }); 4738 }); 4739 } 4740 /** 4741 * Called when there is a fatal error 4742 * @param error 4743 */ 4744 _onFailed(error) { 4745 clearTimeout(this._echoTimer); 4746 clearTimeout(this._signalingTimeoutTimer); 4747 this._releaseHandlers(); 4748 this._endTime = Date.now(); 4749 this._status = PreflightTest.Status.Failed; 4750 this._log.debug(`#${PreflightTest.Events.Failed}`, error);
4751 this.emit(PreflightTest.Events.Failed, error); 4752 } 4753 /** 4754 * Called when the device goes offline. 4755 * This indicates that the test has been completed, but we won't know if it failed or not. 4756 * The onError event will be the indicator whether the test failed. 4757 */ 4758 _onUnregistered() { 4759 // We need to make sure we always execute preflight.on('completed') last 4760 // as client SDK sometimes emits 'offline' event before emitting fatal errors. 4761 setTimeout(() => { 4762 if (this._status === PreflightTest.Status.Failed) { 4763 return; 4764 } 4765 clearTimeout(this._echoTimer); 4766 clearTimeout(this._signalingTimeoutTimer); 4767 this._releaseHandlers(); 4768 this._endTime = Date.now(); 4769 this._status = PreflightTest.Status.Completed; 4770 this._report = this._getReport(); 4771 this._log.debug(`#${PreflightTest.Events.Completed}`, JSON.stringify(this._report)); 4772 this.emit(PreflightTest.Events.Completed, this._report); 4773 }, 10); 4774 } 4775 /** 4776 * Clean up all handlers for device and call 4777 */ 4778 _releaseHandlers() { 4779 [this._device, this._call].forEach((emitter) => { 4780 if (emitter) { 4781 emitter.eventNames().forEach((name) => emitter.removeAllListeners(name)); 4782 } 4783 }); 4784 } 4785 /** 4786 * Setup the event handlers for the {@link Call} of the test call 4787 * @param call 4788 */ 4789 _setupCallHandlers(call) { 4790 if (this._options.fakeMicInput) { 4791 // When volume events start emitting, it means all audio outputs have been created. 4792 // Let's mute them if we're using fake mic input. 4793 call.once('volume', () => { 4794 call['_mediaHandler'].outputs 4795 .forEach((output) => output.audio.muted = true); 4796 }); 4797 } 4798 call.on('warning', (name, data) => { 4799 this._emitWarning(name, 'Received an RTCWarning. See .rtcWarning for the RTCWarning', data); 4800 }); 4801 call.once('accept', () => { 4802 this._callSid = call['_mediaHandler'].callSid; 4803 this._status = PreflightTest.Status.Connected; 4804 this._log.debug(`#${PreflightTest.Events.Connected}`); 4805 this.emit(PreflightTest.Events.Connected); 4806 }); 4807 call.on('sample', (sample) => __awaiter(this, void 0, void 0, function* () { 4808 // RTC Stats are ready. We only need to get ICE candidate stats report once. 4809 if (!this._latestSample) { 4810 this._rtcIceCandidateStatsReport = yield (this._options.getRTCIceCandidateStatsReport || getRTCIceCandidateStatsReport)(call['_mediaHandler'].version.pc); 4811 } 4812 this._latestSample = sample; 4813 this._samples.push(sample); 4814 this._log.debug(`#${PreflightTest.Events.Sample}`, JSON.stringify(sample)); 4815 this.emit(PreflightTest.Events.Sample, sample); 4816 })); 4817 // TODO: Update the following once the SDK supports emitting these events 4818 // Let's shim for now 4819 [{ 4820 reportLabel: 'peerConnection', 4821 type: 'pcconnection', 4822 }, { 4823 reportLabel: 'ice', 4824 type: 'iceconnection', 4825 }, { 4826 reportLabel: 'dtls', 4827 type: 'dtlstransport', 4828 }, { 4829 reportLabel: 'signaling', 4830 type: 'signaling', 4831 }].forEach(({ type, reportLabel }) => { 4832 const handlerName = `on${type}statechange`; 4833 const originalHandler = call['_mediaHandler'][handlerName]; 4834 call['_mediaHandler'][handlerName] = (state) => { 4835 const timing = this._networkTiming[reportLabel] 4836 = this._networkTiming[reportLabel] || { start: 0 }; 4837 if (state === 'connecting' || state === 'checking') { 4838 timing.start = Date.now(); 4839 } 4840 else if ((state === 'connected' || state === 'stable') && !timing.duration) { 4841 timing.end = Date.now(); 4842 timing.duration = timing.end - timing.start; 4843 } 4844 originalHandler(state); 4845 }; 4846 }); 4847 } 4848 /** 4849 * The callsid generated for the test call. 4850 */ 4851 get callSid() { 4852 return this._callSid; 4853 } 4854 /** 4855 * A timestamp in milliseconds of when the test ended. 4856 */ 4857 get endTime() { 4858 return this._endTime; 4859 } 4860 /** 4861 * The latest WebRTC sample collected. 4862 */ 4863 get latestSample() { 4864 return this._latestSample; 4865 } 4866 /** 4867 * The report for this test. 4868 */ 4869 get report() { 4870 return this._report; 4871 } 4872 /** 4873 * A timestamp in milliseconds of when the test started. 4874 */ 4875 get startTime() { 4876 return this._startTime; 4877 } 4878 /** 4879 * The status of the test. 4880 */ 4881 get status() { 4882 return this._status; 4883 } 4884 } 4885 /** 4886 * @mergeModuleWith PreflightTest 4887 */ 4888 (function (PreflightTest) { 4889 (function (CallQuality) { 4890 /** 4891 * If the average mos is over 4.2. 4892 */ 4893 CallQuality["Excellent"] = "excellent";
4894 /** 4895 * If the average mos is between 4.1 and 4.2 both inclusive. 4896 */ 4897 CallQuality["Great"] = "great"; 4898 /** 4899 * If the average mos is between 3.7 and 4.0 both inclusive. 4900 */ 4901 CallQuality["Good"] = "good"; 4902 /** 4903 * If the average mos is between 3.1 and 3.6 both inclusive. 4904 */ 4905 CallQuality["Fair"] = "fair"; 4906 /** 4907 * If the average mos is 3.0 or below. 4908 */ 4909 CallQuality["Degraded"] = "degraded"; 4910 })(PreflightTest.CallQuality || (PreflightTest.CallQuality = {})); 4911 (function (Events) { 4912 /** 4913 * See [[PreflightTest.completedEvent]] 4914 */ 4915 Events["Completed"] = "completed"; 4916 /** 4917 * See [[PreflightTest.connectedEvent]] 4918 */ 4919 Events["Connected"] = "connected"; 4920 /** 4921 * See [[PreflightTest.failedEvent]] 4922 */ 4923 Events["Failed"] = "failed"; 4924 /** 4925 * See [[PreflightTest.sampleEvent]] 4926 */ 4927 Events["Sample"] = "sample"; 4928 /** 4929 * See [[PreflightTest.warningEvent]] 4930 */ 4931 Events["Warning"] = "warning"; 4932 })(PreflightTest.Events || (PreflightTest.Events = {})); 4933 (function (Status) { 4934 /** 4935 * Call to Twilio has initiated. 4936 */ 4937 Status["Connecting"] = "connecting"; 4938 /** 4939 * Call to Twilio has been established. 4940 */ 4941 Status["Connected"] = "connected"; 4942 /** 4943 * The connection to Twilio has been disconnected and the test call has completed. 4944 */ 4945 Status["Completed"] = "completed"; 4946 /** 4947 * The test has stopped and failed. 4948 */ 4949 Status["Failed"] = "failed"; 4950 })(PreflightTest.Status || (PreflightTest.Status = {})); 4951 })(PreflightTest || (PreflightTest = {})); 4952 4953 const WebSocket = globalThis.WebSocket; 4954 const CONNECT_SUCCESS_TIMEOUT = 10000; 4955 const CONNECT_TIMEOUT = 5000; 4956 const HEARTBEAT_TIMEOUT = 15000; 4957 const MAX_PREFERRED_DURATION = 15000; 4958 const MAX_PRIMARY_DURATION = Infinity; 4959 const MAX_PREFERRED_DELAY = 1000; 4960 const MAX_PRIMARY_DELAY = 20000; 4961 /** 4962 * All possible states of WSTransport. 4963 */ 4964 var WSTransportState; 4965 (function (WSTransportState) { 4966 /** 4967 * The WebSocket is not open but is trying to connect. 4968 */ 4969 WSTransportState["Connecting"] = "connecting"; 4970 /** 4971 * The WebSocket is not open and is not trying to connect. 4972 */ 4973 WSTransportState["Closed"] = "closed"; 4974 /** 4975 * The underlying WebSocket is open and active. 4976 */ 4977 WSTransportState["Open"] = "open"; 4978 })(WSTransportState || (WSTransportState = {})); 4979 /** 4980 * WebSocket Transport 4981 */ 4982 class WSTransport extends eventsExports.EventEmitter { 4983 /** 4984 * @constructor 4985 * @param uris - List of URI of the endpoints to connect to. 4986 * @param [options] - Constructor options. 4987 */ 4988 constructor(uris, options = {}) { 4989 super(); 4990 /** 4991 * The current state of the WSTransport. 4992 */ 4993 this.state = WSTransportState.Closed; 4994 /** 4995 * Start timestamp values for backoffs. 4996 */ 4997 this._backoffStartTime = { 4998 preferred: null, 4999 primary: null, 5000 }; 5001 /** 5002 * The URI that the transport is connecting or connected to. The value of this 5003 * property is `null` if a connection attempt has not been made yet. 5004 */ 5005 this._connectedUri = null; 5006 /** 5007 * An instance of Logger to use. 5008 */ 5009 this._log = new Log('WSTransport'); 5010 /** 5011 * Whether we should attempt to fallback if we receive an applicable error 5012 * when trying to connect to a signaling endpoint. 5013 */ 5014 this._shouldFallback = false; 5015 /** 5016 * The current uri index that the transport is connected to. 5017 */ 5018 this._uriIndex = 0; 5019 /** 5020 * Move the uri index to the next index 5021 * If the index is at the end, the index goes back to the first one. 5022 */ 5023 this._moveUriIndex = () => { 5024 this._uriIndex++; 5025 if (this._uriIndex >= this._uris.length) { 5026 this._uriIndex = 0; 5027 } 5028 }; 5029 /**
5030 * Called in response to WebSocket#close event. 5031 */ 5032 this._onSocketClose = (event) => { 5033 this._log.error(`Received websocket close event code: ${event.code}. Reason: ${event.reason}`); 5034 // 1006: Abnormal close. When the server is unreacheable 5035 // 1015: TLS Handshake error 5036 if (event.code === 1006 || event.code === 1015) { 5037 this.emit('error', { 5038 code: 31005, 5039 message: event.reason || 5040 'Websocket connection to Twilio\'s signaling servers were ' + 5041 'unexpectedly ended. If this is happening consistently, there may ' + 5042 'be an issue resolving the hostname provided. If a region or an ' + 5043 'edge is being specified in Device setup, ensure it is valid.', 5044 twilioError: new SignalingErrors.ConnectionError(), 5045 }); 5046 const wasConnected = ( 5047 // Only in Safari and certain Firefox versions, on network interruption, websocket drops right away with 1006 5048 // Let's check current state if it's open, meaning we should not fallback 5049 // because we're coming from a previously connected session 5050 this.state === WSTransportState.Open || 5051 // But on other browsers, websocket doesn't drop 5052 // but our heartbeat catches it, setting the internal state to "Connecting".
5053 // With this, we should check the previous state instead. 5054 this._previousState === WSTransportState.Open); 5055 // Only fallback if this is not the first error 5056 // and if we were not connected previously 5057 if (this._shouldFallback || !wasConnected) { 5058 this._moveUriIndex(); 5059 } 5060 this._shouldFallback = true; 5061 } 5062 this._closeSocket(); 5063 }; 5064 /** 5065 * Called in response to WebSocket#error event. 5066 */ 5067 this._onSocketError = (err) => { 5068 this._log.error(`WebSocket received error: ${err.message}`); 5069 this.emit('error', { 5070 code: 31000, 5071 message: err.message || 'WSTransport socket error', 5072 twilioError: new SignalingErrors.ConnectionDisconnected(), 5073 }); 5074 }; 5075 /** 5076 * Called in response to WebSocket#message event. 5077 */ 5078 this._onSocketMessage = (message) => { 5079 // Clear heartbeat timeout on any incoming message, as they 5080 // all indicate an active connection. 5081 this._setHeartbeatTimeout(); 5082 // Filter and respond to heartbeats 5083 if (this._socket && message.data === '\n') { 5084 this._socket.send('\n'); 5085 this._log.debug('heartbeat'); 5086 return; 5087 } 5088 if (message && typeof message.data === 'string') { 5089 this._log.debug(`Received: ${message.data}`); 5090 } 5091 this.emit('message', message); 5092 }; 5093 /** 5094 * Called in response to WebSocket#open event. 5095 */ 5096 this._onSocketOpen = () => { 5097 this._log.info('WebSocket opened successfully.'); 5098 this._timeOpened = Date.now(); 5099 this._shouldFallback = false; 5100 this._setState(WSTransportState.Open); 5101 clearTimeout(this._connectTimeout); 5102 this._resetBackoffs(); 5103 this._setHeartbeatTimeout(); 5104 this.emit('open'); 5105 }; 5106 this._options = Object.assign(Object.assign({}, WSTransport.defaultConstructorOptions), options); 5107 this._uris = uris; 5108 this._backoff = this._setupBackoffs(); 5109 } 5110 /** 5111 * Close the WebSocket, and don't try to reconnect. 5112 */ 5113 close() { 5114 this._log.info('WSTransport.close() called...'); 5115 this._close(); 5116 } 5117 /** 5118 * Attempt to open a WebSocket connection. 5119 */ 5120 open() { 5121 this._log.info('WSTransport.open() called...'); 5122 if (this._socket && 5123 (this._socket.readyState === WebSocket.CONNECTING || 5124 this._socket.readyState === WebSocket.OPEN)) { 5125 this._log.info('WebSocket already open.'); 5126 return; 5127 } 5128 if (this._preferredUri) { 5129 this._connect(this._preferredUri); 5130 } 5131 else { 5132 this._connect(this._uris[this._uriIndex]); 5133 } 5134 } 5135 /** 5136 * Send a message through the WebSocket connection. 5137 * @param message - A message to send to the endpoint. 5138 * @returns Whether the message was sent. 5139 */ 5140 send(message) { 5141 this._log.debug(`Sending: ${message}`); 5142 // We can't send the message if the WebSocket isn't open 5143 if (!this._socket || this._socket.readyState !== WebSocket.OPEN) { 5144 this._log.debug('Cannot send message. WebSocket is not open.'); 5145 return false; 5146 } 5147 try { 5148 this._socket.send(message); 5149 } 5150 catch (e) { 5151 // Some unknown error occurred. Reset the socket to get a fresh session. 5152 this._log.error('Error while sending message:', e.message); 5153 this._closeSocket(); 5154 return false; 5155 } 5156 return true; 5157 } 5158 /** 5159 * Update the preferred URI to connect to. Useful for Call signaling 5160 * reconnection, which requires connecting on the same edge. If `null` is 5161 * passed, the preferred URI is unset and the original `uris` array and 5162 * `uriIndex` is used to determine the signaling URI to connect to. 5163 * @param uri 5164 */ 5165 updatePreferredURI(uri) { 5166 this._preferredUri = uri; 5167 } 5168 /** 5169 * Update acceptable URIs to reconnect to. Resets the URI index to 0. 5170 */ 5171 updateURIs(uris) { 5172 if (typeof uris === 'string') { 5173 uris = [uris]; 5174 } 5175 this._uris = uris; 5176 this._uriIndex = 0; 5177 } 5178 /** 5179 * Close the WebSocket, and don't try to reconnect. 5180 */ 5181 _close() { 5182 this._setState(WSTransportState.Closed); 5183 this._closeSocket(); 5184 } 5185 /** 5186 * Close the WebSocket and remove all event listeners. 5187 */ 5188 _closeSocket() { 5189 clearTimeout(this._connectTimeout); 5190 clearTimeout(this._heartbeatTimeout); 5191 this._log.info('Closing and cleaning up WebSocket...'); 5192 if (!this._socket) { 5193 this._log.info('No WebSocket to clean up.'); 5194 return; 5195 } 5196 this._socket.removeEventListener('close', this._onSocketClose); 5197 this._socket.removeEventListener('error', this._onSocketError); 5198 this._socket.removeEventListener('message', this._onSocketMessage); 5199 this._socket.removeEventListener('open', this._onSocketOpen); 5200 if (this._socket.readyState === WebSocket.CONNECTING || 5201 this._socket.readyState === WebSocket.OPEN) { 5202 this._socket.close(); 5203 } 5204 // Reset backoff counter if connection was open for long enough to be considered successful 5205 if (this._timeOpened && Date.now() - this._timeOpened > CONNECT_SUCCESS_TIMEOUT) { 5206 this._resetBackoffs(); 5207 } 5208 if (this.state !== WSTransportState.Closed) { 5209 this._performBackoff(); 5210 } 5211 delete this._socket;
5212 this.emit('close'); 5213 } 5214 /** 5215 * Attempt to connect to the endpoint via WebSocket. 5216 * @param [uri] - URI string to connect to. 5217 * @param [retryCount] - Retry number, if this is a retry. Undefined if 5218 * first attempt, 1+ if a retry. 5219 */ 5220 _connect(uri, retryCount) { 5221 this._log.info(typeof retryCount === 'number' 5222 ? `Attempting to reconnect (retry #${retryCount})...` 5223 : 'Attempting to connect...'); 5224 this._closeSocket(); 5225 this._setState(WSTransportState.Connecting); 5226 this._connectedUri = uri; 5227 try { 5228 this._socket = new this._options.WebSocket(this._connectedUri); 5229 } 5230 catch (e) { 5231 this._log.error('Could not connect to endpoint:', e.message); 5232 this._close(); 5233 this.emit('error', { 5234 code: 31000, 5235 message: e.message || `Could not connect to ${this._connectedUri}`, 5236 twilioError: new SignalingErrors.ConnectionDisconnected(), 5237 }); 5238 return; 5239 } 5240 this._socket.addEventListener('close', this._onSocketClose); 5241 this._socket.addEventListener('error', this._onSocketError); 5242 this._socket.addEventListener('message', this._onSocketMessage); 5243 this._socket.addEventListener('open', this._onSocketOpen); 5244 delete this._timeOpened; 5245 this._connectTimeout = setTimeout(() => { 5246 this._log.info('WebSocket connection attempt timed out.'); 5247 this._moveUriIndex(); 5248 this._closeSocket(); 5249 }, this._options.connectTimeoutMs); 5250 } 5251 /** 5252 * Perform a backoff. If a preferred URI is set (not null), then backoff 5253 * using the preferred mechanism. Otherwise, use the primary mechanism. 5254 */ 5255 _performBackoff() { 5256 if (this._preferredUri) { 5257 this._log.info('Preferred URI set; backing off.'); 5258 this._backoff.preferred.backoff(); 5259 } 5260 else { 5261 this._log.info('Preferred URI not set; backing off.'); 5262 this._backoff.primary.backoff(); 5263 } 5264 } 5265 /** 5266 * Reset both primary and preferred backoff mechanisms. 5267 */ 5268 _resetBackoffs() { 5269 this._backoff.preferred.reset(); 5270 this._backoff.primary.reset(); 5271 this._backoffStartTime.preferred = null; 5272 this._backoffStartTime.primary = null; 5273 } 5274 /** 5275 * Set a timeout to reconnect after HEARTBEAT_TIMEOUT milliseconds 5276 * have passed without receiving a message over the WebSocket. 5277 */ 5278 _setHeartbeatTimeout() { 5279 clearTimeout(this._heartbeatTimeout); 5280 this._heartbeatTimeout = setTimeout(() => { 5281 this._log.info(`No messages received in ${HEARTBEAT_TIMEOUT / 1000} seconds. Reconnecting...`); 5282 this._shouldFallback = true; 5283 this._closeSocket(); 5284 }, HEARTBEAT_TIMEOUT); 5285 } 5286 /** 5287 * Set the current and previous state 5288 */ 5289 _setState(state) { 5290 this._previousState = this.state; 5291 this.state = state; 5292 } 5293 /** 5294 * Set up the primary and preferred backoff mechanisms. 5295 */ 5296 _setupBackoffs() { 5297 const preferredBackoffConfig = { 5298 factor: 2.0, 5299 jitter: 0.40, 5300 max: this._options.maxPreferredDelayMs, 5301 min: 100, 5302 }; 5303 this._log.info('Initializing preferred transport backoff using config: ', preferredBackoffConfig); 5304 const preferredBackoff = new Backoff(preferredBackoffConfig); 5305 preferredBackoff.on('backoff', (attempt, delay) => { 5306 if (this.state === WSTransportState.Closed) { 5307 this._log.info('Preferred backoff initiated but transport state is closed; not attempting a connection.'); 5308 return; 5309 } 5310 this._log.info(`Will attempt to reconnect Websocket to preferred URI in ${delay}ms`); 5311 if (attempt === 0) { 5312 this._backoffStartTime.preferred = Date.now(); 5313 this._log.info(`Preferred backoff start; ${this._backoffStartTime.preferred}`); 5314 } 5315 }); 5316 preferredBackoff.on('ready', (attempt, _delay) => { 5317 if (this.state === WSTransportState.Closed) { 5318 this._log.info('Preferred backoff ready but transport state is closed; not attempting a connection.'); 5319 return; 5320 } 5321 if (this._backoffStartTime.preferred === null) { 5322 this._log.info('Preferred backoff start time invalid; not attempting a connection.'); 5323 return; 5324 } 5325 if (Date.now() - this._backoffStartTime.preferred > this._options.maxPreferredDurationMs) { 5326 this._log.info('Max preferred backoff attempt time exceeded; falling back to primary backoff.'); 5327 this._preferredUri = null; 5328 this._backoff.primary.backoff(); 5329 return; 5330 } 5331 if (typeof this._preferredUri !== 'string') { 5332 this._log.info('Preferred URI cleared; falling back to primary backoff.'); 5333 this._preferredUri = null; 5334 this._backoff.primary.backoff(); 5335 return; 5336 } 5337 this._connect(this._preferredUri, attempt + 1); 5338 }); 5339 const primaryBackoffConfig = { 5340 factor: 2.0, 5341 jitter: 0.40, 5342 max: this._options.maxPrimaryDelayMs, 5343 // We only want a random initial delay if there are any fallback edges 5344 // Initial delay between 1s and 5s both inclusive 5345 min: this._uris && this._uris.length > 1 5346 ? Math.floor(Math.random() * (5000 - 1000 + 1)) + 1000 5347 : 100, 5348 }; 5349 this._log.info('Initializing primary transport backoff using config: ', primaryBackoffConfig); 5350 const primaryBackoff = new Backoff(primaryBackoffConfig); 5351 primaryBackoff.on('backoff', (attempt, delay) => { 5352 if (this.state === WSTransportState.Closed) { 5353 this._log.info('Primary backoff initiated but transport state is closed; not attempting a connection.'); 5354 return; 5355 }
5356 this._log.info(`Will attempt to reconnect WebSocket in ${delay}ms`); 5357 if (attempt === 0) { 5358 this._backoffStartTime.primary = Date.now(); 5359 this._log.info(`Primary backoff start; ${this._backoffStartTime.primary}`); 5360 } 5361 }); 5362 primaryBackoff.on('ready', (attempt, _delay) => { 5363 if (this.state === WSTransportState.Closed) { 5364 this._log.info('Primary backoff ready but transport state is closed; not attempting a connection.'); 5365 return; 5366 } 5367 if (this._backoffStartTime.primary === null) { 5368 this._log.info('Primary backoff start time invalid; not attempting a connection.'); 5369 return; 5370 } 5371 if (Date.now() - this._backoffStartTime.primary > this._options.maxPrimaryDurationMs) { 5372 this._log.info('Max primary backoff attempt time exceeded; not attempting a connection.'); 5373 return; 5374 } 5375 this._connect(this._uris[this._uriIndex], attempt + 1); 5376 }); 5377 return { 5378 preferred: preferredBackoff, 5379 primary: primaryBackoff, 5380 }; 5381 } 5382 /** 5383 * The uri the transport is currently connected to 5384 */ 5385 get uri() { 5386 return this._connectedUri; 5387 } 5388 } 5389 WSTransport.defaultConstructorOptions = { 5390 WebSocket, 5391 connectTimeoutMs: CONNECT_TIMEOUT, 5392 maxPreferredDelayMs: MAX_PREFERRED_DELAY, 5393 maxPreferredDurationMs: MAX_PREFERRED_DURATION, 5394 maxPrimaryDelayMs: MAX_PRIMARY_DELAY, 5395 maxPrimaryDurationMs: MAX_PRIMARY_DURATION, 5396 }; 5397 5398 // @ts-nocheck 5399 const PSTREAM_VERSION = '1.6'; 5400 // In seconds 5401 const MAX_RECONNECT_TIMEOUT_ALLOWED = 30; 5402 /** 5403 * Constructor for PStream objects. 5404 * 5405 * @exports PStream as Twilio.PStream 5406 * @memberOf Twilio 5407 * @borrows EventEmitter#addListener as #addListener 5408 * @borrows EventEmitter#removeListener as #removeListener 5409 * @borrows EventEmitter#emit as #emit 5410 * @borrows EventEmitter#hasListener as #hasListener 5411 * @constructor 5412 * @param {string} token The Twilio capabilities JWT 5413 * @param {string[]} uris An array of PStream endpoint URIs 5414 * @param {object} [options] 5415 * @config {boolean} [options.backoffMaxMs=20000] Enable debugging 5416 */ 5417 class PStream extends eventsExports.EventEmitter { 5418 constructor(token, uris, options) { 5419 super(); 5420 if (!(this instanceof PStream)) { 5421 return new PStream(token, uris, options); 5422 } 5423 const defaults = { 5424 TransportFactory: WSTransport, 5425 }; 5426 options = options || {}; 5427 for (const prop in defaults) { 5428 if (prop in options) { 5429 continue; 5430 } 5431 options[prop] = defaults[prop]; 5432 } 5433 this.options = options; 5434 this.token = token || ''; 5435 this.status = 'disconnected'; 5436 this.gateway = null; 5437 this.region = null; 5438 this._messageQueue = []; 5439 this._preferredUri = null; 5440 this._uris = uris; 5441 this._handleTransportClose = this._handleTransportClose.bind(this); 5442 this._handleTransportError = this._handleTransportError.bind(this); 5443 this._handleTransportMessage = this._handleTransportMessage.bind(this); 5444 this._handleTransportOpen = this._handleTransportOpen.bind(this); 5445 this._log = new Log('PStream'); 5446 // NOTE(mroberts): EventEmitter requires that we catch all errors. 5447 this.on('error', () => { 5448 this._log.warn('Unexpected error handled in pstream'); 5449 }); 5450 /* 5451 *events used by device 5452 *'invite', 5453 *'ready', 5454 *'error', 5455 *'offline', 5456 * 5457 *'cancel', 5458 *'presence', 5459 *'roster', 5460 *'answer', 5461 *'candidate', 5462 *'hangup' 5463 */ 5464 const self = this; 5465 this.addListener('ready', () => { 5466 self.status = 'ready'; 5467 }); 5468 this.addListener('offline', () => { 5469 self.status = 'offline'; 5470 }); 5471 this.addListener('close', () => { 5472 self._log.info('Received "close" from server. Destroying PStream...'); 5473 self._destroy(); 5474 });
5475 this.transport = new this.options.TransportFactory(this._uris, { 5476 backoffMaxMs: this.options.backoffMaxMs, 5477 maxPreferredDurationMs: this.options.maxPreferredDurationMs, 5478 }); 5479 Object.defineProperties(this, { 5480 uri: { 5481 enumerable: true, 5482 get() { 5483 return this.transport.uri; 5484 }, 5485 }, 5486 }); 5487 this.transport.on('close', this._handleTransportClose); 5488 this.transport.on('error', this._handleTransportError); 5489 this.transport.on('message', this._handleTransportMessage); 5490 this.transport.on('open', this._handleTransportOpen); 5491 this.transport.open(); 5492 return this; 5493 } 5494 } 5495 PStream.prototype._handleTransportClose = function () { 5496 this.emit('transportClose'); 5497 if (this.status !== 'disconnected') { 5498 if (this.status !== 'offline') { 5499 this.emit('offline', this); 5500 } 5501 this.status = 'disconnected'; 5502 } 5503 }; 5504 PStream.prototype._handleTransportError = function (error) { 5505 if (!error) { 5506 this.emit('error', { error: { 5507 code: 31000, 5508 message: 'Websocket closed without a provided reason', 5509 twilioError: new SignalingErrors.ConnectionDisconnected(), 5510 } }); 5511 return; 5512 } 5513 // We receive some errors without call metadata (just the error). We need to convert these 5514 // to be contained within the 'error' field so that these errors match the expected format. 5515 this.emit('error', typeof error.code !== 'undefined' ? { error } : error); 5516 }; 5517 PStream.prototype._handleTransportMessage = function (msg) { 5518 if (!msg || !msg.data || typeof msg.data !== 'string') { 5519 return; 5520 } 5521 const { type, payload = {} } = JSON.parse(msg.data); 5522 this.gateway = payload.gateway || this.gateway; 5523 this.region = payload.region || this.region; 5524 if (type === 'error' && payload.error) { 5525 payload.error.twilioError = new SignalingErrors.ConnectionError(); 5526 } 5527 this.emit(type, payload); 5528 }; 5529 PStream.prototype._handleTransportOpen = function () { 5530 this.status = 'connected'; 5531 this.setToken(this.token); 5532 this.emit('transportOpen'); 5533 const messages = this._messageQueue.splice(0, this._messageQueue.length); 5534 messages.forEach(message => this._publish(...message)); 5535 }; 5536 /** 5537 * @return {string} 5538 */ 5539 PStream.toString = () => '[Twilio.PStream class]'; 5540 PStream.prototype.toString = () => '[Twilio.PStream instance]'; 5541 PStream.prototype.setToken = function (token) { 5542 this._log.info('Setting token and publishing listen'); 5543 this.token = token; 5544 let reconnectTimeout = 0; 5545 const t = this.options.maxPreferredDurationMs; 5546 this._log.info(`maxPreferredDurationMs:${t}`); 5547 if (typeof t === 'number' && t >= 0) { 5548 reconnectTimeout = Math.min(Math.ceil(t / 1000), MAX_RECONNECT_TIMEOUT_ALLOWED); 5549 } 5550 this._log.info(`reconnectTimeout:${reconnectTimeout}`); 5551 const payload = { 5552 browserinfo: getBrowserInfo(), 5553 reconnectTimeout, 5554 token, 5555 }; 5556 this._publish('listen', payload); 5557 }; 5558 PStream.prototype.sendMessage = function (callsid, content, contenttype = 'application/json', messagetype, voiceeventsid) { 5559 const payload = { 5560 callsid, 5561 content, 5562 contenttype, 5563 messagetype, 5564 voiceeventsid, 5565 }; 5566 this._publish('message', payload, true); 5567 }; 5568 PStream.prototype.register = function (mediaCapabilities) { 5569 const regPayload = { media: mediaCapabilities }; 5570 this._publish('register', regPayload, true); 5571 }; 5572 PStream.prototype.invite = function (sdp, callsid, params) { 5573 const payload = { 5574 callsid, 5575 sdp, 5576 twilio: params ? { params } : {}, 5577 }; 5578 this._publish('invite', payload, true); 5579 }; 5580 PStream.prototype.reconnect = function (sdp, callsid, reconnect) { 5581 const payload = { 5582 callsid, 5583 reconnect, 5584 sdp, 5585 twilio: {}, 5586 }; 5587 this._publish('invite', payload, true); 5588 }; 5589 PStream.prototype.answer = function (sdp, callsid) { 5590 this._publish('answer', { sdp, callsid }, true); 5591 }; 5592 PStream.prototype.dtmf = function (callsid, digits) { 5593 this._publish('dtmf', { callsid, dtmf: digits }, true); 5594 };
5595 PStream.prototype.hangup = function (callsid, message) { 5596 const payload = message ? { callsid, message } : { callsid }; 5597 this._publish('hangup', payload, true); 5598 }; 5599 PStream.prototype.reject = function (callsid) { 5600 this._publish('reject', { callsid }, true); 5601 }; 5602 PStream.prototype.reinvite = function (sdp, callsid) { 5603 this._publish('reinvite', { sdp, callsid }, false); 5604 }; 5605 PStream.prototype._destroy = function () { 5606 this.transport.removeListener('close', this._handleTransportClose); 5607 this.transport.removeListener('error', this._handleTransportError); 5608 this.transport.removeListener('message', this._handleTransportMessage); 5609 this.transport.removeListener('open', this._handleTransportOpen); 5610 this.transport.close(); 5611 this.emit('offline', this); 5612 }; 5613 PStream.prototype.destroy = function () { 5614 this._log.info('PStream.destroy() called...'); 5615 this._destroy(); 5616 return this; 5617 }; 5618 PStream.prototype.updatePreferredURI = function (uri) { 5619 this._preferredUri = uri; 5620 this.transport.updatePreferredURI(uri); 5621 }; 5622 PStream.prototype.updateURIs = function (uris) { 5623 this._uris = uris; 5624 this.transport.updateURIs(this._uris); 5625 }; 5626 PStream.prototype.publish = function (type, payload) { 5627 return this._publish(type, payload, true); 5628 }; 5629 PStream.prototype._publish = function (type, payload, shouldRetry) { 5630 const msg = JSON.stringify({ 5631 payload, 5632 type, 5633 version: PSTREAM_VERSION, 5634 }); 5635 const isSent = !!this.transport.send(msg); 5636 if (!isSent) { 5637 this.emit('error', { error: { 5638 code: 31009, 5639 message: 'No transport available to send or receive messages', 5640 twilioError: new GeneralErrors.TransportError(), 5641 } }); 5642 if (shouldRetry) { 5643 this._messageQueue.push([type, payload, true]); 5644 } 5645 } 5646 }; 5647 function getBrowserInfo() { 5648 const nav = typeof navigator !== 'undefined' ? navigator : {}; 5649 const info = { 5650 browser: { 5651 platform: nav.platform || 'unknown', 5652 userAgent: nav.userAgent || 'unknown', 5653 }, 5654 p: 'browser', 5655 plugin: 'rtc', 5656 v: RELEASE_VERSION, 5657 }; 5658 return info; 5659 } 5660 5661 /** 5662 * Valid edges. 5663 */ 5664 var Edge; 5665 (function (Edge) { 5666 /** 5667 * Public edges 5668 */ 5669 Edge["Sydney"] = "sydney"; 5670 Edge["SaoPaulo"] = "sao-paulo"; 5671 Edge["Dublin"] = "dublin"; 5672 Edge["Frankfurt"] = "frankfurt"; 5673 Edge["Tokyo"] = "tokyo"; 5674 Edge["Singapore"] = "singapore"; 5675 Edge["Ashburn"] = "ashburn"; 5676 Edge["Umatilla"] = "umatilla"; 5677 Edge["Roaming"] = "roaming"; 5678 /** 5679 * Interconnect edges 5680 */ 5681 Edge["AshburnIx"] = "ashburn-ix"; 5682 Edge["SanJoseIx"] = "san-jose-ix"; 5683 Edge["LondonIx"] = "london-ix"; 5684 Edge["FrankfurtIx"] = "frankfurt-ix"; 5685 Edge["SingaporeIx"] = "singapore-ix"; 5686 Edge["SydneyIx"] = "sydney-ix"; 5687 Edge["TokyoIx"] = "tokyo-ix"; 5688 })(Edge || (Edge = {})); 5689 /** 5690 * Valid current regions. 5691 * 5692 * @deprecated 5693 * 5694 * CLIENT-6831 5695 * This is no longer used or updated for checking validity of regions in the 5696 * SDK. We now allow any string to be passed for region. Invalid regions won't 5697 * be able to connect, and won't throw an exception. 5698 * 5699 * CLIENT-7519 5700 * This is used again to temporarily convert edge values to regions as part of 5701 * Phase 1 Regional. This is still considered deprecated. 5702 * 5703 * @private 5704 */ 5705 var Region; 5706 (function (Region) { 5707 Region["Au1"] = "au1"; 5708 Region["Au1Ix"] = "au1-ix"; 5709 Region["Br1"] = "br1"; 5710 Region["De1"] = "de1"; 5711 Region["De1Ix"] = "de1-ix"; 5712 Region["Gll"] = "gll"; 5713 Region["Ie1"] = "ie1"; 5714 Region["Ie1Ix"] = "ie1-ix"; 5715 Region["Ie1Tnx"] = "ie1-tnx"; 5716 Region["Jp1"] = "jp1"; 5717 Region["Jp1Ix"] = "jp1-ix"; 5718 Region["Sg1"] = "sg1"; 5719 Region["Sg1Ix"] = "sg1-ix"; 5720 Region["Sg1Tnx"] = "sg1-tnx"; 5721 Region["Us1"] = "us1"; 5722 Region["Us1Ix"] = "us1-ix"; 5723 Region["Us1Tnx"] = "us1-tnx"; 5724 Region["Us2"] = "us2"; 5725 Region["Us2Ix"] = "us2-ix"; 5726 Region["Us2Tnx"] = "us2-tnx"; 5727 })(Region || (Region = {})); 5728 /** 5729 * Region shortcodes. Maps the full region name from AWS to the Twilio shortcode. 5730 * @private 5731 */ 5732 const regionShortcodes = { 5733 ASIAPAC_SINGAPORE: Region.Sg1, 5734 ASIAPAC_SYDNEY: Region.Au1, 5735 ASIAPAC_TOKYO: Region.Jp1, 5736 EU_FRANKFURT: Region.De1, 5737 EU_IRELAND: Region.Ie1, 5738 SOUTH_AMERICA_SAO_PAULO: Region.Br1, 5739 US_EAST_VIRGINIA: Region.Us1, 5740 US_WEST_OREGON: Region.Us2, 5741 }; 5742 /** 5743 * Region to edge mapping, as part of Phase 1 Regional (CLIENT-7519). 5744 * Temporary. 5745 * @private 5746 */ 5747 const regionToEdge = { 5748 [Region.Au1]: Edge.Sydney, 5749 [Region.Br1]: Edge.SaoPaulo, 5750 [Region.Ie1]: Edge.Dublin, 5751 [Region.De1]: Edge.Frankfurt, 5752 [Region.Jp1]: Edge.Tokyo, 5753 [Region.Sg1]: Edge.Singapore, 5754 [Region.Us1]: Edge.Ashburn, 5755 [Region.Us2]: Edge.Umatilla, 5756 [Region.Gll]: Edge.Roaming, 5757 /** 5758 * Interconnect edges 5759 */ 5760 [Region.Us1Ix]: Edge.AshburnIx, 5761 [Region.Us2Ix]: Edge.SanJoseIx, 5762 [Region.Ie1Ix]: Edge.LondonIx, 5763 [Region.De1Ix]: Edge.FrankfurtIx, 5764 [Region.Sg1Ix]: Edge.SingaporeIx, 5765 [Region.Au1Ix]: Edge.SydneyIx, 5766 [Region.Jp1Ix]: Edge.TokyoIx, 5767 /** 5768 * Tnx regions 5769 */ 5770 [Region.Us1Tnx]: Edge.AshburnIx, 5771 [Region.Us2Tnx]: Edge.AshburnIx, 5772 [Region.Ie1Tnx]: Edge.LondonIx, 5773 [Region.Sg1Tnx]: Edge.SingaporeIx, 5774 }; 5775 /** 5776 * The default edge to connect to and create a chunder uri from, if the edge 5777 * parameter is not specified during setup in `Device`. 5778 * @constant 5779 */ 5780 const defaultEdge = Edge.Roaming; 5781 /** 5782 * The default event gateway URI to publish to. 5783 * @constant 5784 * @private 5785 */ 5786 const defaultEventGatewayURI = 'eventgw.twilio.com'; 5787 /** 5788 * String template for an edge chunder URI 5789 * @param edge - The edge. 5790 */ 5791 function createChunderEdgeURI(edge) { 5792 return `voice-js.${edge}.twilio.com`; 5793 }
5794 /** 5795 * String template for a region insights URI 5796 * @param region - The region. 5797 */ 5798 function createEventGatewayURI(region) { 5799 return region 5800 ? `eventgw.${region}.twilio.com` 5801 : defaultEventGatewayURI; 5802 } 5803 /** 5804 * Create a signaling endpoint URL to connect a websocket to from a chunder URI. 5805 * @param uri the chunder URI to create a signaling endpoint URL for 5806 */ 5807 function createSignalingEndpointURL(uri) { 5808 return `wss://${uri}/signal`; 5809 } 5810 /** 5811 * Get the URI associated with the passed edge. 5812 * @private 5813 * @param edge - A string or an array of edge values 5814 * @returns An array of chunder URIs 5815 */ 5816 function getChunderURIs(edge) { 5817 if (!!edge && typeof edge !== 'string' && !Array.isArray(edge)) { 5818 throw new InvalidArgumentError('If `edge` is provided, it must be of type `string` or an array of strings.'); 5819 } 5820 let uris; 5821 if (edge) { 5822 const edgeParams = Array.isArray(edge) ? edge : [edge]; 5823 uris = edgeParams.map((param) => createChunderEdgeURI(param)); 5824 } 5825 else { 5826 uris = [createChunderEdgeURI(defaultEdge)]; 5827 } 5828 return uris; 5829 } 5830 /** 5831 * Get the region shortcode by its full AWS region string. 5832 * 5833 * @private 5834 * @param region - The region's full AWS string. 5835 */ 5836 function getRegionShortcode(region) { 5837 return regionShortcodes[region] || null; 5838 } 5839 5840 // @ts-nocheck 5841 const ptToFixedBitrateAudioCodecName = { 5842 0: 'PCMU', 5843 8: 'PCMA', 5844 }; 5845 const defaultOpusId = 111; 5846 const BITRATE_MAX = 510000; 5847 const BITRATE_MIN = 6000; 5848 function getPreferredCodecInfo(sdp) { 5849 const [, codecId, codecName] = /a=rtpmap:(\d+) (\S+)/m.exec(sdp) || [null, '', '']; 5850 const regex = new RegExp(`a=fmtp:${codecId} (\\S+)`, 'm'); 5851 const [, codecParams] = regex.exec(sdp) || [null, '']; 5852 return { codecName, codecParams }; 5853 } 5854 function setIceAggressiveNomination(sdp) { 5855 // This only works on Chrome. We don't want any side effects on other browsers 5856 // https://bugs.chromium.org/p/chromium/issues/detail?id=1024096 5857 // https://issues.corp.twilio.com/browse/CLIENT-6911 5858 if (!isChrome(window, window.navigator)) { 5859 return sdp; 5860 } 5861 return sdp.split('\n') 5862 .filter(line => line.indexOf('a=ice-lite') === -1) 5863 .join('\n'); 5864 } 5865 function setMaxAverageBitrate(sdp, maxAverageBitrate) { 5866 if (typeof maxAverageBitrate !== 'number' 5867 || maxAverageBitrate < BITRATE_MIN 5868 || maxAverageBitrate > BITRATE_MAX) { 5869 return sdp; 5870 } 5871 const matches = /a=rtpmap:(\d+) opus/m.exec(sdp); 5872 const opusId = matches && matches.length ? matches[1] : defaultOpusId; 5873 const regex = new RegExp(`a=fmtp:${opusId}`); 5874 const lines = sdp.split('\n').map(line => regex.test(line) 5875 ? line + `;maxaveragebitrate=${maxAverageBitrate}` 5876 : line); 5877 return lines.join('\n'); 5878 } 5879 /** 5880 * Return a new SDP string with the re-ordered codec preferences. 5881 * @param {string} sdp 5882 * @param {Array<AudioCodec>} preferredCodecs - If empty, the existing order 5883 * of audio codecs is preserved 5884 * @returns {string} Updated SDP string 5885 */ 5886 function setCodecPreferences(sdp, preferredCodecs) { 5887 const mediaSections = getMediaSections(sdp); 5888 const session = sdp.split('\r\nm=')[0]; 5889 return [session].concat(mediaSections.map(section => { 5890 // Codec preferences should not be applied to m=application sections. 5891 if (!/^m=(audio|video)/.test(section)) { 5892 return section; 5893 } 5894 const kind = section.match(/^m=(audio|video)/)[1]; 5895 const codecMap = createCodecMapForMediaSection(section); 5896 const payloadTypes = getReorderedPayloadTypes(codecMap, preferredCodecs); 5897 const newSection = setPayloadTypesInMediaSection(payloadTypes, section); 5898 const pcmaPayloadTypes = codecMap.get('pcma') || []; 5899 const pcmuPayloadTypes = codecMap.get('pcmu') || []; 5900 const fixedBitratePayloadTypes = kind === 'audio' 5901 ? new Set(pcmaPayloadTypes.concat(pcmuPayloadTypes)) 5902 : new Set(); 5903 return fixedBitratePayloadTypes.has(payloadTypes[0]) 5904 ? newSection.replace(/\r\nb=(AS|TIAS):([0-9]+)/g, '') 5905 : newSection; 5906 })).join('\r\n'); 5907 } 5908 /** 5909 * Get the m= sections of a particular kind and direction from an sdp. 5910 * @param {string} sdp - SDP string 5911 * @param {string} [kind] - Pattern for matching kind 5912 * @param {string} [direction] - Pattern for matching direction 5913 * @returns {Array<string>} mediaSections 5914 */ 5915 function getMediaSections(sdp, kind, direction) { 5916 return sdp.replace(/\r\n\r\n$/, '\r\n').split('\r\nm=').slice(1).map(mediaSection => `m=${mediaSection}`).filter(mediaSection => {
5917 const kindPattern = new RegExp(`m=${'.*'}`, 'gm'); 5918 const directionPattern = new RegExp(`a=${'.*'}`, 'gm'); 5919 return kindPattern.test(mediaSection) && directionPattern.test(mediaSection); 5920 }); 5921 } 5922 /** 5923 * Create a Codec Map for the given m= section. 5924 * @param {string} section - The given m= section 5925 * @returns {Map<Codec, Array<PT>>} 5926 */ 5927 function createCodecMapForMediaSection(section) { 5928 return Array.from(createPtToCodecName(section)).reduce((codecMap, pair) => { 5929 const pt = pair[0]; 5930 const codecName = pair[1]; 5931 const pts = codecMap.get(codecName) || []; 5932 return codecMap.set(codecName, pts.concat(pt)); 5933 }, new Map()); 5934 } 5935 /** 5936 * Create the reordered Codec Payload Types based on the preferred Codec Names. 5937 * @param {Map<Codec, Array<PT>>} codecMap - Codec Map 5938 * @param {Array<Codec>} preferredCodecs - Preferred Codec Names 5939 * @returns {Array<PT>} Reordered Payload Types 5940 */ 5941 function getReorderedPayloadTypes(codecMap, preferredCodecs) { 5942 preferredCodecs = preferredCodecs.map(codecName => codecName.toLowerCase()); 5943 const preferredPayloadTypes = flatMap(preferredCodecs, codecName => codecMap.get(codecName) || []); 5944 const remainingCodecs = difference(Array.from(codecMap.keys()), preferredCodecs); 5945 const remainingPayloadTypes = flatMap(remainingCodecs, codecName => codecMap.get(codecName)); 5946 return preferredPayloadTypes.concat(remainingPayloadTypes); 5947 } 5948 /** 5949 * Set the given Codec Payload Types in the first line of the given m= section. 5950 * @param {Array<PT>} payloadTypes - Payload Types 5951 * @param {string} section - Given m= section 5952 * @returns {string} - Updated m= section 5953 */ 5954 function setPayloadTypesInMediaSection(payloadTypes, section) { 5955 const lines = section.split('\r\n'); 5956 let mLine = lines[0]; 5957 const otherLines = lines.slice(1); 5958 mLine = mLine.replace(/([0-9]+\s?)+$/, payloadTypes.join(' ')); 5959 return [mLine].concat(otherLines).join('\r\n'); 5960 } 5961 /** 5962 * Create a Map from PTs to codec names for the given m= section. 5963 * @param {string} mediaSection - The given m= section. 5964 * @returns {Map<PT, Codec>} ptToCodecName 5965 */ 5966 function createPtToCodecName(mediaSection) { 5967 return getPayloadTypesInMediaSection(mediaSection).reduce((ptToCodecName, pt) => { 5968 const rtpmapPattern = new RegExp(`a=rtpmap:${pt} ([^/]+)`); 5969 const matches = mediaSection.match(rtpmapPattern); 5970 const codecName = matches 5971 ? matches[1].toLowerCase() 5972 : ptToFixedBitrateAudioCodecName[pt] 5973 ? ptToFixedBitrateAudioCodecName[pt].toLowerCase() 5974 : ''; 5975 return ptToCodecName.set(pt, codecName); 5976 }, new Map()); 5977 } 5978 /** 5979 * Get the Codec Payload Types present in the first line of the given m= section 5980 * @param {string} section - The m= section 5981 * @returns {Array<PT>} Payload Types 5982 */ 5983 function getPayloadTypesInMediaSection(section) { 5984 const mLine = section.split('\r\n')[0]; 5985 // In "m=<kind> <port> <proto> <payload_type_1> <payload_type_2> ... <payload_type_n>", 5986 // the regex matches <port> and the PayloadTypes. 5987 const matches = mLine.match(/([0-9]+)/g); 5988 // This should not happen, but in case there are no PayloadTypes in 5989 // the m= line, return an empty array. 5990 if (!matches) { 5991 return []; 5992 } 5993 // Since only the PayloadTypes are needed, we discard the <port>. 5994 return matches.slice(1).map(match => parseInt(match, 10)); 5995 } 5996 5997 // @ts-nocheck 5998 // tslint:disable only-arrow-functions 5999 /* global webkitRTCPeerConnection, mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate */ 6000 function RTCPC(options) { 6001 this.log = new Log('RTCPC'); 6002 if (typeof window === 'undefined') { 6003 this.log.info('No RTCPeerConnection implementation available. The window object was not found.'); 6004 return; 6005 } 6006 if (options && options.RTCPeerConnection) { 6007 this.RTCPeerConnection = options.RTCPeerConnection; 6008 } 6009 else if (typeof window.RTCPeerConnection === 'function') { 6010 this.RTCPeerConnection = window.RTCPeerConnection; 6011 } 6012 else if (typeof window.webkitRTCPeerConnection === 'function') { 6013 this.RTCPeerConnection = webkitRTCPeerConnection; 6014 } 6015 else if (typeof window.mozRTCPeerConnection === 'function') { 6016 this.RTCPeerConnection = mozRTCPeerConnection; 6017 window.RTCSessionDescription = mozRTCSessionDescription; 6018 window.RTCIceCandidate = mozRTCIceCandidate; 6019 } 6020 else { 6021 this.log.info('No RTCPeerConnection implementation available'); 6022 } 6023 } 6024 RTCPC.prototype.create = function (rtcConfiguration) { 6025 this.pc = new this.RTCPeerConnection(rtcConfiguration); 6026 }; 6027 RTCPC.prototype.createModernConstraints = c => { 6028 // createOffer differs between Chrome 23 and Chrome 24+. 6029 // See https://groups.google.com/forum/?fromgroups=#!topic/discuss-webrtc/JBDZtrMumyU 6030 // Unfortunately I haven't figured out a way to detect which format 6031 // is required ahead of time, so we'll first try the old way, and 6032 // if we get an exception, then we'll try the new way. 6033 if (typeof c === 'undefined') { 6034 return null; 6035 } 6036 // NOTE(mroberts): As of Chrome 38, Chrome still appears to expect 6037 // constraints under the 'mandatory' key, and with the first letter of each 6038 // constraint capitalized. Firefox, on the other hand, has deprecated the 6039 // 'mandatory' key and does not expect the first letter of each constraint 6040 // capitalized. 6041 const nc = Object.assign({}, c); 6042 if (typeof webkitRTCPeerConnection !== 'undefined' && !isLegacyEdge()) { 6043 nc.mandatory = {}; 6044 if (typeof c.audio !== 'undefined') { 6045 nc.mandatory.OfferToReceiveAudio = c.audio; 6046 } 6047 if (typeof c.video !== 'undefined') { 6048 nc.mandatory.OfferToReceiveVideo = c.video; 6049 } 6050 } 6051 else { 6052 if (typeof c.audio !== 'undefined') { 6053 nc.offerToReceiveAudio = c.audio; 6054 } 6055 if (typeof c.video !== 'undefined') { 6056 nc.offerToReceiveVideo = c.video; 6057 } 6058 } 6059 delete nc.audio; 6060 delete nc.video; 6061 return nc; 6062 }; 6063 RTCPC.prototype.createOffer = function (maxAverageBitrate, constraints, onSuccess, onError) { 6064 constraints = this.createModernConstraints(constraints); 6065 return promisifyCreate(this.pc.createOffer, this.pc)(constraints).then(offer => { 6066 if (!this.pc) { 6067 return Promise.resolve(); 6068 } 6069 const sdp = setMaxAverageBitrate(offer.sdp, maxAverageBitrate); 6070 return promisifySet(this.pc.setLocalDescription, this.pc)(new RTCSessionDescription({ 6071 sdp, 6072 type: 'offer', 6073 })); 6074 }).then(onSuccess, onError); 6075 }; 6076 RTCPC.prototype.createAnswer = function (maxAverageBitrate, constraints, onSuccess, onError) { 6077 constraints = this.createModernConstraints(constraints); 6078 return promisifyCreate(this.pc.createAnswer, this.pc)(constraints).then(answer => { 6079 if (!this.pc) { 6080 return Promise.resolve(); 6081 } 6082 const sdp = setMaxAverageBitrate(answer.sdp, maxAverageBitrate); 6083 return promisifySet(this.pc.setLocalDescription, this.pc)(new RTCSessionDescription({ 6084 sdp, 6085 type: 'answer', 6086 })); 6087 }).then(onSuccess, onError); 6088 }; 6089 RTCPC.prototype.processSDP = function (maxAverageBitrate, codecPreferences, sdp, constraints, onSuccess, onError) { 6090 sdp = setCodecPreferences(sdp, codecPreferences); 6091 const desc = new RTCSessionDescription({ sdp, type: 'offer' }); 6092 return promisifySet(this.pc.setRemoteDescription, this.pc)(desc).then(() => { 6093 this.createAnswer(maxAverageBitrate, constraints, onSuccess, onError); 6094 }); 6095 }; 6096 RTCPC.prototype.getSDP = function () { 6097 return this.pc.localDescription.sdp; 6098 }; 6099 RTCPC.prototype.processAnswer = function (codecPreferences, sdp, onSuccess, onError) { 6100 if (!this.pc) { 6101 return Promise.resolve(); 6102 } 6103 sdp = setCodecPreferences(sdp, codecPreferences); 6104 return promisifySet(this.pc.setRemoteDescription, this.pc)(new RTCSessionDescription({ sdp, type: 'answer' })).then(onSuccess, onError); 6105 }; 6106 /* NOTE(mroberts): Firefox 18 through 21 include a `mozRTCPeerConnection` 6107 object, but attempting to instantiate it will throw the error 6108 6109 Error: PeerConnection not enabled (did you set the pref?) 6110 6111 unless the `media.peerconnection.enabled` pref is enabled. So we need to test 6112 if we can actually instantiate `mozRTCPeerConnection`; however, if the user 6113 *has* enabled `media.peerconnection.enabled`, we need to perform the same 6114 test that we use to detect Firefox 24 and above, namely: 6115 6116 typeof (new mozRTCPeerConnection()).getLocalStreams === 'function' 6117 6118 NOTE(rrowland): We no longer support Legacy Edge as of Sep 1, 2020. 6119 */ 6120 RTCPC.test = () => { 6121 if (typeof navigator === 'object') { 6122 const getUserMedia = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) 6123 || navigator.webkitGetUserMedia 6124 || navigator.mozGetUserMedia 6125 || navigator.getUserMedia; 6126 if (isLegacyEdge(navigator)) { 6127 return false;
6128 } 6129 if (getUserMedia && typeof window.RTCPeerConnection === 'function') { 6130 return true; 6131 } 6132 else if (getUserMedia && typeof window.webkitRTCPeerConnection === 'function') { 6133 return true; 6134 } 6135 else if (getUserMedia && typeof window.mozRTCPeerConnection === 'function') { 6136 try { 6137 const test = new window.mozRTCPeerConnection(); 6138 if (typeof test.getLocalStreams !== 'function') { 6139 return false; 6140 } 6141 } 6142 catch (e) { 6143 return false; 6144 } 6145 return true; 6146 } 6147 else if (typeof RTCIceGatherer !== 'undefined') { 6148 return true; 6149 } 6150 } 6151 return false; 6152 }; 6153 function promisify(fn, ctx, areCallbacksFirst, checkRval) { 6154 return function () { 6155 const args = Array.prototype.slice.call(arguments); 6156 return new Promise(resolve => { 6157 const returnValue = fn.apply(ctx, args); 6158 if (!checkRval) { 6159 resolve(returnValue); 6160 return; 6161 } 6162 if (typeof returnValue === 'object' && typeof returnValue.then === 'function') { 6163 resolve(returnValue); 6164 } 6165 else { 6166 throw new Error(); 6167 } 6168 }).catch(() => new Promise((resolve, reject) => { 6169 fn.apply(ctx, areCallbacksFirst 6170 ? [resolve, reject].concat(args) 6171 : args.concat([resolve, reject])); 6172 })); 6173 }; 6174 } 6175 function promisifyCreate(fn, ctx) { 6176 return promisify(fn, ctx, true, true); 6177 } 6178 function promisifySet(fn, ctx) { 6179 return promisify(fn, ctx, false, false); 6180 } 6181 6182 // @ts-nocheck 6183 const ICE_GATHERING_TIMEOUT = 15000; 6184 const ICE_GATHERING_FAIL_NONE = 'none'; 6185 const ICE_GATHERING_FAIL_TIMEOUT = 'timeout'; 6186 const INITIAL_ICE_CONNECTION_STATE = 'new'; 6187 const VOLUME_INTERVAL_MS = 50; 6188 /** 6189 * @typedef {Object} PeerConnection 6190 * @param audioHelper 6191 * @param pstream 6192 * @param options 6193 * @return {PeerConnection} 6194 * @constructor 6195 */ 6196 function PeerConnection(audioHelper, pstream, options) { 6197 if (!audioHelper || !pstream) { 6198 throw new InvalidArgumentError('Audiohelper, and pstream are required arguments'); 6199 } 6200 if (!(this instanceof PeerConnection)) { 6201 return new PeerConnection(audioHelper, pstream, options); 6202 } 6203 this._log = new Log('PeerConnection'); 6204 function noop() { 6205 this._log.warn('Unexpected noop call in peerconnection'); 6206 } 6207 this.onaudio = noop; 6208 this.onopen = noop; 6209 this.onerror = noop; 6210 this.onclose = noop; 6211 this.ondisconnected = noop; 6212 this.onfailed = noop; 6213 this.onconnected = noop; 6214 this.onreconnected = noop; 6215 this.onsignalingstatechange = noop; 6216 this.ondtlstransportstatechange = noop; 6217 this.onicegatheringfailure = noop; 6218 this.onicegatheringstatechange = noop; 6219 this.oniceconnectionstatechange = noop; 6220 this.onpcconnectionstatechange = noop; 6221 this.onicecandidate = noop; 6222 this.onselectedcandidatepairchange = noop; 6223 this.onvolume = noop; 6224 this.version = null; 6225 this.pstream = pstream; 6226 this.stream = null; 6227 this.sinkIds = new Set(['default']); 6228 this.outputs = new Map(); 6229 this.status = 'connecting'; 6230 this.callSid = null; 6231 this.isMuted = false; 6232 const AudioContext = typeof window !== 'undefined' 6233 && (window.AudioContext || window.webkitAudioContext); 6234 this._isSinkSupported = !!AudioContext && 6235 typeof HTMLAudioElement !== 'undefined' && HTMLAudioElement.prototype.setSinkId; 6236 // NOTE(mmalavalli): Since each Connection creates its own AudioContext, 6237 // after 6 instances an exception is thrown. Refer https://www.w3.org/2011/audio/track/issues/3. 6238 // In order to get around it, we are re-using the Device's AudioContext. 6239 this._audioContext = AudioContext && audioHelper._audioContext; 6240 this._audioHelper = audioHelper; 6241 this._hasIceCandidates = false; 6242 this._hasIceGatheringFailures = false;
6243 this._iceGatheringTimeoutId = null; 6244 this._masterAudio = null; 6245 this._masterAudioDeviceId = null; 6246 this._mediaStreamSource = null; 6247 this._dtmfSender = null; 6248 this._dtmfSenderUnsupported = false; 6249 this._callEvents = []; 6250 this._nextTimeToPublish = Date.now(); 6251 this._onAnswerOrRinging = noop; 6252 this._onHangup = noop; 6253 this._remoteStream = null; 6254 this._shouldManageStream = true; 6255 this._iceState = INITIAL_ICE_CONNECTION_STATE; 6256 this.options = options = options || {}; 6257 this.navigator = options.navigator 6258 || (typeof navigator !== 'undefined' ? navigator : null); 6259 this.util = options.util || util; 6260 this.codecPreferences = options.codecPreferences; 6261 return this; 6262 } 6263 PeerConnection.prototype.uri = function () { 6264 return this._uri; 6265 }; 6266 /** 6267 * Open the underlying RTCPeerConnection with a MediaStream obtained by 6268 * passed constraints. The resulting MediaStream is created internally 6269 * and will therefore be managed and destroyed internally. 6270 * @param {MediaStreamConstraints} constraints 6271 */ 6272 PeerConnection.prototype.openDefaultDeviceWithConstraints = function (constraints) { 6273 return this._audioHelper._openDefaultDeviceWithConstraints(constraints) 6274 .then(this._setInputTracksFromStream.bind(this, false)); 6275 }; 6276 /** 6277 * Replace the existing input audio tracks with the audio tracks from the 6278 * passed input audio stream. We re-use the existing stream because 6279 * the AnalyzerNode is bound to the stream. 6280 * @param {MediaStream} stream 6281 */ 6282 PeerConnection.prototype.setInputTracksFromStream = function (stream) { 6283 const self = this; 6284 return this._setInputTracksFromStream(true, stream).then(() => { 6285 self._shouldManageStream = false; 6286 }); 6287 }; 6288 PeerConnection.prototype._createAnalyser = (audioContext, options) => { 6289 options = Object.assign({ 6290 fftSize: 32, 6291 smoothingTimeConstant: 0.3, 6292 }, options); 6293 const analyser = audioContext.createAnalyser(); 6294 // tslint:disable-next-line 6295 for (const field in options) { 6296 analyser[field] = options[field]; 6297 } 6298 return analyser; 6299 }; 6300 PeerConnection.prototype._setVolumeHandler = function (handler) { 6301 this.onvolume = handler; 6302 }; 6303 PeerConnection.prototype._startPollingVolume = function () { 6304 if (!this._audioContext || !this.stream || !this._remoteStream) { 6305 return; 6306 } 6307 const audioContext = this._audioContext; 6308 const inputAnalyser = this._inputAnalyser = this._createAnalyser(audioContext); 6309 const inputBufferLength = inputAnalyser.frequencyBinCount; 6310 const inputDataArray = new Uint8Array(inputBufferLength); 6311 this._inputAnalyser2 = this._createAnalyser(audioContext, { 6312 maxDecibels: 0, 6313 minDecibels: -127, 6314 smoothingTimeConstant: 0, 6315 }); 6316 const outputAnalyser = this._outputAnalyser = this._createAnalyser(audioContext); 6317 const outputBufferLength = outputAnalyser.frequencyBinCount; 6318 const outputDataArray = new Uint8Array(outputBufferLength); 6319 this._outputAnalyser2 = this._createAnalyser(audioContext, { 6320 maxDecibels: 0, 6321 minDecibels: -127, 6322 smoothingTimeConstant: 0, 6323 }); 6324 this._updateInputStreamSource(this.stream); 6325 this._updateOutputStreamSource(this._remoteStream); 6326 const self = this; 6327 setTimeout(function emitVolume() { 6328 if (!self._audioContext) { 6329 return; 6330 } 6331 else if (self.status === 'closed') { 6332 self._inputAnalyser.disconnect(); 6333 self._outputAnalyser.disconnect(); 6334 self._inputAnalyser2.disconnect(); 6335 self._outputAnalyser2.disconnect(); 6336 return; 6337 } 6338 self._inputAnalyser.getByteFrequencyData(inputDataArray); 6339 const inputVolume = self.util.average(inputDataArray); 6340 self._inputAnalyser2.getByteFrequencyData(inputDataArray); 6341 const inputVolume2 = self.util.average(inputDataArray); 6342 self._outputAnalyser.getByteFrequencyData(outputDataArray); 6343 const outputVolume = self.util.average(outputDataArray); 6344 self._outputAnalyser2.getByteFrequencyData(outputDataArray); 6345 const outputVolume2 = self.util.average(outputDataArray); 6346 self.onvolume(inputVolume / 255, outputVolume / 255, inputVolume2, outputVolume2); 6347 setTimeout(emitVolume, VOLUME_INTERVAL_MS); 6348 }, VOLUME_INTERVAL_MS); 6349 }; 6350 PeerConnection.prototype._stopStream = function _stopStream() { 6351 // We shouldn't stop the tracks if they were not created inside 6352 // this PeerConnection. 6353 if (!this._shouldManageStream) { 6354 return; 6355 } 6356 this._audioHelper._stopDefaultInputDeviceStream(); 6357 }; 6358 /** 6359 * Update the stream source with the new input audio stream. 6360 * @param {MediaStream} stream 6361 * @private 6362 */ 6363 PeerConnection.prototype._updateInputStreamSource = function (stream) { 6364 if (this._inputStreamSource) { 6365 this._inputStreamSource.disconnect(); 6366 } 6367 try { 6368 this._inputStreamSource = this._audioContext.createMediaStreamSource(stream); 6369 this._inputStreamSource.connect(this._inputAnalyser); 6370 this._inputStreamSource.connect(this._inputAnalyser2); 6371 } 6372 catch (ex) { 6373 this._log.warn('Unable to update input MediaStreamSource', ex); 6374 this._inputStreamSource = null; 6375 } 6376 }; 6377 /** 6378 * Update the stream source with the new ouput audio stream. 6379 * @param {MediaStream} stream 6380 * @private 6381 */ 6382 PeerConnection.prototype._updateOutputStreamSource = function (stream) { 6383 if (this._outputStreamSource) { 6384 this._outputStreamSource.disconnect(); 6385 } 6386 try { 6387 this._outputStreamSource = this._audioContext.createMediaStreamSource(stream); 6388 this._outputStreamSource.connect(this._outputAnalyser); 6389 this._outputStreamSource.connect(this._outputAnalyser2); 6390 } 6391 catch (ex) { 6392 this._log.warn('Unable to update output MediaStreamSource', ex); 6393 this._outputStreamSource = null; 6394 } 6395 }; 6396 /** 6397 * Replace the tracks of the current stream with new tracks. We do this rather than replacing the
6398 * whole stream because AnalyzerNodes are bound to a stream. 6399 * @param {Boolean} shouldClone - Whether the stream should be cloned if it is the first 6400 * stream, or set directly. As a rule of thumb, streams that are passed in externally may have 6401 * their lifecycle managed externally, and should be cloned so that we do not tear it or its tracks 6402 * down when the call ends. Streams that we create internally (inside PeerConnection) should be set 6403 * directly so that when the call ends it is disposed of. 6404 * @param {MediaStream} newStream - The new stream to copy the tracks over from. 6405 * @private 6406 */ 6407 PeerConnection.prototype._setInputTracksFromStream = function (shouldClone, newStream) { 6408 if (!newStream) { 6409 return Promise.reject(new InvalidArgumentError('Can not set input stream to null while in a call')); 6410 } 6411 if (!newStream.getAudioTracks().length) { 6412 return Promise.reject(new InvalidArgumentError('Supplied input stream has no audio tracks')); 6413 } 6414 const localStream = this.stream; 6415 const getStreamPromise = () => { 6416 // Apply mute settings to new input track 6417 this.mute(this.isMuted); 6418 return Promise.resolve(this.stream); 6419 }; 6420 if (!localStream) { 6421 // We can't use MediaStream.clone() here because it stopped copying over tracks 6422 // as of Chrome 61. https://bugs.chromium.org/p/chromium/issues/detail?id=770908 6423 this.stream = shouldClone ? cloneStream(newStream, this.options.MediaStream) : newStream; 6424 } 6425 else { 6426 // If the call was started with gUM, and we are now replacing that track with an 6427 // external stream's tracks, we should stop the old managed track. 6428 if (this._shouldManageStream) { 6429 this._stopStream(); 6430 } 6431 if (!this._sender) { 6432 this._sender = this.version.pc.getSenders()[0]; 6433 } 6434 return this._sender.replaceTrack(newStream.getAudioTracks()[0]).then(() => { 6435 this._updateInputStreamSource(newStream); 6436 this.stream = shouldClone ? cloneStream(newStream, this.options.MediaStream) : newStream; 6437 return getStreamPromise(); 6438 }); 6439 } 6440 return getStreamPromise(); 6441 }; 6442 PeerConnection.prototype._onInputDevicesChanged = function () { 6443 if (!this.stream) { 6444 return; 6445 } 6446 // If all of our active tracks are ended, then our active input was lost 6447 const activeInputWasLost = this.stream.getAudioTracks().every(track => track.readyState === 'ended'); 6448 // We only want to act if we manage the stream in PeerConnection (It was created 6449 // here, rather than passed in.) 6450 if (activeInputWasLost && this._shouldManageStream) { 6451 this.openDefaultDeviceWithConstraints({ audio: true }); 6452 } 6453 }; 6454 PeerConnection.prototype._onIceGatheringFailure = function (type) { 6455 this._hasIceGatheringFailures = true; 6456 this.onicegatheringfailure(type); 6457 }; 6458 PeerConnection.prototype._onMediaConnectionStateChange = function (newState) { 6459 const previousState = this._iceState; 6460 if (previousState === newState 6461 || (newState !== 'connected' 6462 && newState !== 'disconnected' 6463 && newState !== 'failed')) { 6464 return; 6465 } 6466 this._iceState = newState; 6467 let message; 6468 switch (newState) { 6469 case 'connected': 6470 if (previousState === 'disconnected' || previousState === 'failed') { 6471 message = 'ICE liveliness check succeeded. Connection with Twilio restored'; 6472 this._log.info(message); 6473 this.onreconnected(message); 6474 } 6475 else { 6476 message = 'Media connection established.'; 6477 this._log.info(message); 6478 this.onconnected(message); 6479 } 6480 this._stopIceGatheringTimeout(); 6481 this._hasIceGatheringFailures = false; 6482 break; 6483 case 'disconnected': 6484 message = 'ICE liveliness check failed. May be having trouble connecting to Twilio'; 6485 this._log.warn(message); 6486 this.ondisconnected(message); 6487 break; 6488 case 'failed': 6489 message = 'Connection with Twilio was interrupted.'; 6490 this._log.warn(message); 6491 this.onfailed(message); 6492 break; 6493 } 6494 }; 6495 PeerConnection.prototype._setSinkIds = function (sinkIds) { 6496 if (!this._isSinkSupported) { 6497 return Promise.reject(new NotSupportedError('Audio output selection is not supported by this browser')); 6498 } 6499 this.sinkIds = new Set(sinkIds.forEach ? sinkIds : [sinkIds]); 6500 return this.version 6501 ? this._updateAudioOutputs() 6502 : Promise.resolve(); 6503 }; 6504 /** 6505 * Start timeout for ICE Gathering 6506 */ 6507 PeerConnection.prototype._startIceGatheringTimeout = function startIceGatheringTimeout() { 6508 this._stopIceGatheringTimeout(); 6509 this._iceGatheringTimeoutId = setTimeout(() => { 6510 this._onIceGatheringFailure(ICE_GATHERING_FAIL_TIMEOUT); 6511 }, ICE_GATHERING_TIMEOUT); 6512 }; 6513 /** 6514 * Stop timeout for ICE Gathering 6515 */ 6516 PeerConnection.prototype._stopIceGatheringTimeout = function stopIceGatheringTimeout() { 6517 clearInterval(this._iceGatheringTimeoutId); 6518 }; 6519 PeerConnection.prototype._updateAudioOutputs = function updateAudioOutputs() { 6520 const addedOutputIds = Array.from(this.sinkIds).filter(function (id) { 6521 return !this.outputs.has(id); 6522 }, this); 6523 const removedOutputIds = Array.from(this.outputs.keys()).filter(function (id) { 6524 return !this.sinkIds.has(id); 6525 }, this); 6526 const self = this; 6527 const createOutputPromises = addedOutputIds.map(this._createAudioOutput, this); 6528 return Promise.all(createOutputPromises).then(() => Promise.all(removedOutputIds.map(self._removeAudioOutput, self))); 6529 }; 6530 PeerConnection.prototype._createAudio = function createAudio(arr) { 6531 const audio = new Audio(arr); 6532 this.onaudio(audio); 6533 return audio; 6534 }; 6535 PeerConnection.prototype._createAudioOutput = function createAudioOutput(id) { 6536 let dest = null; 6537 if (this._mediaStreamSource) { 6538 dest = this._audioContext.createMediaStreamDestination(); 6539 this._mediaStreamSource.connect(dest); 6540 } 6541 const audio = this._createAudio(); 6542 setAudioSource(audio, dest && dest.stream ? dest.stream : this.pcStream); 6543 const self = this; 6544 return audio.setSinkId(id).then(() => audio.play()).then(() => { 6545 self.outputs.set(id, { 6546 audio, 6547 dest, 6548 }); 6549 }); 6550 }; 6551 PeerConnection.prototype._removeAudioOutputs = function removeAudioOutputs() { 6552 if (this._masterAudio && typeof this._masterAudioDeviceId !== 'undefined') { 6553 this._disableOutput(this, this._masterAudioDeviceId); 6554 this.outputs.delete(this._masterAudioDeviceId); 6555 this._masterAudioDeviceId = null; 6556 // Release the audio resources before deleting the audio 6557 if (!this._masterAudio.paused) { 6558 this._masterAudio.pause(); 6559 } 6560 if (typeof this._masterAudio.srcObject !== 'undefined') { 6561 this._masterAudio.srcObject = null; 6562 } 6563 else { 6564 this._masterAudio.src = ''; 6565 } 6566 this._masterAudio = null; 6567 } 6568 return Array.from(this.outputs.keys()).map(this._removeAudioOutput, this); 6569 }; 6570 PeerConnection.prototype._disableOutput = function disableOutput(pc, id) { 6571 const output = pc.outputs.get(id); 6572 if (!output) { 6573 return; 6574 } 6575 if (output.audio) { 6576 output.audio.pause(); 6577 output.audio.src = ''; 6578 } 6579 if (output.dest) { 6580 output.dest.disconnect(); 6581 } 6582 }; 6583 /** 6584 * Disable a non-master output, and update the master output to assume its state. This 6585 * is called when the device ID assigned to the master output has been removed from 6586 * active devices. We can not simply remove the master audio output, so we must 6587 * instead reassign it. 6588 * @private 6589 * @param {PeerConnection} pc 6590 * @param {string} masterId - The current device ID assigned to the master audio element. 6591 */ 6592 PeerConnection.prototype._reassignMasterOutput = function reassignMasterOutput(pc, masterId) { 6593 const masterOutput = pc.outputs.get(masterId); 6594 pc.outputs.delete(masterId); 6595 const self = this; 6596 const activeDeviceId = Array.from(pc.outputs.keys())[0]; 6597 // The audio device key could also be '' on Chrome if no media device permissions are allowed 6598 const idToReplace = typeof activeDeviceId === 'string' ? activeDeviceId : 'default'; 6599 return masterOutput.audio.setSinkId(idToReplace).then(() => { 6600 self._disableOutput(pc, idToReplace); 6601 pc.outputs.set(idToReplace, masterOutput); 6602 pc._masterAudioDeviceId = idToReplace; 6603 }).catch(function rollback() { 6604 pc.outputs.set(masterId, masterOutput); 6605 self._log.info('Could not reassign master output. Attempted to roll back.'); 6606 }); 6607 }; 6608 PeerConnection.prototype._removeAudioOutput = function removeAudioOutput(id) { 6609 if (this._masterAudioDeviceId === id) { 6610 return this._reassignMasterOutput(this, id); 6611 } 6612 this._disableOutput(this, id); 6613 this.outputs.delete(id); 6614 return Promise.resolve(); 6615 }; 6616 /** 6617 * Use an AudioContext to potentially split our audio output stream to multiple 6618 * audio devices. This is only available to browsers with AudioContext and 6619 * HTMLAudioElement.setSinkId() available. We save the source stream in 6620 * _masterAudio, and use it for one of the active audio devices. We keep 6621 * track of its ID because we must replace it if we lose its initial device. 6622 */ 6623 PeerConnection.prototype._onAddTrack = function onAddTrack(pc, stream) { 6624 const audio = pc._masterAudio = this._createAudio(); 6625 setAudioSource(audio, stream); 6626 audio.play(); 6627 // Assign the initial master audio element to a random active output device 6628 const activeDeviceId = Array.from(pc.outputs.keys())[0]; 6629 // The audio device key could also be '' on Chrome if no media device permissions are allowed 6630 const deviceId = typeof activeDeviceId === 'string' ? activeDeviceId : 'default'; 6631 pc._masterAudioDeviceId = deviceId; 6632 pc.outputs.set(deviceId, { audio }); 6633 try { 6634 pc._mediaStreamSource = pc._audioContext.createMediaStreamSource(stream); 6635 } 6636 catch (ex) { 6637 this._log.warn('Unable to create a MediaStreamSource from onAddTrack', ex); 6638 this._mediaStreamSource = null; 6639 } 6640 pc.pcStream = stream; 6641 pc._updateAudioOutputs(); 6642 }; 6643 /** 6644 * Use a single audio element to play the audio output stream. This does not 6645 * support multiple output devices, and is a fallback for when AudioContext 6646 * and/or HTMLAudioElement.setSinkId() is not available to the client. 6647 */ 6648 PeerConnection.prototype._fallbackOnAddTrack = function fallbackOnAddTrack(pc, stream) { 6649 const audio = document && document.createElement('audio'); 6650 audio.autoplay = true; 6651 if (!setAudioSource(audio, stream)) { 6652 pc._log.info('Error attaching stream to element.'); 6653 } 6654 pc.outputs.set('default', { audio }); 6655 }; 6656 PeerConnection.prototype._setEncodingParameters = function (enableDscp) { 6657 if (!enableDscp 6658 || !this._sender 6659 || typeof this._sender.getParameters !== 'function' 6660 || typeof this._sender.setParameters !== 'function') { 6661 return; 6662 } 6663 const params = this._sender.getParameters(); 6664 if (!params.priority && !(params.encodings && params.encodings.length)) { 6665 return; 6666 } 6667 // This is how MDN's RTPSenderParameters defines priority 6668 params.priority = 'high'; 6669 // And this is how it's currently implemented in Chrome M72+ 6670 if (params.encodings && params.encodings.length) {
6671 params.encodings.forEach(encoding => { 6672 encoding.priority = 'high'; 6673 encoding.networkPriority = 'high'; 6674 }); 6675 } 6676 this._sender.setParameters(params); 6677 }; 6678 PeerConnection.prototype._setupPeerConnection = function (rtcConfiguration) { 6679 const self = this; 6680 const version = new (this.options.rtcpcFactory || RTCPC)({ RTCPeerConnection: this.options.RTCPeerConnection }); 6681 version.create(rtcConfiguration); 6682 addStream(version.pc, this.stream); 6683 const supportedCodecs = RTCRtpReceiver.getCapabilities('audio').codecs; 6684 this._log.debug('sorting codecs', supportedCodecs, this.codecPreferences); 6685 const sortedCodecs = sortByMimeTypes(supportedCodecs, this.codecPreferences); 6686 const [transceiver] = version.pc.getTransceivers(); 6687 this._log.debug('setting sorted codecs', sortedCodecs); 6688 transceiver.setCodecPreferences(sortedCodecs); 6689 const eventName = 'ontrack' in version.pc 6690 ? 'ontrack' : 'onaddstream'; 6691 version.pc[eventName] = event => { 6692 const stream = self._remoteStream = event.stream || event.streams[0]; 6693 if (typeof version.pc.getSenders === 'function') { 6694 this._sender = version.pc.getSenders()[0]; 6695 } 6696 if (self._isSinkSupported) { 6697 self._onAddTrack(self, stream); 6698 } 6699 else { 6700 self._fallbackOnAddTrack(self, stream); 6701 } 6702 self._startPollingVolume(); 6703 }; 6704 return version; 6705 }; 6706 PeerConnection.prototype._maybeSetIceAggressiveNomination = function (sdp) { 6707 return this.options.forceAggressiveIceNomination ? setIceAggressiveNomination(sdp) : sdp; 6708 }; 6709 PeerConnection.prototype._setupChannel = function () { 6710 const pc = this.version.pc; 6711 // Chrome 25 supports onopen 6712 this.version.pc.onopen = () => { 6713 this.status = 'open'; 6714 this.onopen(); 6715 }; 6716 // Chrome 26 doesn't support onopen so must detect state change 6717 this.version.pc.onstatechange = () => { 6718 if (this.version.pc && this.version.pc.readyState === 'stable') { 6719 this.status = 'open'; 6720 this.onopen(); 6721 } 6722 }; 6723 // Chrome 27 changed onstatechange to onsignalingstatechange 6724 this.version.pc.onsignalingstatechange = () => { 6725 const state = pc.signalingState; 6726 this._log.info(`signalingState is "${state}"`); 6727 if (this.version.pc && this.version.pc.signalingState === 'stable') { 6728 this.status = 'open'; 6729 this.onopen(); 6730 } 6731 this.onsignalingstatechange(pc.signalingState); 6732 }; 6733 // Chrome 72+ 6734 pc.onconnectionstatechange = event => { 6735 let state = pc.connectionState; 6736 if (!state && event && event.target) { 6737 // VDI environment 6738 const targetPc = event.target; 6739 state = targetPc.connectionState || targetPc.connectionState_; 6740 this._log.info(`pc.connectionState not detected. Using target PC. State=${state}`); 6741 } 6742 if (!state) { 6743 this._log.warn(`onconnectionstatechange detected but state is "${state}"`); 6744 } 6745 else { 6746 this._log.info(`pc.connectionState is "${state}"`); 6747 } 6748 this.onpcconnectionstatechange(state); 6749 this._onMediaConnectionStateChange(state); 6750 }; 6751 pc.onicecandidate = event => { 6752 const { candidate } = event; 6753 if (candidate) { 6754 this._hasIceCandidates = true; 6755 this.onicecandidate(candidate); 6756 this._setupRTCIceTransportListener(); 6757 } 6758 this._log.info(`ICE Candidate: ${JSON.stringify(candidate)}`); 6759 }; 6760 pc.onicegatheringstatechange = () => { 6761 const state = pc.iceGatheringState; 6762 if (state === 'gathering') { 6763 this._startIceGatheringTimeout(); 6764 } 6765 else if (state === 'complete') { 6766 this._stopIceGatheringTimeout(); 6767 // Fail if no candidates found 6768 if (!this._hasIceCandidates) { 6769 this._onIceGatheringFailure(ICE_GATHERING_FAIL_NONE); 6770 } 6771 // There was a failure mid-gathering phase. We want to start our timer and issue 6772 // an ice restart if we don't get connected after our timeout 6773 if (this._hasIceCandidates && this._hasIceGatheringFailures) { 6774 this._startIceGatheringTimeout(); 6775 } 6776 } 6777 this._log.info(`pc.iceGatheringState is "${pc.iceGatheringState}"`); 6778 this.onicegatheringstatechange(state); 6779 }; 6780 pc.oniceconnectionstatechange = () => { 6781 this._log.info(`pc.iceConnectionState is "${pc.iceConnectionState}"`); 6782 this.oniceconnectionstatechange(pc.iceConnectionState); 6783 this._onMediaConnectionStateChange(pc.iceConnectionState); 6784 }; 6785 }; 6786 PeerConnection.prototype._initializeMediaStream = function (rtcConfiguration) { 6787 // if mediastream already open then do nothing 6788 if (this.status === 'open') { 6789 return false;
6790 } 6791 if (this.pstream.status === 'disconnected') { 6792 this.onerror({ info: { 6793 code: 31000, 6794 message: 'Cannot establish connection. Client is disconnected', 6795 twilioError: new SignalingErrors.ConnectionDisconnected(), 6796 } }); 6797 this.close(); 6798 return false; 6799 } 6800 this.version = this._setupPeerConnection(rtcConfiguration); 6801 this._setupChannel(); 6802 return true; 6803 }; 6804 /** 6805 * Remove reconnection-related listeners 6806 * @private 6807 */ 6808 PeerConnection.prototype._removeReconnectionListeners = function () { 6809 if (this.pstream) { 6810 this.pstream.removeListener('answer', this._onAnswerOrRinging); 6811 this.pstream.removeListener('hangup', this._onHangup); 6812 } 6813 }; 6814 /** 6815 * Setup a listener for RTCDtlsTransport to capture state changes events 6816 * @private 6817 */ 6818 PeerConnection.prototype._setupRTCDtlsTransportListener = function () { 6819 const dtlsTransport = this.getRTCDtlsTransport(); 6820 if (!dtlsTransport || dtlsTransport.onstatechange) { 6821 return; 6822 } 6823 const handler = () => { 6824 this._log.info(`dtlsTransportState is "${dtlsTransport.state}"`); 6825 this.ondtlstransportstatechange(dtlsTransport.state); 6826 }; 6827 // Publish initial state 6828 handler(); 6829 dtlsTransport.onstatechange = handler; 6830 }; 6831 /** 6832 * Setup a listener for RTCIceTransport to capture selected candidate pair changes 6833 * @private 6834 */ 6835 PeerConnection.prototype._setupRTCIceTransportListener = function () { 6836 const iceTransport = this._getRTCIceTransport(); 6837 if (!iceTransport || iceTransport.onselectedcandidatepairchange) { 6838 return; 6839 } 6840 iceTransport.onselectedcandidatepairchange = () => this.onselectedcandidatepairchange(iceTransport.getSelectedCandidatePair()); 6841 }; 6842 /** 6843 * Restarts ICE for the current connection 6844 * ICE Restart failures are ignored. Retries are managed in Connection 6845 * @private 6846 */ 6847 PeerConnection.prototype.iceRestart = function () { 6848 this._log.info('Attempting to restart ICE...'); 6849 this._hasIceCandidates = false; 6850 this.version.createOffer(this.options.maxAverageBitrate, { iceRestart: true }).then(() => { 6851 this._removeReconnectionListeners(); 6852 this._onAnswerOrRinging = payload => { 6853 this._removeReconnectionListeners(); 6854 if (!payload.sdp || this.version.pc.signalingState !== 'have-local-offer') { 6855 const message = 'Invalid state or param during ICE Restart:' 6856 + `hasSdp:${!!payload.sdp}, signalingState:${this.version.pc.signalingState}`; 6857 this._log.warn(message); 6858 return; 6859 } 6860 const sdp = this._maybeSetIceAggressiveNomination(payload.sdp); 6861 this._answerSdp = sdp; 6862 if (this.status !== 'closed') { 6863 this.version.processAnswer(this.codecPreferences, sdp, null, err => { 6864 const message = err && err.message ? err.message : err; 6865 this._log.error(`Failed to process answer during ICE Restart. Error: ${message}`); 6866 }); 6867 } 6868 }; 6869 this._onHangup = () => { 6870 this._log.info('Received hangup during ICE Restart'); 6871 this._removeReconnectionListeners(); 6872 }; 6873 this.pstream.on('answer', this._onAnswerOrRinging); 6874 this.pstream.on('hangup', this._onHangup); 6875 this.pstream.reinvite(this.version.getSDP(), this.callSid); 6876 }).catch((err) => { 6877 const message = err && err.message ? err.message : err; 6878 this._log.error(`Failed to createOffer during ICE Restart. Error: ${message}`); 6879 // CreateOffer failures doesn't transition ice state to failed 6880 // We need trigger it so it can be picked up by retries 6881 this.onfailed(message); 6882 }); 6883 }; 6884 PeerConnection.prototype.makeOutgoingCall = function (params, signalingReconnectToken, callsid, rtcConfiguration, onMediaStarted) { 6885 if (!this._initializeMediaStream(rtcConfiguration)) { 6886 return; 6887 } 6888 const self = this; 6889 this.callSid = callsid; 6890 function onAnswerSuccess() { 6891 if (self.options) { 6892 self._setEncodingParameters(self.options.dscp); 6893 } 6894 onMediaStarted(self.version.pc); 6895 } 6896 function onAnswerError(err) { 6897 const errMsg = err.message || err; 6898 self.onerror({ info: { 6899 code: 31000, 6900 message: `Error processing answer: ${errMsg}`, 6901 twilioError: new MediaErrors.ClientRemoteDescFailed(), 6902 } }); 6903 } 6904 this._onAnswerOrRinging = payload => { 6905 if (!payload.sdp) { 6906 return; 6907 } 6908 const sdp = this._maybeSetIceAggressiveNomination(payload.sdp); 6909 self._answerSdp = sdp; 6910 if (self.status !== 'closed') { 6911 self.version.processAnswer(this.codecPreferences, sdp, onAnswerSuccess, onAnswerError); 6912 } 6913 self.pstream.removeListener('answer', self._onAnswerOrRinging); 6914 self.pstream.removeListener('ringing', self._onAnswerOrRinging); 6915 }; 6916 this.pstream.on('answer', this._onAnswerOrRinging); 6917 this.pstream.on('ringing', this._onAnswerOrRinging); 6918 function onOfferSuccess() { 6919 if (self.status !== 'closed') { 6920 if (signalingReconnectToken) { 6921 self.pstream.reconnect(self.version.getSDP(), self.callSid, signalingReconnectToken); 6922 } 6923 else { 6924 self.pstream.invite(self.version.getSDP(), self.callSid, params); 6925 } 6926 self._setupRTCDtlsTransportListener(); 6927 } 6928 } 6929 function onOfferError(err) { 6930 const errMsg = err.message || err; 6931 self.onerror({ info: { 6932 code: 31000, 6933 message: `Error creating the offer: ${errMsg}`, 6934 twilioError: new MediaErrors.ClientLocalDescFailed(), 6935 } }); 6936 } 6937 this.version.createOffer(this.options.maxAverageBitrate, { audio: true }, onOfferSuccess, onOfferError); 6938 }; 6939 PeerConnection.prototype.answerIncomingCall = function (callSid, sdp, rtcConfiguration, onMediaStarted) { 6940 if (!this._initializeMediaStream(rtcConfiguration)) { 6941 return; 6942 } 6943 sdp = this._maybeSetIceAggressiveNomination(sdp); 6944 this._answerSdp = sdp.replace(/^a=setup:actpass$/gm, 'a=setup:passive'); 6945 this.callSid = callSid; 6946 const self = this; 6947 function onAnswerSuccess() { 6948 if (self.status !== 'closed') { 6949 self.pstream.answer(self.version.getSDP(), callSid); 6950 if (self.options) { 6951 self._setEncodingParameters(self.options.dscp); 6952 } 6953 onMediaStarted(self.version.pc); 6954 self._setupRTCDtlsTransportListener(); 6955 } 6956 } 6957 function onAnswerError(err) { 6958 const errMsg = err.message || err; 6959 self.onerror({ info: { 6960 code: 31000, 6961 message: `Error creating the answer: ${errMsg}`, 6962 twilioError: new MediaErrors.ClientRemoteDescFailed(), 6963 } }); 6964 } 6965 this.version.processSDP(this.options.maxAverageBitrate, this.codecPreferences, sdp, { audio: true }, onAnswerSuccess, onAnswerError); 6966 }; 6967 PeerConnection.prototype.close = function () { 6968 if (this.version && this.version.pc) { 6969 if (this.version.pc.signalingState !== 'closed') { 6970 this.version.pc.close(); 6971 } 6972 this.version.pc = null; 6973 } 6974 if (this.stream) { 6975 this.mute(false); 6976 this._stopStream(); 6977 } 6978 this.stream = null; 6979 this._removeReconnectionListeners(); 6980 this._stopIceGatheringTimeout(); 6981 Promise.all(this._removeAudioOutputs()).catch(() => { 6982 // We don't need to alert about failures here. 6983 }); 6984 if (this._mediaStreamSource) { 6985 this._mediaStreamSource.disconnect(); 6986 } 6987 if (this._inputAnalyser) { 6988 this._inputAnalyser.disconnect(); 6989 } 6990 if (this._outputAnalyser) { 6991 this._outputAnalyser.disconnect(); 6992 } 6993 if (this._inputAnalyser2) { 6994 this._inputAnalyser2.disconnect(); 6995 } 6996 if (this._outputAnalyser2) { 6997 this._outputAnalyser2.disconnect(); 6998 } 6999 this.status = 'closed'; 7000 this.onclose(); 7001 }; 7002 PeerConnection.prototype.reject = function (callSid) { 7003 this.callSid = callSid; 7004 }; 7005 PeerConnection.prototype.ignore = function (callSid) { 7006 this.callSid = callSid; 7007 }; 7008 /** 7009 * Mute or unmute input audio. If the stream is not yet present, the setting 7010 * is saved and applied to future streams/tracks. 7011 * @params {boolean} shouldMute - Whether the input audio should 7012 * be muted or unmuted. 7013 */ 7014 PeerConnection.prototype.mute = function (shouldMute) { 7015 this.isMuted = shouldMute; 7016 if (!this.stream) { 7017 return; 7018 } 7019 if (this._sender && this._sender.track) { 7020 this._sender.track.enabled = !shouldMute; 7021 } 7022 else { 7023 const audioTracks = typeof this.stream.getAudioTracks === 'function' 7024 ? this.stream.getAudioTracks() 7025 : this.stream.audioTracks; 7026 audioTracks.forEach(track => { 7027 track.enabled = !shouldMute; 7028 }); 7029 } 7030 }; 7031 /** 7032 * Get or create an RTCDTMFSender for the first local audio MediaStreamTrack
7033 * we can get from the RTCPeerConnection. Return null if unsupported. 7034 * @instance 7035 * @returns ?RTCDTMFSender 7036 */ 7037 PeerConnection.prototype.getOrCreateDTMFSender = function getOrCreateDTMFSender() { 7038 if (this._dtmfSender || this._dtmfSenderUnsupported) { 7039 return this._dtmfSender || null; 7040 } 7041 const self = this; 7042 const pc = this.version.pc; 7043 if (!pc) { 7044 this._log.warn('No RTCPeerConnection available to call createDTMFSender on'); 7045 return null; 7046 } 7047 if (typeof pc.getSenders === 'function' && (typeof RTCDTMFSender === 'function' || typeof RTCDtmfSender === 'function')) { 7048 const chosenSender = pc.getSenders().find(sender => sender.dtmf); 7049 if (chosenSender) { 7050 this._log.info('Using RTCRtpSender#dtmf'); 7051 this._dtmfSender = chosenSender.dtmf; 7052 return this._dtmfSender; 7053 } 7054 } 7055 if (typeof pc.createDTMFSender === 'function' && typeof pc.getLocalStreams === 'function') { 7056 const track = pc.getLocalStreams().map(stream => { 7057 const tracks = self._getAudioTracks(stream); 7058 return tracks && tracks[0]; 7059 })[0]; 7060 if (!track) { 7061 this._log.warn('No local audio MediaStreamTrack available on the RTCPeerConnection to pass to createDTMFSender'); 7062 return null; 7063 } 7064 this._log.info('Creating RTCDTMFSender'); 7065 this._dtmfSender = pc.createDTMFSender(track); 7066 return this._dtmfSender; 7067 } 7068 this._log.info('RTCPeerConnection does not support RTCDTMFSender'); 7069 this._dtmfSenderUnsupported = true; 7070 return null; 7071 }; 7072 /** 7073 * Get the RTCDtlTransport object from the PeerConnection 7074 * @returns RTCDtlTransport 7075 */ 7076 PeerConnection.prototype.getRTCDtlsTransport = function getRTCDtlsTransport() { 7077 const sender = this.version && this.version.pc 7078 && typeof this.version.pc.getSenders === 'function' 7079 && this.version.pc.getSenders()[0]; 7080 return sender && sender.transport || null; 7081 }; 7082 PeerConnection.prototype._canStopMediaStreamTrack = () => typeof MediaStreamTrack.prototype.stop === 'function'; 7083 PeerConnection.prototype._getAudioTracks = stream => typeof stream.getAudioTracks === 'function' ? 7084 stream.getAudioTracks() : stream.audioTracks; 7085 /** 7086 * Get the RTCIceTransport object from the PeerConnection 7087 * @returns RTCIceTransport 7088 */ 7089 PeerConnection.prototype._getRTCIceTransport = function _getRTCIceTransport() { 7090 const dtlsTransport = this.getRTCDtlsTransport(); 7091 return dtlsTransport && dtlsTransport.iceTransport || null; 7092 }; 7093 // Is PeerConnection.protocol used outside of our SDK? We should remove this if not. 7094 PeerConnection.protocol = ((() => RTCPC.test() ? new RTCPC() : null))(); 7095 function addStream(pc, stream) { 7096 if (typeof pc.addTrack === 'function') { 7097 stream.getAudioTracks().forEach(track => { 7098 // The second parameters, stream, should not be necessary per the latest editor's 7099 // draft, but FF requires it. https://bugzilla.mozilla.org/show_bug.cgi?id=1231414 7100 pc.addTrack(track, stream); 7101 }); 7102 } 7103 else { 7104 pc.addStream(stream); 7105 } 7106 } 7107 function cloneStream(oldStream, _MediaStream) { 7108 let newStream; 7109 if (_MediaStream) { 7110 newStream = new _MediaStream(); 7111 } 7112 else if (typeof MediaStream !== 'undefined') { 7113 newStream = new MediaStream(); 7114 } 7115 else { 7116 newStream = new webkitMediaStream(); 7117 } 7118 oldStream.getAudioTracks().forEach(newStream.addTrack, newStream); 7119 return newStream; 7120 } 7121 /** 7122 * Set the source of an HTMLAudioElement to the specified MediaStream 7123 * @param {HTMLAudioElement} audio 7124 * @param {MediaStream} stream 7125 * @returns {boolean} Whether the audio source was set successfully 7126 */ 7127 function setAudioSource(audio, stream) { 7128 if (typeof audio.srcObject !== 'undefined') { 7129 audio.srcObject = stream; 7130 } 7131 else if (typeof audio.mozSrcObject !== 'undefined') { 7132 audio.mozSrcObject = stream; 7133 } 7134 else if (typeof audio.src !== 'undefined') { 7135 const _window = audio.options.window || window; 7136 audio.src = (_window.URL || _window.webkitURL).createObjectURL(stream); 7137 } 7138 else { 7139 return false;
7140 } 7141 return true; 7142 } 7143 PeerConnection.enabled = RTCPC.test(); 7144 7145 // @ts-nocheck 7146 function enabled() { 7147 return RTCPC.test(); 7148 } 7149 function getMediaEngine() { 7150 return typeof RTCIceGatherer !== 'undefined' ? 'ORTC' : 'WebRTC'; 7151 } 7152 7153 // @ts-nocheck 7154 function getUserMedia(constraints, options) { 7155 options = options || {}; 7156 options.util = options.util || util; 7157 options.navigator = options.navigator 7158 || (typeof navigator !== 'undefined' ? navigator : null); 7159 return new Promise((resolve, reject) => { 7160 if (!options.navigator) { 7161 throw new NotSupportedError('getUserMedia is not supported'); 7162 } 7163 switch ('function') { 7164 case typeof (options.navigator.mediaDevices && options.navigator.mediaDevices.getUserMedia): 7165 return resolve(options.navigator.mediaDevices.getUserMedia(constraints)); 7166 case typeof options.navigator.webkitGetUserMedia: 7167 return options.navigator.webkitGetUserMedia(constraints, resolve, reject); 7168 case typeof options.navigator.mozGetUserMedia: 7169 return options.navigator.mozGetUserMedia(constraints, resolve, reject); 7170 case typeof options.navigator.getUserMedia: 7171 return options.navigator.getUserMedia(constraints, resolve, reject); 7172 default: 7173 throw new NotSupportedError('getUserMedia is not supported'); 7174 } 7175 }).catch(e => { 7176 throw (options.util.isFirefox() && e.name === 'NotReadableError') 7177 ? new NotSupportedError('Firefox does not currently support opening multiple audio input tracks' + 7178 'simultaneously, even across different tabs.\n' + 7179 'Related Bugzilla thread: https://bugzilla.mozilla.org/show_bug.cgi?id=1299324') 7180 : e; 7181 }); 7182 } 7183 7184 /** 7185 * Generates a 128-bit long random string that is formatted as a 32 long string 7186 * of characters where each character is from the set: 7187 * [0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f]. 7188 */ 7189 function generateRandomizedString() { 7190 if (typeof window !== 'object') { 7191 throw new NotSupportedError('This platform is not supported.'); 7192 } 7193 const { crypto } = window; 7194 if (typeof crypto !== 'object') { 7195 throw new NotSupportedError('The `crypto` module is not available on this platform.'); 7196 } 7197 if (typeof crypto.getRandomValues !== 'function') { 7198 throw new NotSupportedError('The function `crypto.getRandomValues` is not available on this ' + 7199 'platform.'); 7200 } 7201 if (typeof window.Uint8Array !== 'function') { 7202 throw new NotSupportedError('The `Uint8Array` module is not available on this platform.'); 7203 } 7204 return crypto 7205 .getRandomValues(new window.Uint8Array(16)) 7206 .reduce((r, n) => `${r}${n.toString(16).padStart(2, '0')}`, ''); 7207 } 7208 function generateVoiceEventSid() { 7209 return `KX${generateRandomizedString()}`; 7210 } 7211 7212 /** 7213 * Deferred Promise 7214 */ 7215 let Deferred$1 = class Deferred { 7216 /** 7217 * @constructor 7218 */ 7219 constructor() { 7220 this._promise = new Promise((resolve, reject) => { 7221 this._resolve = resolve; 7222 this._reject = reject; 7223 }); 7224 } 7225 /** 7226 * @returns The {@link Deferred} Promise 7227 */ 7228 get promise() { 7229 return this._promise; 7230 } 7231 /** 7232 * Rejects this promise 7233 */ 7234 reject(reason) { 7235 this._reject(reason); 7236 } 7237 /** 7238 * Resolves this promise 7239 */ 7240 resolve(value) { 7241 this._resolve(value); 7242 } 7243 }; 7244 7245 /** 7246 * Queue async operations and executes them synchronously. 7247 */ 7248 class AsyncQueue { 7249 constructor() { 7250 /** 7251 * The list of async operations in this queue 7252 */ 7253 this._operations = []; 7254 } 7255 /** 7256 * Adds the async operation to the queue 7257 * @param callback An async callback that returns a promise 7258 * @returns A promise that will get resolved or rejected after executing the callback 7259 */ 7260 enqueue(callback) { 7261 const hasPending = !!this._operations.length; 7262 const deferred = new Deferred$1(); 7263 this._operations.push({ deferred, callback }); 7264 if (!hasPending) { 7265 this._processQueue(); 7266 } 7267 return deferred.promise; 7268 } 7269 /** 7270 * Start processing the queue. This executes the first item and removes it after. 7271 * Then do the same for next items until the queue is emptied. 7272 */ 7273 _processQueue() { 7274 return __awaiter(this, void 0, void 0, function* () { 7275 while (this._operations.length) { 7276 // Grab first item, don't remove from array yet until it's resolved/rejected 7277 const { deferred, callback } = this._operations[0]; 7278 // We want to capture the result/error first so we can remove the item from the queue later 7279 let result; 7280 let error; 7281 // Sometimes result and error are empty. So let's use a separate flag to determine if the promise has resolved 7282 let hasResolved; 7283 try { 7284 result = yield callback(); 7285 hasResolved = true; 7286 } 7287 catch (e) { 7288 error = e; 7289 } 7290 // Remove the item 7291 this._operations.shift(); 7292 if (hasResolved) { 7293 deferred.resolve(result); 7294 } 7295 else { 7296 deferred.reject(error); 7297 } 7298 } 7299 }); 7300 } 7301 } 7302 7303 // @ts-nocheck 7304 class Deferred { 7305 get reject() { return this._reject; } 7306 get resolve() { return this._resolve; } 7307 constructor() { 7308 this.promise = new Promise((resolve, reject) => { 7309 this._resolve = resolve; 7310 this._reject = reject; 7311 }); 7312 } 7313 } 7314 7315 // @ts-nocheck 7316 class EventTarget { 7317 constructor() { 7318 this._eventEmitter = new eventsExports.EventEmitter(); 7319 } 7320 addEventListener(name, handler) { 7321 return this._eventEmitter.addListener(name, handler); 7322 } 7323 dispatchEvent(name, ...args) { 7324 return this._eventEmitter.emit(name, ...args); 7325 } 7326 removeEventListener(name, handler) { 7327 return this._eventEmitter.removeListener(name, handler); 7328 } 7329 } 7330
7331 /** 7332 * An {@link AudioPlayer} is an HTMLAudioElement-like object that uses AudioContext 7333 * to circumvent browser limitations. 7334 * @private 7335 */ 7336 class AudioPlayer extends EventTarget { 7337 get destination() { return this._destination; } 7338 get loop() { return this._loop; } 7339 set loop(shouldLoop) { 7340 const self = this; 7341 function pauseAfterPlaythrough() { 7342 self._audioNode.removeEventListener('ended', pauseAfterPlaythrough); 7343 self.pause(); 7344 } 7345 // If a sound is already looping, it should continue playing 7346 // the current playthrough and then stop. 7347 if (!shouldLoop && this.loop && !this.paused) { 7348 this._audioNode.addEventListener('ended', pauseAfterPlaythrough); 7349 } 7350 this._loop = shouldLoop; 7351 } 7352 /** 7353 * Whether the audio element is muted. 7354 */ 7355 get muted() { return this._gainNode.gain.value === 0; } 7356 set muted(shouldBeMuted) { 7357 this._gainNode.gain.value = shouldBeMuted ? 0 : 1; 7358 } 7359 /** 7360 * Whether the sound is paused. this._audioNode only exists when sound is playing; 7361 * otherwise AudioPlayer is considered paused. 7362 */ 7363 get paused() { return this._audioNode === null; } 7364 get src() { return this._src; } 7365 set src(src) { 7366 this._load(src); 7367 } 7368 /** 7369 * The srcObject of the HTMLMediaElement 7370 */ 7371 get srcObject() { 7372 return this._audioElement.srcObject; 7373 } 7374 set srcObject(srcObject) { 7375 this._audioElement.srcObject = srcObject; 7376 } 7377 get sinkId() { return this._sinkId; } 7378 /** 7379 * @private 7380 */ 7381 constructor(audioContext, srcOrOptions = {}, options = {}) { 7382 super(); 7383 /** 7384 * The AudioBufferSourceNode of the actively loaded sound. Null if a sound 7385 * has not been loaded yet. This is re-used for each time the sound is 7386 * played. 7387 */ 7388 this._audioNode = null; 7389 /** 7390 * Whether or not the audio element should loop. If disabled during playback, 7391 * playing continues until the sound ends and then stops looping. 7392 */ 7393 this._loop = false; 7394 /** 7395 * An Array of deferred-like objects for each pending `play` Promise. When 7396 * .pause() is called or .src is set, all pending play Promises are 7397 * immediately rejected. 7398 */ 7399 this._pendingPlayDeferreds = []; 7400 /** 7401 * The current sinkId of the device audio is being played through. 7402 */ 7403 this._sinkId = 'default'; 7404 /** 7405 * The source URL of the sound to play. When set, the currently playing sound will stop. 7406 */ 7407 this._src = ''; 7408 if (typeof srcOrOptions !== 'string') { 7409 options = srcOrOptions; 7410 } 7411 this._audioContext = audioContext; 7412 this._audioElement = new (options.AudioFactory || Audio)(); 7413 this._bufferPromise = this._createPlayDeferred().promise; 7414 this._destination = this._audioContext.destination; 7415 this._gainNode = this._audioContext.createGain(); 7416 this._gainNode.connect(this._destination); 7417 this._XMLHttpRequest = options.XMLHttpRequestFactory || XMLHttpRequest; 7418 this.addEventListener('canplaythrough', () => { 7419 this._resolvePlayDeferreds(); 7420 }); 7421 if (typeof srcOrOptions === 'string') { 7422 this.src = srcOrOptions; 7423 } 7424 } 7425 /** 7426 * Stop any ongoing playback and reload the source file. 7427 */ 7428 load() { 7429 this._load(this._src); 7430 } 7431 /** 7432 * Pause the audio coming from this AudioPlayer. This will reject any pending 7433 * play Promises. 7434 */ 7435 pause() { 7436 if (this.paused) { 7437 return; 7438 } 7439 this._audioElement.pause(); 7440 this._audioNode.stop(); 7441 this._audioNode.disconnect(this._gainNode); 7442 this._audioNode = null; 7443 this._rejectPlayDeferreds(new Error('The play() request was interrupted by a call to pause().')); 7444 } 7445 /** 7446 * Play the sound. If the buffer hasn't loaded yet, wait for the buffer to load. If 7447 * the source URL is not set yet, this Promise will remain pending until a source 7448 * URL is set. 7449 */ 7450 play() { 7451 return __awaiter(this, void 0, void 0, function* () { 7452 if (!this.paused) { 7453 yield this._bufferPromise; 7454 if (!this.paused) { 7455 return; 7456 } 7457 throw new Error('The play() request was interrupted by a call to pause().'); 7458 } 7459 this._audioNode = this._audioContext.createBufferSource(); 7460 this._audioNode.loop = this.loop; 7461 this._audioNode.addEventListener('ended', () => { 7462 if (this._audioNode && this._audioNode.loop) { 7463 return; 7464 } 7465 this.dispatchEvent('ended'); 7466 }); 7467 const buffer = yield this._bufferPromise; 7468 if (this.paused) { 7469 throw new Error('The play() request was interrupted by a call to pause().'); 7470 } 7471 this._audioNode.buffer = buffer; 7472 this._audioNode.connect(this._gainNode); 7473 this._audioNode.start(); 7474 if (this._audioElement.srcObject) { 7475 return this._audioElement.play(); 7476 } 7477 }); 7478 } 7479 /** 7480 * Change which device the sound should play through. 7481 * @param sinkId - The sink of the device to play sound through. 7482 */ 7483 setSinkId(sinkId) { 7484 return __awaiter(this, void 0, void 0, function* () { 7485 if (typeof this._audioElement.setSinkId !== 'function') { 7486 throw new Error('This browser does not support setSinkId.'); 7487 } 7488 if (sinkId === this.sinkId) { 7489 return; 7490 } 7491 if (sinkId === 'default') { 7492 if (!this.paused) { 7493 this._gainNode.disconnect(this._destination); 7494 } 7495 this._audioElement.srcObject = null; 7496 this._destination = this._audioContext.destination; 7497 this._gainNode.connect(this._destination); 7498 this._sinkId = sinkId; 7499 return; 7500 } 7501 yield this._audioElement.setSinkId(sinkId); 7502 if (this._audioElement.srcObject) { 7503 return; 7504 } 7505 this._gainNode.disconnect(this._audioContext.destination); 7506 this._destination = this._audioContext.createMediaStreamDestination(); 7507 this._audioElement.srcObject = this._destination.stream; 7508 this._sinkId = sinkId; 7509 this._gainNode.connect(this._destination); 7510 }); 7511 } 7512 /** 7513 * Create a Deferred for a Promise that will be resolved when .src is set or rejected 7514 * when .pause is called. 7515 */ 7516 _createPlayDeferred() { 7517 const deferred = new Deferred(); 7518 this._pendingPlayDeferreds.push(deferred); 7519 return deferred; 7520 } 7521 /** 7522 * Stop current playback and load a sound file. 7523 * @param src - The source URL of the file to load 7524 */ 7525 _load(src) { 7526 if (this._src && this._src !== src) { 7527 this.pause(); 7528 } 7529 this._src = src; 7530 this._bufferPromise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { 7531 if (!src) { 7532 return this._createPlayDeferred().promise; 7533 } 7534 const buffer = yield bufferSound(this._audioContext, this._XMLHttpRequest, src); 7535 this.dispatchEvent('canplaythrough'); 7536 resolve(buffer); 7537 })); 7538 } 7539 /**
7540 * Reject all deferreds for the Play promise. 7541 * @param reason 7542 */ 7543 _rejectPlayDeferreds(reason) { 7544 const deferreds = this._pendingPlayDeferreds; 7545 deferreds.splice(0, deferreds.length).forEach(({ reject }) => reject(reason)); 7546 } 7547 /** 7548 * Resolve all deferreds for the Play promise. 7549 * @param result 7550 */ 7551 _resolvePlayDeferreds(result) { 7552 const deferreds = this._pendingPlayDeferreds; 7553 deferreds.splice(0, deferreds.length).forEach(({ resolve }) => resolve(result)); 7554 } 7555 } 7556 /** 7557 * Use XMLHttpRequest to load the AudioBuffer of a remote audio asset. 7558 * @private 7559 * @param context - The AudioContext to use to decode the audio data 7560 * @param RequestFactory - The XMLHttpRequest factory to build 7561 * @param src - The URL of the audio asset to load. 7562 * @returns A Promise containing the decoded AudioBuffer. 7563 */ 7564 // tslint:disable-next-line:variable-name 7565 function bufferSound(context, RequestFactory, src) { 7566 return __awaiter(this, void 0, void 0, function* () { 7567 const request = new RequestFactory(); 7568 request.open('GET', src, true); 7569 request.responseType = 'arraybuffer'; 7570 const event = yield new Promise(resolve => { 7571 request.addEventListener('load', resolve); 7572 request.send(); 7573 }); 7574 // Safari uses a callback here instead of a Promise. 7575 try { 7576 return context.decodeAudioData(event.target.response); 7577 } 7578 catch (e) { 7579 return new Promise(resolve => { 7580 context.decodeAudioData(event.target.response, resolve); 7581 }); 7582 } 7583 }); 7584 } 7585 7586 // @ts-nocheck 7587 /** 7588 * @class 7589 * @param {string} name - Name of the sound 7590 * @param {string} url - URL of the sound 7591 * @param {Sound#ConstructorOptions} options 7592 * @property {boolean} isPlaying - Whether the Sound is currently playing audio. 7593 * @property {string} name - Name of the sound 7594 * @property {string} url - URL of the sound 7595 * @property {AudioContext} audioContext - The AudioContext to use if available for AudioPlayer. 7596 */ 7597 /** 7598 * @typedef {Object} Sound#ConstructorOptions 7599 * @property {number} [maxDuration=0] - The maximum length of time to play the sound 7600 * before stopping it. 7601 * @property {Boolean} [shouldLoop=false] - Whether the sound should be looped. 7602 */ 7603 function Sound(name, url, options) { 7604 if (!(this instanceof Sound)) { 7605 return new Sound(name, url, options); 7606 } 7607 if (!name || !url) { 7608 throw new InvalidArgumentError('name and url are required arguments'); 7609 } 7610 options = Object.assign({ 7611 AudioFactory: typeof Audio !== 'undefined' ? Audio : null, 7612 maxDuration: 0, 7613 shouldLoop: false, 7614 }, options); 7615 options.AudioPlayer = options.audioContext 7616 ? AudioPlayer.bind(AudioPlayer, options.audioContext) 7617 : options.AudioFactory; 7618 Object.defineProperties(this, { 7619 _Audio: { value: options.AudioPlayer }, 7620 _activeEls: { value: new Map() }, 7621 _isSinkSupported: { 7622 value: options.AudioFactory !== null 7623 && typeof options.AudioFactory.prototype.setSinkId === 'function', 7624 }, 7625 _maxDuration: { value: options.maxDuration }, 7626 _maxDurationTimeout: { 7627 value: null, 7628 writable: true, 7629 }, 7630 _operations: { value: new AsyncQueue() }, 7631 _playPromise: { 7632 value: null, 7633 writable: true, 7634 }, 7635 _shouldLoop: { value: options.shouldLoop }, 7636 _sinkIds: { value: ['default'] }, 7637 isPlaying: { 7638 enumerable: true, 7639 get() { 7640 return !!this._playPromise; 7641 }, 7642 }, 7643 name: { 7644 enumerable: true, 7645 value: name, 7646 }, 7647 url: { 7648 enumerable: true, 7649 value: url, 7650 }, 7651 }); 7652 if (this._Audio) { 7653 // Play it (muted and should not loop) as soon as possible so that it does not get incorrectly caught by Chrome's 7654 // "gesture requirement for media playback" feature. 7655 // https://plus.google.com/+FrancoisBeaufort/posts/6PiJQqJzGqX 7656 this._play(true, false); 7657 } 7658 } 7659 function destroyAudioElement(audioElement) { 7660 if (audioElement) { 7661 audioElement.pause(); 7662 audioElement.src = ''; 7663 audioElement.srcObject = null; 7664 audioElement.load(); 7665 } 7666 } 7667 /** 7668 * Plays the audio element that was initialized using the speficied sinkId 7669 */ 7670 Sound.prototype._playAudioElement = function _playAudioElement(sinkId, isMuted, shouldLoop) { 7671 const audioElement = this._activeEls.get(sinkId); 7672 if (!audioElement) { 7673 throw new InvalidArgumentError(`sinkId: "${sinkId}" doesn't have an audio element`); 7674 } 7675 audioElement.muted = !!isMuted; 7676 audioElement.loop = !!shouldLoop; 7677 return audioElement.play() 7678 .then(() => audioElement) 7679 .catch((reason) => { 7680 destroyAudioElement(audioElement); 7681 this._activeEls.delete(sinkId); 7682 throw reason; 7683 }); 7684 }; 7685 /** 7686 * Start playing the sound. Will stop the currently playing sound first. 7687 * If it exists, the audio element that was initialized for the sinkId will be used 7688 */ 7689 Sound.prototype._play = function _play(forceIsMuted, forceShouldLoop) { 7690 if (this.isPlaying) { 7691 this._stop(); 7692 } 7693 if (this._maxDuration > 0) { 7694 this._maxDurationTimeout = setTimeout(this._stop.bind(this), this._maxDuration); 7695 } 7696 forceShouldLoop = typeof forceShouldLoop === 'boolean' ? forceShouldLoop : this._shouldLoop; 7697 const self = this;
7698 const playPromise = this._playPromise = Promise.all(this._sinkIds.map(function createAudioElement(sinkId) { 7699 if (!self._Audio) { 7700 return Promise.resolve(); 7701 } 7702 let audioElement = self._activeEls.get(sinkId); 7703 if (audioElement) { 7704 return self._playAudioElement(sinkId, forceIsMuted, forceShouldLoop); 7705 } 7706 audioElement = new self._Audio(self.url); 7707 // Make sure the browser always retrieves the resource using CORS. 7708 // By default when using media tags, origin header is not sent to server 7709 // which causes the server to not return CORS headers. When this caches 7710 // on the CDN or browser, it causes issues to future requests that needs CORS, 7711 // which is true when using AudioContext. Please note that we won't have to do this 7712 // once we migrate to CloudFront. 7713 if (typeof audioElement.setAttribute === 'function') { 7714 audioElement.setAttribute('crossorigin', 'anonymous'); 7715 } 7716 /** 7717 * (rrowland) Bug in Chrome 53 & 54 prevents us from calling Audio.setSinkId without 7718 * crashing the tab. https://bugs.chromium.org/p/chromium/issues/detail?id=655342 7719 */ 7720 return new Promise(resolve => { 7721 audioElement.addEventListener('canplaythrough', resolve); 7722 }).then(() => { 7723 return (self._isSinkSupported 7724 ? audioElement.setSinkId(sinkId) 7725 : Promise.resolve()).then(function setSinkIdSuccess() { 7726 self._activeEls.set(sinkId, audioElement); 7727 // Stop has been called, bail out 7728 if (!self._playPromise) { 7729 return Promise.resolve(); 7730 } 7731 return self._playAudioElement(sinkId, forceIsMuted, forceShouldLoop); 7732 }); 7733 }); 7734 })); 7735 return playPromise; 7736 }; 7737 /** 7738 * Stop playing the sound. 7739 */ 7740 Sound.prototype._stop = function _stop() { 7741 this._activeEls.forEach((audioEl, sinkId) => { 7742 if (this._sinkIds.includes(sinkId)) { 7743 audioEl.pause(); 7744 audioEl.currentTime = 0; 7745 } 7746 else { 7747 // Destroy the ones that are not used anymore 7748 destroyAudioElement(audioEl); 7749 this._activeEls.delete(sinkId); 7750 } 7751 }); 7752 clearTimeout(this._maxDurationTimeout); 7753 this._playPromise = null; 7754 this._maxDurationTimeout = null; 7755 }; 7756 /** 7757 * Update the sinkIds of the audio output devices this sound should play through. 7758 */ 7759 Sound.prototype.setSinkIds = function setSinkIds(ids) { 7760 if (!this._isSinkSupported) { 7761 return; 7762 } 7763 ids = ids.forEach ? ids : [ids]; 7764 [].splice.apply(this._sinkIds, [0, this._sinkIds.length].concat(ids)); 7765 }; 7766 /** 7767 * Add a stop operation to the queue 7768 */ 7769 Sound.prototype.stop = function stop() { 7770 this._operations.enqueue(() => { 7771 this._stop(); 7772 return Promise.resolve(); 7773 }); 7774 }; 7775 /** 7776 * Add a play operation to the queue 7777 */ 7778 Sound.prototype.play = function play() { 7779 return this._operations.enqueue(() => this._play()); 7780 }; 7781 7782 const REGISTRATION_INTERVAL = 30000; 7783 const RINGTONE_PLAY_TIMEOUT = 2000; 7784 const PUBLISHER_PRODUCT_NAME = 'twilio-js-sdk'; 7785 const INVALID_TOKEN_MESSAGE = 'Parameter "token" must be of type "string".'; 7786 /** 7787 * Twilio Device. Allows registration for incoming calls, and placing outgoing calls. 7788 */ 7789 class Device extends eventsExports.EventEmitter { 7790 /** 7791 * The AudioContext to be used by {@link Device} instances. 7792 * @private 7793 */ 7794 static get audioContext() { 7795 return Device._audioContext; 7796 } 7797 /** 7798 * Which sound file extension is supported. 7799 * @private 7800 */ 7801 static get extension() { 7802 // NOTE(mroberts): Node workaround. 7803 const a = typeof document !== 'undefined' 7804 ? document.createElement('audio') : { canPlayType: false }; 7805 let canPlayMp3; 7806 try { 7807 canPlayMp3 = a.canPlayType && !!a.canPlayType('audio/mpeg').replace(/no/, ''); 7808 } 7809 catch (e) { 7810 canPlayMp3 = false;
7811 } 7812 let canPlayVorbis; 7813 try { 7814 canPlayVorbis = a.canPlayType && !!a.canPlayType('audio/ogg;codecs=\'vorbis\'').replace(/no/, ''); 7815 } 7816 catch (e) { 7817 canPlayVorbis = false; 7818 } 7819 return (canPlayVorbis && !canPlayMp3) ? 'ogg' : 'mp3'; 7820 } 7821 /** 7822 * Whether or not this SDK is supported by the current browser. 7823 */ 7824 static get isSupported() { return enabled(); } 7825 /** 7826 * Package name of the SDK. 7827 */ 7828 static get packageName() { return PACKAGE_NAME; } 7829 /** 7830 * Run some tests to identify issues, if any, prohibiting successful calling. 7831 * @param token - A Twilio JWT token string 7832 * @param options 7833 */ 7834 static runPreflight(token, options) { 7835 return new PreflightTest(token, Object.assign({ audioContext: Device._getOrCreateAudioContext() }, options)); 7836 } 7837 /** 7838 * String representation of {@link Device} class. 7839 * @private 7840 */ 7841 static toString() { 7842 return '[Twilio.Device class]'; 7843 } 7844 /** 7845 * Current SDK version. 7846 */ 7847 static get version() { return RELEASE_VERSION; } 7848 /** 7849 * Initializes the AudioContext instance shared across the Voice SDK, 7850 * or returns the existing instance if one has already been initialized. 7851 */ 7852 static _getOrCreateAudioContext() { 7853 if (!Device._audioContext) { 7854 if (typeof AudioContext !== 'undefined') { 7855 Device._audioContext = new AudioContext(); 7856 } 7857 else if (typeof webkitAudioContext !== 'undefined') { 7858 Device._audioContext = new webkitAudioContext(); 7859 } 7860 } 7861 return Device._audioContext; 7862 } 7863 /** 7864 * Construct a {@link Device} instance. The {@link Device} can be registered 7865 * to make and listen for calls using {@link Device.register}. 7866 * @param options 7867 */ 7868 constructor(token, options = {}) { 7869 super(); 7870 /** 7871 * The currently active {@link Call}, if there is one. 7872 */ 7873 this._activeCall = null; 7874 /** 7875 * The AudioHelper instance associated with this {@link Device}. 7876 */ 7877 this._audio = null; 7878 /** 7879 * The AudioProcessorEventObserver instance to use 7880 */ 7881 this._audioProcessorEventObserver = null; 7882 /** 7883 * An audio input MediaStream to pass to new {@link Call} instances. 7884 */ 7885 this._callInputStream = null; 7886 /** 7887 * An array of {@link Call}s. Though only one can be active, multiple may exist when there 7888 * are multiple incoming, unanswered {@link Call}s. 7889 */ 7890 this._calls = []; 7891 /** 7892 * An array of {@link Device} IDs to be used to play sounds through, to be passed to 7893 * new {@link Call} instances. 7894 */ 7895 this._callSinkIds = ['default']; 7896 /** 7897 * The list of chunder URIs that will be passed to PStream 7898 */ 7899 this._chunderURIs = []; 7900 /** 7901 * Default options used by {@link Device}. 7902 */ 7903 this._defaultOptions = { 7904 allowIncomingWhileBusy: false, 7905 closeProtection: false, 7906 codecPreferences: [Call.Codec.PCMU, Call.Codec.Opus], 7907 dscp: true, 7908 enableImprovedSignalingErrorPrecision: false, 7909 forceAggressiveIceNomination: false, 7910 logLevel: loglevelExports.levels.ERROR, 7911 maxCallSignalingTimeoutMs: 0, 7912 preflight: false, 7913 sounds: {}, 7914 tokenRefreshMs: 10000, 7915 voiceEventSidGenerator: generateVoiceEventSid, 7916 }; 7917 /** 7918 * The name of the edge the {@link Device} is connected to. 7919 */ 7920 this._edge = null; 7921 /** 7922 * The name of the home region the {@link Device} is connected to. 7923 */ 7924 this._home = null; 7925 /** 7926 * The identity associated with this Device. 7927 */ 7928 this._identity = null; 7929 /** 7930 * An instance of Logger to use. 7931 */ 7932 this._log = new Log('Device'); 7933 /** 7934 * The internal promise created when calling {@link Device.makeCall}. 7935 */ 7936 this._makeCallPromise = null; 7937 /** 7938 * The options passed to {@link Device} constructor or {@link Device.updateOptions}. 7939 */ 7940 this._options = {}; 7941 /** 7942 * The preferred URI to (re)-connect signaling to. 7943 */ 7944 this._preferredURI = null; 7945 /** 7946 * An Insights Event Publisher. 7947 */ 7948 this._publisher = null; 7949 /** 7950 * The region the {@link Device} is connected to. 7951 */ 7952 this._region = null; 7953 /** 7954 * A timeout ID for a setTimeout schedule to re-register the {@link Device}. 7955 */ 7956 this._regTimer = null; 7957 /** 7958 * Boolean representing whether or not the {@link Device} was registered when 7959 * receiving a signaling `offline`. Determines if the {@link Device} attempts 7960 * a `re-register` once signaling is re-established when receiving a 7961 * `connected` event from the stream. 7962 */ 7963 this._shouldReRegister = false;
7964 /** 7965 * A Map of Sounds to play. 7966 */ 7967 this._soundcache = new Map(); 7968 /** 7969 * The current status of the {@link Device}. 7970 */ 7971 this._state = Device.State.Unregistered; 7972 /** 7973 * A map from {@link Device.State} to {@link Device.EventName}. 7974 */ 7975 this._stateEventMapping = { 7976 [Device.State.Destroyed]: Device.EventName.Destroyed, 7977 [Device.State.Unregistered]: Device.EventName.Unregistered, 7978 [Device.State.Registering]: Device.EventName.Registering, 7979 [Device.State.Registered]: Device.EventName.Registered, 7980 }; 7981 /** 7982 * The Signaling stream. 7983 */ 7984 this._stream = null; 7985 /** 7986 * A promise that will resolve when the Signaling stream is ready. 7987 */ 7988 this._streamConnectedPromise = null; 7989 /** 7990 * A timeout to track when the current AccessToken will expire. 7991 */ 7992 this._tokenWillExpireTimeout = null; 7993 /** 7994 * Create the default Insights payload 7995 * @param call 7996 */ 7997 this._createDefaultPayload = (call) => { 7998 const payload = { 7999 aggressive_nomination: this._options.forceAggressiveIceNomination, 8000 browser_extension: this._isBrowserExtension, 8001 dscp: !!this._options.dscp, 8002 ice_restart_enabled: true, 8003 platform: getMediaEngine(), 8004 sdk_version: RELEASE_VERSION, 8005 }; 8006 function setIfDefined(propertyName, value) { 8007 if (value) { 8008 payload[propertyName] = value; 8009 } 8010 } 8011 if (call) { 8012 const callSid = call.parameters.CallSid; 8013 setIfDefined('call_sid', /^TJ/.test(callSid) ? undefined : callSid); 8014 setIfDefined('temp_call_sid', call.outboundConnectionId); 8015 setIfDefined('audio_codec', call.codec); 8016 payload.direction = call.direction; 8017 } 8018 setIfDefined('gateway', this._stream && this._stream.gateway); 8019 setIfDefined('region', this._stream && this._stream.region); 8020 return payload; 8021 }; 8022 /** 8023 * Called when a 'close' event is received from the signaling stream. 8024 */ 8025 this._onSignalingClose = () => { 8026 this._stream = null; 8027 this._streamConnectedPromise = null; 8028 }; 8029 /** 8030 * Called when a 'connected' event is received from the signaling stream. 8031 */ 8032 this._onSignalingConnected = (payload) => { 8033 var _a; 8034 const region = getRegionShortcode(payload.region); 8035 this._edge = payload.edge || regionToEdge[region] || payload.region; 8036 this._region = region || payload.region; 8037 this._home = payload.home; 8038 (_a = this._publisher) === null || _a === void 0 ? void 0 : _a.setHost(createEventGatewayURI(payload.home)); 8039 if (payload.token) { 8040 this._identity = payload.token.identity; 8041 if (typeof payload.token.ttl === 'number' && 8042 typeof this._options.tokenRefreshMs === 'number') { 8043 const ttlMs = payload.token.ttl * 1000; 8044 const timeoutMs = Math.max(0, ttlMs - this._options.tokenRefreshMs); 8045 this._tokenWillExpireTimeout = setTimeout(() => { 8046 this._log.debug('#tokenWillExpire'); 8047 this.emit('tokenWillExpire', this); 8048 if (this._tokenWillExpireTimeout) { 8049 clearTimeout(this._tokenWillExpireTimeout); 8050 this._tokenWillExpireTimeout = null; 8051 } 8052 }, timeoutMs); 8053 } 8054 } 8055 const preferredURIs = this._getChunderws() || getChunderURIs(this._edge); 8056 if (preferredURIs.length > 0) { 8057 const [preferredURI] = preferredURIs; 8058 this._preferredURI = createSignalingEndpointURL(preferredURI); 8059 } 8060 else { 8061 this._log.warn('Could not parse a preferred URI from the stream#connected event.'); 8062 } 8063 // The signaling stream emits a `connected` event after reconnection, if the 8064 // device was registered before this, then register again. 8065 if (this._shouldReRegister) { 8066 this.register(); 8067 } 8068 }; 8069 /** 8070 * Called when an 'error' event is received from the signaling stream. 8071 */ 8072 this._onSignalingError = (payload) => { 8073 if (typeof payload !== 'object') { 8074 this._log.warn('Invalid signaling error payload', payload); 8075 return; 8076 } 8077 const { error: originalError, callsid, voiceeventsid } = payload; 8078 // voiceeventsid is for call message events which are handled in the call object 8079 // missing originalError shouldn't be possible but check here to fail properly 8080 if (typeof originalError !== 'object' || !!voiceeventsid) { 8081 this._log.warn('Ignoring signaling error payload', { originalError, voiceeventsid }); 8082 return; 8083 } 8084 const call = (typeof callsid === 'string' && this._findCall(callsid)) || undefined; 8085 const { code, message: customMessage } = originalError; 8086 let { twilioError } = originalError; 8087 if (typeof code === 'number') { 8088 if (code === 31201) { 8089 twilioError = new AuthorizationErrors.AuthenticationFailed(originalError); 8090 } 8091 else if (code === 31204) { 8092 twilioError = new AuthorizationErrors.AccessTokenInvalid(originalError); 8093 } 8094 else if (code === 31205) { 8095 // Stop trying to register presence after token expires 8096 this._stopRegistrationTimer(); 8097 twilioError = new AuthorizationErrors.AccessTokenExpired(originalError); 8098 } 8099 else { 8100 const errorConstructor = getPreciseSignalingErrorByCode(!!this._options.enableImprovedSignalingErrorPrecision, code); 8101 if (typeof errorConstructor !== 'undefined') { 8102 twilioError = new errorConstructor(originalError); 8103 } 8104 } 8105 }
8106 if (!twilioError) { 8107 this._log.error('Unknown signaling error: ', originalError); 8108 twilioError = new GeneralErrors.UnknownError(customMessage, originalError); 8109 } 8110 this._log.error('Received error: ', twilioError); 8111 this._log.debug('#error', originalError); 8112 this.emit(Device.EventName.Error, twilioError, call); 8113 }; 8114 /** 8115 * Called when an 'invite' event is received from the signaling stream. 8116 */ 8117 this._onSignalingInvite = (payload) => __awaiter(this, void 0, void 0, function* () { 8118 var _a; 8119 const wasBusy = !!this._activeCall; 8120 if (wasBusy && !this._options.allowIncomingWhileBusy) { 8121 this._log.info('Device busy; ignoring incoming invite'); 8122 return; 8123 } 8124 if (!payload.callsid || !payload.sdp) { 8125 this._log.debug('#error', payload); 8126 this.emit(Device.EventName.Error, new ClientErrors.BadRequest('Malformed invite from gateway')); 8127 return; 8128 } 8129 const callParameters = payload.parameters || {}; 8130 callParameters.CallSid = callParameters.CallSid || payload.callsid; 8131 const customParameters = Object.assign({}, queryToJson(callParameters.Params)); 8132 this._makeCallPromise = this._makeCall(customParameters, { 8133 callParameters, 8134 enableImprovedSignalingErrorPrecision: !!this._options.enableImprovedSignalingErrorPrecision, 8135 offerSdp: payload.sdp, 8136 reconnectToken: payload.reconnect, 8137 voiceEventSidGenerator: this._options.voiceEventSidGenerator, 8138 }); 8139 let call; 8140 try { 8141 call = yield this._makeCallPromise; 8142 } 8143 finally { 8144 this._makeCallPromise = null; 8145 } 8146 this._calls.push(call); 8147 call.once('accept', () => { 8148 this._soundcache.get(Device.SoundName.Incoming).stop(); 8149 this._publishNetworkChange(); 8150 }); 8151 const play = (((_a = this._audio) === null || _a === void 0 ? void 0 : _a.incoming()) && !wasBusy) 8152 ? () => this._soundcache.get(Device.SoundName.Incoming).play() 8153 : () => Promise.resolve(); 8154 this._showIncomingCall(call, play); 8155 }); 8156 /** 8157 * Called when an 'offline' event is received from the signaling stream. 8158 */ 8159 this._onSignalingOffline = () => { 8160 this._log.info('Stream is offline'); 8161 this._edge = null; 8162 this._region = null; 8163 this._shouldReRegister = this.state !== Device.State.Unregistered; 8164 this._setState(Device.State.Unregistered); 8165 }; 8166 /** 8167 * Called when a 'ready' event is received from the signaling stream. 8168 */ 8169 this._onSignalingReady = () => { 8170 this._log.info('Stream is ready'); 8171 this._setState(Device.State.Registered); 8172 }; 8173 /** 8174 * Publish a NetworkInformation#change event to Insights if there's an active {@link Call}. 8175 */ 8176 this._publishNetworkChange = () => { 8177 if (!this._activeCall) { 8178 return; 8179 } 8180 if (this._networkInformation) { 8181 this._publisher.info('network-information', 'network-change', { 8182 connection_type: this._networkInformation.type, 8183 downlink: this._networkInformation.downlink, 8184 downlinkMax: this._networkInformation.downlinkMax, 8185 effective_type: this._networkInformation.effectiveType, 8186 rtt: this._networkInformation.rtt, 8187 }, this._activeCall); 8188 } 8189 }; 8190 /** 8191 * Update the input stream being used for calls so that any current call and all future calls 8192 * will use the new input stream. 8193 * @param inputStream 8194 */ 8195 this._updateInputStream = (inputStream) => { 8196 const call = this._activeCall; 8197 if (call && !inputStream) { 8198 return Promise.reject(new InvalidStateError('Cannot unset input device while a call is in progress.')); 8199 } 8200 this._callInputStream = inputStream; 8201 return call 8202 ? call._setInputTracksFromStream(inputStream) 8203 : Promise.resolve(); 8204 }; 8205 /** 8206 * Update the device IDs of output devices being used to play sounds through. 8207 * @param type - Whether to update ringtone or speaker sounds 8208 * @param sinkIds - An array of device IDs 8209 */ 8210 this._updateSinkIds = (type, sinkIds) => { 8211 const promise = type === 'ringtone' 8212 ? this._updateRingtoneSinkIds(sinkIds) 8213 : this._updateSpeakerSinkIds(sinkIds); 8214 return promise.then(() => { 8215 this._publisher.info('audio', `${type}-devices-set`, { 8216 audio_device_ids: sinkIds, 8217 }, this._activeCall); 8218 }, error => { 8219 this._publisher.error('audio', `${type}-devices-set-failed`, { 8220 audio_device_ids: sinkIds, 8221 message: error.message, 8222 }, this._activeCall); 8223 throw error; 8224 }); 8225 }; 8226 // Setup loglevel asap to avoid missed logs 8227 this._setupLoglevel(options.logLevel); 8228 this._logOptions('constructor', options); 8229 this.updateToken(token); 8230 if (isLegacyEdge()) { 8231 throw new NotSupportedError('Microsoft Edge Legacy (https://support.microsoft.com/en-us
8231/help/4533505/what-is-microsoft-edge-legacy) ' + 8232 'is deprecated and will not be able to connect to Twilio to make or receive calls after September 1st, 2020. ' + 8233 'Please see this documentation for a list of supported browsers ' + 8234 'https://www.twilio.com/docs/voice/client/javascript#supported-browsers'); 8235 } 8236 if (!Device.isSupported && options.ignoreBrowserSupport) { 8237 if (window && window.location && window.location.protocol === 'http:') { 8238 throw new NotSupportedError(`twilio.js wasn't able to find WebRTC browser support. \ 8239 This is most likely because this page is served over http rather than https, \ 8240 which does not support WebRTC in many browsers. Please load this page over https and \ 8241 try again.`); 8242 } 8243 throw new NotSupportedError(`twilio.js 1.3+ SDKs require WebRTC browser support. \ 8244 For more information, see <https://www.twilio.com/docs/api/client/twilio-js>. \ 8245 If you have any questions about this announcement, please contact \ 8246 Twilio Support at <[email protected]>.`); 8247 } 8248 const root = globalThis; 8249 const browser = root.msBrowser || root.browser || root.chrome; 8250 this._isBrowserExtension = (!!browser && !!browser.runtime && !!browser.runtime.id)
8251 || (!!root.safari && !!root.safari.extension); 8252 if (this._isBrowserExtension) { 8253 this._log.info('Running as browser extension.'); 8254 } 8255 if (navigator) { 8256 const n = navigator; 8257 this._networkInformation = n.connection 8258 || n.mozConnection 8259 || n.webkitConnection; 8260 } 8261 if (this._networkInformation && typeof this._networkInformation.addEventListener === 'function') { 8262 this._networkInformation.addEventListener('change', this._publishNetworkChange); 8263 } 8264 Device._getOrCreateAudioContext(); 8265 if (Device._audioContext) { 8266 if (!Device._dialtonePlayer) { 8267 Device._dialtonePlayer = new DialtonePlayer(Device._audioContext); 8268 } 8269 } 8270 this._boundDestroy = this.destroy.bind(this); 8271 this._boundConfirmClose = this._confirmClose.bind(this); 8272 if (typeof window !== 'undefined' && window.addEventListener) { 8273 window.addEventListener('unload', this._boundDestroy); 8274 window.addEventListener('pagehide', this._boundDestroy); 8275 } 8276 this.updateOptions(options); 8277 } 8278 /** 8279 * Return the {@link AudioHelper} used by this {@link Device}. 8280 */ 8281 get audio() { 8282 return this._audio; 8283 } 8284 /** 8285 * Make an outgoing Call. 8286 * @param options 8287 */ 8288 connect() { 8289 return __awaiter(this, arguments, void 0, function* (options = {}) { 8290 this._log.debug('.connect', JSON.stringify(options)); 8291 this._throwIfDestroyed(); 8292 if (this._activeCall) { 8293 throw new InvalidStateError('A Call is already active'); 8294 } 8295 let customParameters; 8296 let parameters; 8297 let signalingReconnectToken; 8298 if (options.connectToken) { 8299 try { 8300 const connectTokenParts = JSON.parse(decodeURIComponent(atob(options.connectToken))); 8301 customParameters = connectTokenParts.customParameters; 8302 parameters = connectTokenParts.parameters; 8303 signalingReconnectToken = connectTokenParts.signalingReconnectToken; 8304 } 8305 catch (_a) { 8306 throw new InvalidArgumentError('Cannot parse connectToken'); 8307 } 8308 if (!parameters || !parameters.CallSid || !signalingReconnectToken) { 8309 throw new InvalidArgumentError('Invalid connectToken'); 8310 } 8311 } 8312 let isReconnect = false; 8313 let twimlParams = {}; 8314 const callOptions = { 8315 enableImprovedSignalingErrorPrecision: !!this._options.enableImprovedSignalingErrorPrecision, 8316 rtcConfiguration: options.rtcConfiguration, 8317 voiceEventSidGenerator: this._options.voiceEventSidGenerator, 8318 }; 8319 if (signalingReconnectToken && parameters) { 8320 isReconnect = true; 8321 callOptions.callParameters = parameters; 8322 callOptions.reconnectCallSid = parameters.CallSid; 8323 callOptions.reconnectToken = signalingReconnectToken; 8324 twimlParams = customParameters || twimlParams; 8325 } 8326 else { 8327 twimlParams = options.params || twimlParams; 8328 } 8329 let activeCall; 8330 this._makeCallPromise = this._makeCall(twimlParams, callOptions, isReconnect); 8331 try { 8332 activeCall = this._activeCall = yield this._makeCallPromise; 8333 } 8334 finally { 8335 this._makeCallPromise = null; 8336 } 8337 // Make sure any incoming calls are ignored 8338 this._calls.splice(0).forEach(call => call.ignore()); 8339 // Stop the incoming sound if it's playing 8340 this._soundcache.get(Device.SoundName.Incoming).stop(); 8341 activeCall.accept({ rtcConstraints: options.rtcConstraints }); 8342 this._publishNetworkChange(); 8343 return activeCall; 8344 }); 8345 } 8346 /** 8347 * Return the calls that this {@link Device} is maintaining. 8348 */ 8349 get calls() { 8350 return this._calls; 8351 } 8352 /** 8353 * Destroy the {@link Device}, freeing references to be garbage collected. 8354 */ 8355 destroy() { 8356 var _a; 8357 this._log.debug('.destroy'); 8358 this._log.debug('Rejecting any incoming calls'); 8359 const calls = this._calls.slice(0);
8360 calls.forEach((call) => call.reject()); 8361 this.disconnectAll(); 8362 this._stopRegistrationTimer(); 8363 this._destroyStream(); 8364 this._destroyAudioHelper(); 8365 (_a = this._audioProcessorEventObserver) === null || _a === void 0 ? void 0 : _a.destroy(); 8366 this._destroyPublisher(); 8367 if (this._networkInformation && typeof this._networkInformation.removeEventListener === 'function') { 8368 this._networkInformation.removeEventListener('change', this._publishNetworkChange); 8369 } 8370 if (typeof window !== 'undefined' && window.removeEventListener) { 8371 window.removeEventListener('beforeunload', this._boundConfirmClose); 8372 window.removeEventListener('unload', this._boundDestroy); 8373 window.removeEventListener('pagehide', this._boundDestroy); 8374 } 8375 this._setState(Device.State.Destroyed); 8376 eventsExports.EventEmitter.prototype.removeAllListeners.call(this); 8377 } 8378 /** 8379 * Disconnect all {@link Call}s. 8380 */ 8381 disconnectAll() { 8382 this._log.debug('.disconnectAll'); 8383 const calls = this._calls.splice(0); 8384 calls.forEach((call) => call.disconnect()); 8385 if (this._activeCall) { 8386 this._activeCall.disconnect(); 8387 } 8388 } 8389 /** 8390 * Returns the {@link Edge} value the {@link Device} is currently connected 8391 * to. The value will be `null` when the {@link Device} is offline. 8392 */ 8393 get edge() { 8394 return this._edge; 8395 } 8396 /** 8397 * Returns the home value the {@link Device} is currently connected 8398 * to. The value will be `null` when the {@link Device} is offline. 8399 */ 8400 get home() { 8401 return this._home; 8402 } 8403 /** 8404 * Returns the identity associated with the {@link Device} for incoming calls. Only 8405 * populated when registered. 8406 */ 8407 get identity() { 8408 return this._identity; 8409 } 8410 /** 8411 * Whether the Device is currently on an active Call. 8412 */ 8413 get isBusy() { 8414 return !!this._activeCall; 8415 } 8416 /** 8417 * Register the `Device` to the Twilio backend, allowing it to receive calls. 8418 */ 8419 register() { 8420 return __awaiter(this, void 0, void 0, function* () { 8421 this._log.debug('.register'); 8422 if (this.state !== Device.State.Unregistered) { 8423 throw new InvalidStateError(`Attempt to register when device is in state "${this.state}". ` + 8424 `Must be "${Device.State.Unregistered}".`); 8425 } 8426 this._shouldReRegister = false; 8427 this._setState(Device.State.Registering); 8428 yield (this._streamConnectedPromise || this._setupStream()); 8429 yield this._sendPresence(true); 8430 yield promisifyEvents(this, Device.State.Registered, Device.State.Unregistered); 8431 }); 8432 } 8433 /** 8434 * Get the state of this {@link Device} instance 8435 */ 8436 get state() { 8437 return this._state; 8438 } 8439 /** 8440 * Get the token used by this {@link Device}. 8441 */ 8442 get token() { 8443 return this._token; 8444 } 8445 /** 8446 * String representation of {@link Device} instance. 8447 * @private 8448 */ 8449 toString() { 8450 return '[Twilio.Device instance]'; 8451 } 8452 /** 8453 * Unregister the `Device` to the Twilio backend, disallowing it to receive 8454 * calls. 8455 */ 8456 unregister() { 8457 return __awaiter(this, void 0, void 0, function* () { 8458 this._log.debug('.unregister'); 8459 if (this.state !== Device.State.Registered) { 8460 throw new InvalidStateError(`Attempt to unregister when device is in state "${this.state}". ` + 8461 `Must be "${Device.State.Registered}".`); 8462 } 8463 this._shouldReRegister = false; 8464 const stream = yield this._streamConnectedPromise; 8465 const streamOfflinePromise = new Promise(resolve => { 8466 stream.on('offline', resolve); 8467 }); 8468 yield this._sendPresence(false); 8469 yield streamOfflinePromise; 8470 }); 8471 } 8472 /** 8473 * Set the options used within the {@link Device}. 8474 * @param options 8475 */ 8476 updateOptions(options = {}) { 8477 this._logOptions('updateOptions', options); 8478 if (this.state === Device.State.Destroyed) { 8479 throw new InvalidStateError(`Attempt to "updateOptions" when device is in state "${this.state}".`); 8480 } 8481 this._options = Object.assign(Object.assign(Object.assign({}, this._defaultOptions), this._options), options); 8482 const originalChunderURIs = new Set(this._chunderURIs); 8483 const newChunderURIs = this._chunderURIs = (this._getChunderws() || getChunderURIs(this._options.edge)).map(createSignalingEndpointURL); 8484 let hasChunderURIsChanged = originalChunderURIs.size !== newChunderURIs.length; 8485 if (!hasChunderURIsChanged) { 8486 for (const uri of newChunderURIs) { 8487 if (!originalChunderURIs.has(uri)) { 8488 hasChunderURIsChanged = true; 8489 break; 8490 } 8491 } 8492 } 8493 if (this.isBusy && hasChunderURIsChanged) { 8494 throw new InvalidStateError('Cannot change Edge while on an active
8494Call'); 8495 } 8496 this._setupLoglevel(this._options.logLevel); 8497 for (const name of Object.keys(Device._defaultSounds)) { 8498 const soundDef = Device._defaultSounds[name]; 8499 const defaultUrl = `${SOUNDS_BASE_URL}/${soundDef.filename}.${Device.extension}` 8500 + `?cache=${RELEASE_VERSION}`; 8501 const soundUrl = this._options.sounds && this._options.sounds[name] || defaultUrl; 8502 const sound = new (this._options.Sound || Sound)(name, soundUrl, { 8503 audioContext: this._options.disableAudioContextSounds ? null : Device.audioContext, 8504 maxDuration: soundDef.maxDuration, 8505 shouldLoop: soundDef.shouldLoop, 8506 }); 8507 this._soundcache.set(name, sound); 8508 } 8509 this._setupAudioHelper(); 8510 this._setupPublisher(); 8511 if (hasChunderURIsChanged && this._streamConnectedPromise) { 8512 this._setupStream(); 8513 } 8514 // Setup close protection and make sure we clean up ongoing calls on unload. 8515 if (typeof window !== 'undefined' && 8516 typeof window.addEventListener === 'function' && 8517 this._options.closeProtection) { 8518 window.removeEventListener('beforeunload', this._boundConfirmClose); 8519 window.addEventListener('beforeunload', this._boundConfirmClose); 8520 } 8521 } 8522 /** 8523 * Update the token used by this {@link Device} to connect to Twilio. 8524 * It is recommended to call this API after [[Device.tokenWillExpireEvent]] is emitted, 8525 * and before or after a call to prevent a potential ~1s audio loss during the update process. 8526 * @param token 8527 */ 8528 updateToken(token) { 8529 this._log.debug('.updateToken'); 8530 if (this.state === Device.State.Destroyed) { 8531 throw new InvalidStateError(`Attempt to "updateToken" when device is in state "${this.state}".`); 8532 } 8533 if (typeof token !== 'string') { 8534 throw new InvalidArgumentError(INVALID_TOKEN_MESSAGE); 8535 } 8536 this._token = token; 8537 if (this._stream) { 8538 this._stream.setToken(this._token); 8539 } 8540 if (this._publisher) { 8541 this._publisher.setToken(this._token); 8542 } 8543 } 8544 /** 8545 * Called on window's beforeunload event if closeProtection is enabled, 8546 * preventing users from accidentally navigating away from an active call. 8547 * @param event 8548 */ 8549 _confirmClose(event) { 8550 if (!this._activeCall) { 8551 return ''; 8552 } 8553 const closeProtection = this._options.closeProtection || false; 8554 const confirmationMsg = typeof closeProtection !== 'string' 8555 ? 'A call is currently in-progress. Leaving or reloading this page will end the call.' 8556 : closeProtection; 8557 (event || window.event).returnValue = confirmationMsg; 8558 return confirmationMsg; 8559 } 8560 /** 8561 * Destroy the AudioHelper. 8562 */ 8563 _destroyAudioHelper() { 8564 if (!this._audio) { 8565 return; 8566 } 8567 this._audio._destroy(); 8568 this._audio = null; 8569 } 8570 /** 8571 * Destroy the publisher. 8572 */ 8573 _destroyPublisher() { 8574 // Attempt to destroy non-existent publisher. 8575 if (!this._publisher) { 8576 return; 8577 } 8578 this._publisher = null; 8579 } 8580 /** 8581 * Destroy the connection to the signaling server. 8582 */ 8583 _destroyStream() { 8584 if (this._stream) { 8585 this._stream.removeListener('close', this._onSignalingClose); 8586 this._stream.removeListener('connected', this._onSignalingConnected); 8587 this._stream.removeListener('error', this._onSignalingError); 8588 this._stream.removeListener('invite', this._onSignalingInvite); 8589 this._stream.removeListener('offline', this._onSignalingOffline); 8590 this._stream.removeListener('ready', this._onSignalingReady); 8591 this._stream.destroy(); 8592 this._stream = null; 8593 } 8594 this._onSignalingOffline(); 8595 this._streamConnectedPromise = null; 8596 } 8597 /** 8598 * Find a {@link Call} by its CallSid. 8599 * @param callSid 8600 */ 8601 _findCall(callSid) { 8602 return this._calls.find(call => call.parameters.CallSid === callSid 8603 || call.outboundConnectionId === callSid) || null; 8604 } 8605 /** 8606 * Get chunderws array from the chunderw param 8607 */ 8608 _getChunderws() { 8609 return typeof this._options.chunderw === 'string' ? [this._options.chunderw] 8610 : Array.isArray(this._options.chunderw) ? this._options.chunderw : null; 8611 } 8612 /** 8613 * Utility function to log device options 8614 */ 8615 _logOptions(caller, options = {}) { 8616 // Selectively log options that users can modify. 8617 // Also, convert user overrides. 8618 // This prevents potential app crash when calling JSON.stringify 8619 // and when sending log strings remotely 8620 const userOptions = [
8621 'allowIncomingWhileBusy', 8622 'appName', 8623 'appVersion', 8624 'closeProtection', 8625 'codecPreferences', 8626 'disableAudioContextSounds', 8627 'dscp', 8628 'edge', 8629 'enableImprovedSignalingErrorPrecision', 8630 'forceAggressiveIceNomination', 8631 'logLevel', 8632 'maxAverageBitrate', 8633 'maxCallSignalingTimeoutMs', 8634 'sounds', 8635 'tokenRefreshMs', 8636 ]; 8637 const userOptionOverrides = [ 8638 'RTCPeerConnection', 8639 'enumerateDevices', 8640 'getUserMedia', 8641 'MediaStream', 8642 ]; 8643 if (typeof options === 'object') { 8644 const toLog = Object.assign({}, options); 8645 Object.keys(toLog).forEach((key) => { 8646 if (!userOptions.includes(key) && !userOptionOverrides.includes(key)) { 8647 delete toLog[key]; 8648 } 8649 if (userOptionOverrides.includes(key)) { 8650 toLog[key] = true; 8651 } 8652 });
8653 this._log.debug(`.${caller}`, JSON.stringify(toLog)); 8654 } 8655 } 8656 /** 8657 * Create a new {@link Call}. 8658 * @param twimlParams - A flat object containing key:value pairs to be sent to the TwiML app. 8659 * @param options - Options to be used to instantiate the {@link Call}. 8660 */ 8661 _makeCall(twimlParams_1, options_1) { 8662 return __awaiter(this, arguments, void 0, function* (twimlParams, options, isReconnect = false) { 8663 var _a; 8664 // Wait for the input device if it's set by the user 8665 const inputDevicePromise = (_a = this._audio) === null || _a === void 0 ? void 0 : _a._getInputDevicePromise(); 8666 if (inputDevicePromise) { 8667 this._log.debug('inputDevicePromise detected, waiting...'); 8668 yield inputDevicePromise; 8669 this._log.debug('inputDevicePromise resolved'); 8670 } 8671 const config = { 8672 audioHelper: this._audio, 8673 onIgnore: () => { 8674 this._soundcache.get(Device.SoundName.Incoming).stop(); 8675 }, 8676 pstream: yield (this._streamConnectedPromise || this._setupStream()), 8677 publisher: this._publisher, 8678 soundcache: this._soundcache, 8679 }; 8680 options = Object.assign({ 8681 MediaStream: this._options.MediaStream, 8682 RTCPeerConnection: this._options.RTCPeerConnection, 8683 beforeAccept: (currentCall) => { 8684 if (!this._activeCall || this._activeCall === currentCall) { 8685 return; 8686 } 8687 this._activeCall.disconnect(); 8688 this._removeCall(this._activeCall); 8689 }, 8690 codecPreferences: this._options.codecPreferences, 8691 customSounds: this._options.sounds, 8692 dialtonePlayer: Device._dialtonePlayer, 8693 dscp: this._options.dscp, 8694 // TODO(csantos): Remove forceAggressiveIceNomination option in 3.x 8695 forceAggressiveIceNomination: this._options.forceAggressiveIceNomination, 8696 getInputStream: () => this._options.fileInputStream || this._callInputStream, 8697 getSinkIds: () => this._callSinkIds, 8698 maxAverageBitrate: this._options.maxAverageBitrate, 8699 preflight: this._options.preflight, 8700 rtcConstraints: this._options.rtcConstraints, 8701 shouldPlayDisconnect: () => { var _a; return (_a = this._audio) === null || _a === void 0 ? void 0 : _a.disconnect(); }, 8702 twimlParams, 8703 voiceEventSidGenerator: this._options.voiceEventSidGenerator, 8704 }, options); 8705 const maybeUnsetPreferredUri = () => { 8706 if (!this._stream) { 8707 this._log.warn('UnsetPreferredUri called without a stream'); 8708 return; 8709 } 8710 if (this._activeCall === null && this._calls.length === 0) { 8711 this._stream.updatePreferredURI(null); 8712 } 8713 }; 8714 const call = new (this._options.Call || Call)(config, options); 8715 this._publisher.info('settings', 'init', { 8716 MediaStream: !!this._options.MediaStream, 8717 RTCPeerConnection: !!this._options.RTCPeerConnection, 8718 enumerateDevices: !!this._options.enumerateDevices, 8719 getUserMedia: !!this._options.getUserMedia, 8720 }, call); 8721 call.once('accept', () => { 8722 var _a, _b, _c; 8723 this._stream.updatePreferredURI(this._preferredURI); 8724 this._removeCall(call); 8725 this._activeCall = call; 8726 if (this._audio) { 8727 this._audio._maybeStartPollingVolume(); 8728 } 8729 if (call.direction === Call.CallDirection.Outgoing && ((_a = this._audio) === null || _a === void 0 ? void 0 : _a.outgoing()) && !isReconnect) {
8730 this._soundcache.get(Device.SoundName.Outgoing).play(); 8731 } 8732 const data = { edge: this._edge || this._region }; 8733 if (this._options.edge) { 8734 data['selected_edge'] = Array.isArray(this._options.edge) 8735 ? this._options.edge 8736 : [this._options.edge]; 8737 } 8738 this._publisher.info('settings', 'edge', data, call); 8739 if ((_b = this._audio) === null || _b === void 0 ? void 0 : _b.processedStream) { 8740 (_c = this._audioProcessorEventObserver) === null || _c === void 0 ? void 0 : _c.emit('enabled'); 8741 } 8742 }); 8743 call.addListener('error', (error) => { 8744 if (call.status() === 'closed') { 8745 this._removeCall(call); 8746 maybeUnsetPreferredUri(); 8747 } 8748 if (this._audio) { 8749 this._audio._maybeStopPollingVolume(); 8750 } 8751 this._maybeStopIncomingSound(); 8752 }); 8753 call.once('cancel', () => { 8754 this._log.info(`Canceled: ${call.parameters.CallSid}`); 8755 this._removeCall(call); 8756 maybeUnsetPreferredUri(); 8757 if (this._audio) { 8758 this._audio._maybeStopPollingVolume(); 8759 } 8760 this._maybeStopIncomingSound(); 8761 }); 8762 call.once('disconnect', () => { 8763 if (this._audio) { 8764 this._audio._maybeStopPollingVolume(); 8765 } 8766 this._removeCall(call); 8767 maybeUnsetPreferredUri(); 8768 /** 8769 * NOTE(kamalbennani): We need to stop the incoming sound when the call is 8770 * disconnected right after the user has accepted the call (activeCall.accept()), and before 8771 * the call has been fully connected (i.e. before the `pstream.answer` event) 8772 */ 8773 this._maybeStopIncomingSound(); 8774 }); 8775 call.once('reject', () => { 8776 this._log.info(`Rejected: ${call.parameters.CallSid}`); 8777 if (this._audio) { 8778 this._audio._maybeStopPollingVolume(); 8779 } 8780 this._removeCall(call); 8781 maybeUnsetPreferredUri(); 8782 this._maybeStopIncomingSound(); 8783 }); 8784 call.on('transportClose', () => { 8785 if (call.status() !== Call.State.Pending) { 8786 return; 8787 } 8788 if (this._audio) { 8789 this._audio._maybeStopPollingVolume(); 8790 } 8791 this._removeCall(call); 8792 /** 8793 * NOTE(mhuynh): We don't want to call `maybeUnsetPreferredUri` because 8794 * a `transportClose` will happen during signaling reconnection. 8795 */ 8796 this._maybeStopIncomingSound(); 8797 }); 8798 return call; 8799 }); 8800 } 8801 /** 8802 * Stop the incoming sound if no {@link Call}s remain. 8803 */ 8804 _maybeStopIncomingSound() { 8805 if (!this._calls.length) { 8806 this._soundcache.get(Device.SoundName.Incoming).stop(); 8807 } 8808 } 8809 /** 8810 * Remove a {@link Call} from device.calls by reference 8811 * @param call 8812 */ 8813 _removeCall(call) { 8814 if (this._activeCall === call) { 8815 this._activeCall = null; 8816 this._makeCallPromise = null; 8817 } 8818 for (let i = this._calls.length - 1; i >= 0; i--) { 8819 if (call === this._calls[i]) { 8820 this._calls.splice(i, 1); 8821 } 8822 } 8823 } 8824 /** 8825 * Register with the signaling server. 8826 */ 8827 _sendPresence(presence) { 8828 return __awaiter(this, void 0, void 0, function* () { 8829 const stream = yield this._streamConnectedPromise; 8830 if (!stream) { 8831 return; 8832 } 8833 stream.register({ audio: presence }); 8834 if (presence) { 8835 this._startRegistrationTimer(); 8836 } 8837 else { 8838 this._stopRegistrationTimer(); 8839 } 8840 }); 8841 } 8842 /** 8843 * Helper function that sets and emits the state of the device. 8844 * @param state The new state of the device. 8845 */ 8846 _setState(state) { 8847 if (state === this.state) { 8848 return; 8849 } 8850 this._state = state; 8851 const name = this._stateEventMapping[state]; 8852 this._log.debug(`#${name}`);
8853 this.emit(name); 8854 } 8855 /** 8856 * Set up an audio helper for usage by this {@link Device}. 8857 */ 8858 _setupAudioHelper() { 8859 if (!this._audioProcessorEventObserver) { 8860 this._audioProcessorEventObserver = new AudioProcessorEventObserver(); 8861 this._audioProcessorEventObserver.on('event', ({ name, group }) => { 8862 this._publisher.info(group, name, {}, this._activeCall); 8863 }); 8864 } 8865 const audioOptions = { 8866 audioContext: Device.audioContext, 8867 audioProcessorEventObserver: this._audioProcessorEventObserver, 8868 beforeSetInputDevice: () => { 8869 if (this._makeCallPromise) { 8870 this._log.debug('beforeSetInputDevice pause detected'); 8871 return this._makeCallPromise; 8872 } 8873 else { 8874 this._log.debug('beforeSetInputDevice pause not detected, setting default'); 8875 return Promise.resolve(); 8876 } 8877 }, 8878 enumerateDevices: this._options.enumerateDevices, 8879 getUserMedia: this._options.getUserMedia || getUserMedia, 8880 }; 8881 if (this._audio) { 8882 this._log.info('Found existing audio helper; updating options...'); 8883 this._audio._updateUserOptions(audioOptions); 8884 return; 8885 } 8886 this._audio = new (this._options.AudioHelper || AudioHelper$1)(this._updateSinkIds, this._updateInputStream, audioOptions); 8887 this._audio.on('deviceChange', (lostActiveDevices) => { 8888 const activeCall = this._activeCall; 8889 const deviceIds = lostActiveDevices.map((device) => device.deviceId); 8890 this._publisher.info('audio', 'device-change', { 8891 lost_active_device_ids: deviceIds, 8892 }, activeCall); 8893 if (activeCall) { 8894 activeCall['_mediaHandler']._onInputDevicesChanged(); 8895 } 8896 }); 8897 } 8898 /** 8899 * Setup logger's loglevel 8900 */ 8901 _setupLoglevel(logLevel) { 8902 const level = typeof logLevel === 'number' || 8903 typeof logLevel === 'string' ? 8904 logLevel : loglevelExports.levels.ERROR; 8905 this._log.setDefaultLevel(level); 8906 this._log.info('Set logger default level to', level); 8907 } 8908 /** 8909 * Create and set a publisher for the {@link Device} to use. 8910 */ 8911 _setupPublisher() { 8912 if (this._publisher) { 8913 this._log.info('Found existing publisher; destroying...'); 8914 this._destroyPublisher(); 8915 } 8916 const publisherOptions = { 8917 defaultPayload: this._createDefaultPayload, 8918 metadata: { 8919 app_name: this._options.appName, 8920 app_version: this._options.appVersion, 8921 }, 8922 }; 8923 if (this._options.eventgw) { 8924 publisherOptions.host = this._options.eventgw; 8925 } 8926 if (this._home) { 8927 publisherOptions.host = createEventGatewayURI(this._home); 8928 } 8929 this._publisher = new (this._options.Publisher || EventPublisher)(PUBLISHER_PRODUCT_NAME, this.token, publisherOptions); 8930 if (this._options.publishEvents === false) { 8931 this._publisher.disable(); 8932 } 8933 else { 8934 this._publisher.on('error', (error) => { 8935 this._log.warn('Cannot connect to insights.', error); 8936 }); 8937 } 8938 return this._publisher; 8939 } 8940 /** 8941 * Set up the connection to the signaling server. Tears down an existing 8942 * stream if called while a stream exists. 8943 */ 8944 _setupStream() { 8945 if (this._stream) { 8946 this._log.info('Found existing stream; destroying...'); 8947 this._destroyStream(); 8948 } 8949 this._log.info('Setting up VSP'); 8950 this._stream = new (this._options.PStream || PStream)(this.token, this._chunderURIs, { 8951 backoffMaxMs: this._options.backoffMaxMs, 8952 maxPreferredDurationMs: this._options.maxCallSignalingTimeoutMs, 8953 }); 8954 this._stream.addListener('close', this._onSignalingClose); 8955 this._stream.addListener('connected', this._onSignalingConnected); 8956 this._stream.addListener('error', this._onSignalingError); 8957 this._stream.addListener('invite', this._onSignalingInvite); 8958 this._stream.addListener('offline', this._onSignalingOffline); 8959 this._stream.addListener('ready', this._onSignalingReady); 8960 return this._streamConnectedPromise = 8961 promisifyEvents(this._stream, 'connected', 'close').then(() => this._stream); 8962 } 8963 /** 8964 * Start playing the incoming ringtone, and subsequently emit the incoming event. 8965 * @param call 8966 * @param play - The function to be used to play the sound. Must return a Promise. 8967 */ 8968 _showIncomingCall(call, play) { 8969 let timeout; 8970 return Promise.race([ 8971 play(), 8972 new Promise((resolve, reject) => { 8973 timeout = setTimeout(() => { 8974 const msg = 'Playing incoming ringtone took too long; it might not play. Continuing execution...'; 8975 reject(new Error(msg)); 8976 }, RINGTONE_PLAY_TIMEOUT); 8977 }), 8978 ]).catch(reason => { 8979 this._log.warn(reason.message); 8980 }).then(() => { 8981 clearTimeout(timeout); 8982 this._log.debug('#incoming', JSON.stringify({ 8983 customParameters: call.customParameters, 8984 parameters: call.parameters, 8985 }));
8986 this.emit(Device.EventName.Incoming, call); 8987 }); 8988 } 8989 /** 8990 * Set a timeout to send another register message to the signaling server. 8991 */ 8992 _startRegistrationTimer() { 8993 this._stopRegistrationTimer(); 8994 this._regTimer = setTimeout(() => { 8995 this._sendPresence(true); 8996 }, REGISTRATION_INTERVAL); 8997 } 8998 /** 8999 * Stop sending registration messages to the signaling server. 9000 */ 9001 _stopRegistrationTimer() { 9002 if (this._regTimer) { 9003 clearTimeout(this._regTimer); 9004 } 9005 } 9006 /** 9007 * Throw an error if the {@link Device} is destroyed. 9008 */ 9009 _throwIfDestroyed() { 9010 if (this.state === Device.State.Destroyed) { 9011 throw new InvalidStateError('Device has been destroyed.'); 9012 } 9013 } 9014 /** 9015 * Update the device IDs of output devices being used to play the incoming ringtone through. 9016 * @param sinkIds - An array of device IDs 9017 */ 9018 _updateRingtoneSinkIds(sinkIds) { 9019 return Promise.resolve(this._soundcache.get(Device.SoundName.Incoming).setSinkIds(sinkIds)); 9020 } 9021 /** 9022 * Update the device IDs of output devices being used to play the non-ringtone sounds 9023 * and Call audio through. 9024 * @param sinkIds - An array of device IDs 9025 */ 9026 _updateSpeakerSinkIds(sinkIds) { 9027 Array.from(this._soundcache.entries()) 9028 .filter(entry => entry[0] !== Device.SoundName.Incoming) 9029 .forEach(entry => entry[1].setSinkIds(sinkIds)); 9030 this._callSinkIds = sinkIds; 9031 const call = this._activeCall; 9032 return call 9033 ? call._setSinkIds(sinkIds) 9034 : Promise.resolve(); 9035 } 9036 } 9037 Device._defaultSounds = { 9038 disconnect: { filename: 'disconnect', maxDuration: 3000 }, 9039 dtmf0: { filename: 'dtmf-0', maxDuration: 1000 }, 9040 dtmf1: { filename: 'dtmf-1', maxDuration: 1000 }, 9041 dtmf2: { filename: 'dtmf-2', maxDuration: 1000 }, 9042 dtmf3: { filename: 'dtmf-3', maxDuration: 1000 }, 9043 dtmf4: { filename: 'dtmf-4', maxDuration: 1000 }, 9044 dtmf5: { filename: 'dtmf-5', maxDuration: 1000 }, 9045 dtmf6: { filename: 'dtmf-6', maxDuration: 1000 }, 9046 dtmf7: { filename: 'dtmf-7', maxDuration: 1000 }, 9047 dtmf8: { filename: 'dtmf-8', maxDuration: 1000 }, 9048 dtmf9: { filename: 'dtmf-9', maxDuration: 1000 }, 9049 dtmfh: { filename: 'dtmf-hash', maxDuration: 1000 }, 9050 dtmfs: { filename: 'dtmf-star', maxDuration: 1000 }, 9051 incoming: { filename: 'incoming', shouldLoop: true }, 9052 outgoing: { filename: 'outgoing', maxDuration: 3000 }, 9053 }; 9054 /** 9055 * @mergeModuleWith Device 9056 */ 9057 (function (Device) { 9058 (function (EventName) { 9059 EventName["Error"] = "error"; 9060 EventName["Incoming"] = "incoming"; 9061 EventName["Destroyed"] = "destroyed"; 9062 EventName["Unregistered"] = "unregistered"; 9063 EventName["Registering"] = "registering"; 9064 EventName["Registered"] = "registered"; 9065 EventName["TokenWillExpire"] = "tokenWillExpire"; 9066 })(Device.EventName || (Device.EventName = {})); 9067 (function (State) { 9068 State["Destroyed"] = "destroyed"; 9069 State["Unregistered"] = "unregistered"; 9070 State["Registering"] = "registering"; 9071 State["Registered"] = "registered"; 9072 })(Device.State || (Device.State = {})); 9073 (function (SoundName) { 9074 SoundName["Incoming"] = "incoming"; 9075 SoundName["Outgoing"] = "outgoing"; 9076 SoundName["Disconnect"] = "disconnect"; 9077 SoundName["Dtmf0"] = "dtmf0"; 9078 SoundName["Dtmf1"] = "dtmf1"; 9079 SoundName["Dtmf2"] = "dtmf2"; 9080 SoundName["Dtmf3"] = "dtmf3"; 9081 SoundName["Dtmf4"] = "dtmf4"; 9082 SoundName["Dtmf5"] = "dtmf5"; 9083 SoundName["Dtmf6"] = "dtmf6"; 9084 SoundName["Dtmf7"] = "dtmf7"; 9085 SoundName["Dtmf8"] = "dtmf8"; 9086 SoundName["Dtmf9"] = "dtmf9"; 9087 SoundName["DtmfS"] = "dtmfs"; 9088 SoundName["DtmfH"] = "dtmfh"; 9089 })(Device.SoundName || (Device.SoundName = {})); 9090 })(Device || (Device = {})); 9091 9092 /** 9093 * {@link RTCIceCandidate} parses an ICE candidate gathered by the browser 9094 * and returns a IceCandidate object 9095 */ 9096 class IceCandidate { 9097 /** 9098 * @constructor 9099 * @param iceCandidate RTCIceCandidate coming from the browser 9100 */ 9101 constructor(iceCandidate, isRemote = false) { 9102 /** 9103 * Whether this is deleted from the list of candidate gathered 9104 */ 9105 this.deleted = false;
9106 let cost; 9107 const parts = iceCandidate.candidate.split('network-cost '); 9108 if (parts[1]) { 9109 cost = parseInt(parts[1], 10); 9110 } 9111 this.candidateType = iceCandidate.type; 9112 this.ip = iceCandidate.ip || iceCandidate.address; 9113 this.isRemote = isRemote; 9114 this.networkCost = cost; 9115 this.port = iceCandidate.port; 9116 this.priority = iceCandidate.priority; 9117 this.protocol = iceCandidate.protocol; 9118 this.relatedAddress = iceCandidate.relatedAddress; 9119 this.relatedPort = iceCandidate.relatedPort; 9120 this.tcpType = iceCandidate.tcpType; 9121 this.transportId = iceCandidate.sdpMid; 9122 } 9123 /** 9124 * Get the payload object for insights 9125 */ 9126 toPayload() { 9127 return { 9128 'candidate_type': this.candidateType, 9129 'deleted': this.deleted, 9130 'ip': this.ip, 9131 'is_remote': this.isRemote, 9132 'network-cost': this.networkCost, 9133 'port': this.port, 9134 'priority': this.priority, 9135 'protocol': this.protocol, 9136 'related_address': this.relatedAddress, 9137 'related_port': this.relatedPort, 9138 'tcp_type': this.tcpType, 9139 'transport_id': this.transportId, 9140 }; 9141 } 9142 } 9143 9144 const r0 = 94.768; // Constant used in computing "rFactor". 9145 /** 9146 * Calculate the mos score of a stats object 9147 * @param {number} rtt 9148 * @param {number} jitter 9149 * @param {number} fractionLost - The fraction of packets that have been lost. 9150 * Calculated by packetsLost / totalPackets 9151 * @return {number | null} mos - Calculated MOS, `1.0` through roughly `4.5`. 9152 * Returns `null` when any of the input parameters are not a `non-negative` 9153 * number. 9154 */ 9155 function calculate(rtt, jitter, fractionLost) { 9156 if (typeof rtt !== 'number' || 9157 typeof jitter !== 'number' || 9158 typeof fractionLost !== 'number' || 9159 !isNonNegativeNumber(rtt) || 9160 !isNonNegativeNumber(jitter) || 9161 !isNonNegativeNumber(fractionLost)) { 9162 return null; 9163 } 9164 // Compute the effective latency. 9165 const effectiveLatency = rtt + (jitter * 2) + 10; 9166 // Compute the initial "rFactor" from effective latency. 9167 let rFactor = 0; 9168 switch (true) { 9169 case effectiveLatency < 160: 9170 rFactor = r0 - (effectiveLatency / 40); 9171 break; 9172 case effectiveLatency < 1000: 9173 rFactor = r0 - ((effectiveLatency - 120) / 10); 9174 break; 9175 } 9176 // Adjust "rFactor" with the fraction of packets lost. 9177 switch (true) { 9178 case fractionLost <= (rFactor / 2.5): 9179 rFactor = Math.max(rFactor - fractionLost * 2.5, 6.52); 9180 break; 9181 default: 9182 rFactor = 0; 9183 break; 9184 } 9185 // Compute MOS from "rFactor". 9186 const mos = 1 + 9187 (0.035 * rFactor) + 9188 (0.000007 * rFactor) * 9189 (rFactor - 60) * 9190 (100 - rFactor); 9191 return mos; 9192 } 9193 /** 9194 * Returns true if and only if the parameter passed is a number, is not `NaN`, 9195 * is finite, and is greater than or equal to `0`. 9196 * @param n 9197 */ 9198 function isNonNegativeNumber(n) { 9199 return typeof n === 'number' && !isNaN(n) && isFinite(n) && n >= 0; 9200 } 9201 var Mos = { 9202 calculate, 9203 isNonNegativeNumber, 9204 }; 9205 9206 // How many samples we use when testing metric thresholds 9207 const SAMPLE_COUNT_METRICS = 5; 9208 // How many samples that need to cross the threshold to 9209 // raise or clear a warning. 9210 const SAMPLE_COUNT_CLEAR = 0; 9211 const SAMPLE_COUNT_RAISE = 3; 9212 const SAMPLE_INTERVAL = 1000; 9213 const WARNING_TIMEOUT = 5 * 1000; 9214 const DEFAULT_THRESHOLDS = { 9215 audioInputLevel: { minStandardDeviation: 327.67, sampleCount: 10 }, 9216 audioOutputLevel: { minStandardDeviation: 327.67, sampleCount: 10 }, 9217 bytesReceived: { clearCount: 2, min: 1, raiseCount: 3, sampleCount: 3 }, 9218 bytesSent: { clearCount: 2, min: 1, raiseCount: 3, sampleCount: 3 }, 9219 jitter: { max: 30 }, 9220 mos: { min: 3 }, 9221 packetsLostFraction: [{ 9222 max: 1, 9223 }, { 9224 clearValue: 1, 9225 maxAverage: 3, 9226 sampleCount: 7, 9227 }], 9228 rtt: { max: 400 }, 9229 }; 9230 /** 9231 * Count the number of values that cross the max threshold. 9232 * @private 9233 * @param max - The max allowable value. 9234 * @param values - The values to iterate over. 9235 * @returns The amount of values in which the stat crossed the threshold. 9236 */ 9237 function countHigh(max, values) { 9238 return values.reduce((highCount, value) => highCount += (value > max) ? 1 : 0, 0); 9239 } 9240 /** 9241 * Count the number of values that cross the min threshold. 9242 * @private 9243 * @param min - The minimum allowable value. 9244 * @param values - The values to iterate over. 9245 * @returns The amount of values in which the stat crossed the threshold. 9246 */ 9247 function countLow(min, values) { 9248 return values.reduce((lowCount, value) => lowCount += (value < min) ? 1 : 0, 0); 9249 } 9250 /** 9251 * Calculate the standard deviation from a list of numbers. 9252 * @private 9253 * @param values The list of numbers to calculate the standard deviation from. 9254 * @returns The standard deviation of a list of numbers. 9255 */ 9256 function calculateStandardDeviation(values) { 9257 if (values.length <= 0) { 9258 return null; 9259 } 9260 const valueAverage = values.reduce((partialSum, value) => partialSum + value, 0) / values.length; 9261 const diffSquared = values.map((value) => Math.pow(value - valueAverage, 2)); 9262 const stdDev = Math.sqrt(diffSquared.reduce((partialSum, value) => partialSum + value, 0) / diffSquared.length); 9263 return stdDev; 9264 }
9265 /** 9266 * Flatten a set of numerical sample sets into a single array of samples. 9267 * @param sampleSets 9268 */ 9269 function flattenSamples(sampleSets) { 9270 return sampleSets.reduce((flat, current) => [...flat, ...current], []); 9271 } 9272 /** 9273 * {@link StatsMonitor} polls a peerConnection via PeerConnection.getStats 9274 * and emits warnings when stats cross the specified threshold values. 9275 */ 9276 class StatsMonitor extends eventsExports.EventEmitter { 9277 /** 9278 * @constructor 9279 * @param [options] - Optional settings 9280 */ 9281 constructor(options) { 9282 super(); 9283 /** 9284 * A map of warnings with their raised time 9285 */ 9286 this._activeWarnings = new Map(); 9287 /** 9288 * A map of stats with the number of exceeded thresholds 9289 */ 9290 this._currentStreaks = new Map(); 9291 /** 9292 * Keeps track of input volumes in the last second 9293 */ 9294 this._inputVolumes = []; 9295 /** 9296 * Keeps track of output volumes in the last second 9297 */ 9298 this._outputVolumes = []; 9299 /** 9300 * Sample buffer. Saves most recent samples 9301 */ 9302 this._sampleBuffer = []; 9303 /** 9304 * Keeps track of supplemental sample values. 9305 * 9306 * Currently used for constant audio detection. Contains an array of volume 9307 * samples for each sample interval. 9308 */ 9309 this._supplementalSampleBuffers = { 9310 audioInputLevel: [], 9311 audioOutputLevel: [], 9312 }; 9313 /** 9314 * Whether warnings should be enabled 9315 */ 9316 this._warningsEnabled = true; 9317 options = options || {}; 9318 this._getRTCStats = options.getRTCStats || getRTCStats; 9319 this._mos = options.Mos || Mos; 9320 this._peerConnection = options.peerConnection; 9321 this._thresholds = Object.assign(Object.assign({}, DEFAULT_THRESHOLDS), options.thresholds); 9322 const thresholdSampleCounts = Object.values(this._thresholds) 9323 .map((threshold) => threshold.sampleCount) 9324 .filter((sampleCount) => !!sampleCount); 9325 this._maxSampleCount = Math.max(SAMPLE_COUNT_METRICS, ...thresholdSampleCounts); 9326 if (this._peerConnection) { 9327 this.enable(this._peerConnection); 9328 } 9329 } 9330 /** 9331 * Called when a volume sample is available 9332 * @param inputVolume - Input volume level from 0 to 32767 9333 * @param outputVolume - Output volume level from 0 to 32767 9334 */ 9335 addVolumes(inputVolume, outputVolume) { 9336 this._inputVolumes.push(inputVolume); 9337 this._outputVolumes.push(outputVolume); 9338 } 9339 /** 9340 * Stop sampling RTC statistics for this {@link StatsMonitor}. 9341 * @returns The current {@link StatsMonitor}. 9342 */ 9343 disable() { 9344 if (this._sampleInterval) { 9345 clearInterval(this._sampleInterval); 9346 delete this._sampleInterval; 9347 } 9348 return this; 9349 } 9350 /** 9351 * Disable warnings for this {@link StatsMonitor}. 9352 * @returns The current {@link StatsMonitor}. 9353 */ 9354 disableWarnings() { 9355 if (this._warningsEnabled) { 9356 this._activeWarnings.clear(); 9357 } 9358 this._warningsEnabled = false; 9359 return this; 9360 } 9361 /** 9362 * Start sampling RTC statistics for this {@link StatsMonitor}. 9363 * @param peerConnection - A PeerConnection to monitor. 9364 * @returns The current {@link StatsMonitor}. 9365 */ 9366 enable(peerConnection) { 9367 if (peerConnection) { 9368 if (this._peerConnection && peerConnection !== this._peerConnection) { 9369 throw new InvalidArgumentError('Attempted to replace an existing PeerConnection in StatsMonitor.enable'); 9370 } 9371 this._peerConnection = peerConnection; 9372 } 9373 if (!this._peerConnection) { 9374 throw new InvalidArgumentError('Can not enable StatsMonitor without a PeerConnection'); 9375 } 9376 this._sampleInterval = this._sampleInterval || 9377 setInterval(this._fetchSample.bind(this), SAMPLE_INTERVAL); 9378 return this; 9379 } 9380 /** 9381 * Enable warnings for this {@link StatsMonitor}. 9382 * @returns The current {@link StatsMonitor}. 9383 */ 9384 enableWarnings() { 9385 this._warningsEnabled = true; 9386 return this; 9387 } 9388 /** 9389 * Check if there is an active warning for a specific stat and threshold 9390 * @param statName - The name of the stat to check 9391 * @param thresholdName - The name of the threshold to check 9392 * @returns Whether there is an active warning for a specific stat and threshold 9393 */ 9394 hasActiveWarning(statName, thresholdName) { 9395 const warningId = `${statName}:${thresholdName}`; 9396 return !!this._activeWarnings.get(warningId); 9397 } 9398 /** 9399 * Add a sample to our sample buffer and remove the oldest if we are over the limit. 9400 * @param sample - Sample to add 9401 */ 9402 _addSample(sample) { 9403 const samples = this._sampleBuffer; 9404 samples.push(sample); 9405 // We store 1 extra sample so that we always have (current, previous) 9406 // available for all {sampleBufferSize} threshold validations. 9407 if (samples.length > this._maxSampleCount) { 9408 samples.splice(0, samples.length - this._maxSampleCount); 9409 } 9410 } 9411 /** 9412 * Clear an active warning. 9413 * @param statName - The name of the stat to clear. 9414 * @param thresholdName - The name of the threshold to clear 9415 * @param [data] - Any relevant sample data. 9416 */ 9417 _clearWarning(statName, thresholdName, data) { 9418 const warningId = `${statName}:${thresholdName}`; 9419 const activeWarning = this._activeWarnings.get(warningId); 9420 if (!activeWarning || Date.now() - activeWarning.timeRaised < WARNING_TIMEOUT) { 9421 return; 9422 } 9423 this._activeWarnings.delete(warningId);
9424 this.emit('warning-cleared', Object.assign(Object.assign({}, data), { name: statName, threshold: { 9425 name: thresholdName, 9426 value: this._thresholds[statName][thresholdName], 9427 } })); 9428 } 9429 /** 9430 * Create a sample object from a stats object using the previous sample, if available. 9431 * @param stats - Stats retrieved from getStatistics 9432 * @param [previousSample=null] - The previous sample to use to calculate deltas. 9433 * @returns A universally-formatted version of RTC stats. 9434 */ 9435 _createSample(stats, previousSample) { 9436 const previousBytesSent = previousSample && previousSample.totals.bytesSent || 0; 9437 const previousBytesReceived = previousSample && previousSample.totals.bytesReceived || 0; 9438 const previousPacketsSent = previousSample && previousSample.totals.packetsSent || 0; 9439 const previousPacketsReceived = previousSample && previousSample.totals.packetsReceived || 0; 9440 const previousPacketsLost = previousSample && previousSample.totals.packetsLost || 0; 9441 const currentBytesSent = stats.bytesSent - previousBytesSent; 9442 const currentBytesReceived = stats.bytesReceived - previousBytesReceived; 9443 const currentPacketsSent = stats.packetsSent - previousPacketsSent; 9444 const currentPacketsReceived = stats.packetsReceived - previousPacketsReceived; 9445 const currentPacketsLost = stats.packetsLost - previousPacketsLost; 9446 const currentInboundPackets = currentPacketsReceived + currentPacketsLost; 9447 const currentPacketsLostFraction = (currentInboundPackets > 0) ? 9448 (currentPacketsLost / currentInboundPackets) * 100 : 0; 9449 const totalInboundPackets = stats.packetsReceived + stats.packetsLost; 9450 const totalPacketsLostFraction = (totalInboundPackets > 0) ? 9451 (stats.packetsLost / totalInboundPackets) * 100 : 100; 9452 const rttValue = (typeof stats.rtt === 'number' || !previousSample) ? stats.rtt : previousSample.rtt; 9453 const audioInputLevelValues = this._inputVolumes.splice(0); 9454 this._supplementalSampleBuffers.audioInputLevel.push(audioInputLevelValues); 9455 const audioOutputLevelValues = this._outputVolumes.splice(0); 9456 this._supplementalSampleBuffers.audioOutputLevel.push(audioOutputLevelValues); 9457 return { 9458 audioInputLevel: Math.round(average(audioInputLevelValues)), 9459 audioOutputLevel: Math.round(average(audioOutputLevelValues)), 9460 bytesReceived: currentBytesReceived, 9461 bytesSent: currentBytesSent, 9462 codecName: stats.codecName, 9463 jitter: stats.jitter, 9464 mos: this._mos.calculate(rttValue, stats.jitter, previousSample && currentPacketsLostFraction), 9465 packetsLost: currentPacketsLost, 9466 packetsLostFraction: currentPacketsLostFraction, 9467 packetsReceived: currentPacketsReceived, 9468 packetsSent: currentPacketsSent, 9469 rtt: rttValue, 9470 timestamp: stats.timestamp, 9471 totals: { 9472 bytesReceived: stats.bytesReceived, 9473 bytesSent: stats.bytesSent, 9474 packetsLost: stats.packetsLost, 9475 packetsLostFraction: totalPacketsLostFraction, 9476 packetsReceived: stats.packetsReceived, 9477 packetsSent: stats.packetsSent, 9478 }, 9479 }; 9480 } 9481 /** 9482 * Get stats from the PeerConnection and add it to our list of samples. 9483 */ 9484 _fetchSample() { 9485 this._getSample().then(sample => { 9486 this._addSample(sample); 9487 this._raiseWarnings(); 9488 this.emit('sample', sample); 9489 }).catch(error => { 9490 this.disable(); 9491 // We only bubble up any errors coming from pc.getStats() 9492 // No need to attach a twilioError 9493 this.emit('error', error); 9494 }); 9495 } 9496 /** 9497 * Get stats from the PeerConnection. 9498 * @returns A universally-formatted version of RTC stats. 9499 */ 9500 _getSample() { 9501 return this._getRTCStats(this._peerConnection).then((stats) => { 9502 let previousSample = null; 9503 if (this._sampleBuffer.length) { 9504 previousSample = this._sampleBuffer[this._sampleBuffer.length - 1]; 9505 } 9506 return this._createSample(stats, previousSample); 9507 }); 9508 } 9509 /** 9510 * Raise a warning and log its raised time. 9511 * @param statName - The name of the stat to raise. 9512 * @param thresholdName - The name of the threshold to raise 9513 * @param [data] - Any relevant sample data. 9514 */ 9515 _raiseWarning(statName, thresholdName, data) { 9516 const warningId = `${statName}:${thresholdName}`; 9517 if (this._activeWarnings.has(warningId)) { 9518 return; 9519 } 9520 this._activeWarnings.set(warningId, { timeRaised: Date.now() }); 9521 const thresholds = this._thresholds[statName]; 9522 let thresholdValue; 9523 if (Array.isArray(thresholds)) { 9524 const foundThreshold = thresholds.find(threshold => thresholdName in threshold); 9525 if (foundThreshold) { 9526 thresholdValue = foundThreshold[thresholdName]; 9527 } 9528 } 9529 else { 9530 thresholdValue = this._thresholds[statName][thresholdName]; 9531 }
9532 this.emit('warning', Object.assign(Object.assign({}, data), { name: statName, threshold: { 9533 name: thresholdName, 9534 value: thresholdValue, 9535 } })); 9536 } 9537 /** 9538 * Apply our thresholds to our array of RTCStat samples. 9539 */ 9540 _raiseWarnings() { 9541 if (!this._warningsEnabled) { 9542 return; 9543 } 9544 Object.keys(this._thresholds).forEach(name => this._raiseWarningsForStat(name)); 9545 } 9546 /** 9547 * Apply thresholds for a given stat name to our array of 9548 * RTCStat samples and raise or clear any associated warnings. 9549 * @param statName - Name of the stat to compare. 9550 */ 9551 _raiseWarningsForStat(statName) { 9552 const limits = Array.isArray(this._thresholds[statName]) 9553 ? this._thresholds[statName] 9554 : [this._thresholds[statName]]; 9555 limits.forEach((limit) => { 9556 const samples = this._sampleBuffer; 9557 const clearCount = limit.clearCount || SAMPLE_COUNT_CLEAR; 9558 const raiseCount = limit.raiseCount || SAMPLE_COUNT_RAISE; 9559 const sampleCount = limit.sampleCount || this._maxSampleCount; 9560 let relevantSamples = samples.slice(-sampleCount); 9561 const values = relevantSamples.map(sample => sample[statName]); 9562 // (rrowland) If we have a bad or missing value in the set, we don't 9563 // have enough information to throw or clear a warning. Bail out. 9564 const containsNull = values.some(value => typeof value === 'undefined' || value === null); 9565 if (containsNull) { 9566 return; 9567 } 9568 let count; 9569 if (typeof limit.max === 'number') { 9570 count = countHigh(limit.max, values); 9571 if (count >= raiseCount) { 9572 this._raiseWarning(statName, 'max', { values, samples: relevantSamples }); 9573 } 9574 else if (count <= clearCount) { 9575 this._clearWarning(statName, 'max', { values, samples: relevantSamples }); 9576 } 9577 } 9578 if (typeof limit.min === 'number') { 9579 count = countLow(limit.min, values); 9580 if (count >= raiseCount) { 9581 this._raiseWarning(statName, 'min', { values, samples: relevantSamples }); 9582 } 9583 else if (count <= clearCount) { 9584 this._clearWarning(statName, 'min', { values, samples: relevantSamples }); 9585 } 9586 } 9587 if (typeof limit.maxDuration === 'number' && samples.length > 1) { 9588 relevantSamples = samples.slice(-2); 9589 const prevValue = relevantSamples[0][statName]; 9590 const curValue = relevantSamples[1][statName]; 9591 const prevStreak = this._currentStreaks.get(statName) || 0; 9592 const streak = (prevValue === curValue) ? prevStreak + 1 : 0; 9593 this._currentStreaks.set(statName, streak); 9594 if (streak >= limit.maxDuration) { 9595 this._raiseWarning(statName, 'maxDuration', { value: streak }); 9596 } 9597 else if (streak === 0) { 9598 this._clearWarning(statName, 'maxDuration', { value: prevStreak }); 9599 } 9600 } 9601 if (typeof limit.minStandardDeviation === 'number') { 9602 const sampleSets = this._supplementalSampleBuffers[statName]; 9603 if (!sampleSets || sampleSets.length < limit.sampleCount) { 9604 return; 9605 } 9606 if (sampleSets.length > limit.sampleCount) { 9607 sampleSets.splice(0, sampleSets.length - limit.sampleCount); 9608 } 9609 const flatSamples = flattenSamples(sampleSets.slice(-sampleCount)); 9610 const stdDev = calculateStandardDeviation(flatSamples); 9611 if (typeof stdDev !== 'number') { 9612 return; 9613 } 9614 if (stdDev < limit.minStandardDeviation) { 9615 this._raiseWarning(statName, 'minStandardDeviation', { value: stdDev }); 9616 } 9617 else { 9618 this._clearWarning(statName, 'minStandardDeviation', { value: stdDev }); 9619 } 9620 } 9621 [ 9622 ['maxAverage', (x, y) => x > y], 9623 ['minAverage', (x, y) => x < y], 9624 ].forEach(([thresholdName, comparator]) => { 9625 if (typeof limit[thresholdName] === 'number' && values.length >
9625= sampleCount) { 9626 const avg = average(values); 9627 if (comparator(avg, limit[thresholdName])) { 9628 this._raiseWarning(statName, thresholdName, { values, samples: relevantSamples }); 9629 } 9630 else if (!comparator(avg, limit.clearValue || limit[thresholdName])) { 9631 this._clearWarning(statName, thresholdName, { values, samples: relevantSamples }); 9632 } 9633 } 9634 }); 9635 }); 9636 } 9637 } 9638 9639 const BACKOFF_CONFIG = { 9640 factor: 1.1, 9641 jitter: 0.5, 9642 max: 30000, 9643 min: 1, 9644 }; 9645 const DTMF_INTER_TONE_GAP = 70; 9646 const DTMF_PAUSE_DURATION = 500; 9647 const DTMF_TONE_DURATION = 160; 9648 const METRICS_BATCH_SIZE = 10; 9649 const METRICS_DELAY = 5000; 9650 const MEDIA_DISCONNECT_ERROR = { 9651 disconnect: true, 9652 info: { 9653 code: 31003, 9654 message: 'Connection with Twilio was interrupted.', 9655 twilioError: new MediaErrors.ConnectionError(), 9656 }, 9657 }; 9658 const MULTIPLE_THRESHOLD_WARNING_NAMES = { 9659 // The stat `packetsLostFraction` is monitored by two separate thresholds, 9660 // `maxAverage` and `max`. Each threshold emits a different warning name. 9661 packetsLostFraction: { 9662 max: 'packet-loss', 9663 maxAverage: 'packets-lost-fraction', 9664 }, 9665 }; 9666 const WARNING_NAMES = { 9667 audioInputLevel: 'audio-input-level', 9668 audioOutputLevel: 'audio-output-level', 9669 bytesReceived: 'bytes-received', 9670 bytesSent: 'bytes-sent', 9671 jitter: 'jitter', 9672 mos: 'mos', 9673 rtt: 'rtt', 9674 }; 9675 const WARNING_PREFIXES = { 9676 max: 'high-', 9677 maxAverage: 'high-', 9678 maxDuration: 'constant-', 9679 min: 'low-', 9680 minStandardDeviation: 'constant-', 9681 }; 9682 /** 9683 * A {@link Call} represents a media and signaling connection to a TwiML application. 9684 */ 9685 class Call extends eventsExports.EventEmitter { 9686 /** 9687 * Whether this {@link Call} is incoming or outgoing. 9688 */ 9689 get direction() { 9690 return this._direction; 9691 } 9692 /** 9693 * Audio codec used for this {@link Call}. Expecting {@link Call.Codec} but 9694 * will copy whatever we get from RTC stats. 9695 */ 9696 get codec() { 9697 return this._codec; 9698 } 9699 /** 9700 * The connect token is available as soon as the call is established 9701 * and connected to Twilio. Use this token to reconnect to a call via the {@link Device.connect} 9702 * method. 9703 * 9704 * For incoming calls, it is available in the call object after the {@link Device.incomingEvent} is emitted. 9705 * For outgoing calls, it is available after the {@link Call.acceptEvent} is emitted. 9706 */ 9707 get connectToken() { 9708 const signalingReconnectToken = this._signalingReconnectToken; 9709 const callSid = this.parameters && this.parameters.CallSid ? this.parameters.CallSid : undefined; 9710 if (!signalingReconnectToken || !callSid) { 9711 return; 9712 } 9713 const customParameters = this.customParameters && typeof this.customParameters.keys === 'function' ? 9714 Array.from(this.customParameters.keys()).reduce((result, key) => { 9715 result[key] = this.customParameters.get(key); 9716 return result; 9717 }, {}) : {}; 9718 const parameters = this.parameters || {}; 9719 return btoa(encodeURIComponent(JSON.stringify({ 9720 customParameters, 9721 parameters, 9722 signalingReconnectToken, 9723 }))); 9724 } 9725 /** 9726 * @internal 9727 * @param config - Mandatory configuration options 9728 * @param options - Optional settings 9729 */ 9730 constructor(config, options) { 9731 super(); 9732 /** 9733 * Call parameters received from Twilio for an incoming call. 9734 */ 9735 this.parameters = {}; 9736 /** 9737 * The number of times input volume has been the same consecutively. 9738 */ 9739 this._inputVolumeStreak = 0; 9740 /** 9741 * Whether the call has been answered. 9742 */ 9743 this._isAnswered = false; 9744 /** 9745 * Whether the call has been cancelled. 9746 */ 9747 this._isCancelled = false;
9748 /** 9749 * Whether the call has been rejected 9750 */ 9751 this._isRejected = false; 9752 /** 9753 * The most recent public input volume value. 0 -> 1 representing -100 to -30 dB. 9754 */ 9755 this._latestInputVolume = 0; 9756 /** 9757 * The most recent public output volume value. 0 -> 1 representing -100 to -30 dB. 9758 */ 9759 this._latestOutputVolume = 0; 9760 /** 9761 * An instance of Logger to use. 9762 */ 9763 this._log = new Log('Call'); 9764 /** 9765 * State of the {@link Call}'s media. 9766 */ 9767 this._mediaStatus = Call.State.Pending; 9768 /** 9769 * A map of messages sent via sendMessage API using voiceEventSid as the key. 9770 * The message will be deleted once an 'ack' or an error is received from the server. 9771 */ 9772 this._messages = new Map(); 9773 /** 9774 * A batch of metrics samples to send to Insights. Gets cleared after 9775 * each send and appended to on each new sample. 9776 */ 9777 this._metricsSamples = []; 9778 /** 9779 * Options passed to this {@link Call}. 9780 */ 9781 this._options = { 9782 MediaHandler: PeerConnection, 9783 MediaStream: null, 9784 enableImprovedSignalingErrorPrecision: false, 9785 offerSdp: null, 9786 shouldPlayDisconnect: () => true, 9787 voiceEventSidGenerator: generateVoiceEventSid, 9788 }; 9789 /** 9790 * The number of times output volume has been the same consecutively. 9791 */ 9792 this._outputVolumeStreak = 0; 9793 /** 9794 * Whether the {@link Call} should send a hangup on disconnect. 9795 */ 9796 this._shouldSendHangup = true; 9797 /** 9798 * State of the {@link Call}'s signaling. 9799 */ 9800 this._signalingStatus = Call.State.Pending; 9801 /** 9802 * A Map of Sounds to play. 9803 */ 9804 this._soundcache = new Map(); 9805 /** 9806 * State of the {@link Call}. 9807 */ 9808 this._status = Call.State.Pending; 9809 /** 9810 * Whether the {@link Call} has been connected. Used to determine if we are reconnected. 9811 */ 9812 this._wasConnected = false; 9813 /** 9814 * String representation of {@link Call} instance. 9815 * @internal 9816 */ 9817 this.toString = () => '[Twilio.Call instance]'; 9818 this._emitWarning = (groupPrefix, warningName, threshold, value, wasCleared, warningData) => { 9819 const groupSuffix = wasCleared ? '-cleared' : '-raised'; 9820 const groupName = `${groupPrefix}warning${groupSuffix}`; 9821 // Ignore constant input if the Call is muted (Expected) 9822 if (warningName === 'constant-audio-input-level' && this.isMuted()) { 9823 return; 9824 } 9825 let level = wasCleared ? 'info' : 'warning'; 9826 // Avoid throwing false positives as warnings until we refactor volume metrics 9827 if (warningName === 'constant-audio-output-level') { 9828 level = 'info'; 9829 } 9830 const payloadData = { threshold }; 9831 if (value) { 9832 if (value instanceof Array) { 9833 payloadData.values = value.map((val) => { 9834 if (typeof val === 'number') { 9835 return Math.round(val * 100) / 100; 9836 } 9837 return value; 9838 }); 9839 } 9840 else { 9841 payloadData.value = value; 9842 } 9843 } 9844 this._publisher.post(level, groupName, warningName, { data: payloadData }, this); 9845 if (warningName !== 'constant-audio-output-level') { 9846 const emitName = wasCleared ? 'warning-cleared' : 'warning'; 9847 this._log.debug(`#${emitName}`, warningName); 9848 this.emit(emitName, warningName, warningData && !wasCleared ? warningData : null); 9849 } 9850 }; 9851 /** 9852 * Called when the {@link Call} receives an ack from signaling 9853 * @param payload 9854 */ 9855 this._onAck = (payload) => { 9856 const { acktype, callsid, voiceeventsid } = payload; 9857 if (this.parameters.CallSid !== callsid) { 9858 this._log.warn(`Received ack from a different callsid: ${callsid}`); 9859 return; 9860 } 9861 if (acktype === 'message') { 9862 this._onMessageSent(voiceeventsid); 9863 } 9864 }; 9865 /** 9866 * Called when the {@link Call} is answered. 9867 * @param payload 9868 */ 9869 this._onAnswer = (payload) => { 9870 if (typeof payload.reconnect === 'string') { 9871 this._signalingReconnectToken = payload.reconnect; 9872 } 9873 // answerOnBridge=false will send a 183 which we need to catch in _onRinging when 9874 // the enableRingingState flag is disabled. In that case, we will receive a 200 after 9875 // the callee accepts the call firing a second `accept` event if we don't 9876 // short circuit here. 9877 if (this._isAnswered && this._status !== Call.State.Reconnecting) { 9878 return; 9879 } 9880 this._setCallSid(payload); 9881 this._isAnswered = true; 9882 this._maybeTransitionToOpen(); 9883 }; 9884 /** 9885 * Called when the {@link Call} is cancelled. 9886 * @param payload 9887 */ 9888 this._onCancel = (payload) => { 9889 // (rrowland) Is this check necessary? Verify, and if so move to pstream / VSP module. 9890 const callsid = payload.callsid; 9891 if (this.parameters.CallSid === callsid) { 9892 this._isCancelled = true; 9893 this._publisher.info('connection', 'cancel', null, this); 9894 this._cleanupEventListeners(); 9895 this._mediaHandler.close(); 9896 this._status = Call.State.Closed; 9897 this._log.debug('#cancel');
9898 this.emit('cancel'); 9899 this._pstream.removeListener('cancel', this._onCancel); 9900 } 9901 }; 9902 /** 9903 * Called when we receive a connected event from pstream. 9904 * Re-emits the event. 9905 */ 9906 this._onConnected = () => { 9907 this._log.info('Received connected from pstream'); 9908 if (this._signalingReconnectToken && this._mediaHandler.version) { 9909 this._pstream.reconnect(this._mediaHandler.version.getSDP(), this.parameters.CallSid, this._signalingReconnectToken); 9910 } 9911 }; 9912 /** 9913 * Called when the {@link Call} is hung up. 9914 * @param payload 9915 */ 9916 this._onHangup = (payload) => { 9917 if (this.status() === Call.State.Closed) { 9918 return; 9919 } 9920 /** 9921 * see if callsid passed in message matches either callsid or outbound id 9922 * call should always have either callsid or outbound id 9923 * if no callsid passed hangup anyways 9924 */ 9925 if (payload.callsid && (this.parameters.CallSid || this.outboundConnectionId)) { 9926 if (payload.callsid !== this.parameters.CallSid 9927 && payload.callsid !== this.outboundConnectionId) { 9928 return; 9929 } 9930 } 9931 else if (payload.callsid) { 9932 // hangup is for another call 9933 return; 9934 } 9935 this._log.info('Received HANGUP from gateway'); 9936 if (payload.error) { 9937 const code = payload.error.code; 9938 const errorConstructor = getPreciseSignalingErrorByCode(this._options.enableImprovedSignalingErrorPrecision, code); 9939 const error = typeof errorConstructor !== 'undefined' 9940 ? new errorConstructor(payload.error.message) 9941 : new GeneralErrors.ConnectionError('Error sent from gateway in HANGUP', payload.error); 9942 this._log.error('Received an error from the gateway:', error); 9943 this._log.debug('#error', error); 9944 this.emit('error', error); 9945 } 9946 this._shouldSendHangup = false; 9947 this._publisher.info('connection', 'disconnected-by-remote', null, this); 9948 this._disconnect(null, true); 9949 this._cleanupEventListeners(); 9950 }; 9951 /** 9952 * Called when there is a media failure. 9953 * Manages all media-related states and takes action base on the states 9954 * @param type - Type of media failure 9955 */ 9956 this._onMediaFailure = (type) => { 9957 const { ConnectionDisconnected, ConnectionFailed, IceGatheringFailed, LowBytes, } = Call.MediaFailure; 9958 // These types signifies the end of a single ICE cycle 9959 const isEndOfIceCycle = type === ConnectionFailed || type === IceGatheringFailed; 9960 // All browsers except chrome doesn't update pc.iceConnectionState and pc.connectionState
9961 // after issuing an ICE Restart, which we use to determine if ICE Restart is complete. 9962 // Since we cannot detect if ICE Restart is complete, we will not retry. 9963 if (!isChrome(window, window.navigator) && type === ConnectionFailed) { 9964 return this._mediaHandler.onerror(MEDIA_DISCONNECT_ERROR); 9965 } 9966 // Ignore subsequent requests if ice restart is in progress 9967 if (this._mediaStatus === Call.State.Reconnecting) { 9968 // This is a retry. Previous ICE Restart failed 9969 if (isEndOfIceCycle) { 9970 // We already exceeded max retry time. 9971 if (Date.now() - this._mediaReconnectStartTime > BACKOFF_CONFIG.max) { 9972 this._log.warn('Exceeded max ICE retries'); 9973 return this._mediaHandler.onerror(MEDIA_DISCONNECT_ERROR); 9974 } 9975 // Issue ICE restart with backoff 9976 try { 9977 this._mediaReconnectBackoff.backoff(); 9978 } 9979 catch (error) { 9980 // Catch and ignore 'Backoff in progress.' errors. If a backoff is 9981 // ongoing and we try to start another one, there shouldn't be a 9982 // problem. 9983 if (!(error.message && error.message === 'Backoff in progress.')) { 9984 throw error; 9985 } 9986 } 9987 } 9988 return; 9989 } 9990 const pc = this._mediaHandler.version.pc; 9991 const isIceDisconnected = pc && pc.iceConnectionState === 'disconnected'; 9992 const hasLowBytesWarning = this._monitor.hasActiveWarning('bytesSent', 'min') 9993 || this._monitor.hasActiveWarning('bytesReceived', 'min'); 9994 // Only certain conditions can trigger media reconnection 9995 if ((type === LowBytes && isIceDisconnected) 9996 || (type === ConnectionDisconnected && hasLowBytesWarning) 9997 || isEndOfIceCycle) { 9998 const mediaReconnectionError = new MediaErrors.ConnectionError('Media connection failed.'); 9999 this._log.warn('ICE Connection disconnected.'); 10000 this._publisher.warn('connection', 'error', mediaReconnectionError, this); 10001 this._publisher.info('connection', 'reconnecting', null, this); 10002 this._mediaReconnectStartTime = Date.now(); 10003 this._status = Call.State.Reconnecting; 10004 this._mediaStatus = Call.State.Reconnecting; 10005 this._mediaReconnectBackoff.reset(); 10006 this._mediaReconnectBackoff.backoff(); 10007 this._log.debug('#reconnecting'); 10008 this.emit('reconnecting', mediaReconnectionError); 10009 } 10010 }; 10011 /** 10012 * Called when media call is restored 10013 */ 10014 this._onMediaReconnected = () => { 10015 // Only trigger once. 10016 // This can trigger on pc.onIceConnectionChange and pc.onConnectionChange. 10017 if (this._mediaStatus !== Call.State.Reconnecting) { 10018 return; 10019 } 10020 this._log.info('ICE Connection reestablished.'); 10021 this._mediaStatus = Call.State.Open; 10022 if (this._signalingStatus === Call.State.Open) { 10023 this._publisher.info('connection', 'reconnected', null, this); 10024 this._log.debug('#reconnected'); 10025 this.emit('reconnected'); 10026 this._status = Call.State.Open; 10027 } 10028 }; 10029 /** 10030 * Raised when a Call receives a message from the backend. 10031 * @param payload - A record representing the payload of the message from the 10032 * Twilio backend. 10033 */ 10034 this._onMessageReceived = (payload) => { 10035 const { callsid, content, contenttype, messagetype, voiceeventsid } = payload; 10036 if (this.parameters.CallSid !== callsid) { 10037 this._log.warn(`Received a message from a different callsid: ${callsid}`); 10038 return; 10039 } 10040 const data = { 10041 content, 10042 contentType: contenttype, 10043 messageType: messagetype, 10044 voiceEventSid: voiceeventsid, 10045 };
10046 this._publisher.info('call-message', messagetype, { 10047 content_type: contenttype, 10048 event_type: 'received', 10049 voice_event_sid: voiceeventsid, 10050 }, this); 10051 this._log.debug('#messageReceived', JSON.stringify(data)); 10052 this.emit('messageReceived', data); 10053 }; 10054 /** 10055 * Raised when a Call receives an 'ack' with an 'acktype' of 'message. 10056 * This means that the message sent via sendMessage API has been received by the signaling server. 10057 * @param voiceEventSid 10058 */ 10059 this._onMessageSent = (voiceEventSid) => { 10060 if (!this._messages.has(voiceEventSid)) { 10061 this._log.warn(`Received a messageSent with a voiceEventSid that doesn't exists: ${voiceEventSid}`); 10062 return; 10063 } 10064 const message = this._messages.get(voiceEventSid); 10065 this._messages.delete(voiceEventSid); 10066 this._publisher.info('call-message', message === null || message === void 0 ? void 0 : message.messageType, { 10067 content_type: message === null || message === void 0 ? void 0 : message.contentType, 10068 event_type: 'sent', 10069 voice_event_sid: voiceEventSid, 10070 }, this); 10071 this._log.debug('#messageSent', JSON.stringify(message)); 10072 this.emit('messageSent', message); 10073 }; 10074 /** 10075 * When we get a RINGING signal from PStream, update the {@link Call} status. 10076 * @param payload 10077 */ 10078 this._onRinging = (payload) => { 10079 this._setCallSid(payload); 10080 // If we're not in 'connecting' or 'ringing' state, this event was received out of order. 10081 if (this._status !== Call.State.Connecting && this._status !== Call.State.Ringing) { 10082 return; 10083 } 10084 const hasEarlyMedia = !!payload.sdp; 10085 this._status = Call.State.Ringing; 10086 this._publisher.info('connection', 'outgoing-ringing', { hasEarlyMedia }, this); 10087 this._log.debug('#ringing'); 10088 this.emit('ringing', hasEarlyMedia); 10089 }; 10090 /** 10091 * Called each time StatsMonitor emits a sample. 10092 * Emits stats event and batches the call stats metrics and sends them to Insights. 10093 * @param sample 10094 */ 10095 this._onRTCSample = (sample) => { 10096 const callMetrics = Object.assign(Object.assign({}, sample), { inputVolume: this._latestInputVolume, outputVolume: this._latestOutputVolume }); 10097 this._codec = callMetrics.codecName; 10098 this._metricsSamples.push(callMetrics); 10099 if (this._metricsSamples.length >= METRICS_BATCH_SIZE) { 10100 this._publishMetrics(); 10101 } 10102 this.emit('sample', sample); 10103 }; 10104 /** 10105 * Called when an 'error' event is received from the signaling stream. 10106 */ 10107 this._onSignalingError = (payload) => { 10108 const { callsid, voiceeventsid, error } = payload; 10109 if (this.parameters.CallSid !== callsid) { 10110 this._log.warn(`Received an error from a different callsid: ${callsid}`); 10111 return; 10112 } 10113 if (voiceeventsid && this._messages.has(voiceeventsid)) { 10114 // Do not emit an error here. Device is handling all signaling related errors. 10115 this._messages.delete(voiceeventsid); 10116 this._log.warn(`Received an error while sending a message.`, payload); 10117 this._publisher.error('call-message', 'error', { 10118 code: error.code, 10119 message: error.message, 10120 voice_event_sid: voiceeventsid, 10121 }, this); 10122 let twilioError; 10123 const errorConstructor = getPreciseSignalingErrorByCode(!!this._options.enableImprovedSignalingErrorPrecision, error.code); 10124 if (typeof errorConstructor !== 'undefined') { 10125 twilioError = new errorConstructor(error); 10126 }
10127 if (!twilioError) { 10128 this._log.error('Unknown Call Message Error: ', error); 10129 twilioError = new GeneralErrors.UnknownError(error.message, error); 10130 } 10131 this._log.debug('#error', error, twilioError); 10132 this.emit('error', twilioError); 10133 } 10134 }; 10135 /** 10136 * Called when signaling is restored 10137 */ 10138 this._onSignalingReconnected = () => { 10139 if (this._signalingStatus !== Call.State.Reconnecting) { 10140 return; 10141 } 10142 this._log.info('Signaling Connection reestablished.'); 10143 this._signalingStatus = Call.State.Open; 10144 if (this._mediaStatus === Call.State.Open) { 10145 this._publisher.info('connection', 'reconnected', null, this); 10146 this._log.debug('#reconnected'); 10147 this.emit('reconnected'); 10148 this._status = Call.State.Open; 10149 } 10150 }; 10151 /** 10152 * Called when we receive a transportClose event from pstream. 10153 * Re-emits the event. 10154 */ 10155 this._onTransportClose = () => { 10156 this._log.error('Received transportClose from pstream'); 10157 this._log.debug('#transportClose'); 10158 this.emit('transportClose'); 10159 if (this._signalingReconnectToken) { 10160 this._status = Call.State.Reconnecting; 10161 this._signalingStatus = Call.State.Reconnecting; 10162 this._publisher.info('connection', 'reconnecting', null, this); 10163 this._log.debug('#reconnecting'); 10164 this.emit('reconnecting', new SignalingErrors.ConnectionDisconnected()); 10165 } 10166 else { 10167 this._status = Call.State.Closed; 10168 this._signalingStatus = Call.State.Closed; 10169 } 10170 }; 10171 /** 10172 * Re-emit an StatsMonitor warning as a {@link Call}.warning or .warning-cleared event. 10173 * @param warningData 10174 * @param wasCleared - Whether this is a -cleared or -raised event. 10175 */ 10176 this._reemitWarning = (warningData, wasCleared) => { 10177 const groupPrefix = /^audio/.test(warningData.name) ? 10178 'audio-level-' : 'network-quality-'; 10179 const warningPrefix = WARNING_PREFIXES[warningData.threshold.name]; 10180 /** 10181 * NOTE: There are two "packet-loss" warnings: `high-packet-loss` and 10182 * `high-packets-lost-fraction`, so in this case we need to use a different 10183 * `WARNING_NAME` mapping. 10184 */ 10185 let warningName; 10186 if (warningData.name in MULTIPLE_THRESHOLD_WARNING_NAMES) { 10187 warningName = MULTIPLE_THRESHOLD_WARNING_NAMES[warningData.name][warningData.threshold.name]; 10188 } 10189 else if (warningData.name in WARNING_NAMES) { 10190 warningName = WARNING_NAMES[warningData.name]; 10191 } 10192 const warning = warningPrefix + warningName; 10193 this._emitWarning(groupPrefix, warning, warningData.threshold.value, warningData.values || warningData.value, wasCleared, warningData); 10194 }; 10195 /** 10196 * Re-emit an StatsMonitor warning-cleared as a .warning-cleared event. 10197 * @param warningData 10198 */ 10199 this._reemitWarningCleared = (warningData) => { 10200 this._reemitWarning(warningData, true); 10201 }; 10202 this._soundcache = config.soundcache; 10203 if (typeof config.onIgnore === 'function') { 10204 this._onIgnore = config.onIgnore; 10205 } 10206 const message = options && options.twimlParams || {}; 10207 this.customParameters = new Map(Object.entries(message).map(([key, val]) => [key, String(val)])); 10208 Object.assign(this._options, options); 10209 if (this._options.callParameters) { 10210 this.parameters = this._options.callParameters; 10211 } 10212 if (this._options.reconnectToken) { 10213 this._signalingReconnectToken = this._options.reconnectToken; 10214 } 10215 this._voiceEventSidGenerator = 10216 this._options.voiceEventSidGenerator || generateVoiceEventSid; 10217 this._direction = this.parameters.CallSid && !this._options.reconnectCallSid ? 10218 Call.CallDirection.Incoming : Call.CallDirection.Outgoing; 10219 if (this.parameters) { 10220 this.callerInfo = this.parameters.StirStatus 10221 ? { isVerified: this.parameters.StirStatus === 'TN-Validation-Passed-A' } 10222 : null; 10223 } 10224 else { 10225 this.callerInfo = null; 10226 } 10227 this._mediaReconnectBackoff = new Backoff(BACKOFF_CONFIG); 10228 this._mediaReconnectBackoff.on('ready', () => this._mediaHandler.iceRestart()); 10229 // temporary call sid to be used for outgoing calls 10230 this.outboundConnectionId = generateTempCallSid(); 10231 const publisher = this._publisher = config.publisher; 10232 if (this._direction === Call.CallDirection.Incoming) { 10233 publisher.info('connection', 'incoming', null, this); 10234 } 10235 else { 10236 publisher.info('connection', 'outgoing', { 10237 preflight: this._options.preflight, 10238 reconnect: !!this._options.reconnectCallSid, 10239 }, this); 10240 } 10241 const monitor = this._monitor = new (this._options.StatsMonitor || StatsMonitor)(); 10242 monitor.on('sample', this._onRTCSample); 10243 // First 20 seconds or so are choppy, so let's not bother with these warnings. 10244 monitor.disableWarnings(); 10245 setTimeout(() => monitor.enableWarnings(), METRICS_DELAY); 10246 monitor.on('warning', (data, wasCleared) => { 10247 if (data.name === 'bytesSent' || data.name === 'bytesReceived') { 10248 this._onMediaFailure(Call.MediaFailure.LowBytes); 10249 } 10250 this._reemitWarning(data, wasCleared); 10251 });
10252 monitor.on('warning-cleared', (data) => { 10253 this._reemitWarningCleared(data); 10254 }); 10255 this._mediaHandler = new (this._options.MediaHandler)(config.audioHelper, config.pstream, { 10256 MediaStream: this._options.MediaStream, 10257 RTCPeerConnection: this._options.RTCPeerConnection, 10258 codecPreferences: this._options.codecPreferences, 10259 dscp: this._options.dscp, 10260 forceAggressiveIceNomination: this._options.forceAggressiveIceNomination, 10261 maxAverageBitrate: this._options.maxAverageBitrate, 10262 }); 10263 this.on('volume', (inputVolume, outputVolume) => { 10264 this._inputVolumeStreak = this._checkVolume(inputVolume, this._inputVolumeStreak, this._latestInputVolume, 'input'); 10265 this._outputVolumeStreak = this._checkVolume(outputVolume, this._outputVolumeStreak, this._latestOutputVolume, 'output'); 10266 this._latestInputVolume = inputVolume; 10267 this._latestOutputVolume = outputVolume; 10268 }); 10269 this._mediaHandler.onaudio = (remoteAudio) => { 10270 this._log.debug('#audio'); 10271 this.emit('audio', remoteAudio); 10272 }; 10273 this._mediaHandler.onvolume = (inputVolume, outputVolume, internalInputVolume, internalOutputVolume) => { 10274 // (rrowland) These values mock the 0 -> 32767 format used by legacy getStats. We should look into 10275 // migrating to a newer standard, either 0.0 -> linear or -127 to 0 in dB, matching the range 10276 // chosen below. 10277 monitor.addVolumes((internalInputVolume / 255) * 32767, (internalOutputVolume / 255) * 32767); 10278 // (rrowland) 0.0 -> 1.0 linear 10279 this.emit('volume', inputVolume, outputVolume); 10280 }; 10281 this._mediaHandler.ondtlstransportstatechange = (state) => { 10282 const level = state === 'failed' ? 'error' : 'debug'; 10283 this._publisher.post(level, 'dtls-transport-state', state, null, this); 10284 }; 10285 this._mediaHandler.onpcconnectionstatechange = (state) => { 10286 let level = 'debug'; 10287 const dtlsTransport = this._mediaHandler.getRTCDtlsTransport(); 10288 if (state === 'failed') { 10289 level = dtlsTransport && dtlsTransport.state === 'failed' ? 'error' : 'warning'; 10290 } 10291 this._publisher.post(level, 'pc-connection-state', state, null, this); 10292 }; 10293 this._mediaHandler.onicecandidate = (candidate) => { 10294 const payload = new IceCandidate(candidate).toPayload(); 10295 this._publisher.debug('ice-candidate', 'ice-candidate', payload, this); 10296 }; 10297 this._mediaHandler.onselectedcandidatepairchange = (pair) => { 10298 const localCandidatePayload = new IceCandidate(pair.local).toPayload(); 10299 const remoteCandidatePayload = new IceCandidate(pair.remote, true).toPayload(); 10300 this._publisher.debug('ice-candidate', 'selected-ice-candidate-pair', { 10301 local_candidate: localCandidatePayload, 10302 remote_candidate: remoteCandidatePayload, 10303 }, this); 10304 }; 10305 this._mediaHandler.oniceconnectionstatechange = (state) => { 10306 const level = state === 'failed' ? 'error' : 'debug'; 10307 this._publisher.post(level, 'ice-connection-state', state, null, this); 10308 }; 10309 this._mediaHandler.onicegatheringfailure = (type) => { 10310 this._publisher.warn('ice-gathering-state', type, null, this); 10311 this._onMediaFailure(Call.MediaFailure.IceGatheringFailed); 10312 }; 10313 this._mediaHandler.onicegatheringstatechange = (state) => { 10314 this._publisher.debug('ice-gathering-state', state, null, this); 10315 }; 10316 this._mediaHandler.onsignalingstatechange = (state) => { 10317 this._publisher.debug('signaling-state', state, null, this); 10318 }; 10319 this._mediaHandler.ondisconnected = (msg) => { 10320 this._log.warn(msg); 10321 this._publisher.warn('network-quality-warning-raised', 'ice-connectivity-lost', { 10322 message: msg, 10323 }, this); 10324 this._log.debug('#warning', 'ice-connectivity-lost');
10325 this.emit('warning', 'ice-connectivity-lost'); 10326 this._onMediaFailure(Call.MediaFailure.ConnectionDisconnected); 10327 }; 10328 this._mediaHandler.onfailed = (msg) => { 10329 this._onMediaFailure(Call.MediaFailure.ConnectionFailed); 10330 }; 10331 this._mediaHandler.onconnected = () => { 10332 // First time _mediaHandler is connected, but ICE Gathering issued an ICE restart and succeeded. 10333 if (this._status === Call.State.Reconnecting) { 10334 this._onMediaReconnected(); 10335 } 10336 }; 10337 this._mediaHandler.onreconnected = (msg) => { 10338 this._log.info(msg); 10339 this._publisher.info('network-quality-warning-cleared', 'ice-connectivity-lost', { 10340 message: msg, 10341 }, this); 10342 this._log.debug('#warning-cleared', 'ice-connectivity-lost'); 10343 this.emit('warning-cleared', 'ice-connectivity-lost'); 10344 this._onMediaReconnected(); 10345 }; 10346 this._mediaHandler.onerror = (e) => { 10347 if (e.disconnect === true) { 10348 this._disconnect(e.info && e.info.message); 10349 } 10350 const error = e.info.twilioError || new GeneralErrors.UnknownError(e.info.message); 10351 this._log.error('Received an error from MediaStream:', e); 10352 this._log.debug('#error', error); 10353 this.emit('error', error); 10354 }; 10355 this._mediaHandler.onopen = () => { 10356 // NOTE(mroberts): While this may have been happening in previous 10357 // versions of Chrome, since Chrome 45 we have seen the 10358 // PeerConnection's onsignalingstatechange handler invoked multiple 10359 // times in the same signalingState 'stable'. When this happens, we 10360 // invoke this onopen function. If we invoke it twice without checking 10361 // for _status 'open', we'd accidentally close the PeerConnection. 10362 // 10363 // See <https://code.google.com/p/webrtc/issues/detail?id=4996>. 10364 if (this._status === Call.State.Open || this._status === Call.State.Reconnecting) { 10365 return; 10366 } 10367 else if (this._status === Call.State.Ringing || this._status === Call.State.Connecting) { 10368 this.mute(this._mediaHandler.isMuted); 10369 this._mediaStatus = Call.State.Open; 10370 this._maybeTransitionToOpen(); 10371 } 10372 else { 10373 // call was probably canceled sometime before this 10374 this._mediaHandler.close(); 10375 } 10376 }; 10377 this._mediaHandler.onclose = () => { 10378 this._status = Call.State.Closed; 10379 if (this._options.shouldPlayDisconnect && this._options.shouldPlayDisconnect() 10380 // Don't play disconnect sound if this was from a cancel event. i.e. the call 10381 // was ignored or hung up even before it was answered. 10382 // Similarly, don't play disconnect sound if the call was rejected. 10383 && !this._isCancelled && !this._isRejected) { 10384 this._soundcache.get(Device.SoundName.Disconnect).play(); 10385 } 10386 monitor.disable(); 10387 this._publishMetrics(); 10388 if (!this._isCancelled && !this._isRejected) { 10389 // tslint:disable no-console 10390 this._log.debug('#disconnect'); 10391 this.emit('disconnect', this); 10392 } 10393 }; 10394 this._pstream = config.pstream; 10395 this._pstream.on('ack', this._onAck); 10396 this._pstream.on('cancel', this._onCancel); 10397 this._pstream.on('error', this._onSignalingError); 10398 this._pstream.on('ringing', this._onRinging); 10399 this._pstream.on('transportClose', this._onTransportClose); 10400 this._pstream.on('connected', this._onConnected); 10401 this._pstream.on('message', this._onMessageReceived); 10402 this.on('error', error => { 10403 this._publisher.error('connection', 'error', { 10404 code: error.code, message: error.message, 10405 }, this); 10406 if (this._pstream && this._pstream.status === 'disconnected') { 10407 this._cleanupEventListeners(); 10408 } 10409 }); 10410 this.on('disconnect', () => { 10411 this._cleanupEventListeners(); 10412 }); 10413 } 10414 /** 10415 * Set the audio input tracks from a given stream. 10416 * @internal 10417 * @param stream 10418 */ 10419 _setInputTracksFromStream(stream) { 10420 return this._mediaHandler.setInputTracksFromStream(stream); 10421 } 10422 /** 10423 * Set the audio output sink IDs. 10424 * @internal 10425 * @param sinkIds 10426 */ 10427 _setSinkIds(sinkIds) { 10428 return this._mediaHandler._setSinkIds(sinkIds); 10429 } 10430 /** 10431 * Accept the incoming {@link Call}. 10432 * @param [options] 10433 */ 10434 accept(options) { 10435 this._log.debug('.accept', options); 10436 if (this._status !== Call.State.Pending) { 10437 this._log.debug(`.accept noop. status is '${this._status}'`); 10438 return; 10439 } 10440 options = options || {}; 10441 const rtcConfiguration = options.rtcConfiguration || this._options.rtcConfiguration; 10442 const rtcConstraints = options.rtcConstraints || this._options.rtcConstraints || {}; 10443 const audioConstraints = { 10444 audio: typeof rtcConstraints.audio !== 'undefined' ? rtcConstraints.audio : true, 10445 }; 10446 this._status = Call.State.Connecting; 10447 const connect = () => { 10448 if (this._status !== Call.State.Connecting) { 10449 // call must have been canceled 10450 this._cleanupEventListeners(); 10451 this._mediaHandler.close(); 10452 return; 10453 } 10454 const onAnswer = (pc) => { 10455 // Report that the call was answered, and directionality 10456 const eventName = this._direction === Call.CallDirection.Incoming 10457 ? 'accepted-by-local' 10458 : 'accepted-by-remote'; 10459 this._publisher.info('connection', eventName, null, this); 10460 // Report the preferred codec and params as they appear in the SDP 10461 const { codecName, codecParams } = getPreferredCodecInfo(this._mediaHandler.version.getSDP()); 10462 this._publisher.info('settings', 'codec', { 10463 codec_params: codecParams, 10464 selected_codec: codecName, 10465 }, this); 10466 // Enable RTC monitoring 10467 this._monitor.enable(pc); 10468 }; 10469 const sinkIds = typeof this._options.getSinkIds === 'function' && this._options.getSinkIds(); 10470 if (Array.isArray(sinkIds)) { 10471 this._mediaHandler._setSinkIds(sinkIds).catch(() => { 10472 // (rrowland) We don't want this to throw to console since the customer 10473 // can't control this. This will most commonly be rejected on browsers 10474 // that don't support setting sink IDs. 10475 }); 10476 } 10477 this._pstream.addListener('hangup', this._onHangup); 10478 if (this._direction === Call.CallDirection.Incoming) { 10479 this._isAnswered = true; 10480 this._pstream.on('answer', this._onAnswer); 10481 this._mediaHandler.answerIncomingCall(this.parameters.CallSid, this._options.offerSdp, rtcConfiguration, onAnswer); 10482 } 10483 else { 10484 const params = Array.from(this.customParameters.entr
10484ies()).map(pair => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1])}`).join('&'); 10485 this._pstream.on('answer', this._onAnswer); 10486 this._mediaHandler.makeOutgoingCall(params, this._signalingReconnectToken, this._options.reconnectCallSid || this.outboundConnectionId, rtcConfiguration, onAnswer); 10487 } 10488 }; 10489 if (this._options.beforeAccept) { 10490 this._options.beforeAccept(this); 10491 } 10492 const inputStream = typeof this._options.getInputStream === 'function' && this._options.getInputStream(); 10493 const promise = inputStream 10494 ? this._mediaHandler.setInputTracksFromStream(inputStream) 10495 : this._mediaHandler.openDefaultDeviceWithConstraints(audioConstraints); 10496 promise.then(() => { 10497 this._publisher.info('get-user-media', 'succeeded', { 10498 data: { audioConstraints }, 10499 }, this); 10500 connect(); 10501 }, (error) => { 10502 let twilioError; 10503 if (error.code === 31208 10504 || ['PermissionDeniedError', 'NotAllowedError'].indexOf(error.name) !== -1) { 10505 twilioError = new UserMediaErrors.PermissionDeniedError(); 10506 this._publisher.error('get-user-media', 'denied', { 10507 data: { 10508 audioConstraints, 10509 error, 10510 }, 10511 }, this); 10512 } 10513 else { 10514 twilioError = new UserMediaErrors.AcquisitionFailedError(); 10515 this._publisher.error('get-user-media', 'failed', { 10516 data: { 10517 audioConstraints, 10518 error, 10519 }, 10520 }, this); 10521 } 10522 this._disconnect(); 10523 this._log.debug('#error', error); 10524 this.emit('error', twilioError); 10525 }); 10526 } 10527 /** 10528 * Disconnect from the {@link Call}. 10529 */ 10530 disconnect() { 10531 this._log.debug('.disconnect'); 10532 this._disconnect(); 10533 } 10534 /** 10535 * Get the local MediaStream, if set. 10536 */ 10537 getLocalStream() { 10538 return this._mediaHandler && this._mediaHandler.stream; 10539 } 10540 /** 10541 * Get the remote MediaStream, if set. 10542 */ 10543 getRemoteStream() { 10544 return this._mediaHandler && this._mediaHandler._remoteStream; 10545 } 10546 /** 10547 * Ignore the incoming {@link Call}. 10548 */ 10549 ignore() { 10550 this._log.debug('.ignore'); 10551 if (this._status !== Call.State.Pending) { 10552 this._log.debug(`.ignore noop. status is '${this._status}'`); 10553 return; 10554 } 10555 this._status = Call.State.Closed; 10556 this._mediaHandler.ignore(this.parameters.CallSid); 10557 this._publisher.info('connection', 'ignored-by-local', null, this); 10558 if (this._onIgnore) { 10559 this._onIgnore(); 10560 } 10561 } 10562 /** 10563 * Check whether call is muted 10564 */ 10565 isMuted() { 10566 return this._mediaHandler.isMuted; 10567 } 10568 /** 10569 * Mute incoming audio. 10570 * @param shouldMute - Whether the incoming audio should be muted. Defaults to true. 10571 */ 10572 mute(shouldMute = true) { 10573 this._log.debug('.mute', shouldMute); 10574 const wasMuted = this._mediaHandler.isMuted; 10575 this._mediaHandler.mute(shouldMute); 10576 const isMuted = this._mediaHandler.isMuted; 10577 if (wasMuted !== isMuted) { 10578 this._publisher.info('connection', isMuted ? 'muted' : 'unmuted', null, this); 10579 this._log.debug('#mute', isMuted); 10580 this.emit('mute', isMuted, this); 10581 } 10582 } 10583 /** 10584 * Post an event to Endpoint Analytics indicating that the end user 10585 * has given call quality feedback. Called without a score, this 10586 * will report that the customer declined to give feedback. 10587 * @param score - The end-user's rating of the call; an 10588 * integer 1 through 5. Or undefined if the user declined to give 10589 * feedback. 10590 * @param issue - The primary issue the end user 10591 * experienced on the call. Can be: ['one-way-audio', 'choppy-audio', 10592 * 'dropped-call', 'audio-latency', 'noisy-call', 'echo'] 10593 */ 10594 postFeedback(score, issue) { 10595 if (typeof score === 'undefined' || score === null) { 10596 return this._postFeedbackDeclined(); 10597 } 10598 if (!Object.values(Call.FeedbackScore).includes(score)) { 10599 throw new InvalidArgumentError(`Feedback score must be one of: ${Object.values(Call.FeedbackScore)}`); 10600 } 10601 if (typeof issue !== 'undefined' && issue !== null && !Object.values(Call.FeedbackIssue).includes(issue)) { 10602 throw new InvalidArgumentError(`Feedback issue must be one of: ${Object.values(Call.FeedbackIssue)}`); 10603 } 10604 return this._publisher.info('feedback', 'received', { 10605 issue_name: issue, 10606 quality_score: score, 10607 }, this, true); 10608 } 10609 /**
10610 * Reject the incoming {@link Call}. 10611 */ 10612 reject() { 10613 this._log.debug('.reject'); 10614 if (this._status !== Call.State.Pending) { 10615 this._log.debug(`.reject noop. status is '${this._status}'`); 10616 return; 10617 } 10618 this._isRejected = true; 10619 this._pstream.reject(this.parameters.CallSid); 10620 this._mediaHandler.reject(this.parameters.CallSid); 10621 this._publisher.info('connection', 'rejected-by-local', null, this); 10622 this._cleanupEventListeners(); 10623 this._mediaHandler.close(); 10624 this._status = Call.State.Closed; 10625 this._log.debug('#reject'); 10626 this.emit('reject'); 10627 } 10628 /** 10629 * Send a string of digits. 10630 * @param digits 10631 */ 10632 sendDigits(digits) { 10633 this._log.debug('.sendDigits', digits); 10634 if (digits.match(/[^0-9*#w]/)) { 10635 throw new InvalidArgumentError('Illegal character passed into sendDigits'); 10636 } 10637 const customSounds = this._options.customSounds || {}; 10638 const sequence = []; 10639 digits.split('').forEach((digit) => { 10640 let dtmf = (digit !== 'w') ? `dtmf${digit}` : ''; 10641 if (dtmf === 'dtmf*') { 10642 dtmf = 'dtmfs'; 10643 } 10644 if (dtmf === 'dtmf#') { 10645 dtmf = 'dtmfh'; 10646 } 10647 sequence.push(dtmf); 10648 }); 10649 const playNextDigit = () => { 10650 const digit = sequence.shift(); 10651 if (digit) { 10652 if (this._options.dialtonePlayer && !customSounds[digit]) { 10653 this._options.dialtonePlayer.play(digit); 10654 } 10655 else { 10656 this._soundcache.get(digit).play(); 10657 } 10658 } 10659 if (sequence.length) { 10660 setTimeout(() => playNextDigit(), 200); 10661 } 10662 }; 10663 playNextDigit(); 10664 const dtmfSender = this._mediaHandler.getOrCreateDTMFSender(); 10665 function insertDTMF(dtmfs) { 10666 if (!dtmfs.length) { 10667 return; 10668 } 10669 const dtmf = dtmfs.shift(); 10670 if (dtmf && dtmf.length) { 10671 dtmfSender.insertDTMF(dtmf, DTMF_TONE_DURATION, DTMF_INTER_TONE_GAP); 10672 } 10673 setTimeout(insertDTMF.bind(null, dtmfs), DTMF_PAUSE_DURATION); 10674 } 10675 if (dtmfSender) { 10676 if (!('canInsertDTMF' in dtmfSender) || dtmfSender.canInsertDTMF) { 10677 this._log.info('Sending digits using RTCDTMFSender'); 10678 // NOTE(mroberts): We can't just map 'w' to ',' since 10679 // RTCDTMFSender's pause duration is 2 s and Twilio's is more 10680 // like 500 ms. Instead, we will fudge it with setTimeout. 10681 insertDTMF(digits.split('w')); 10682 return; 10683 } 10684 this._log.info('RTCDTMFSender cannot insert DTMF'); 10685 } 10686 // send pstream message to send DTMF 10687 this._log.info('Sending digits over PStream'); 10688 if (this._pstream !== null && this._pstream.status !== 'disconnected') { 10689 this._pstream.dtmf(this.parameters.CallSid, digits); 10690 } 10691 else { 10692 const error = new GeneralErrors.ConnectionError('Could not send DTMF: Signaling channel is disconnected'); 10693 this._log.debug('#error', error); 10694 this.emit('error', error); 10695 } 10696 } 10697 /** 10698 * Send a message to Twilio. Your backend application can listen for these 10699 * messages to allow communication between your frontend and backend applications. 10700 * <br/><br/>This feature is currently in Beta. 10701 * @param message - The message object to send. 10702 * @returns A voice event sid that uniquely identifies the message that was sent. 10703 */ 10704 sendMessage(message) { 10705 this._log.debug('.sendMessage', JSON.stringify(message)); 10706 const { content, contentType, messageType } = message; 10707 if (typeof content === 'undefined' || content === null) { 10708 throw new InvalidArgumentError('`content` is empty'); 10709 } 10710 if (typeof messageType !== 'string') { 10711 throw new InvalidArgumentError('`messageType` must be a string.'); 10712 } 10713 if (messageType.length === 0) { 10714 throw new InvalidArgumentError('`messageType` must be a non-empty string.'); 10715 } 10716 if (this._pstream === null) { 10717 throw new InvalidStateError('Could not send CallMessage; Signaling channel is disconnected'); 10718 } 10719 const callSid = this.parameters.CallSid; 10720 if (typeof this.parameters.CallSid === 'undefined') { 10721 throw new InvalidStateError('Could not send CallMessage; Call has no CallSid'); 10722 } 10723 const voiceEventSid = this._voiceEventSidGenerator(); 10724 this._messages.set(voiceEventSid, { content, contentType, messageType, voiceEventSid }); 10725 this._pstream.sendMessage(callSid, content, contentType, messageType, voiceEventSid); 10726 return voiceEventSid; 10727 } 10728 /** 10729 * Get the current {@link Call} status. 10730 */ 10731 status() { 10732 return this._status; 10733 } 10734 /** 10735 * Check the volume passed, emitting a warning if one way audio is detected or cleared. 10736 * @param currentVolume - The current volume for this direction 10737 * @param streakFieldName - The name of the field on the {@link Call} object that tracks how many times the 10738 * current value has been repeated consecutively. 10739 * @param lastValueFieldName - The name of the field on the {@link Call} object that tracks the most recent 10740 * volume for this direction 10741 * @param direction - The directionality of this audio track, either 'input' or 'output' 10742 * @returns The current streak; how many times in a row the same value has been polled. 10743 */ 10744 _checkVolume(currentVolume, currentStreak, lastValue, direction) { 10745 const wasWarningRaised = currentStreak >= 10; 10746 let newStreak = 0; 10747 if (lastValue === currentVolume) { 10748 newStreak = currentStreak; 10749 } 10750 if (newStreak >= 10) { 10751 this._emitWarning('audio-level-', `constant-audio-${direction}-level`, 10, newStreak, false); 10752 } 10753 else if (wasWarningRaised) { 10754 this._emitWarning('audio-level-', `constant-audio-${direction}-level`, 10, newStreak, true); 10755 } 10756 return newStreak; 10757 } 10758 /** 10759 * Clean up event listeners. 10760 */ 10761 _cleanupEventListeners() { 10762 const cleanup = () => { 10763 if (!this._pstream) { 10764 return; 10765 } 10766 this._pstream.removeListener('ack', this._onAck); 10767 this._pstream.removeListener('answer', this._onAnswer); 10768 this._pstream.removeListener('cancel', this._onCancel); 10769 this._pstream.removeListener('error', this._onSignalingError); 10770 this._pstream.removeListener('hangup', this._onHangup); 10771 this._pstream.removeListener('ringing', this._onRinging); 10772 this._pstream.removeListener('transportClose', this._onTransportClose); 10773 this._pstream.removeListener('connected', this._onConnected); 10774 this._pstream.removeListener('message', this._onMessageReceived); 10775 }; 10776 // This is kind of a hack, but it lets us avoid rewriting more code. 10777 // Basically, there's a sequencing problem with the way PeerConnection raise
10777s 10778 // the 10779 // 10780 // Cannot establish call. SDK is disconnected 10781 // 10782 // error in Call#accept. It calls PeerConnection#onerror, which emits 10783 // the error event on Call. An error handler on Call then calls 10784 // cleanupEventListeners, but then control returns to Call#accept. It's 10785 // at this point that we add a listener for the answer event that never gets 10786 // removed. setTimeout will allow us to rerun cleanup again, _after_ 10787 // Call#accept returns. 10788 cleanup(); 10789 setTimeout(cleanup, 0); 10790 } 10791 /** 10792 * Create the payload wrapper for a batch of metrics to be sent to Insights. 10793 */ 10794 _createMetricPayload() { 10795 const payload = { 10796 call_sid: this.parameters.CallSid, 10797 dscp: !!this._options.dscp, 10798 sdk_version: RELEASE_VERSION, 10799 }; 10800 if (this._options.gateway) { 10801 payload.gateway = this._options.gateway; 10802 } 10803 payload.direction = this._direction; 10804 return payload; 10805 } 10806 /** 10807 * Disconnect the {@link Call}. 10808 * @param message - A message explaining why the {@link Call} is being disconnected. 10809 * @param wasRemote - Whether the disconnect was triggered locally or remotely. 10810 */ 10811 _disconnect(message, wasRemote) { 10812 message = typeof message === 'string' ? message : null; 10813 if (this._status !== Call.State.Open 10814 && this._status !== Call.State.Connecting 10815 && this._status !== Call.State.Reconnecting 10816 && this._status !== Call.State.Ringing) { 10817 return; 10818 } 10819 this._log.info('Disconnecting...'); 10820 // send pstream hangup message 10821 if (this._pstream !== null && this._pstream.status !== 'disconnected' && this._shouldSendHangup) { 10822 const callsid = this.parameters.CallSid || this.outboundConnectionId; 10823 if (callsid) { 10824 this._pstream.hangup(callsid, message); 10825 } 10826 } 10827 this._cleanupEventListeners(); 10828 this._mediaHandler.close(); 10829 if (!wasRemote) { 10830 this._publisher.info('connection', 'disconnected-by-local', null, this); 10831 } 10832 } 10833 /** 10834 * Transition to {@link CallStatus.Open} if criteria is met. 10835 */ 10836 _maybeTransitionToOpen() { 10837 this._wasConnected; 10838 if (this._isAnswered) { 10839 this._onSignalingReconnected(); 10840 this._signalingStatus = Call.State.Open; 10841 if (this._mediaHandler && this._mediaHandler.status === 'open') { 10842 this._status = Call.State.Open; 10843 if (!this._wasConnected) { 10844 this._wasConnected = true; 10845 this._log.debug('#accept'); 10846 this.emit('accept', this); 10847 } 10848 } 10849 } 10850 } 10851 /** 10852 * Post an event to Endpoint Analytics indicating that the end user 10853 * has ignored a request for feedback. 10854 */ 10855 _postFeedbackDeclined() { 10856 return this._publisher.info('feedback', 'received-none', null, this, true); 10857 } 10858 /** 10859 * Publish the current set of queued metrics samples to Insights. 10860 */ 10861 _publishMetrics() { 10862 if (this._metricsSamples.length === 0) { 10863 return; 10864 } 10865 this._publisher.postMetrics('quality-metrics-samples', 'metrics-sample', this._metricsSamples.splice(0), this._createMetricPayload(), this).catch((e) => { 10866 this._log.warn('Unable to post metrics to Insights. Received error:', e); 10867 }); 10868 } 10869 /** 10870 * Set the CallSid 10871 * @param payload 10872 */ 10873 _setCallSid(payload) { 10874 const callSid = payload.callsid; 10875 if (!callSid) { 10876 return; 10877 } 10878 this.parameters.CallSid = callSid; 10879 this._mediaHandler.callSid = callSid; 10880 } 10881 } 10882 /** 10883 * String representation of the {@link Call} class. 10884 */ 10885 Call.toString = () => '[Twilio.Call class]'; 10886 /** 10887 * @mergeModuleWith Call 10888 */ 10889 (function (Call) { 10890 (function (State) { 10891 State["Closed"] = "closed"; 10892 State["Connecting"] = "connecting"; 10893 State["Open"] = "open"; 10894 State["Pending"] = "pending"; 10895 State["Reconnecting"] = "reconnecting"; 10896 State["Ringing"] = "ringing"; 10897 })(Call.State || (Call.State = {})); 10898 (function (FeedbackIssue) { 10899 FeedbackIssue["AudioLatency"] = "audio-latency"; 10900 FeedbackIssue["ChoppyAudio"] = "choppy-audio"; 10901 FeedbackIssue["DroppedCall"] = "dropped-call"; 10902 FeedbackIssue["Echo"] = "echo"; 10903 FeedbackIssue["NoisyCall"] = "noisy-call"; 10904 FeedbackIssue["OneWayAudio"] = "one-way-audio"; 10905 })(Call.FeedbackIssue || (Call.FeedbackIssue = {})); 10906 (function (FeedbackScore) { 10907 FeedbackScore[FeedbackScore["One"] = 1] = "One"; 10908 FeedbackScore[FeedbackScore["Two"] = 2] = "Two"; 10909 FeedbackScore[FeedbackScore["Three"] = 3] = "Three"; 10910 FeedbackScore[FeedbackScore["Four"] = 4] = "Four"; 10911 FeedbackScore[FeedbackScore["Five"] = 5] = "Five"; 10912 })(Call.FeedbackScore || (Call.FeedbackScore = {})); 10913 (function (CallDirection) { 10914 CallDirection["Incoming"] = "INCOMING"; 10915 CallDirection["Outgoing"] = "OUTGOING"; 10916 })(Call.CallDirection || (Call.CallDirection = {})); 10917 (function (Codec) { 10918 Codec["Opus"] = "opus"; 10919 Codec["PCMU"] = "pcmu"; 10920 })(Call.Codec || (Call.Codec = {})); 10921 (function (IceGatheringFailureReason) { 10922 IceGatheringFailureReason["None"] = "none"; 10923 IceGatheringFailureReason["Timeout"] = "timeout"; 10924 })(Call.IceGatheringFailureReason || (Call.IceGatheringFailureReason = {})); 10925 (function (MediaFailure) { 10926 MediaFailure["ConnectionDisconnected"] = "ConnectionDisconnected"; 10927 MediaFailure["ConnectionFailed"] = "ConnectionFailed"; 10928 MediaFailure["IceGatheringFailed"] = "IceGatheringFailed"; 10929 MediaFailure["LowBytes"] = "LowBytes"; 10930 })(Call.MediaFailure || (Call.MediaFailure = {})); 10931 })(Call || (Call = {})); 10932 function generateTempCallSid() { 10933 return 'TJSxxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 10934 /* tslint:disable:no-bitwise */ 10935 const r = Math.random() * 16 | 0; 10936 const v = c === 'x' ? r : (r & 0x3 | 0x8); 10937 /* tslint:enable:no-bitwise */ 10938 return v.toString(16); 10939 }); 10940 } 10941 10942 // goxee-voice.js 10943 10944 /** 10945 * A wrapper that exposes simple global functions: 10946 * - GoxeeVoice.createDevice(token) 10947 * - GoxeeVoice.getDevice() 10948 * 10949 * This makes the new Twilio Voice SDK v2 act similar to Twilio.Device from v1. 10950 */ 10951 window.GoxeeVoice = (function () { 10952 let device = null; 10953 10954 function createDevice(token, options = {}) { 10955 if (!device) { 10956 device = new Device(token, options); 10957 device.register(); // Registers for incoming calls 10958 window.twilioDevice = device; // for debugging (optional) 10959 } else { 10960 device.updateToken(token); 10961 } 10962 return device; 10963 } 10964 10965 function getDevice() { 10966 return device; 10967 } 10968 10969 return { 10970 createDevice, 10971 getDevice, 10972 }; 10973 })(); 10974 10975})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.