CallKit是什么

苹果在WWDC 2016发布了iOS 10的新框架CallKit,它允许开发者在VoIP类型APP整合系统原生语音界面,以获得更好的用户体验。

接入CallKit后,APP里面的通话会被写入系统通话记录,而且APP通话时的优先级比一般VoIP的APP优先级要高。

CallKit主要类

743ef574cad53b6cd95a0c15f47cbe93

我们可以把系统提供的CallKit框架理解成一个通话的界面UI,我们做的只是把这个通话界面跟我们的应用联系起来,

比如有一个语音电话过来了,那么我们就呼起这个界面,用户在这个界面上点击了静音按钮,我们相应地做静音的处理。

同样地,如果用户在应用里面点击了静音按钮,也要把状态同步到系统的界面。

呼起CallKit & CallKit回调
CXProviderConfiguration 描述系统通话界面的标题、logo、来电铃声等
CXCallUpdate 描述来电信息,如来电用户名、是否视频聊天等
CXProvider 同步连接状态到CallKit
CXProviderDelegate 用户在系统通话界面上动作的回调

应用把状态同步到系统:
CXCallAction 用户不同动作的描述,如:呼出电话、接听、挂断、静音等(分别对应CXStartCallAction、CXAnswerCallAction、CXEndCallAction、CXSetMutedCallAction)
CXTransaction 把用户的不同动作打包成CXTransaction再传递给系统
CXCallController 把CXTransaction传递给系统

原理

1、呼入

来电

9be34d79d5f65ffbc36d6789fb739e07

1.CallKitTestApp接收到服务器的来电信息(后台时VoIP Push)

2.创建CXCallUpdate对象记录来电信息,并记录该通话唯一标识

3.通过CXProvider对象把来电信息通知系统

4.系统接收到来电信息后显示原生来电UI

响应来电

1224bc11bb8896323fedf104c82daacd

1.用户在原生来电UI上点击接听按钮(或挂断按钮)

2.系统把动作封装成CXAnswerCallAction,通过CXProvider的Delegate回调到CallKitTestApp

3.CallKitTestApp收到回调后会开始配置mediasdk,调整UI等操作,最后开始通话

结束来电

7a366e5e7912a7b713bc35b4730d54bb

1.CallKitTestApp在前台时,用户点击挂断按钮

2.创建CXTransaction对象,用于包装挂断动作CXEndCallAction(包含该通话唯一标识)

3.通过CXCallController对象把挂断动作通知系统

4.系统成功挂断该通话后,通过CXProvider的Delegate回调到CallKitTestApp

5.CallKitTestApp收到回调后会开始释放mediasdk(这里要注意释放顺序,不能在系统回调前把sdk释放掉),调整UI等操作

2、呼出

12a3efe995310a25d8415b8713cb7ef4

1.用户在CallKitTestApp呼出电话

2.创建CXTransaction对象,用于包装呼出动作CXStartCallAction(包含该通话唯一标识、呼出号码等信息)

3.通过CXCallController对象把呼出动作通知系统

4.系统成功呼出该通话后,通过CXProvider的Delegate回调到CallKitTestApp

5.CallKitTestApp收到回调后会开始配置mediasdk,调整UI等操作,最后开始通话

以上呼出逻辑是CallKitTestApp中IP-IP电话的处理逻辑,因为CallKitTestApp里面还有自动转换IP-PSTN逻辑,涉及到mediasdk资源释放问题,导致应用不稳定,内测版中暂时把呼出部分屏蔽。

CallKitTestApp如何接入

1、mediasdk做兼容

1.系统资源申请成功后再配置mediasdk

2.等系统资源释放后再释放mediasdk

为满足以上两点,mediasdk开放了下面几个接口,在CallKit模式下,等系统回调后再做mediasdk配置、激活、释放等操作

// In CallKit, it should be called by `perform Answer/start CallAction`
bool callKit_test_configAudio(MEDIA_HANDLE handle);

// In CallKit, it should be Called by `didActivateAudioSession`
bool callKit_test_startAudio(MEDIA_HANDLE handle);

// In CallKit, it should be Called by `didDeactivateAudioSession`
void callKit_test_stopAudio(MEDIA_HANDLE handle);

// In CallKit, it should be Called by `performEndCallAction`
void callKit_test_releaseAudio(MEDIA_HANDLE handle);

2、CallKitTestApp在前台状态

1.在CallKitTestApp项目中添加BOProviderDelegate对象,作为与CallKit通信的桥梁。

2.CallKitTestApp在前台,并且linkd正常连接,CallController负责监听来电拓传

3.接收到来电拓传后,初始化mediasdk,并通过BOProviderDelegate对象通知系统,触发系统来电页面,并监听系统回调

4.系统回调- provider:performAnswerCallAction:时,调整CallKitTestApp的来电UI

5.系统回调- provider:didActivateAudioSession:时,配置mediasdk音频参数callKit_test_configAudio(),并激活音频通信callKit_test_startAudio()

6.用户点击挂断或者对方点击挂断,通过BOProviderDelegate对象通知系统通话结束,并监听系统回调

