CallKit-PushKit iOS
Working in background
iOS has most strict limitations related to work in background comparing to other supported platforms. App can make and receive calls only when it’s active (working in foreground) and can’t do that in all other states (background/suspended/not running).
Adding library to the app can’t overcome these limitations, except the case when there is already connected call. Established audio session allows app to stay in background and continue conversation.
Recommended solution which allows iOS app to receive incoming calls in background - use PushKit framework. It provides an efficient way to handle calls that doesn’t require app to be running. PushKit notifications wake up or launch your app and give it time to respond. Key requirement for using PushKit is to use also CallKit. It makes implementations tricky, as app has to display CallKit UI immediately after receiving push even when SIP request hasn’t received yet.
Library provides ready to use PushKit+CallKit implementation as part of the Flutter plugin available on PubDev.
PushKit+CallKit Implementation details
Application can enable both these features using following code on initialization stage:
InitData iniData = InitData(); ... iniData.enableCallKit = true; iniData.enablePushKit = true; iniData.unregOnDestroy = false; SiprixVoipSdk().initialize(iniData, logsModel);
enableCallKit can be set separately. In this mode plugin reacts on the library’s events and methods and automatically creates CallKit window when initiated new outgoing call or received incoming call. When app enabled only this option it can’t help to overcome background mode limitations explained above.
enablePushKit option can’t work without enableCallKit. In this mode plugin creates PKPushRegistry, which allows to get VoIP push-token and also automatically handles receving push notification, creates CallKit window and notifies application. In this mode plugin doesn’t create CallKit window when received SIP INVITE request.
unregOnDestroy option prevents sending unregister request when users swipes application out.
Attention
Don’t enable option iniData.enablePushKit = true; when push notification support hasn’t added yet.
When app will receive incoming call without push notification it will not trigger CalKit window,
which follows to don’t activate audio session and cause “no audio” issue.
Integrate PushKit+CallKit into Flutter application
Enable Pushkit+CallKit options as shown above.
Implement sending push token to SIP server with
REGISTERrequestEach time when app adds new account it should invoke method getPushKitToken and set received value as value of some X-header or as value in Contact Uri params or send/use token in its own way.
Example:
String? token = await SiprixVoipSdk().getPushKitToken(); //Adds 'X-Token' header to REGISTER request account.xheaders = {"X-Token" : token}; //Adds 'xToken' param to 'Contact' heeader of REGISTER request account.xContactUriParams["xToken"] = token;
See also how it’s implemented in the Flutter example app
Configure sending push notification on SIP Server (PBX) side
Before forward SIP call to the client PBX should check had it provided push token. When token found - PBX should send push notification to that client, wait a bit (let client to receive message and restore/refresh registration) and only after that forward
INVITErequest or report error.PBX should put some data in the push notification payload, which allows application to display call details and match SIP INVITE with previosly received push.
Here is example of curl command, which can be used for testing:
curl -v \ -d '{"aps":{"alert":"test","callerId":"someCallerId1", "pushHint":"somehint"}}' \ -H "apns-push-type: voip" \ -H "apns-priority: 10" \ --cert-type P12 --cert Certificates.p12 \ --http2 \ https://api.sandbox.push.apple.com/3/device/${DEVICE_TOKEN}See also: Sending notification requests to APNs.
Note
Part of the URL
${DEVICE_TOKEN}in example of script above, means device token, resolved on previous stage. To test this command copy actual token value.Note
It’s recommended to set short value in the apns-expiration header which prevents sending push notification after long delay. When APNs can’t send push during 20~40 seconds it’s better to don’t try to send it anymore as that SIP call doesn’t exist.
Handle push notification on app side.
Each time when push notification received plugin raises event which is handled by method onIncomingPush of the CallsModel. App should parse received payload and update CallKit window using method updateCallKitCallDetails:
Here is fragment of example app implementation:
@override void onIncomingPush(String callkit_CallUUID, Map<String, dynamic> pushPayload) { _logs?.print('onIncomingPush callkit_CallUUID:$callkit_CallUUID $pushPayload'); //Get data from 'pushPayload', which contains app specific details Map<String, dynamic>? apsPayload; try { apsPayload = Map<String, dynamic>.from(pushPayload["aps"]); } catch (err) { _logs?.print('onIncomingPush get payload err: $err'); } String pushHint = apsPayload?["pushHint"] ?? "pushHint"; String genericHandle = apsPayload?["callerNumber"] ?? "genericHandle"; String localizedCallerName = apsPayload?["callerName"] ?? "callerName"; bool withVideo = apsPayload?["withVideo"] ?? false; _callMatchers.add(CallMatcher(callkit_CallUUID, pushHint)); //Update CallKit SiprixVoipSdk().updateCallKitCallDetails(callkit_CallUUID, null, localizedCallerName, genericHandle, withVideo); } //Method arguments: void updateCallKitCallDetails(String callkit_CallUUID,int? sip_callId, [String? localizedCallerName=null, String? genericHandle=null, bool? withVideo=null]) - 'callkit_CallUUID': unique id of the CallKit call. - 'sip_callId': callId assigned by library when SIP INVITE received. - 'localizedCallerName': value which plugin should put into 'CXCallUpdate.localizedCallerName' - 'genericHandle': value which plugin should put into 'CXCallUpdate.remoteHandle' - 'withVideo': value which plugin should put into 'CXCallUpdate.hasVideo'
Note
Library is able to detect app state changes and automatically restores registration when app become foreground (received push notif).
In this state UI of the call has already presented, but actual SIP call not received yet.
Handle received
SIP INVITErequest.When received SIP INVITE app should check does exist CallKit call, which matches this SIP request, and update it with callId assigned by Library.
Here is fragment of example app implementation:
@override void onIncomingSip(int callId, int accId, bool withVideo, String hdrFrom, String hdrTo) async { super.onIncomingSip(callId, accId, withVideo, hdrFrom, hdrTo); //TODO Match push and sip calls using just received SIP INVITE and data from push (put to '_callMatchers') //Get some hint from just received SIP INVITE (added by remote server) or math this SIP-call with CallKit-call String? pushHintHeaderVal = await SiprixVoipSdk().getSipHeader(callId, "X-PushHint"); _logs?.print('onIncomingSip got pushHint:$pushHintHeaderVal'); int index = _callMatchers.indexWhere((c) => c.push_Hint == pushHintHeaderVal); if(index != -1) { _logs?.print('onIncomingSip match call:${_callMatchers[index].callkit_CallUUID} <=> $callId'); //Update CallKit with 'callId' _callMatchers[index].sip_CallId = callId; SiprixVoipSdk().updateCallKitCallDetails(_callMatchers[index].callkit_CallUUID, callId, null, null, null); } }
Note
[!] App should be able to check does the push notification and SIP request belong to the same call or they are from different calls received in unexpected order.
Ideally if SIP Server is able to generate some unique key for each call and put it into the push payload and SIP request.