UNPKG

@twilio/rtc-diagnostics

Version:

Various diagnostics functions to help analyze connections to Twilio

431 lines 20.1 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); var events_1 = require("events"); var constants_1 = require("./constants"); var errors_1 = require("./errors"); var polyfills_1 = require("./polyfills"); var optionValidation_1 = require("./utils/optionValidation"); /** * [[AudioOutputTest]] class that parses options and starts an audio output device * test. * * Please see [[testAudioOutputDevice]] for details and recommended practices. */ var AudioOutputTest = /** @class */ (function (_super) { __extends(AudioOutputTest, _super); /** * Sets up several things for the [[AudioOutputTest]] to run later in the * `_startTest` function. * @param options Optional settings to pass to the test. */ function AudioOutputTest(options) { var _this = _super.call(this) || this; /** * Holds `AudioElement`s that are attached to the DOM to load and play audio. */ _this._audio = []; /** * An `AudioContext` that is used to process the audio source. */ _this._audioContext = null; /** * The default media devices when starting the test. */ _this._defaultDevices = {}; /** * A timestamp of when the test ends. */ _this._endTime = null; /** * An array of errors encountered by the test during its run time. */ _this._errors = []; /** * Volume values generated by the test over its run time. */ _this._values = []; /** * Timeout created by `setTimeout`, used to loop the volume logic. */ _this._volumeTimeout = null; _this._options = __assign(__assign({}, AudioOutputTest.defaultOptions), options); _this._startTime = Date.now(); // We need to use a `setTimeout` here to prevent a race condition. // This allows event listeners to bind before the test starts. setTimeout(function () { return _this._startTest(); }); return _this; } /** * Stops the test. */ AudioOutputTest.prototype.stop = function () { if (this._endTime) { this._onWarning(new errors_1.AlreadyStoppedError()); return; } // Clean up the test. this._cleanup(); this._endTime = Date.now(); var report = { deviceId: this._options.deviceId || (this._defaultDevices.audiooutput && this._defaultDevices.audiooutput.deviceId), errors: this._errors, testName: AudioOutputTest.testName, testTiming: { duration: this._endTime - this._startTime, end: this._endTime, start: this._startTime, }, testURI: this._options.testURI, values: this._values, }; this.emit(AudioOutputTest.Events.End, report); }; /** * Cleanup the test. */ AudioOutputTest.prototype._cleanup = function () { if (this._volumeTimeout) { clearTimeout(this._volumeTimeout); } if (this._audioContext) { this._audioContext.close(); } this._audio.forEach(function (audio) { audio.pause(); }); }; /** * Error event handler. Adds the error to the internal list of errors that is * forwarded in the report. * @param error */ AudioOutputTest.prototype._onError = function (error) { this._errors.push(error); this.emit(AudioOutputTest.Events.Error, error); }; /** * Volume event handler, adds the value to the list `_values` and emits it * under the event `volume`. * @param volume */ AudioOutputTest.prototype._onVolume = function (volume) { this._values.push(volume); this.emit(AudioOutputTest.Events.Volume, volume); }; /** * Warning event handler. * @param warning */ AudioOutputTest.prototype._onWarning = function (error) { if (this._options.debug) { // tslint:disable-next-line no-console console.warn(error); } }; /** * Entry point of the test, called after setup in the constructor. * Emits the volume levels of the audio. * * @event [[AudioOutputTest.Events.Volume]] */ AudioOutputTest.prototype._startTest = function () { var _a, _b; return __awaiter(this, void 0, void 0, function () { var invalidReasons, setSinkIdSupported, devices, numberOutputDevices, sourceAudio_1, sourceNode, analyser_1, frequencyDataBytes_1, volumeEvent_1, destinationNode, destinationAudio, error_1; var _this = this; return __generator(this, function (_c) { switch (_c.label) { case 0: _c.trys.push([0, 9, , 10]); return [4 /*yield*/, optionValidation_1.validateOptions(this._options, { deviceId: optionValidation_1.validateDeviceId, duration: optionValidation_1.validateTime, volumeEventIntervalMs: optionValidation_1.validateTime, })]; case 1: invalidReasons = _c.sent(); if (invalidReasons) { throw new errors_1.InvalidOptionsError(invalidReasons); } if (!this._options.audioElementFactory) { throw polyfills_1.AudioUnsupportedError; } if (!this._options.audioContextFactory) { throw polyfills_1.AudioContextUnsupportedError; } setSinkIdSupported = typeof this._options.audioElementFactory.prototype.setSinkId === 'function'; if (!setSinkIdSupported) return [3 /*break*/, 3]; if (!this._options.enumerateDevices) { throw polyfills_1.EnumerateDevicesUnsupportedError; } return [4 /*yield*/, this._options.enumerateDevices()]; case 2: devices = _c.sent(); numberOutputDevices = devices.filter(function (device) { return device.kind === 'audiooutput'; }).length; if (numberOutputDevices === 0) { throw new errors_1.DiagnosticError(undefined, 'No output devices found.'); } this._defaultDevices = polyfills_1.getDefaultDevices(devices); _c.label = 3; case 3: this._audioContext = new this._options.audioContextFactory(); sourceAudio_1 = new this._options.audioElementFactory(this._options.testURI); sourceAudio_1.setAttribute('crossorigin', 'anonymous'); sourceAudio_1.loop = !!this._options.doLoop; sourceNode = this._audioContext.createMediaElementSource(sourceAudio_1); analyser_1 = this._audioContext.createAnalyser(); analyser_1.smoothingTimeConstant = 0.4; analyser_1.fftSize = 64; sourceNode.connect(analyser_1); frequencyDataBytes_1 = new Uint8Array(analyser_1.frequencyBinCount); volumeEvent_1 = function () { if (_this._endTime) { return; } analyser_1.getByteFrequencyData(frequencyDataBytes_1); var volume = frequencyDataBytes_1.reduce(function (sum, val) { return sum + val; }, 0) / frequencyDataBytes_1.length; _this._onVolume(volume); // Check stop conditions var isTimedOut = Date.now() - _this._startTime > _this._options.duration; var stop = _this._options.doLoop ? isTimedOut : sourceAudio_1.ended || isTimedOut; if (stop) { _this.stop(); } else { _this._volumeTimeout = setTimeout(volumeEvent_1, _this._options.volumeEventIntervalMs); } }; if (!(this._options.deviceId && setSinkIdSupported)) return [3 /*break*/, 6]; destinationNode = this._audioContext.createMediaStreamDestination(); analyser_1.connect(destinationNode); destinationAudio = new this._options.audioElementFactory(); destinationAudio.loop = !!this._options.doLoop; destinationAudio.srcObject = destinationNode.stream; return [4 /*yield*/, ((_b = (_a = destinationAudio).setSinkId) === null || _b === void 0 ? void 0 : _b.call(_a, this._options.deviceId))]; case 4: _c.sent(); return [4 /*yield*/, destinationAudio.play()]; case 5: _c.sent(); this._audio.push(destinationAudio); return [3 /*break*/, 7]; case 6: if (this._options.deviceId && !setSinkIdSupported) { throw new errors_1.UnsupportedError('A `deviceId` was passed to the `AudioOutputTest` but `setSinkId` is ' + 'not supported in this browser.'); } analyser_1.connect(this._audioContext.destination); _c.label = 7; case 7: return [4 /*yield*/, sourceAudio_1.play()]; case 8: _c.sent(); this._audio.push(sourceAudio_1); this._volumeTimeout = setTimeout(volumeEvent_1, this._options.volumeEventIntervalMs); return [3 /*break*/, 10]; case 9: error_1 = _c.sent(); if (error_1 instanceof errors_1.DiagnosticError) { this._onError(error_1); } else if (typeof DOMException !== 'undefined' && error_1 instanceof DOMException) { this._onError(new errors_1.DiagnosticError(error_1, 'A DOMException has occurred.')); } else if (typeof DOMError !== 'undefined' && error_1 instanceof DOMError) { this._onError(new errors_1.DiagnosticError(error_1, 'A DOMError has occurred.')); } else if (typeof Error !== 'undefined' && error_1 instanceof Error) { this._onError(new errors_1.DiagnosticError(error_1, 'An error has occurred.')); } else { this._onError(new errors_1.DiagnosticError(undefined, 'Unknown error occurred.')); this._onWarning(error_1); } this.stop(); return [3 /*break*/, 10]; case 10: return [2 /*return*/]; } }); }); }; /** * The name of the test. */ AudioOutputTest.testName = 'audio-output-test'; /** * Default options for the [[AudioOutputTest]]. Overwritten by any option passed * during the construction of the test. */ AudioOutputTest.defaultOptions = { audioContextFactory: polyfills_1.AudioContext, audioElementFactory: polyfills_1.Audio, debug: false, doLoop: true, duration: Infinity, enumerateDevices: polyfills_1.enumerateDevices, testURI: constants_1.INCOMING_SOUND_URL, volumeEventIntervalMs: 100, }; return AudioOutputTest; }(events_1.EventEmitter)); exports.AudioOutputTest = AudioOutputTest; (function (AudioOutputTest) { /** * Events that the [[AudioOutputTest]] will emit as it runs. * Please see [[AudioOutputTest.on]] for how to listen to these * events. */ var Events; (function (Events) { Events["End"] = "end"; Events["Error"] = "error"; Events["Volume"] = "volume"; })(Events = AudioOutputTest.Events || (AudioOutputTest.Events = {})); })(AudioOutputTest = exports.AudioOutputTest || (exports.AudioOutputTest = {})); exports.AudioOutputTest = AudioOutputTest; /** * [[AudioOutputTest]] tests audio output capabilities. It serves to help diagnose * potential audio device issues that would prevent a user from being able to * hear audio. * * --- * * The [[AudioOutputTest]] class is an `EventEmitter` (please see [[AudioOutputTest.on]] for * events and their details) and helps to diagnose issues by playing a sound clip * (by default the sound clip is the ringing tone from the `twilio-client.js` * SDK) and emitting volume events of the sound clip as it plays. * ```ts * import { AudioOutputTest, testAudioOutputDevice } from '@twilio/rtc-diagnostics'; * const options: AudioOutputTest.Options = { ... }; * // `options` may be left `undefined` to use default option values * const audioOutputTest: AudioOutputTest = testAudioOutputDevice(options); * ``` * The application can use the volume events to show in its UI that audio is * playing and that the end-user should be hearing something. * ```ts * audioOutputTest.on(AudioOutputTest.Events.Volume, (volume: number) => { * ui.updateVolume(volume); // Update your UI with the volume value here. * }); * ``` * * The application should ask the end-user to confirm that the sound being played * can be heard. The application should call [[AudioOutputTest.stop]] with `true` if * the end-user hears the sound, and `false` if not. * ```ts * // If the user was able to hear the audio, the UI should indicate they should * // click this button... * const passButton = ...; * passButton.on('click', () => { * audioOutputTest.stop(); * // display a confirmation dialog to the user * }); * * // ...conversely, if they were not able to hear the audio, they should click * // this one. * const failButton = ...; * failButton.on('click', () => { * audioOutputTest.stop(); * // display a warning to the user * }); * ``` * Caling [[AudioOutputTest.stop]] will immediately end the test. * * --- * * The [[AudioOutputTest]] object will always emit a [[AudioOutputTest.Report]] with * the [[AudioOutputTest.Events.End]] event, regardless of the occurence of errors * during the runtime of the test. * * Fatal errors will immediately end the test and emit a report such that the * value of [[AudioOutputTest.Report.errors]] will contain the fatal error. * * Non-fatal errors will not end the test, but will be included in the value of * [[AudioOutputTest.Report.errors]] upon completion of the test. * * If the data at `testURI` is unable to be loaded, meaning the error event is * raised on the audio element, a fatal error has occurred. * * If `doLoop` is set to `false`, then the test will run for either the option * `duration`, or the full duration of the audio file, which ever is shorter. * If `doLoop` is set to `true`, it will only run as long as the `duration` * option. * * --- * * The function [[testAudioOutputDevice]] serves as factory function that accepts * [[AudioOutputTest.Options]] as its only parameter and will instantiate an * [[AudioOutputTest]] object with those options. * ```ts * import { AudioOutputTest, testAudioOutputDevice } from '@twilio/rtc-diagnostics'; * const options: AudioOutputTest.Options = { ... }; * const audioOutputTest: AudioOutputTest = testAudioOutputDevice(options); * ``` * @param options Options to pass to the [[AudioOutputTest]] constructor. */ function testAudioOutputDevice(options) { return new AudioOutputTest(options); } exports.testAudioOutputDevice = testAudioOutputDevice; //# sourceMappingURL=AudioOutputTest.js.map