The PreflightTest for Voice Mobile SDKs 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 mobile Twilio Voice call. The PreflightTest performs a test call to Twilio and provides a [JSON-serialized report 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.
Below is an example of creating a PreflightTest in the iOS and Android SDKs and listening for the events that the test emits. Note that the test requires an Access Token; learn more about how to generate the Access Token below.
You do not instantiate a TVOPreflightTest (iOS)
or PreflightTest (Android)
instance directly. Instead, the SDK returns a PreflightTest object when you call runPreflightTestWithAccessToken
(iOS) or runPreflight
(Android).
12class ViewController: UIViewController {3var preflightTest: PreflightTest? = nil45func performPreflight() {6let preflightOptions = PreflightOptions(accessToken: accessToken, block: { builder in7builder.preferredAudioCodecs = [OpusCodec()]8})910preflightTest = TwilioVoiceSDK.runPreflightTest(options: preflightOptions, delegate: self)11}12}1314extension ViewController: PreflightDelegate {15func preflightDidComplete(preflightTest: PreflightTest, report: PreflightReport) {16// Check the result in the report17}1819func preflightDidFail(preflightTest: PreflightTest, error: any Error) {20// Check the failure reason in the error21}2223func preflightDidConnect(preflightTest: PreflightTest) {24// preflight test has connected25}26}
You need an Access Token and a Voice application (either a TwiML app or your own backend server that generates TwiML) to initiate the test call. The endpoint you've configured in your Voice application for the test call should be able to record audio from a microphone and play it back to the browser.
See Create a TwiML App and Access Token for the PreflightTest for an example application that can handle the PreflightTest test call and more information.
The PreflightTest emits the following events that your frontend application can listen for:
Raised when the PreflightTest has successfully completed and the TestFlight object's status has transitioned to completed
. It provides a JSON-serialized report object. This event does not occur if the test encountered a fatal error.
The following code shows how to listen for this event. The event passes the completed JSON-serialized report to the event listener.
preflightDidComplete(preflightTest:report:)
See the Report section below to view an example of this completed report.
Raised when the PreflightTest has transitioned to the connected
state. This indicates that the connection to Twilio has been established.
The following code shows how to listen for this event.
preflightDidConnect(preflightTest:)
Raised when the Preflight test has failed. This happens the test cannot establish a connection to Twilio, a test call encounters a fatal error, or stop()
is called while the test is in progress. It passes an error object to the event handler.
The following code shows how to listen for this event. The event passes the error to the event listener.
preflightDidFail(preflightTest:error:)
Raised when the test call gets a WebRTC sample object. The test publishes this event every second.
The following code shows how to listen for this event. The event passes the test stats to the event listener.
preflightDidReceiveStatsSample(preflightTest:statsSample:)
Raised whenever the test call encounters a warning, or when a previously raised warning clear. The warnings relate to call or network quality issues.
The following code shows how to listen for this event. The event passes the current set of quality warnings to the event listener, along with a set of previously raised warnings. You can use these two sets to see if there is a new warning or previous warnings have cleared.
preflightDidReceiveQualityWarnings(preflightTest:currentWarnings:previousWarnings:)
Calling this will result in preflight test to be in TVOPreflightTestStatusFailed
state and will raise a failed
event with an error code 31008
indicating that the call has been cancelled.
The call SID for the preflight test call.
Preflight test start time. Unix timestamp in milliseconds.
Preflight test end time. Unix timestamp in milliseconds.
The preflight test report. If accessed when the preflight status is anything other than TVOPreflightTestStatusCompleted
or COMPLETED
, an empty report object will be returned.
Returns the latest stats sample for the preflight test.
The current state of the preflight test. Below are the possible values for this property.
Value | Description |
---|---|
TVOPreflightTestStatusCompleted | The connection to Twilio has been disconnected and the test call has completed. |
TVOPreflightTestStatusConnected | The connection to Twilio has been established. |
TVOPreflightTestStatusConnecting | Connecting to Twilio has started. |
TVOPreflightTestStatusFailed | The test has stopped and failed. |
Below is an example report returned to the completed
event handler or accessed via TVOPreflightTest.preflightReport
(iOS) or PreflightTest.getReport()
(Android). See below for an explanation of the report's fields.
1{2"edge":"US_WEST_OREGON",3"callSid":"CAxxxxx....xxxxxx",4"selectedIceCandidatePair":{5"remoteCandidate":{6"candidateType":"local",7"transportId":"0",8"protocol":"udp",9"networkCost":0,10"networkId":0,11"url":"",12"priority":2130706431,13"networkType":"Unknown",14"relatedAddress":"",15"deleted":false,16"ip":"168.86.145.92",17"tcpType":"",18"isRemote":true,19"relatedPort":0,20"port":1207421},22"localCandidate":{23"candidateType":"relay",24"transportId":"0",25"protocol":"udp",26"networkCost":10,27"networkId":1,28"url":"turn:global.turn.twilio.com:3478?transport=udp",29"priority":41820671,30"networkType":"Wifi",31"relatedAddress":"0.0.0.0",32"deleted":false,33"ip":"54.244.51.46",34"tcpType":"",35"isRemote":false,36"relatedPort":0,37"port":5439738}39},40"networkStats":{41"jitter":{42"average":1.4090909090909092,43"min":0,44"max":545},46"mos":{47"average":4.4161981818181824,48"min":4.4081799999999998,49"max":4.419548999999999950},51"rtt":{52"average":27.454545454545453,53"min":0,54"max":4055}56},57"iceCandidates":[58{59"candidateType":"relay",60"transportId":"0",61"protocol":"udp",62"networkCost":10,63"networkId":1,64"url":"turn:global.turn.twilio.com:3478?transport=udp",65"priority":41820671,66"networkType":"Wifi",67"relatedAddress":"0.0.0.0",68"deleted":false,69"ip":"54.244.51.46",70"tcpType":"",71"isRemote":false,72"relatedPort":0,73"port":5439774},75{76"candidateType":"relay",77"transportId":"0",78"protocol":"udp",79"networkCost":10,80"networkId":1,81"url":"turn:global.turn.twilio.com:443?transport=tcp",82"priority":25042943,83"networkType":"Wifi",84"relatedAddress":"0.0.0.0",85"deleted":false,86"ip":"54.244.51.46",87"tcpType":"",88"isRemote":false,89"relatedPort":0,90"port":3998991}92],93"selectedEdge":"roaming",94"statsSamples":[95{96"packetsReceived":0,97"rtt":0,98"audioInputLevel":0,99"packetsSent":0,100"timestamp":"1731699119243.266113",101"jitter":0,102"mos":4.4081799999999998,103"audioOutputLevel":0,104"bytesReceived":0,105"packetsLost":0,106"packetsLostFraction":0,107"codec":"",108"bytesSent":0109},110{111"packetsReceived":1048,112"rtt":34,113"audioInputLevel":36,114"packetsSent":1026,115"timestamp":"1731699140337.642090",116"jitter":1,117"mos":4.4139400000000002,118"audioOutputLevel":924,119"bytesReceived":166942,120"packetsLost":0,121"packetsLostFraction":0,122"codec":"PCMU",123"bytesSent":164160124}125],126"callQuality":0,127"warnings":[128{129"values":"0.305590",130"timestamp":1731699128298,131"name":"constant-audio-input-level",132"threshold":"1.000000"133}134],135"networkTiming":{136"iceConnection":{137"endTime":1731699119809,138"startTime":1731699119090,139"duration":719140},141"preflightTest":{142"endTime":1731699141306,143"startTime":1731699117268,144"duration":24038145},146"signaling":{147"endTime":1731699117884,148"startTime":1731699117268,149"duration":616150},151"peerConnection":{152"endTime":1731699119243,153"startTime":1731699119135,154"duration":108155}156},157"isTurnRequired":"true",158"warningsCleared":[159{160"name":"constant-audio-output-level",161"timestamp":1731699132313162}163]164}165
Name | Description |
---|---|
callSid | The Call SID for the test call. |
edge | The Twilio Edge location that the test call connected to. |
iceCandidates | An array of WebRTC stats for the ICE candidates gathered when connecting to media. Each item is an RTCIceCandidateStats object which provides information related to an ICE candidate. |
networkTiming | - ice : Measurements for establishing ICE connection. This is measured from ICE connection checking to connected state. See the documentation for RTCPeerConnection.iceConnectionState for more information.- peerConnection : Measurements for establishing a Peer Connection. This is measured from PeerConnection connecting to connected state. See the documentation for RTCPeerConnection.connectionState for more information.- signaling : Measurements for establishing Signaling connection. This is measured from initiating a connection using device.connect() up to when RTCPeerConnection.signalingState transitions to stable state. See the documentation for RTCPeerConnection.signalingState for more information. |
statsSamples | WebRTC samples collected during the test. See the object format on the RTCSample Interface reference page. |
selectedEdge | The edge you set via the TwilioVoiceSDK before placing the call. If you did not explicitly set the edge, it defaults to gll . |
networkTiming | Timing measurements from the test. Includes millisecond timestamps and duration. - iceConnection : How long the ICE connection took to establish.- preflightTest : The total duration of the PreflightTest.- signaling : How long it took to establish the signaling connection.- peerConnection : How long it took to establish the peer connection. |
warnings | All warnings detected during the test, including warnings that clea. |
warningsCleared | Warnings cleared during the test. |
selectedIceCandidatePair | A WebRTC stats object for the ICE candidate pair used to connect to media, if candidates were selected. Each item is an RTCIceCandidatePairStats object which provides information related to an ICE candidate. |
isTurnRequired | Whether a TURN server is required to connect to media. This is dependent on the selected ICE candidates, and will be true if either is of type "relay", false if both are of another type, or undefined if there are no selected ICE candidates. |
callQuality | The quality of the call, determined by the MOS (Mean Opinion Score) of the audio stream. Possible values include: - 0: If the average mos is over 4.2.- 1: If the average mos is between 4.1 and 4.2 both inclusive.- 2: If the average mos is between 3.7 and 4.0 both inclusive.- 3: If the average mos is between 3.1 and 3.6 both inclusive.- 4: If the average mos is 3.0 or below. |
The PreflightTest requires a Twilio Access Token to run the test. It also requires a backend application that can generate TwiML to handle incoming phone calls from the PreflightTest. The app should be able to record audio from a microphone or an audio file and play it back to the browser.
Below are instructions for creating an TwiML App to handle PreflightTest calls and generate an Access Token.
The PreflightTest API requires a server-side application that can record audio from a microphone or an audio file and play the recorded audio back to the browser. This is how the PreflightTest verifies your connection to Twilio and the end user's network quality.
To accomplish this via a TwiML App, you need two TwiML endpoints: one to capture and record the audio, and another one to play the recorded audio.
This example uses TwiML Bins, which are serverless functions that Twilio hosts where you can provide TwiML instructions for the TwiML app. Start by going to the TwiML Bin page in the Twilio Console.
Create a new TwiML Bin with the "+" button and use "Playback" as the Bin's friendly name.
Then paste the following TwiML under the "TwiML" section:
1<?xml version="1.0" encoding="UTF-8"?>23<Response>4<Say>You said:</Say>5<Play loop="1">{{RecordingUrl}}</Play>6<Say>Now waiting for a few seconds to gather audio performance metrics.</Say>7<Pause length="3"/>8<Say>Hanging up now.</Say>9</Response>
This TwiML tells Twilio to do the following when it receives an incoming call:
Using the TwiML Bin page, create another TwiML Bin by clicking the plus button on that screen and use "Record" as the friendly name.
Replace the action URL in the following template (action=
) with your "Playback" TwiML Bin's URL that you created previously.
1<?xml version="1.0" encoding="UTF-8"?>23<Response>4<Say>Record a message in 3, 2, 1</Say>5<Record maxLength="5" action="<your Playback TwiML Bin's URL>"></Record>6<Say>Did not detect a message to record</Say>7</Response>
This TwiML tells Twilio to do the following when it receives an incoming call:
RecordingUrl
parameter, which the Playback TwiML Bin uses to play the recording of this call.Now that you have created the two TwiML Bins to record and playback a Voice call, create a TwiML app to connect the TwiML Bin to a Twilio phone number.
Go to the TwiML Apps page of the Twilio Console.
You can now use this TwiML app SID to create an Access Token to use when creating the PreflightTest.
Follow the instructions on this page for creating a Voice Access Token. Substitute the outgoingApplicationSid
value with your new TwiML App's SID. Then, you can use the generated Access Token to make PreflightTest calls.
When the PreflightTest initiates a call using the Access Token, Twilio makes an HTTP request to the TwiML App that you associated with the token. That TwiML App then runs the TwiML Bins you created to record and playback the call.