--- name: telnyx-webrtc-client-android description: >- Build VoIP calling apps on Android using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, push notifications (FCM), call quality metrics, and AI Agent integration. Use when implementing real-time voice communication on Android. metadata: author: telnyx product: webrtc language: kotlin platform: android --- # Telnyx WebRTC - Android SDK Build real-time voice communication into Android applications using Telnyx WebRTC. > **Prerequisites**: Create WebRTC credentials and generate a login token using the Telnyx server-side SDK. See the `telnyx-webrtc-*` skill in your server language plugin (e.g., `telnyx-python`, `telnyx-javascript`). ## Installation Add JitPack repository to your project's `build.gradle`: ```gradle allprojects { repositories { maven { url 'https://jitpack.io' } } } ``` Add the dependency: ```gradle dependencies { implementation 'com.github.team-telnyx:telnyx-webrtc-android:latest-version' } ``` ## Required Permissions Add to `AndroidManifest.xml`: ```xml ``` --- ## Authentication ### Option 1: Credential-Based Login ```kotlin val telnyxClient = TelnyxClient(context) telnyxClient.connect() val credentialConfig = CredentialConfig( sipUser = "your_sip_username", sipPassword = "your_sip_password", sipCallerIDName = "Display Name", sipCallerIDNumber = "+15551234567", fcmToken = fcmToken, // Optional: for push notifications logLevel = LogLevel.DEBUG, autoReconnect = true ) telnyxClient.credentialLogin(credentialConfig) ``` ### Option 2: Token-Based Login (JWT) ```kotlin val tokenConfig = TokenConfig( sipToken = "your_jwt_token", sipCallerIDName = "Display Name", sipCallerIDNumber = "+15551234567", fcmToken = fcmToken, logLevel = LogLevel.DEBUG, autoReconnect = true ) telnyxClient.tokenLogin(tokenConfig) ``` ### Configuration Options | Parameter | Type | Description | |-----------|------|-------------| | `sipUser` / `sipToken` | String | Credentials from Telnyx Portal | | `sipCallerIDName` | String? | Caller ID name displayed to recipients | | `sipCallerIDNumber` | String? | Caller ID number | | `fcmToken` | String? | Firebase Cloud Messaging token for push | | `ringtone` | Any? | Raw resource ID or URI for ringtone | | `ringBackTone` | Int? | Raw resource ID for ringback tone | | `logLevel` | LogLevel | NONE, ERROR, WARNING, DEBUG, INFO, ALL | | `autoReconnect` | Boolean | Auto-retry login on failure (3 attempts) | | `region` | Region | AUTO, US_EAST, US_WEST, EU_WEST | --- ## Making Outbound Calls ```kotlin // Create a new outbound call telnyxClient.call.newInvite( callerName = "John Doe", callerNumber = "+15551234567", destinationNumber = "+15559876543", clientState = "my-custom-state" ) ``` --- ## Receiving Inbound Calls Listen for socket events using SharedFlow (recommended): ```kotlin lifecycleScope.launch { telnyxClient.socketResponseFlow.collect { response -> when (response.status) { SocketStatus.ESTABLISHED -> { // Socket connected } SocketStatus.MESSAGERECEIVED -> { response.data?.let { data -> when (data.method) { SocketMethod.CLIENT_READY.methodName -> { // Ready to make/receive calls } SocketMethod.LOGIN.methodName -> { // Successfully logged in } SocketMethod.INVITE.methodName -> { // Incoming call! val invite = data.result as InviteResponse // Show incoming call UI, then accept: telnyxClient.acceptCall( invite.callId, invite.callerIdNumber ) } SocketMethod.ANSWER.methodName -> { // Call was answered } SocketMethod.BYE.methodName -> { // Call ended } SocketMethod.RINGING.methodName -> { // Remote party is ringing } } } } SocketStatus.ERROR -> { // Handle error: response.errorCode } SocketStatus.DISCONNECT -> { // Socket disconnected } } } } ``` --- ## Call Controls ```kotlin // Get current call val currentCall: Call? = telnyxClient.calls[callId] // End call currentCall?.endCall(callId) // Mute/Unmute currentCall?.onMuteUnmutePressed() // Hold/Unhold currentCall?.onHoldUnholdPressed(callId) // Send DTMF tone currentCall?.dtmf(callId, "1") ``` ### Handling Multiple Calls ```kotlin // Get all active calls val calls: Map = telnyxClient.calls // Iterate through calls calls.forEach { (callId, call) -> // Handle each call } ``` --- ## Push Notifications (FCM) ### 1. Setup Firebase Add Firebase to your project and get an FCM token: ```kotlin FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (task.isSuccessful) { val fcmToken = task.result // Use this token in your login config } } ``` ### 2. Handle Incoming Push In your `FirebaseMessagingService`: ```kotlin class MyFirebaseService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { val params = remoteMessage.data val metadata = JSONObject(params as Map<*, *>).getString("metadata") // Check for missed call if (params["message"] == "Missed call!") { // Show missed call notification return } // Show incoming call notification (use Foreground Service) showIncomingCallNotification(metadata) } } ``` ### 3. Decline Push Call (Simplified) ```kotlin // The SDK now handles decline automatically telnyxClient.connectWithDeclinePush( txPushMetaData = pushMetaData, credentialConfig = credentialConfig ) // SDK connects, sends decline, and disconnects automatically ``` ### Android 14+ Requirements ```xml ``` --- ## Call Quality Metrics Enable metrics to monitor call quality in real-time: ```kotlin val credentialConfig = CredentialConfig( // ... other config debug = true // Enables call quality metrics ) // Listen for quality updates lifecycleScope.launch { currentCall?.callQualityFlow?.collect { metrics -> println("MOS: ${metrics.mos}") println("Jitter: ${metrics.jitter * 1000} ms") println("RTT: ${metrics.rtt * 1000} ms") println("Quality: ${metrics.quality}") // EXCELLENT, GOOD, FAIR, POOR, BAD } } ``` | Quality Level | MOS Range | |---------------|-----------| | EXCELLENT | > 4.2 | | GOOD | 4.1 - 4.2 | | FAIR | 3.7 - 4.0 | | POOR | 3.1 - 3.6 | | BAD | ≤ 3.0 | --- ## AI Agent Integration Connect to a Telnyx Voice AI Agent without traditional SIP credentials: ### 1. Anonymous Login ```kotlin telnyxClient.connectAnonymously( targetId = "your_ai_assistant_id", targetType = "ai_assistant", // Default targetVersionId = "optional_version_id", userVariables = mapOf("user_id" to "12345") ) ``` ### 2. Start Conversation ```kotlin // After anonymous login, call the AI Agent telnyxClient.newInvite( callerName = "User Name", callerNumber = "+15551234567", destinationNumber = "", // Ignored for AI Agent clientState = "state", customHeaders = mapOf( "X-Account-Number" to "123", // Maps to {{account_number}} "X-User-Tier" to "premium" // Maps to {{user_tier}} ) ) ``` ### 3. Receive Transcripts ```kotlin lifecycleScope.launch { telnyxClient.transcriptUpdateFlow.collect { transcript -> transcript.forEach { item -> println("${item.role}: ${item.content}") // role: "user" or "assistant" } } } ``` ### 4. Send Text to AI Agent ```kotlin // Send text message during active call telnyxClient.sendAIAssistantMessage("Hello, I need help with my account") ``` --- ## Custom Logging Implement your own logger: ```kotlin class MyLogger : TxLogger { override fun log(level: LogLevel, tag: String?, message: String, throwable: Throwable?) { // Send to your logging service MyAnalytics.log(level.name, tag ?: "Telnyx", message) } } val config = CredentialConfig( // ... other config logLevel = LogLevel.ALL, customLogger = MyLogger() ) ``` --- ## ProGuard Rules If using code obfuscation, add to `proguard-rules.pro`: ```proguard -keep class com.telnyx.webrtc.** { *; } -dontwarn kotlin.Experimental$Level -dontwarn kotlin.Experimental -dontwarn kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher ``` --- ## Troubleshooting | Issue | Solution | |-------|----------| | No audio | Check RECORD_AUDIO permission is granted | | Push not received | Verify FCM token is passed in config | | Login fails | Verify SIP credentials in Telnyx Portal | | Call drops | Check network stability, enable `autoReconnect` | | sender_id_mismatch (push) | FCM project mismatch - ensure app's `google-services.json` matches server credentials | **[references/webrtc-server-api.md](references/webrtc-server-api.md) has the server-side WebRTC API — credential creation, token generation, and push notification setup. You MUST read it when setting up authentication or push notifications.** ## API Reference ### TelnyxClient `TelnyxClient` is the main entry point for interacting with the Telnyx WebRTC SDK. It handles connection management, call creation, and responses from the Telnyx platform. ### Core Functionalities - **Connection Management**: Establishes and maintains a WebSocket connection to the Telnyx RTC platform. - **Authentication**: Supports authentication via SIP credentials or tokens. - **Call Control**: Provides methods to initiate (`newInvite`), accept (`acceptCall`), and end (`endCall`) calls. - **Event Handling**: Uses `TxSocketListener` to process events from the socket, such as incoming calls (`onOfferReceived`), call answers (`onAnswerReceived`), call termination (`onByeReceived`), and errors (`onErrorReceived`). - **State Exposure**: Exposes connection status, session information, and call events via `SharedFlow` (recommended: `socketResponseFlow`) and deprecated `LiveData` (e.g., `socketResponseLiveData`) for UI consumption. ### Key Components and Interactions - **`TxSocket`**: Manages the underlying WebSocket communication. - **`TxSocketListener`**: An interface implemented by `TelnyxClient` to receive and process socket events. Notably: - `onOfferReceived(jsonObject: JsonObject)`: Handles incoming call invitations. - `onAnswerReceived(jsonObject: JsonObject)`: Processes answers to outgoing calls. - `onByeReceived(jsonObject: JsonObject)`: Handles call termination notifications. The `jsonObject` now contains richer details including `cause`, `causeCode`, `sipCode`, and `sipReason`, allowing the client to populate `CallState.DONE` with a detailed `CallTerminationReason`. - `onErrorReceived(jsonObject: JsonObject)`: Manages errors reported by the socket or platform. - `onClientReady(jsonObject: JsonObject)`: Indicates the client is ready for operations after connection and initial setup. - `onGatewayStateReceived(gatewayState: String, receivedSessionId: String?)`: Provides updates on the registration status with the Telnyx gateway. - **`Call` Class**: Represents individual call sessions. `TelnyxClient` creates and manages instances of `Call`. - **`CallState`**: The client updates the `CallState` of individual `Call` objects based on socket events and network conditions. This includes states like `DROPPED(reason: CallNetworkChangeReason)`, `RECONNECTING(reason: CallNetworkChangeReason)`, and `DONE(reason: CallTerminationReason?)` which now provide more context. - **`socketResponseFlow: SharedFlow>`**: This SharedFlow stream is the recommended approach for applications. It emits `SocketResponse` objects that wrap messages received from the Telnyx platform. For `BYE` messages, the `ReceivedMessageBody` will contain a `com.telnyx.webrtc.sdk.verto.receive.ByeResponse` which is now enriched with termination cause details. - **`socketResponseLiveData: LiveData>`**: **[DEPRECATED]** This LiveData stream is deprecated in favor of `socketResponseFlow`. It's maintained for backward compatibility but new implementations should use SharedFlow. ### Usage Example **Recommended approach using SharedFlow:** ```kotlin // Initializing the client val telnyxClient = TelnyxClient(context) // Observing responses using SharedFlow (Recommended) lifecycleScope.launch { telnyxClient.socketResponseFlow.collect { response -> when (response.status) { SocketStatus.MESSAGERECEIVED -> { response.data?.let { when (it.method) { SocketMethod.INVITE.methodName -> { val invite = it.result as InviteResponse // Handle incoming call invitation } SocketMethod.BYE.methodName -> { val bye = it.result as com.telnyx.webrtc.sdk.verto.receive.ByeResponse // Call ended by remote party, bye.cause, bye.sipCode etc. are available Log.d("TelnyxClient", "Call ended: ${bye.callId}, Reason: ${bye.cause}") } // Handle other methods like ANSWER, RINGING, etc. } } } SocketStatus.ERROR -> { // Handle errors Log.e("TelnyxClient", "Error: ${response.errorMessage}") } // Handle other statuses: ESTABLISHED, LOADING, DISCONNECT } } } ``` **Deprecated approach using LiveData:** ```kotlin @Deprecated("Use socketResponseFlow instead. LiveData is deprecated in favor of Kotlin Flows.") // Observing responses (including errors and BYE messages) telnyxClient.socketResponseLiveData.observe(lifecycleOwner, Observer { response -> when (response.status) { SocketStatus.MESSAGERECEIVED -> { response.data?.let { when (it.method) { SocketMethod.INVITE.methodName -> { val invite = it.result as InviteResponse // Handle incoming call invitation } SocketMethod.BYE.methodName -> { val bye = it.result as com.telnyx.webrtc.sdk.verto.receive.ByeResponse // Call ended by remote party, bye.cause, bye.sipCode etc. are available Log.d("TelnyxClient", "Call ended: ${bye.callId}, Reason: ${bye.cause}") } // Handle other methods like ANSWER, RINGING, etc. } } } SocketStatus.ERROR -> { // Handle errors Log.e("TelnyxClient", "Error: ${response.errorMessage}") } // Handle other statuses: ESTABLISHED, LOADING, DISCONNECT } }) // Connecting and Logging In (example with credentials) telnyxClient.connect( credentialConfig = CredentialConfig( sipUser = "your_sip_username", sipPassword = "your_sip_password", // ... other config ... ) ) // Making a call val outgoingCall = telnyxClient.newInvite( callerName = "My App", callerNumber = "+11234567890", destinationNumber = "+10987654321", clientState = "some_state" ) // Observing the specific call's state outgoingCall.callStateFlow.collect { state -> if (state is CallState.DONE) { Log.d("TelnyxClient", "Outgoing call ended. Reason: ${state.reason?.cause}") } // Handle other states } ``` Refer to the SDK's implementation and specific method documentation for detailed usage patterns and configuration options. ### Telnyx Client NOTE: Remember to add and handle INTERNET, RECORD_AUDIO and ACCESS_NETWORK_STATE permissions

