Voice Calling
Voice Calling Setup
Configure your Xcode project for outgoing calls.
In your app's
info.plist
, add theNSMicrophoneUsageDescription
key. This is required to request microphone permission.In your app's capabilities, add the background mode capabilities
Audio, Airplay, and Picture in Picture
,Voice over IP
, andRemote Notifications
Add the
Push Notifications
capability.
Making an Outgoing Call
Outgoing calls are made using the createVoiceCall
method. The active call is returned to the provided delegate or listener once it connects. During this time the call can be considered to be in progress.
let localNumber: PhoneNumber!
let remoteNumber = "2024561414"
let delegate: ActiveCallDelegate
try! telephonyClient.createVoiceCall(localNumber: localNumber, remoteNumber: remoteNumber, delegate: delegate)
Interacting With an Active Call
The active call delegate (iOS) / listener (Android) must be provided when a call is created and is used to communicate active call events such as when the call connects, disconnects, or fails.
If a call connects successfully, the delegate/listener will be provided with anActiveVoiceCall
object via the activeVoiceCallDidConnect
method. This object provides functions for manipulating the call such as muting the microphone, ending the call, or switching the audio to speakerphone.
public protocol ActiveCallDelegate {
/// Notifies the delegate that the call has connected
/// - Parameters:
/// - call: The `ActiveVoiceCall`
func activeVoiceCallDidConnect(_ call: ActiveVoiceCall)
/// Notifies the delegate that the call failed to connect
/// - Parameters:
/// - error: `CallingError` that occurred.
func activeVoiceCallDidFailToConnect(withError error: CallingError)
/// Notifies the delegate that the call has been disconnected
/// - Parameters:
/// - call: The `ActiveVoiceCall`
/// - error: Error that caused the call to disconnect if one occurred.
func activeVoiceCall(_ call: ActiveVoiceCall, didDisconnectWithError error: Error?)
/// Notifies the delegate that the call has been muted or un-muted
/// - Parameters:
/// - call: The `ActiveVoiceCall`
/// - isMuted: Whether outgoing call audio is muted
func activeVoiceCall(_ call: ActiveVoiceCall, didChangeMuteState isMuted: Bool)
/// Called when the system audio route has changed.
/// Use `call.isOnSpeaker` to determine if the call is on speaker.
///
/// - Parameter call: The `ActiveVoiceCall`
func activeVoiceCallAudioRouteDidChange(_ call: ActiveVoiceCall)
}
Receiving an Incoming Call
Subscribe to Incoming Calls
In AppDelegate.swift import PushKit and add a PKPushRegistry
variable:
import PushKit
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
var pushRegistry: PKPushRegistry!
}
In the applicationDidFinishLaunchingWithOptions
function set up the push registry:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...
self.pushRegistry = PKPushRegistry(queue: nil)
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
return true
}
Implement the PKPushRegistryDelegate
methods to register, deregister and handle incoming call push notifications. When a push notification comes in, pass the payload from the voip push on to the SudoTelephonyClient
along with an IncomingCallNotificationDelegate
:
extension AppDelegate: PKPushRegistryDelegate {
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
try! self.telephonyClient.registerForIncomingCalls(with: pushCredentials.token, useSandbox: true, completion: { (error) in
if let error = error {
// Error updating push credentials
}
else {
// Push credentials updated with telephony SDK
}
})
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
let _ = try! self.telephonyClient.handleIncomingPushNotificationPayload(payload.dictionaryPayload, notificationDelegate: self)
completion()
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
try! self.telephonyClient.deregisterForIncomingCalls(completion: { (error) in
if let error = error {
// Error de-registering for push notifications
} else {
// Successfully de-registered
}
})
}
}
IncomingCallNotificationDelegate:
public protocol IncomingCallNotificationDelegate: class {
/// Called when an incoming call is recieved. This is called before reporting the call to callkit so the correct
/// display name can be provided.
///
/// In your implementation of this function you should set the `displayName` property of the incoming call that should be displayed by callKit.
///
/// You should also set the `delegate` of the incoming call. When the call is answered, `incomingCallAnswered(_ call: ActiveVoiceCall)` will be called
/// and future call related events will be delivered to the delegate.
func incomingCallReceived(_ call: IncomingCall) -> Void
/// Indicates that the incoming call was canceled.
/// - Parameter call: The incoming call
/// - Parameter error: The error that occured, if any.
func incomingCall(_ call: IncomingCall, cancelledWithError error: Error?)
/// An incoming call was answered through CallKit and is in the process of connecting.
/// No more updates on the incoming call will be provided to this delegate.
/// Future call updates will be provided to the delegate set on the incoming call delegate in `incomingCallReceived(_ call: IncomingCall) -> Void`.
func incomingCallAnswered(_ call: ActiveVoiceCall)
}
When the incomingCallReceived()
function is called, you can take the IncomingCall
object and either accept the call or decline the call. The accept method takes an ActiveCallDelegate
instance which will then handle all of the call events.
IncomingCall:
public protocol IncomingCall: class {
/// When an incoming call is recieved the delegate should be set in order to get updates on call related events.
/// For example if the call is answered through CallKit the delegate will recieve the call connected event with
/// a handle to the call.
var delegate: ActiveCallDelegate? { get set }
/// The phone number receiving the call
var localNumber: String { get }
/// The phone number the call is coming from
var remoteNumber: String { get }
/// The UUID for the call
var uuid: UUID { get }
/// Accepts the call with a delegate to provide call lifecycle updates.
func accept(with delegate: ActiveCallDelegate)
/// Decline the call.
func decline()
/// The display name used by callKit when reporting a call
var displayName: CallKitDisplayName? { get set }
}
Manage Call Records
A call record is generated for each call made and received. The SDK provides three methods for accessing that data and one for deleting call records.
Get Call Records
Retrieve a list of call records with a provided PhoneNumber
and an optional limit and token for pagination:
// localNumber: PhoneNumber
// nextToken: String
try telephonyClient.getCallRecords(localNumber: localNumber, limit: nil, nextToken: nextToken) { (result) in
switch result {
case .success(let listToken):
// listToken: TelephonyListToken<CallRecord>
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Get Call Record
Get a CallRecord
providing its ID.
// callRecordId: String
try telephonyClient.getCallRecord(callRecordId: callRecordId) { (result) in
switch result {
case .success(let callRecord):
// callRecord: CallRecord
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Subscribe To Call Records
Subscribe to CallRecord
events and state changes. On iOS the subscribeToCallRecords()
function returns a SubscriptionToken
which can be used to cancel the subscription. On Android the function takes a CallRecordSubscriber
instance which will receive CallRecord
updates.
let subscriptionToken = try telephonyClient.subscribeToCallRecords(resultHandler: { (result) in
switch result {
case .success(let callRecord):
// callRecord: CallRecord
case .failure(let error):
// error: SudoTelephonyClientError
}
})
Delete Call Record
Delete a CallRecord
providing its ID.
// callRecordId: String
try telephonyClient.deleteCallRecord(id: callRecordId) { (result) in
switch result {
case .success(let deletedId):
// deletedId: String
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Call Record
A CallRecord
contains all the information for a voice call made or received.
public struct CallRecord {
/// Enum that represents the direction of the call.
public enum Direction {
case inbound
case outbound
case unknown
init(internalDirection: SudoTelephony.Direction) {
switch internalDirection {
case .inbound:
self = .inbound
case .outbound:
self = .outbound
case .unknown:
self = .unknown
}
}
}
// Enum that represents the state of the call record.
public enum State {
case authorized
case queued
case ringing
case answered
case completed
case unanswered
case unknown
init(internalState: CallState) {
switch internalState {
case .authorized:
self = .authorized
case .queued:
self = .queued
case .ringing:
self = .ringing
case .answered:
self = .answered
case .completed:
self = .completed
case .unanswered:
self = .unanswered
case .unknown:
self = .unknown
}
}
}
/// Voicemail data belonging to a call record
public struct VoicemailData {
/// Unique ID of the voicemail record
public let id: String
/// The duration of the voicemail recording in seconds
public let durationSeconds: UInt
/// The media object that can be used to download the voicemail recording
public let media: MediaObject
}
/// Unique ID of the call record
public let id: String
/// The user ID of the owner of the call record (also referred to as the subject)
public let owner: String
/// The ID of the sudo that made or received the call
public let sudoOwner: String
/// The ID of the `PhoneNumber` associated with the call record
public let phoneNumberId: String
/// The direction of the call. Either `outbound` or `inbound`
public let direction: Direction
/// The state of the call record
public let state: State
/// Timestamp that represents the last time the call was updated
public let updated: Date
/// Timestamp that represents the time the call was created
public let created: Date
/// The E164Number of the local phone number
public let localPhoneNumber: String
/// The E164Number of the remote phone number
public let remotePhoneNumber: String
/// The duration of the call in seconds
public let durationSeconds: UInt32
/// The voicemail data of this call record if it exists
public let voicemail: VoicemailData?
}
Voicemail
When an incoming call is rejected or times out, the caller is sent to voicemail. After hearing a recorded greeting, the caller can record up to 3 minutes of voicemail. The SDK provides several methods to manage these stored voicemail recordings.
Get Voicemails
Retrieve a list of voicemail records with a provided PhoneNumber
and an optional limit and token for pagination:
// localNumber: PhoneNumber
// nextToken: String
telephonyClient.getVoicemails(localNumber: localNumber, limit: nil, nextToken: nextToken) { (result) in
switch result {
case .success(let listToken):
// listToken: TelephonyListToken<Voicemail>
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Get Voicemail
Get a Voicemail
by its ID.
// voicemailId: String
telephonyClient.getVoicemail(id: voicemailId) { (result) in
switch result {
case .success(let voicemail):
// voicemail: Voicemail
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Subscribe To Voicemails
Subscribe to Voicemail
create events, update events, and delete events. On iOS the subscribeToVoicemails()
function returns a SubscriptionToken
which can be used to cancel the subscription. On Android the function takes a VoicemailSubscriber
instance which will receive Voicemail events.
let subscriptionToken = try telephonyClient.subscribeToVoicemails(resultHandler: { (result) in
switch result {
case .success(let voicemail):
// voicemail: Voicemail
case .failure(let error):
// error: SudoTelephonyClientError
}
})
Delete Voicemail
Delete a Voicemail
by its ID.
// voicemailId: String
try telephonyClient.deleteVoicemail(id: voicemailId) { (result) in
switch result {
case .success:
// successfully deleted
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Voicemail Record
A Voicemail
contains all the information for a voicemail recording.
public struct Voicemail {
/// Unique ID of the voicemail record
public let id: String
/// The ID of the Sudo Platform user that owns the voicemail.
public let owner: String
/// The ID of the Sudo that received the voicemail.
public let sudoOwner: String
/// The ID of the `PhoneNumber` associated with the voicemail.
public let phoneNumberId: String
/// The ID of the `CallRecord` associated with the voicemail.
public let callRecordId: String?
/// Timestamp that represents the time the voicemail was created.
public let created: Date
/// Timestamp that represents the last time the voicemail was updated.
public let updated: Date
/// The E.164 formatted phone number that received the voicemail.
public let localPhoneNumber: String
/// The E.164 formatted phone number of the caller.
public let remotePhoneNumber: String
/// The duration of the voicemail recording in seconds.
public let durationSeconds: UInt
/// The media object that can be used to download the voicemail recording.
public let media: MediaObject
}
Downloading Voicemail Audio Data
When voicemail records are retrieved, the audio data is not automatically downloaded. This gives you control over performance when retrieving voicemail records, particularly those with long recordings.
To download voicemail audio data, use the downloadData()
method, passing it a MediaObject
which can be retrieved from the media
property of a Voicemail
. This method returns the raw audio data.
import AVFoundation
let voicemail: Voicemail
telephonyClient.downloadData(for: voicemail.media) { (result) in
switch result {
case .success(let data):
let audioPlayer = try! AVAudioPlayer(data: data)
audioPlayer.play()
case .failure(let error):
// error: SudoTelephonyClientError
}
}
Associating Voicemail Records with Call Records
Voicemail records and call records can be managed independently. However, a call record that has a corresponding voicemail has an additional voicemail
property containing a copy of the voicemail data. When said voicemail is deleted this copy is also deleted.
let callRecord: CallRecord
if let voicemailData: CallRecord.VoicemailData = callRecord.voicemail {
// The call record has associated voicemail data.
// voicemailData.id: String
// voicemailData.durationSeconds: UInt
// voicemailData.media: MediaObject
}
The Voicemail
record also has an optional callRecordId
property which relates the voicemail to its call record if the call record has not been deleted.
Last updated