UNPKG

@twilio/voice-react-native-sdk

Version:
678 lines (554 loc) 21.6 kB
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /** * Copyright © 2025 Twilio, Inc. All rights reserved. Licensed under the Twilio * license. * * See LICENSE in the project root for license information. */ import { EventEmitter } from 'eventemitter3'; import * as common from './common'; import { Constants } from './constants'; import { InvalidStateError } from './error'; import { constructTwilioError } from './error/utility'; /** * The PreflightTest for Voice React Native SDK allows you to anticipate and * troubleshoot end users' connectivity and bandwidth issues before or during * Twilio Voice calls. * * You can run a PreflightTest before a Twilio Voice call. The PreflightTest * performs a test call to Twilio and provides a * {@link (PreflightTest:namespace).Report} object at the end. The report * includes information about the end user's network connection (including * jitter, packet loss, and round trip time) and connection settings. * * @example * ```typescript * const accessToken = ...; * const preflightTest = voice.runPreflightTest(accessToken); * * preflightTest.on(PreflightTest.Event.Connected, () => { * // handle when preflightTest connects * }); * * preflightTest.on(PreflightTest.Event.Completed, (report: PreflightTest.Report) => { * // handle when preflightTest is complete * }); * * preflightTest.on(PreflightTest.Event.Failed, (error: TwilioError) => { * // handle preflightTest errors * }); * * preflightTest.on( * PreflightTest.Event.QualityWarning, * (currentWarnings: Call.QualityWarning[], previousWarnings: Call.QualityWarning[]) => { * // handle preflightTest quality warnings * }, * ); * * preflightTest.on(PreflightTest.Event.Sample, (sample: PreflightTest.Sample) => { * // handle preflightTest sample * }); * ``` */ export class PreflightTest extends EventEmitter { /** * UUID of the PreflightTest. This is generated by the native layer and used * to link events emitted by the native layer to the respective JS object. */ /** * PreflightTest constructor. * * @internal */ constructor(_uuid) { super(); _defineProperty(this, "_uuid", void 0); _defineProperty(this, "_handleNativeEvent", nativePreflightTestEvent => { const uuid = nativePreflightTestEvent[Constants.PreflightTestEventKeyUuid]; if (typeof uuid !== 'string') { throw new InvalidStateError(`Unexpected PreflightTest UUID type: "${uuid}".`); } if (uuid !== this._uuid) { return; } // VBLOCKS-5083 // Update this member access when we upgrade typescript for this project. switch (nativePreflightTestEvent.preflightTestEventKeyType) { case Constants.PreflightTestEventTypeValueCompleted: { return this._handleCompletedEvent(nativePreflightTestEvent); } case Constants.PreflightTestEventTypeValueConnected: { return this._handleConnectedEvent(); } case Constants.PreflightTestEventTypeValueFailed: { return this._handleFailedEvent(nativePreflightTestEvent); } case Constants.PreflightTestEventTypeValueQualityWarning: { return this._handleQualityWarningEvent(nativePreflightTestEvent); } case Constants.PreflightTestEventTypeValueSample: { return this._handleSampleEvent(nativePreflightTestEvent); } default: { const _exhaustiveCheck = nativePreflightTestEvent; throw new InvalidStateError(`Unexpected native PreflightTest event key type: "${_exhaustiveCheck[Constants.PreflightTestEventKeyType]}".`); } } }); _defineProperty(this, "_handleCompletedEvent", nativeEvent => { const report = nativeEvent[Constants.PreflightTestCompletedEventKeyReport]; if (typeof report !== 'string') { throw constructInvalidValueError(PreflightTest.Event.Completed, 'report', 'string', typeof report); } const parsedReport = parseReport(report); this.emit(PreflightTest.Event.Completed, parsedReport); }); _defineProperty(this, "_handleConnectedEvent", () => { this.emit(PreflightTest.Event.Connected); }); _defineProperty(this, "_handleFailedEvent", nativeEvent => { const { message, code } = nativeEvent[Constants.PreflightTestFailedEventKeyError]; if (typeof message !== 'string') { throw constructInvalidValueError(PreflightTest.Event.Failed, 'message', 'string', typeof message); } if (typeof code !== 'number') { throw constructInvalidValueError(PreflightTest.Event.Failed, 'code', 'number', typeof code); } const error = constructTwilioError(message, code); this.emit(PreflightTest.Event.Failed, error); }); _defineProperty(this, "_handleQualityWarningEvent", nativeEvent => { const currentWarnings = nativeEvent[Constants.PreflightTestQualityWarningEventKeyCurrentWarnings]; if (!Array.isArray(currentWarnings)) { throw constructInvalidValueError(PreflightTest.Event.QualityWarning, 'currentWarnings', 'array', typeof currentWarnings); } currentWarnings.forEach(w => { if (typeof w !== 'string') { throw constructInvalidValueError(PreflightTest.Event.QualityWarning, 'element-in-currentWarnings', 'string', typeof w); } }); const previousWarnings = nativeEvent[Constants.PreflightTestQualityWarningEventKeyPreviousWarnings]; if (!Array.isArray(previousWarnings)) { throw constructInvalidValueError(PreflightTest.Event.QualityWarning, 'previousWarnings', 'array', typeof previousWarnings); } previousWarnings.forEach(w => { if (typeof w !== 'string') { throw constructInvalidValueError(PreflightTest.Event.QualityWarning, 'element-in-previousWarnings', 'string', typeof w); } }); this.emit(PreflightTest.Event.QualityWarning, currentWarnings, previousWarnings); }); _defineProperty(this, "_handleSampleEvent", nativeEvent => { const sampleStr = nativeEvent[Constants.PreflightTestSampleEventKeySample]; if (typeof sampleStr !== 'string') { throw constructInvalidValueError(PreflightTest.Event.Sample, 'sample', 'string', typeof sampleStr); } const sampleObj = JSON.parse(sampleStr); this.emit(PreflightTest.Event.Sample, parseSample(sampleObj)); }); this._uuid = _uuid; common.NativeEventEmitter.addListener(Constants.ScopePreflightTest, this._handleNativeEvent); // by using a setTimeout here, we let the call stack empty before we flush // the preflight test events. this way, listeners on this object can bind // before flushing if (common.Platform.OS === 'ios') { common.setTimeout(() => { common.NativeModule.preflightTest_flushEvents(); }); } } /** * Handle all PreflightTest native events. */ /** * Internal helper method to invoke a native method and handle the returned * promise from the native method. */ async _invokeAndCatchNativeMethod(method) { return method(this._uuid).catch(error => { if (typeof error.code === 'number' && error.message) throw constructTwilioError(error.message, error.code); if (error.code === Constants.ErrorCodeInvalidStateError) throw new InvalidStateError(error.message); throw error; }); } /** * Get the CallSid of the underlying Call in the PreflightTest. * * @returns * Promise that * - Resolves with a string representing the CallSid. * - Rejects if the native layer could not find the CallSid for this * PreflightTest object. */ async getCallSid() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getCallSid); } /** * Get the end time of the PreflightTest. * * @returns * A Promise that * - Resolves with `number` if the PreflightTest has ended. * - Resolves with `undefined` if PreflightTest has not ended. * - Rejects if the native layer encountered an error. */ async getEndTime() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getEndTime).then(Number); } /** * Get the latest stats sample generated by the PreflightTest. * * @returns * A Promise that * - Resolves with the last {@link (PreflightTest:namespace).RTCSample} * generated by the PreflightTest. * - Resolves with `undefined` if there is no previously generated sample. * - Rejects if the native layer encountered an error. */ async getLatestSample() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getLatestSample).then(sampleStr => { const sampleObj = JSON.parse(sampleStr); return parseSample(sampleObj); }); } /** * Get the final report generated by the PreflightTest. * * @returns * A Promise that * - Resolves with the final {@link (PreflightTest:namespace).Report}. * - Resolves with `undefined` if the report is unavailable. * - Rejects if the native layer encountered an error. */ async getReport() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getReport).then(parseReport); } /** * Get the start time of the PreflightTest. * * @returns * A Promise that * - Resolves with a `number` representing the start time of the * PreflightTest. * - Rejects if the native layer encountered an error. */ async getStartTime() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getStartTime).then(Number); } /** * Get the state of the PreflightTest. * * @returns * A Promise that * - Resolves with the current state of the PreflightTest. * - Rejects if the native layer encountered an error. */ async getState() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_getState).then(parseState); } /** * Stop the ongoing PreflightTest. * * @returns * A Promise that * - Resolves if the PreflightTest was successfully stopped. * - Rejects if the native layer encountered an error. */ async stop() { return this._invokeAndCatchNativeMethod(common.NativeModule.preflightTest_stop); } } /** * Preflight helper functions to parse JSON strings from the native layer into * proper JS objects to emit from this class. */ /** * Parse native time measurement. */ function parseTimeMeasurement(nativeTimeMeasurement) { return { duration: nativeTimeMeasurement.duration, end: nativeTimeMeasurement.endTime, start: nativeTimeMeasurement.startTime }; } /** * Parse native call quality enum. */ function parseCallQuality(nativeCallQuality) { switch (common.Platform.OS) { case 'android': { return parseCallQualityAndroid(nativeCallQuality); } case 'ios': { return parseCallQualityIos(nativeCallQuality); } default: { throw new InvalidStateError('Invalid platform.'); } } } /** * Parse call quality value for Android platform. */ function parseCallQualityAndroid(nativeCallQuality) { if (typeof nativeCallQuality === 'undefined' || nativeCallQuality === null) { return null; } if (typeof nativeCallQuality !== 'string') { throw new InvalidStateError(`Call quality not of type "string". Found "${typeof nativeCallQuality}".`); } const parsedCallQuality = callQualityMap.android.get(nativeCallQuality); if (typeof parsedCallQuality !== 'string') { throw new InvalidStateError(`Call quality invalid. Expected a string, found "${nativeCallQuality}".`); } return parsedCallQuality; } /** * Parse call quality for iOS platform. */ function parseCallQualityIos(nativeCallQuality) { if (typeof nativeCallQuality === 'undefined' || nativeCallQuality === null) { return null; } if (typeof nativeCallQuality !== 'number') { throw new InvalidStateError(`Call quality not of type "number". Found "${typeof nativeCallQuality}".`); } const parsedCallQuality = callQualityMap.ios.get(nativeCallQuality); if (typeof parsedCallQuality !== 'string') { throw new InvalidStateError(`Call quality invalid. Expected [0, 4], found "${nativeCallQuality}".`); } return parsedCallQuality; } /** * Parse native preflight test state value. */ function parseState(nativeState) { const parsedState = preflightTestStateMap.get(nativeState); if (typeof parsedState !== 'string') { const expectedKeys = Array(preflightTestStateMap.keys()).join(', '); throw new InvalidStateError('PreflightTest state invalid. ' + `Expected one of "[${expectedKeys}]". Got "${nativeState}".`); } return parsedState; } /** * Parse a sample object and transform the keys to match the expected output. */ function parseSample(sampleObject) { const audioInputLevel = sampleObject.audioInputLevel; const audioOutputLevel = sampleObject.audioOutputLevel; const bytesReceived = sampleObject.bytesReceived; const bytesSent = sampleObject.bytesSent; const codec = sampleObject.codec; const jitter = sampleObject.jitter; const mos = sampleObject.mos; const packetsLost = sampleObject.packetsLost; const packetsLostFraction = sampleObject.packetsLostFraction; const packetsReceived = sampleObject.packetsReceived; const packetsSent = sampleObject.packetsSent; const rtt = sampleObject.rtt; const timestamp = Number(sampleObject.timestamp); const sample = { audioInputLevel, audioOutputLevel, bytesReceived, bytesSent, codec, jitter, mos, packetsLost, packetsLostFraction, packetsReceived, packetsSent, rtt, timestamp }; return sample; } /** * Parse native "isTurnRequired" value. */ function parseIsTurnRequired(isTurnRequired) { switch (common.Platform.OS) { case 'android': { return parseIsTurnRequiredAndroid(isTurnRequired); } case 'ios': { return parseIsTurnRequiredIos(isTurnRequired); } default: { throw new InvalidStateError('Invalid platform.'); } } } /** * Parse native "isTurnRequired" value on Android. */ function parseIsTurnRequiredAndroid(isTurnRequired) { if (typeof isTurnRequired === 'undefined' || isTurnRequired === null) { return null; } if (typeof isTurnRequired !== 'boolean') { throw new InvalidStateError(`PreflightTest "isTurnRequired" not valid. Found "${isTurnRequired}".`); } return isTurnRequired; } /** * Parse native "isTurnRequired" value on iOS. */ function parseIsTurnRequiredIos(isTurnRequired) { if (typeof isTurnRequired === 'undefined' || isTurnRequired === null) { return null; } if (typeof isTurnRequired !== 'string') { throw new InvalidStateError('PreflightTest "isTurnRequired" not of type "string". ' + `Found "${isTurnRequired}".`); } const parsedValue = isTurnRequiredMap.ios.get(isTurnRequired); if (typeof parsedValue !== 'boolean') { throw new InvalidStateError(`PreflightTest "isTurnRequired" not valid. Found "${isTurnRequired}".`); } return parsedValue; } /** * Parse native warnings array. */ function parseWarnings(warnings) { if (typeof warnings === 'undefined' || warnings === null) { return []; } if (!Array.isArray(warnings)) { throw new InvalidStateError(`PreflightTest "warnings" invalid. Found "${warnings}".`); } return warnings; } /** * Parse native warningsCleared array. */ function parseWarningsCleared(warningsCleared) { if (typeof warningsCleared === 'undefined' || warningsCleared === null) { return []; } if (!Array.isArray(warningsCleared)) { throw new InvalidStateError(`PreflightTest "warningsCleared" invalid. Found "${warningsCleared}".`); } return warningsCleared; } /** * Parse native preflight report. */ function parseReport(rawReport) { const unprocessedReport = JSON.parse(rawReport); const callSid = unprocessedReport.callSid; // Note: Android returns enum values where the first letter is capitalized. // The helper function normalizes this into all-lowercased values. const callQuality = parseCallQuality(unprocessedReport.callQuality); const edge = unprocessedReport.edge; // Note: key change from `iceCandidates` to `iceCandidateStats` const iceCandidateStats = unprocessedReport.iceCandidates; // Note: iOS returns a string, Android returns a boolean const isTurnRequired = parseIsTurnRequired(unprocessedReport.isTurnRequired); // Note: key change from `networkStats` to `stats`. const stats = unprocessedReport.networkStats; // Note: removing preflightTest from networkTiming and putting it in a // separate testTiming member const unprocessedNetworkTiming = unprocessedReport.networkTiming; // Note: nested key change from `startTime` to `start` and `endTime` to `end`. const networkTiming = { signaling: parseTimeMeasurement(unprocessedNetworkTiming.signaling), peerConnection: parseTimeMeasurement(unprocessedNetworkTiming.peerConnection), ice: parseTimeMeasurement(unprocessedNetworkTiming.iceConnection) }; // Note: nested key change from `startTime` to `start` and `endTime` to `end`. const testTiming = parseTimeMeasurement(unprocessedNetworkTiming.preflightTest); // Note: key change from `statsSamples` to `stats`. const samples = unprocessedReport.statsSamples.map(parseSample); const selectedEdge = unprocessedReport.selectedEdge; // Note: key change from `selectedIceCandidatePair` to `selectedIceCandidatePairStats`. const selectedIceCandidatePairStats = unprocessedReport.selectedIceCandidatePair; // Note: iOS returns undefined where Android returns an empty array // when there were no warnings const warnings = parseWarnings(unprocessedReport.warnings); // Note: iOS returns undefined where Android returns an empty array // when there were no warningsCleared const warningsCleared = parseWarningsCleared(unprocessedReport.warningsCleared); const report = { callSid, callQuality, edge, iceCandidateStats, isTurnRequired, stats, networkTiming, testTiming, samples, selectedEdge, selectedIceCandidatePairStats, warnings, warningsCleared }; return report; } /** * Helper function to construct errors when the native layer sends an * unexpected value to the JS layer. */ function constructInvalidValueError(eventName, valueName, expectedType, actualType) { return new InvalidStateError(`Invalid "preflightTest#${eventName}" value type for "${valueName}". ` + `Expected "${expectedType}"; actual "${actualType}".`); } /** * Helper types for the PrefligthTest class. */ (function (_PreflightTest) { /** * Options to run a PreflightTest. */ let Event; (function (Event) { Event["Connected"] = "connected"; Event["Completed"] = "completed"; Event["Failed"] = "failed"; Event["Sample"] = "sample"; Event["QualityWarning"] = "qualityWarning"; })(Event || (Event = {})); _PreflightTest.Event = Event; let Listener; (function (_Listener) {})(Listener || (Listener = _PreflightTest.Listener || (_PreflightTest.Listener = {}))); let State; (function (State) { State["Connected"] = "connected"; State["Completed"] = "completed"; State["Connecting"] = "connecting"; State["Failed"] = "failed"; })(State || (State = {})); _PreflightTest.State = State; let CallQuality; (function (CallQuality) { CallQuality[CallQuality["Excellent"] = Constants.PreflightCallQualityExcellent] = "Excellent"; CallQuality[CallQuality["Great"] = Constants.PreflightCallQualityGreat] = "Great"; CallQuality[CallQuality["Good"] = Constants.PreflightCallQualityGood] = "Good"; CallQuality[CallQuality["Fair"] = Constants.PreflightCallQualityFair] = "Fair"; CallQuality[CallQuality["Degraded"] = Constants.PreflightCallQualityDegraded] = "Degraded"; })(CallQuality || (CallQuality = {})); _PreflightTest.CallQuality = CallQuality; })(PreflightTest || (PreflightTest = {})); /** * Map of call quality values from the native layer to the expected JS values. */ const callQualityMap = { ios: new Map([[0, PreflightTest.CallQuality.Excellent], [1, PreflightTest.CallQuality.Great], [2, PreflightTest.CallQuality.Good], [3, PreflightTest.CallQuality.Fair], [4, PreflightTest.CallQuality.Degraded]]), android: new Map([['Excellent', PreflightTest.CallQuality.Excellent], ['Great', PreflightTest.CallQuality.Great], ['Good', PreflightTest.CallQuality.Good], ['Fair', PreflightTest.CallQuality.Fair], ['Degraded', PreflightTest.CallQuality.Degraded]]) }; /** * Map of isTurnRequired values from the native layer to the expected JS values. */ const isTurnRequiredMap = { ios: new Map([['true', true], ['false', false]]) }; /** * Map of state values from the native layers/common constants to the expected * JS values. */ const preflightTestStateMap = new Map([[Constants.PreflightTestStateCompleted, PreflightTest.State.Completed], [Constants.PreflightTestStateConnected, PreflightTest.State.Connected], [Constants.PreflightTestStateConnecting, PreflightTest.State.Connecting], [Constants.PreflightTestStateFailed, PreflightTest.State.Failed]]); //# sourceMappingURL=PreflightTest.js.map