### Initialize To initialize the TelnyxClient you will have to provide the application context. ```kotlin telnyxClient = TelnyxClient(context) ``` ### Connect Once an instance is created, you can call the one of two available .connect(....) method to connect to the socket. ```kotlin fun connect( providedServerConfig: TxServerConfiguration = TxServerConfiguration(), credentialConfig: CredentialConfig, txPushMetaData: String? = null, autoLogin: Boolean = true, ) ``` ### Listening for events and reacting We need to react for a socket connection state or incoming calls. We do this by getting the Telnyx Socket Response callbacks from our TelnyxClient. ```kotlin val socketResponseFlow: SharedFlow> ``` ### Call ### Telnyx Call Class that represents a Call and handles all call related actions, including answering and ending a call. ### Creating a call invitation In order to make a call invitation, you need to provide your callerName, callerNumber, the destinationNumber (or SIP credential), and your clientState (any String value). ```kotlin telnyxClient.call.newInvite(callerName, callerNumber, destinationNumber, clientState) ``` ### Accepting a call In order to be able to accept a call, we first need to listen for invitations. We do this by getting the Telnyx Socket Response as LiveData: ```kotlin fun getSocketResponse(): LiveData>? = telnyxClient.getSocketResponse() ``` ### Handling Multiple Calls The Telnyx WebRTC SDK allows for multiple calls to be handled at once. You can use the callId to differentiate the calls.. ### Key Properties - **`callId: UUID`**: A unique identifier for the call. - **`sessionId: String`**: The session ID associated with the Telnyx connection. - **`callStateFlow: StateFlow`**: A Kotlin Flow that emits updates to the call's current state. This is the primary way to observe real-time changes to the call. States include: - `CallState.NEW`: The call has been locally initiated but not yet sent. - `CallState.CONNECTING`: The call is in the process of connecting. - `CallState.RINGING`: The call invitation has been sent, and the remote party is being alerted. - `CallState.ACTIVE`: The call is established and active. - `CallState.HELD`: The call is on hold. - `CallState.DONE(reason: CallTerminationReason?)`: The call has ended. The optional `reason` parameter provides details about why the call terminated (e.g., normal hangup, call rejected, busy, SIP error). `CallTerminationReason` contains `cause`, `causeCode`, `sipCode`, and `sipReason`. - `CallState.ERROR`: An error occurred related to this call. - `CallState.DROPPED(reason: CallNetworkChangeReason)`: The call was dropped, typically due to network issues. The `reason` (`CallNetworkChangeReason.NETWORK_LOST` or `CallNetworkChangeReason.NETWORK_SWITCH`) provides context. - `CallState.RECONNECTING(reason: CallNetworkChangeReason)`: The SDK is attempting to reconnect the call after a network disruption. The `reason` provides context. - **`onCallQualityChange: ((CallQualityMetrics) -> Unit)?`**: A callback for real-time call quality metrics. - **`audioManager: AudioManager`**: Reference to the Android `AudioManager` for controlling audio settings. - **`peerConnection: Peer?`**: Represents the underlying WebRTC peer connection. ### Key Methods - **`newInvite(...)`**: (Typically initiated via `TelnyxClient`) Initiates a new outgoing call. - **`acceptCall(...)`**: (Typically initiated via `TelnyxClient`) Accepts an incoming call. - **`endCall(callId: UUID)`**: Terminates the call. This is usually called on the `TelnyxClient` which then manages the specific `Call` object. - **`onMuteUnmutePressed()`**: Toggles the microphone mute state. - **`onLoudSpeakerPressed()`**: Toggles the loudspeaker state. - **`onHoldUnholdPressed(callId: UUID)`**: Toggles the hold state for the call. - **`dtmf(callId: UUID, tone: String)`**: Sends DTMF tones. ### Observing Call State Applications should observe the `callStateFlow` to react to changes in the call's status and update the UI accordingly. For example, displaying call duration when `ACTIVE`, showing a "reconnecting" indicator when `RECONNECTING`, or presenting termination reasons when `DONE`. ```kotlin // Example: Observing call state in a ViewModel or Composable viewModelScope.launch { myCall.callStateFlow.collect { state -> when (state) { is CallState.ACTIVE -> { // Update UI to show active call controls } is CallState.DONE -> { // Call has ended, update UI // Access state.reason for termination details val reasonDetails = state.reason?.let { "Cause: ${it.cause}, SIP Code: ${it.sipCode}" } ?: "No specific reason provided." Log.d("Call Ended", "Reason: $reasonDetails") } is CallState.DROPPED -> { // Call dropped, possibly show a message with state.reason.description Log.d("Call Dropped", "Reason: ${state.callNetworkChangeReason.description}") } is CallState.RECONNECTING -> { // Call is reconnecting, update UI Log.d("Call Reconnecting", "Reason: ${state.callNetworkChangeReason.description}") } // Handle other states like NEW, CONNECTING, RINGING, HELD, ERROR else -> { /* ... */ } } } } ``` For more details on specific parameters and advanced usage, refer to the SDK's source code and the main `TelnyxClient` documentation. ### ReceivedMessageBody ### ReceivedMessageBody A data class the represents the structure of every message received via the socket connection ```kotlin data class ReceivedMessageBody(val method: String, val result: ReceivedResult?) ``` Where the params are: * method the Telnyx Message Method - ie. INVITE, BYE, MODIFY, etc. @see [SocketMethod] * result the content of the actual message in the structure provided via `ReceivedResult` ### SocketMethod Enum class to detail the Method property of the response from the Telnyx WEBRTC client with the given [methodName] ### Structure ```kotlin data class ReceivedMessageBody( val method: String, // The Telnyx Message Method (e.g., "telnyx_rtc.invite", "telnyx_rtc.bye") val result: ReceivedResult? // The content of the actual message ) ``` - **`method: String`**: This field indicates the type of message received. It corresponds to one of the `SocketMethod` enums (e.g., `SocketMethod.INVITE`, `SocketMethod.ANSWER`, `SocketMethod.BYE`). Your application will typically use this field in a `when` statement to determine how to process the `result`. - **`result: ReceivedResult?`**: This field holds the actual payload of the message. `ReceivedResult` is a sealed class, and the concrete type of `result` will depend on the `method`. For example: - If `method` is `SocketMethod.LOGIN.methodName`, `result` will be a `LoginResponse`. - If `method` is `SocketMethod.INVITE.methodName`, `result` will be an `InviteResponse`. - If `method` is `SocketMethod.ANSWER.methodName`, `result` will be an `AnswerResponse`. - If `method` is `SocketMethod.BYE.methodName`, `result` will be a `com.telnyx.webrtc.sdk.verto.receive.ByeResponse`. Importantly, this `ByeResponse` now includes detailed termination information such as `cause`, `causeCode`, `sipCode`, and `sipReason`, in addition to the `callId`. - Other `ReceivedResult` subtypes include `RingingResponse`, `MediaResponse`, and `DisablePushResponse`. ### Usage When you observe `TelnyxClient.socketResponseLiveData`, you receive a `SocketResponse`. If the status is `SocketStatus.MESSAGERECEIVED`, the `data` field of `SocketResponse` will contain the `ReceivedMessageBody`. ```kotlin telnyxClient.socketResponseLiveData.observe(this, Observer { response -> if (response.status == SocketStatus.MESSAGERECEIVED) { response.data?.let { receivedMessageBody -> Log.d("SDK_APP", "Method: ${receivedMessageBody.method}") when (receivedMessageBody.method) { SocketMethod.LOGIN.methodName -> { val loginResponse = receivedMessageBody.result as? LoginResponse // Process login response } SocketMethod.INVITE.methodName -> { val inviteResponse = receivedMessageBody.result as? InviteResponse // Process incoming call invitation } SocketMethod.BYE.methodName -> { val byeResponse = receivedMessageBody.result as? com.telnyx.webrtc.sdk.verto.receive.ByeResponse byeResponse?.let { // Process call termination, access it.cause, it.sipCode, etc. Log.i("SDK_APP", "Call ${it.callId} ended. Reason: ${it.cause}, SIP Code: ${it.sipCode}") } } // Handle other methods... } } } }) ``` By checking the `method` and casting the `result` to its expected type, your application can effectively handle the diverse messages sent by the Telnyx platform.