Anatomy of a JS SDK Client & Call
While some differences exist between the JS SDK and the mobile SDKs, they follow a similar client lifecycle and call flow.
The JS SDK demo app is used here as it’s far easier to set up the application (just load up webrtc.telnyx.com) and perform debugging using browser tooling.
Overview
The SDK does two main things:
- Establishes an active websocket connection to send and receive signaling messages to and from rtc.telnyx.com.
- Establishes a media session for a call
To achieve the above, it employs the following suite of APIs:
Client Instantiation & Authentication
- Go to webrtc.telnyx.com
- Right click; Inspect; Select Network tab and filter WS traffic
- Follow this page to successfully register the demo app.
In the browser, the following sequence of JSON-RPC messages is observed.
Message 1: client → rtc.telnyx.com
{
"jsonrpc": "2.0",
"id": "2c754d41-b7e6-422d-b39f-661a139cd5b3",
"method": "login",
"params": {
"login": "xxx",
"passwd": "yyy",
"userVariables": {},
"loginParams": {},
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb"
}
}
Message 2: rtc.telnyx.com → client
{
"id": "2c754d41-b7e6-422d-b39f-661a139cd5b3",
"jsonrpc": "2.0",
"result": {
"message": "logged in",
"sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb"
},
"voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw"
}
Message 3: rtc.telnyx.com → client
{
"id": 138417,
"jsonrpc": "2.0",
"method": "telnyx_rtc.clientReady",
"params": {
"reattached_sessions": []
},
"voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw"
}
Message 4: client → rtc.telnyx.com
{
"jsonrpc": "2.0",
"id": "660360fb-3f46-4f63-804b-973e429c7c22",
"method": "telnyx_rtc.gatewayState",
"params": {}
}
Message 5: rtc.telnyx.com → client
{
"id": "660360fb-3f46-4f63-804b-973e429c7c22",
"jsonrpc": "2.0",
"result": {
"params": {
"state": "REGED"
},
"sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb"
},
"voice_sdk_id": "VSDK1Ch8eUTpaTTKMC3HTSjybKJ3apyGYgw"
}
The above interaction is caused by the demo app instantiating and connecting the SDK client.
const client = new TelnyxRTC({login: "xxx", password: "yyy"});
client.connect();
At a high level, when the client is instantiated, it…
- creates a session object and
- registers handlers for all 4 socket events
Invoking the connect
method on the SDK client …
- Initiates a WebSocket connection to rtc.telnyx.com
- Once the socket is open, the login message is sent to rtc.telnyx.com.
At this point, Message #1 and #2 are observed.
Message #3, #4, and #5, is the result of the logic here
- A
telnyx_rtc.clientReady
event from rtc.telnyx.com triggers atelnyx_rtc.gateState
query from the SDK client - A
REGED
event from rtc.telnyx.com bubbles up astelnyx.ready
.
At this point, the SDK client is authenticated to make or receive a call.
Call Initiation
- In another tab, open chrome://webrtc-internals/
- Fill in “Call destination” with +18008648331 (United Airlines IVR)
- Click “Call”
In the browser, the following sequence of JSON-RPC messages is observed.
Message 1: client → rtc.telnyx.com
{
"jsonrpc":"2.0",
"id":"71344c55-10a9-4f6a-b8a8-ebaa9347bc95",
"method":"telnyx_rtc.invite",
"params":{
"sessid":"cf7894a7-c225-428a-a8ae-4145833e6ddb",
"sdp":"[ABRIDGED SDP]",
"dialogParams":{
"audio":true,
"useStereo":false,
"debug":false,
"debugOutput":"socket",
"attach":false,
"screenShare":false,
"userVariables":{
"microphoneLabel":"Default - AirPods"
},
"mediaSettings":{
},
"iceServers":[
{
"urls":"turn:turn.telnyx.com:3478?transport=tcp",
"username":"testuser",
"credential":"testpassword"
},
{
"urls":"stun:stun.telnyx.com:3478"
},
{
"urls":[
"stun:stun.l.google.com:19302"
]
}
],
"localElement":"localVideo",
"remoteElement":"remoteVideo",
"ringtoneFile":"https://webrtc.telnyx.com/sounds/incoming_call.mp3",
"stats":true,
"callID":"074e7e59-6859-4b8b-8743-83df82e7f776",
"destination_number":"+18008648331",
"remote_caller_id_name":"Outbound Call",
"remote_caller_id_number":"+18008648331",
"caller_id_name":"+15734038245",
"caller_id_number":"XXX"
},
"User-Agent":"Web-2.16.0"
}
}
Message 2: rtc.telnyx.com → client
{
"id": "71344c55-10a9-4f6a-b8a8-ebaa9347bc95",
"jsonrpc": "2.0",
"result": {
"callID": "074e7e59-6859-4b8b-8743-83df82e7f776",
"message": "CALL CREATED",
"sessid": "cf7894a7-c225-428a-a8ae-4145833e6ddb"
},
"voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w"
}
Message 3: rtc.telnyx.com → client
{
"id": 254271,
"jsonrpc": "2.0",
"method": "telnyx_rtc.ringing",
"params": {
"callID": "074e7e59-6859-4b8b-8743-83df82e7f776",
"callee_id_name": "Outbound Call",
"callee_id_number": "+18008648331",
"caller_id_name": "+15734038245",
"caller_id_number": "XXX",
"dialogParams": {
"custom_headers": []
},
"display_direction": "inbound",
"telnyx_leg_id": "b6a23a2c-7e12-11ef-ab96-02420aef821f",
"telnyx_session_id": "b6a23ea0-7e12-11ef-a5d3-02420aef821f"
},
"voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w"
}
Message 4: client → rtc.telnyx.com
{
"jsonrpc": "2.0",
"id": 254271,
"result": {
"method": "telnyx_rtc.ringing"
}
}
Message 5: rtc.telnyx.com → client
{
"id": 254274,
"jsonrpc": "2.0",
"method": "telnyx_rtc.media",
"params": {
"callID": "074e7e59-6859-4b8b-8743-83df82e7f776",
"dialogParams": {
"custom_headers": []
},
"sdp": "[ABRIDGED SDP]",
"variables": {
"Core-UUID": "be37d1d2-14e2-45de-af9f-0b2807445761",
"Event-Calling-File": "switch_channel.c",
"Event-Calling-Function": "switch_channel_get_variables_prefix",
"Event-Calling-Line-Number": "4632",
"Event-Date-GMT": "Sun, 29 Sep 2024 03:27:13 GMT",
"Event-Date-Local": "2024-09-29 03:27:13",
"Event-Date-Timestamp": "1727580433259250",
"Event-Name": "CHANNEL_DATA",
"Event-Sequence": "1071399",
"FreeSWITCH-Hostname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413",
"FreeSWITCH-IPv4": "10.33.6.81",
"FreeSWITCH-IPv6": "::1",
"FreeSWITCH-Switchname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413"
}
},
"voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w"
}
Message 6: client → rtc.telnyx.com
{
"jsonrpc": "2.0",
"id": 254274,
"result": {
"method": "telnyx_rtc.media"
}
}
Message 7: rtc.telnyx.com → client
{
"id": 254275,
"jsonrpc": "2.0",
"method": "telnyx_rtc.answer",
"params": {
"callID": "074e7e59-6859-4b8b-8743-83df82e7f776",
"dialogParams": {
"custom_headers": []
},
"variables": {
"Core-UUID": "be37d1d2-14e2-45de-af9f-0b2807445761",
"Event-Calling-File": "switch_channel.c",
"Event-Calling-Function": "switch_channel_get_variables_prefix",
"Event-Calling-Line-Number": "4632",
"Event-Date-GMT": "Sun, 29 Sep 2024 03:27:15 GMT",
"Event-Date-Local": "2024-09-29 03:27:15",
"Event-Date-Timestamp": "1727580435119306",
"Event-Name": "CHANNEL_DATA",
"Event-Sequence": "1071412",
"FreeSWITCH-Hostname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413",
"FreeSWITCH-IPv4": "10.33.6.81",
"FreeSWITCH-IPv6": "::1",
"FreeSWITCH-Switchname": "b2bua-rtc-canary.tel-sy1-ibm-prod-413"
}
},
"voice_sdk_id": "VSDK1CiEGUTpa63GGtH2nSmyTHM7KIwlL_w"
}
Message 8: client → rtc.telnyx.com
{
"jsonrpc": "2.0",
"id": 254275,
"result": {
"method": "telnyx_rtc.answer"
}
}
At this point, the media will flow.
All of the above interaction is the result of the following SDK API call.
client.newCall(options);
Under the hood, the SDK performs many steps before Message #1 (INVITE) is even sent.
Broadly speaking, the following are the essential steps with the relevant data picked out from the JSON dump.
RTCPeerConnection
is instantiated.getUserMedia
is invoked to obtain user’s permission for audio and eventually theMediaStream
{
"audio_track_info": "id:6bd2b2da-d615-4ef3-9ac2-394abb22d6b0 label:Default - AirPods",
"pid": 44341,
"request_id": 29,
"request_type": "getUserMedia",
"rid": 303,
"stream_id": "7cc701ad-3a35-4c2a-9b1e-c4c7b209f30c",
"timestamp": 1727582437707.941
}
addTransceiver
is invoked to add the local stream to the sender of theRTCPeerConnection
.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "transceiverAdded",
"value": "Caused by: addTransceiver\n\ngetTransceivers()[0]:{\n mid:null,\n kind:'audio',\n sender:{\n track:'6bd2b2da-d615-4ef3-9ac2-394abb22d6b0',\n streams:['7cc701ad-3a35-4c2a-9b1e-c4c7b209f30c'],\n encodings: [\n {active: true, },\n ],\n },\n receiver:{\n track:'434cac64-3afe-4705-bfe7-979d9530b58e',\n streams:[],\n },\n direction:'sendrecv',\n currentDirection:null,\n}"
}
- The previous step triggers the
negotiationneeded
event.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "negotiationneeded",
"value": ""
}
- In the event handler, the SDK invokes
createOffer
.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "createOffer",
"value": "options: {offerToReceiveVideo: 0, offerToReceiveAudio: 1, voiceActivityDetection: true, iceRestart: false}"
}
- This API call will eventually create a
RTCSessionDescription
with information on the local media stream.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "createOfferOnSuccess",
"value": "type: offer, sdp: [ABRIDGED SDP]"
}
- The SDK will then invoke the
setLocalDescription
to set the SDP of the client peer.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "setLocalDescription",
"value": "type: offer, sdp: [ABRIDGED SDP]"
}
- Concurrently,
createOffer
also kicks off the ICE candidate gathering.
{
"time": "9/28/2024, 11:00:37 PM",
"type": "icegatheringstatechange",
"value": "gathering"
},
...,
{
"time": "9/28/2024, 11:00:37 PM",
"type": "icecandidate",
"value": "sdpMid: 0, sdpMLineIndex: 0, candidate: candidate:134647514 1 udp 1685855999 18.163.7.125 59374 typ srflx raddr 100.113.237.26 rport 59374 generation 0 ufrag ccb7 network-id 3 network-cost 50, url: stun:stun.l.google.com:19302"
}
- When all is done,
icecandidate
event triggers withcandidate = null
to indicate the process is completed. Subsequently, the existing local SDP parameters are augmented with the ICE candidates. - At this point, all necessary info are present to send the invite to rtc.telnyx.com.
- As a result, on the websocket, Message #1 through #4 are observed.
- At Message #5, rtc.telnyx.com sends over its SDP in the
telnyx_rtc.media
events. - Upon receipt of this message, the SDK invokes
setRemoteDescription
to set the SDP of the remote peer.
{
"time": "9/28/2024, 11:00:44 PM",
"type": "setRemoteDescription",
"value": "type: answer, sdp: [ABRIDGED SDP]"
}
- Finally, two peers of the
RTCPeerConnection
are fully identified. connectionState changes from connecting to connected.
{
"time": "9/28/2024, 11:00:44 PM",
"type": "connectionstatechange",
"value": "connecting"
},
...
{
"time": "9/28/2024, 11:00:45 PM",
"type": "connectionstatechange",
"value": "connected"
}
- Media will flow over UDP.