@twilio/voice-react-native-sdk
Version:
Twilio Voice React Native SDK
678 lines (554 loc) • 21.6 kB
JavaScript
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