7.系统回调- provider:performEndCallAction:后,释放mediasdk音频资源callKit_test_releaseAudio(),释放mediasdk

调用时序如下:

// 来电
2018-05-10 20:05:58.640136+0800 CallKitTest[19394:8610838] [I][CallKit] handleIncomingCallWithUUID 
2018-05-10 20:05:58.725955+0800 CallKitTest[19394:8610838] [I][CallKit] unprepareSDK
2018-05-10 20:05:58.726996+0800 CallKitTest[19394:8610627] [V][CallKit] prepareSDK
2018-05-10 20:06:02.327572+0800 CallKitTest[19394:8610627] [I][CallKit] performAnswerCallAction
2018-05-10 20:06:02.892451+0800 CallKitTest[19394:8610627] [I][CallKit] didActivateAudioSession
2018-05-10 20:06:03.087589+0800 CallKitTest[19394:8610628] [I][CallKit] callKit_test_configAudio
2018-05-10 20:06:03.240767+0800 CallKitTest[19394:8610627] [I][CallKit] callKit_test_startAudio
// 挂断
2018-05-10 20:06:13.660428+0800 CallKitTest[19394:8610838] [I][CallKit] endCallWithCompletion
2018-05-10 20:06:13.680038+0800 CallKitTest[19394:8610838] [I][CallKit] performEndCallAction
2018-05-10 20:06:13.798381+0800 CallKitTest[19394:8610628] [I][CallKit] callKit_test_releaseAudio
2018-05-10 20:06:13.799067+0800 CallKitTest[19394:8610838] [I][CallKit] unprepareSDK
2018-05-10 20:06:14.392857+0800 CallKitTest[19394:8610850] [I][CallKit] didDeactivateAudioSession

CallKitTestApp结构如下:

46dab3b98ce7c6d0452fb2608472bf37

3、CallKitTestApp在后台状态(应用被杀掉、手机锁屏)

VoIP Push

请看 VoIP Push介绍

CallKitTestApp接收到VoIP Push后,如果应用未被完全kill掉,应用则被唤醒,

若被完全kill掉,AppDelegate的-application:didFinishLaunchingWithOptions:会被调起,

linkd正常登录后接收到来电拓传,这时候会走在前台状态的流程,系统通话页面会被呼起。

CallKitTestApp被kill掉后接收到VoIP Push后的调用时序如下: 3671ce8bbfdf128a930e2f94b401386c

风险评估

(风险从高到低排序)

1.mediasdk释放问题

CallKit模式下mediasdk依赖系统的回调,如果mediasdk没被正常释放,会影响到下一次通话,所以code review要更加彻底,稳定性测试上要更加严格

2.VoIP Push延迟问题

目前在内测版中,VoIP存在一定的延迟,CallKitTestApp还有一个自动切换IP-PSTN逻辑,导致CallKit没有被触发,直接触发了IP-PSTN通话。

3.优化CallKitTestApp启动速度

CallKitTestApp被杀掉后台,收到VoIP Push后会开始启动,如果启动速度太慢会影响用户体验

4.异常处理

要处理好手机设置勿扰模式,被系统电话打断等异常处理(接入CallKit后通话的优先级会变高,但系统电话的优先级最高,会被其打断。)

注意点

1、CallKit的通话中UI

通话接通后,在非锁屏状态下,会跳转到CallKitTestApp的通话页面

在锁屏状态下,会直接显示系统通话页面

2、点击系统通话记录无法跳转

接入CallKit后,通话信息会同步到系统的通话记录里面,点击CallKitTestApp对应的通话记录后,正常情况下会跳转到CallKitTestApp并触发以下delegate:

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
    return [[BOStartCallViewModel shareInstance] startCallWithUserActivity:userActivity];
}

但是我遇到了点击通话记录后无法跳转到CallKitTestApp的情况。原因是CXHandle的type设置成了CXHandleTypeGeneric,但是CXProviderConfigurationsupportedHandleTypes并没有包含CXHandleTypeGeneric,所以初始化配置时加上该类型即可。

+ (CXProviderConfiguration *)providerConfiguration API_AVAILABLE(ios(10.0)) {
    static dispatch_once_t onceToken;
    static CXProviderConfiguration *config = nil;
    dispatch_once(&onceToken, ^{
        config = [[CXProviderConfiguration alloc] initWithLocalizedName:@"CallKitTestApp"];
        config.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber), nil];
        UIImage *iconMaskImage = [UIImage imageNamed:@"bo_userCenter_about_icon"];
        config.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage);
        config.ringtoneSound = @"ringback.mp3";
    });
    return config;
}

CXProviderDelegate 需要回复系统的动作执行结果

- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action API_AVAILABLE(ios(10.0)) {
    if (self.currentUUID.UUIDString.length == 0
        || ![action.callUUID.UUIDString isEqualToString:self.currentUUID.UUIDString]) {
        [action fail]; // 执行失败
        return;
    }
    
    if ([self.delegate respondsToSelector:_cmd]) {
        [self.delegate provider:provider performAnswerCallAction:action];
    }
    
    [action fulfill]; // 执行成功
}

参考

官方文档

官方Demo