CallKit是什么
苹果在WWDC 2016发布了iOS 10的新框架CallKit,它允许开发者在VoIP类型APP整合系统原生语音界面,以获得更好的用户体验。
接入CallKit后,APP里面的通话会被写入系统通话记录,而且APP通话时的优先级比一般VoIP的APP优先级要高。
CallKit主要类
我们可以把系统提供的CallKit框架理解成一个通话的界面UI,我们做的只是把这个通话界面跟我们的应用联系起来,
比如有一个语音电话过来了,那么我们就呼起这个界面,用户在这个界面上点击了静音按钮,我们相应地做静音的处理。
同样地,如果用户在应用里面点击了静音按钮,也要把状态同步到系统的界面。
呼起CallKit & CallKit回调
CXProviderConfiguration 描述系统通话界面的标题、logo、来电铃声等
CXCallUpdate 描述来电信息,如来电用户名、是否视频聊天等
CXProvider 同步连接状态到CallKit
CXProviderDelegate 用户在系统通话界面上动作的回调
应用把状态同步到系统:
CXCallAction 用户不同动作的描述,如:呼出电话、接听、挂断、静音等(分别对应CXStartCallAction、CXAnswerCallAction、CXEndCallAction、CXSetMutedCallAction)
CXTransaction 把用户的不同动作打包成CXTransaction再传递给系统
CXCallController 把CXTransaction传递给系统
原理
1、呼入
来电
1.CallKitTestApp接收到服务器的来电信息(后台时VoIP Push)
2.创建CXCallUpdate对象记录来电信息,并记录该通话唯一标识
3.通过CXProvider对象把来电信息通知系统
4.系统接收到来电信息后显示原生来电UI
响应来电
1.用户在原生来电UI上点击接听按钮(或挂断按钮)
2.系统把动作封装成CXAnswerCallAction,通过CXProvider的Delegate回调到CallKitTestApp
3.CallKitTestApp收到回调后会开始配置mediasdk,调整UI等操作,最后开始通话
结束来电
1.CallKitTestApp在前台时,用户点击挂断按钮
2.创建CXTransaction对象,用于包装挂断动作CXEndCallAction(包含该通话唯一标识)
3.通过CXCallController对象把挂断动作通知系统
4.系统成功挂断该通话后,通过CXProvider的Delegate回调到CallKitTestApp
5.CallKitTestApp收到回调后会开始释放mediasdk(这里要注意释放顺序,不能在系统回调前把sdk释放掉),调整UI等操作
2、呼出
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结构如下:
3、CallKitTestApp在后台状态(应用被杀掉、手机锁屏)
VoIP Push
请看 VoIP Push介绍
CallKitTestApp接收到VoIP Push后,如果应用未被完全kill掉,应用则被唤醒,
若被完全kill掉,AppDelegate的-application:didFinishLaunchingWithOptions:
会被调起,
linkd正常登录后接收到来电拓传,这时候会走在前台状态的流程,系统通话页面会被呼起。
CallKitTestApp被kill掉后接收到VoIP Push后的调用时序如下:
风险评估
(风险从高到低排序)
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
,但是CXProviderConfiguration
的supportedHandleTypes
并没有包含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]; // 执行成功
}