telnyx-webrtc-ios
Enable Telnyx real-time communication services on iOS. :telephone_receiver: :fire:
Project structure:
- SDK project: Enable Telnyx WebRTC communications.
- SDK Tests project.
- Demo app project.
Project Setup:
- Clone the repository
- Run the command
pod install
to install de dependencies inside the project root folder. - Open the Workspace :
TelnyxRTC.xcworkspace
- You will find 3 targets to build:
- The SDK
- The SDK Tests
- The Demo App
- Select the target
TelnyxRTC (TelnyxRTC Project)
to build the SDK
- Select the target
TelnyxRTCTests
to run the tests. You will need to long press over the Run button and selectBuild for testing
Select target
TelnyxWebRTCDemo
to run the demo app. The SDK should be manually builded in order to get the app running (Step 5)Enjoy 😎
Credentials Outbound call Incoming call
SIP Credentials
In order to start making and receiving calls using the TelnyxRTC SDK you will need to get SIP Credentials:
- Access to https://portal.telnyx.com/
- Sign up for a Telnyx Account.
- Create a Credential Connection to configure how you connect your calls.
- Create an Outbound Voice Profile to configure your outbound call settings and assign it to your Credential Connection.
For more information on how to generate SIP credentials check the Telnyx WebRTC quickstart guide.
Adding Telnyx SDK to your iOS Client Application:
Currently the iOS SDK is supported using cocoapods.
Cocoapods
If your xcode project is not using cocoapods yet, you will need to configure it.
- Open your podfile and add the TelnyxRTC.
pod 'TelnyxRTC', '~> 0.1.0'
- Install your pods. You can add the flag –repo-update to ensure your cocoapods has the specs updated.
pod install --repo-update
- Open your .xcworkspace
- Import TelnyxRTC at the top level of your class:
import TelnyxRTC
Disable BITCODE (The GoogleWebRTC dependency has BITCODE disabled): Go to the Build Settings tab of your app target, search for “bitcode” and set it to “NO”
Enable VoIP and Audio background modes: Go to Signing & Capabilities tab, press the +Capability button and add those background modes:
Go to your Info.plist file and add the “Privacy - Microphone Usage Description” key with a description that your app requires microphone access in order to make VoIP calls.
You are all set!
Swift Package Manager
Xcode has a built-in support for Swift package manager. To add a package :
- Select Files > Add Packages
- On the Swift Package Manager Screen, Search for the https://github.com/team-telnyx/telnyx-webrtc-ios.git package.
- Select the main brach and click Add Package
NB: if Add Package is stuck downloading try File > Packages > Reset Package Caches or Run the command
rm -rf ~/Library/Caches/org.swift.swiftpm/
in terminal
Read more in Apple documentation
Hint: Use either Cocoapods or Swift Package Manager for Individual Packages to avoid Duplicate binaries
Usage
Telnyx client setup
// Initialize the client
let telnyxClient = TxClient()
// Register to get SDK events
telnyxClient.delegate = self
// Setup yor connection parameters.
// Set the login credentials and the ringtone/ringback configurations if required.
// Ringtone / ringback tone files are not mandatory.
// You can user your sipUser and password
let txConfigUserAndPassowrd = TxConfig(sipUser: sipUser,
password: password,
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
//You can choose the appropriate verbosity level of the SDK.
//Logs are disabled by default
logLevel: .all)
// Or use a JWT Telnyx Token to authenticate
let txConfigToken = TxConfig(token: "MY_JWT_TELNYX_TOKEN",
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
//You can choose the appropriate verbosity level of the SDK. Logs are disabled by default
logLevel: .all)
do {
// Connect and login
// Use `txConfigUserAndPassowrd` or `txConfigToken`
try telnyxClient.connect(txConfig: txConfigToken)
} catch let error {
print("ViewController:: connect Error \(error)")
}
// You can call client.disconnect() when you're done.
Note: you need to relese the delegate manually when you are done.
// Disconnecting and Removing listeners.
telnyxClient.disconnect();
// Release the delegate
telnyxClient.delegate = nil
Telnyx client delegate
You will need to instantiate the client and set the delegate.
// Initialize the client
let telnyxClient = TxClient()
// Register to get SDK events
telnyxClient.delegate = self
Then you will receive the following events:
extension ViewController: TxClientDelegate {
func onRemoteCallEnded(callId: UUID) {
// Call has been removed internally.
}
func onSocketConnected() {
// When the client has successfully connected to the Telnyx Backend.
}
func onSocketDisconnected() {
// When the client from the Telnyx backend
}
func onClientError(error: Error) {
// Something went wrong.
}
func onClientReady() {
// You can start receiving incoming calls or
// start making calls once the client was fully initialized.
}
func onSessionUpdated(sessionId: String) {
// This function will be executed when a sessionId is received.
}
func onIncomingCall(call: Call) {
// Someone is calling you.
// This delegate method will be called when the app is in foreground and the Telnyx Client is connected.
}
func onPushCall(call: Call) {
// If you have configured Push Notifications and app is in background or the Telnyx Client is disconnected
// this delegate method will be called after the push notification is received.
// Update the current call with the incoming call
self.currentCall = call
}
// You can update your UI from here based on the call states.
// Check that the callId is the same as your current call.
func onCallStateUpdated(callState: CallState, callId: UUID) {
// handle the new call state
switch (callState) {
case .CONNECTING:
break
case .RINGING:
break
case .NEW:
break
case .ACTIVE:
break
case .DONE:
break
case .HELD:
break
}
}
}
Calls
Outboud call
// Create a client instance
self.telnyxClient = TxClient()
// Asign the delegate to get SDK events
self.telnyxClient?.delegate = self
// Connect the client (Check TxClient class for more info)
self.telnyxClient?.connect(....)
// Create the call and start calling
self.currentCall = try self.telnyxClient?.newCall(callerName: "Caller name",
callerNumber: "155531234567",
// Destination is required and can be a phone number or SIP URI
destinationNumber: "18004377950",
callId: UUID.init())
This is a general example: In order to fully support outbound calls you will need to implement CallKit to properly handle audio states. For more information check Audio Session Handling WebRTC + CallKit
section.
Inbound call
How to answer an incoming call:
//Init your client
func initTelnyxClient() {
//
self.telnyxClient = TxClient()
// Asign the delegate to get SDK events
self.telnyxClient?.delegate = self
// Connect the client (Check TxClient class for more info)
self.telnyxClient?.connect(....)
}
extension ViewController: TxClientDelegate {
//....
func onIncomingCall(call: Call) {
// We are automatically answering any incoming call as an example, but
// maybe you want to store a reference of the call, and answer the call after a button press.
self.myCall = call.answer()
}
}
This is a general example: In order to fully support inbound calls you will need to implement PushKit + CallKit. For more information check Setting up VoIP push notifications
section.
Setting up VoIP push notifications:
In order to receive incoming calls while the app is running in background or closed, you will need to perform a set of configurations over your Mission Control Portal Account and your application.
VoIP Push - Portal setup
During this process you will learn how to create a VoIP push credential and assign the credential to a SIP Connection.
This process requires:
- A Mission Control Portal Account.
- A SIP Connection.
- Your Apple VoIP push certificate.
For complete instructions on how to setup Push Notifications got to this link.
VoIP Push - App Setup
The following setup is required in your application to receive Telnyx VoIP push notifications:
a. Add Push Notifications capability to your Xcode project
- Open the xcode workspace associated with your app.
- In the Project Navigator (the left-hand menu), select the project icon that represents your mobile app.
- In the top-left corner of the right-hand pane in Xcode, select your app’s target.
Press the +Capabilities button.
Enable Push Notifications
b. Configure PushKit into your app:
- Import pushkit
Swift import PushKit
- Initialize PushKit: “`Swift private var pushRegistry = PKPushRegistry.init(queue: DispatchQueue.main) …
func initPushKit() { pushRegistry.delegate = self pushRegistry.desiredPushTypes = Set([.voIP]) }
3. Implement PKPushRegistryDelegate
```Swift
extension AppDelegate: PKPushRegistryDelegate {
// New push notification token assigned by APNS.
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
if (type == .voIP) {
// This push notification token has to be sent to Telnyx when connecting the Client.
let deviceToken = credentials.token.reduce("", {$0 + String(format: "%02X", $1) })
UserDefaults.standard.savePushToken(pushToken: deviceToken)
}
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
if (type == .voIP) {
// Delete incoming token in user defaults
let userDefaults = UserDefaults.init()
userDefaults.deletePushToken()
}
}
/**
This delegate method is available on iOS 11 and above.
*/
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
if (payload.type == .voIP) {
self.handleVoIPPushNotification(payload: payload)
}
if let version = Float(UIDevice.current.systemVersion), version >= 13.0 {
completion()
}
}
func handleVoIPPushNotification(payload: PKPushPayload) {
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {
let callId = metadata["call_id"] as? String
let callerName = (metadata["caller_name"] as? String) ?? ""
let callerNumber = (metadata["caller_number"] as? String) ?? ""
let caller = callerName.isEmpty ? (callerNumber.isEmpty ? "Unknown" : callerNumber) : callerName
let uuid = UUID(uuidString: callId)
// Re-connect the client and process the push notification when is received.
// You will need to use the credentials of the same user that is receiving the call.
let txConfig = TxConfig(sipUser: sipUser,
password: password,
pushDeviceToken: "APNS_PUSH_TOKEN")
//Call processVoIPNotification method
try telnyxClient?.processVoIPNotification(txConfig: txConfig, serverConfiguration: serverConfig,pushMetaData: metadata)
// Report the incoming call to CallKit framework.
let callHandle = CXHandle(type: .generic, value: from)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.hasVideo = false
provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
if let error = error {
print("AppDelegate:: Failed to report incoming call: \(error.localizedDescription).")
} else {
print("AppDelegate:: Incoming call successfully reported.")
}
}
}
}
- If everything is correctly set-up when the app runs APNS should assign a Push Token.
- In order to receive VoIP push notifications. You will need to send your push token when connecting to the Telnyx Client.
let txConfig = TxConfig(sipUser: sipUser,
password: password,
pushDeviceToken: "DEVICE_APNS_TOKEN",
//You can choose the appropriate verbosity level of the SDK.
logLevel: .all)
// Or use a JWT Telnyx Token to authenticate
let txConfigToken = TxConfig(token: "MY_JWT_TELNYX_TOKEN",
pushDeviceToken: "DEVICE_APNS_TOKEN",
//You can choose the appropriate verbosity level of the SDK. Logs are disabled by default
logLevel: .all)
For more information about Pushkit you can check the official Apple docs.
Important:
- You will need to login at least once to send your device token to Telnyx before start getting Push notifications.
- You will need to provide
pushMetaData
toprocessVoIPNotification()
to get Push calls to work. - You will need to implement ‘CallKit’ to report an incoming call when there’s a VoIP push notification. On iOS 13.0 and later, if you fail to report a call to CallKit, the system will terminate your app. More information on Apple docs
c. Configure CallKit into your App:
PushKit
requires you to use CallKit
when handling VoIP calls. CallKit
ensures that apps providing call-related services on a user’s device work seamlessly together on the user’s device, and respect features like Do Not Disturb. CallKit
also operates the system’s call-related UIs, including the incoming or outgoing call screens. Use CallKit
to present these interfaces and manage interactions with them.
For more information about CallKit
you can check the official Apple docs.
General Setup:
- Import CallKit:
Swift import CallKit
Initialize CallKit
func initCallKit() { let configuration = CXProviderConfiguration(localizedName: "TelnyxRTC") configuration.maximumCallGroups = 1 configuration.maximumCallsPerCallGroup = 1 callKitProvider = CXProvider(configuration: configuration) if let provider = callKitProvider { provider.setDelegate(self, queue: nil) } }
Implement
CXProviderDelegate
methods.
Audio Session Handling WebRTC + CallKit
To get CallKit
properly working with the TelnyxRTC SDK
you need to set the audio device state based on the CallKit
AudioSession state like follows:
extension AppDelegate : CXProviderDelegate {
...
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
self.telnyxClient?.enableAudioSession(audioSession: audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
self.telnyxClient?.disableAudioSession(audioSession: audioSession)
}
}
Reporting calls with CallKit
To properly report calls to callKit with right statuses, you need to invoke the following callKit methods at the right instances:
- Starting A New Call : When ever you start a call, report to callkit using the
provider.reportCall()
method.
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.supportsDTMF = true
callUpdate.supportsHolding = true
callUpdate.supportsGrouping = false
callUpdate.supportsUngrouping = false
callUpdate.hasVideo = false
provider.reportCall(with: uuid, updated: callUpdate)
- When user receives a Call : Use
provider.reportNewIncomingCall(with: uuid, update: callUpdate)
to report an incoming call. This sends a request to callKit the to provide the native call interface to the user.
guard let provider = callKitProvider else {
print("AppDelegate:: CallKit provider not available")
return
}
let callHandle = CXHandle(type: .generic, value: from)
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
// handle error
}
- When callee answers an outgoing call : Use
provider.reportOutgoingCall(with: callKitUUID, connectedAt:nil)
to report a connected outgoing call. This provides the time when the outgoing call goes to active to callKit.Swift if let provider = self.callKitProvider, let callKitUUID = self.callKitUUID { let date = Date() provider.reportOutgoingCall(with: callKitUUID, connectedAt:date) }
NB : This should be used only when the call is outgoing.
Best Practices when Using PushNotifications with Callkit.
- When receiving calls from push notifications, it is always required to wait for the connection to the WebSocket before fulfilling the call answer action. This can be achieved by implementing the CXProviderDelegate in the following way (SDK version >=0.1.11):
Swift func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { self.telnyxClient?.answerFromCallkit(answerAction: action) }
When the answerFromPush(answerAction: action)
is called, Callkit sets the call state to connecting
to alert the user that the call is being connected.
Once the call is active, the timer starts.
Connecting State | Active call |
The previous SDK versions requires handling the websocket connection state on the client side. It can be done in the following way:
var callAnswerPendingFromPush:Bool = false
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
print("AppDelegate:: ANSWER call action: callKitUUID [\(String(describing: self.callKitUUID))] action [\(action.callUUID)]")
if(currentCall != nil){
self.currentCall?.answer()
}else {
self.callAnswerPendingFromPush = true
}
action.fulfill()
}
func onPushCall(call: Call) {
print("AppDelegate:: TxClientDelegate onPushCall() \(call)")
self.currentCall = call //Update the current call with the incoming call
//Answer Call if call was answered from callkit
//This happens when there's a race condition between login and receiving PN
// when User answer's the call from PN and there's no Call or INVITE message yet. Set callAnswerPendingFromPush = true
// Whilst we wait fot onPushCall Method to be called
if(self.callAnswerPendingFromPush){
self.currentCall?.answer()
self.callAnswerPendingFromPush = false
}
}
Likewise for ending calls, the endCallFromCallkit(endAction:action)
method should be called from :
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
self.telnyxClient?.endCallFromCallkit(endAction:action)
}
Calling this method solves the race condition, where call is ended before the client connects to the webserver. This way the call is ended on the callee side once a connection is established.
- Logs on the receiver’s end are essential for thorough debugging of issues related to push notifications. However, the debugger is not attached when the app is completely killed. To address this, you can simply put the app in the background. VOIP push notifications should then come through, and the debugger should capture all logs.
Handling Multiple Calls
To handle multiples, we can rely on the CXProviderDelegate
delegate which invokes functions corresponding to
what action was performed on the callkit user interface.
- End and Accept or Decline : The end and accept button on the callkit user interface accepts the new call and ends the previous call.
Callkit then invokes the
CXAnswerCallAction
andCXEndCallAction
when the end and accept button is pressed. You can handle this scenario by
var currentCall: Call?
var previousCall: Call?
//current calkit uuid
var callKitUUID: UUID?
func onIncomingCall(call: Call) {
guard let callId = call.callInfo?.callId else {
print("AppDelegate:: TxClientDelegate onIncomingCall() Error unknown call UUID")
return
}
print("AppDelegate:: TxClientDelegate onIncomingCall() callKitUUID [\(String(describing: self.callKitUUID))] callId [\(callId)]")
self.callKitUUID = call.callInfo?.callId
//Update the previous call with the current call
self.previousCall = self.currentCall
//Update the current call with the incoming call
self.currentCall = call
..
}
Subsequently, when the user clicks on the End and Accept or Decline Button, you will need to determine which of these buttons was clicked. You can do that as follows:
//Callkit invokes CXEndCallAction and CXAnswerCallAction delegate function for accept and answer
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
print("AppDelegate:: END call action: callKitUUID [\(String(describing: self.callKitUUID))] action [\(action.callUUID)]")
// if the callKitUUID is the same as the one provided by the action
// callkit expects you to end the current call
if(self.callKitUUID == action.callUUID){
if let onGoingCall = self.previousCall {
self.currentCall = onGoingCall
self.callKitUUID = onGoingCall.callInfo?.callId
}
}else {
// callkit expects you to end the previous call
self.callKitUUID = self.currentCall?.callInfo?.callId
}
self.telnyxClient?.endCallFromCallkit(endAction:action)
}
Note
While handling multiple calls, you should report the call end to callkit properly with the right callUUID. This will keep your active calls with the callkit user interface until there are no more active sessions.
- Hold and Accept or Decline: The hold and accept button on the callkit user interface accepts the new call and holds the previous call.
Callkit then invokes the
CXSetHeldCallAction
when the hold and accept button is pressed.
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
print("provider:performSetHeldAction:")
//request to hold previous call, since we have both the current and previous calls
previousCall?.hold()
action.fulfill()
}
Also, you will need to un-hold the previous call when the current call gets ended on CXEndCallAction
.
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
if(previousCall?.callState == .HELD){
print("AppDelegate:: call held.. unholding call")
previousCall?.unhold()
}
...
}
Note
While handling multiple calls, you should report the call end to callkit properly with the right callUUID. This will keep your active calls with the callkit user interface until there are no more active sessions.
Disable Push Notification
Push notifications can be disabled for the current user by calling :
telnyxClient.disablePushNotifications()
Note : Signing back in, using same credentials will re-enable push notifications.
Privacy Manifest
Support for privacy manifest is added from version 0.1.26
Sending Debug Stats
In case of any need to investigate any issue by Telnyx, please enable the debug stats that will be sent to Telnyx for analysis.
currentCall?.startDebugStats()
Please store the debug_stats_id that would be used for investigation
Documentation:
For more information you can:
- Clone the repository
- And check the exported documentation in:
docs/index.html
Questions? Comments? Building something rad? Join our Slack channel and share.