iOS10 Callkit框架整合总结

前不久苹果推出的callkit framework简直就是voip类应用的福音啊,一下把应用的体验提升到了系统级别的高度,加上之前推出的pushkit,形成了一套完整的voip解决方案。正好我们的产品就是一个类voip应用,当然要把callkit加进去,不过callkit刚刚推出,网上的资料实在是少,甚至连官方的api文档里都是大片空白,下面记录一下我对整合callkit的一些总结与理解,也算丰富一下网上的相关资料吧。

一、嵌入后效果

首先,先对Callkit框架嵌入后的实际使用效果有个直观的概念,可以使用手机qq中的qq电话功能(qq已经做了Callkit的框架嵌入)。拨叫、被叫的等业务自己尝试几次,可以发现:

呼出时:直接在qq应用内触发,没有系统电话界面的显示,整个通话过程中的操作都是在qq应用内部完成的,但是通话结束后,系统通话记录中会出现此次对应的通话记录。

被叫时:会显示出系统的电话接听界面,整个通话过程中的操作都可以借助系统通话界面完成,双击home键,可以看到qq应用被启动了,并跳转到了接听界面,用户可以随意切换,通话过程中操作既可以使用系统界面完成,也可以使用qq应用内接听界面完成,最后通话结束后,系统通话记录中会多出此次对应的通话记录。

二、核心类

callkit框架主要围绕的就是CXProvider以及CXCallController两个核心类

关于CXProvider、CXCallController,有文章里是这么描述的:

CXProvider类主要负责 系统->程序方向 的信息、状态传递

CXCallController类主要负责 程序->系统方向的 信息、状态传递

这种描述并没有错误,但是却会让我在有些地方产生困惑,下文我会提到我的困惑,并尝试给出我的理解

三、流程示例

注:不同app的具体实现肯定是各不相同,但会话建立的整体流程应该都是大同小异的,下文的示例是依据我们app的实现写的,看的时候,不要太过纠结于细节或个别环节的差异,应该从整体流程上去体会callkit框架嵌入后带来的变化,最后再结合自己app的实际情况,去做自己的嵌入实现

1.被叫流程

未加入callkit前:

服务器通知app有来电->app显示接听界面->选择接听,进入app通话界面,应答服务器,建立话音通道->通话->挂断电话或被对方挂断 

加入callkit框架后:

服务器通知app有来电->调用CXProvider的reportNewIncomingCallWithUUID方法告知系统有来电,系统显示出系统接听界面->选择接听,系统回调performAnswerCallAction,进入app通话界面->系统通知 audiosession可用didActivateAudioSession->应答服务器,建立话音通道->通话->挂断电话或被对方挂断 

主要区别:

1.未加入callkit前,接听界面是app提供的,整个流程都掌控在app内部;加入后,首先要调用reportNewIncomingCallWithUUID通知系统有来电,让系统显示出系统接听界面。
2.加入callkit后,用户在系统接听界面选择接听后,不能立即建立话音通道,需要等待didActivateAudioSession的回调

被叫流程代码示例

(1)app调用reportNewIncomingCallWithUUID告知系统有来电,系统显示出系统接听界面 

    CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
    CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callInfo.phoneNum];
    callUpdate.remoteHandle = handle;
    callUpdate.localizedCallerName = callInfo.name;
    callUpdate.supportsDTMF = NO;
    callUpdate.supportsHolding = NO;
    callUpdate.supportsGrouping = NO;
    
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    
    [_provider reportNewIncomingCallWithUUID:callInfo.uuid update:callUpdate completion:^(NSError * _Nullable error) {
        if (!error)
        {
            NRS_Log(@"0|============================     reportNewInComingCall success");
            _callInfo = callInfo;
            _callInfo.status = CallInComing;
        }
        else
            _callInfo = nil;
        
        completion(error);
    }];

(1.1)[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; 此处,这句不加的话,后面didActivateAudioSession经常会出现不回调的现象,可能是callkit目前的bug

(1.2)reportNewIncomingCallWithUUID:update:completion: 方法调用,app告知系统有来电。此处,我会感到有些困惑,前文不是说CXProvider负责系统->app方向的信息状态传递吗,CXCallController负责app->系统方向,为什么通知系统有来电(app->系统)reportNewIncomingCallWithUUID方法却是属于CXProvider呢?
我的理解是,我们把状态、信息划分为由外部(app外)触发和自身内部(app内)发起两类。
来电行为,显然应该是一个外部触发行为,有人给你打电话来了,是从服务器(外部)通知给你的客户端,这种由外向内的事情,包括后面一些系统接听界面、通话界面的事件触发回调,都由CXProvider负责
而比如拨打电话(客户端发起),应用内一些按钮点击、状态变化告知系统,这种由内向外的事情,都由CXCallController负责。


(2)选择接听,系统触发回调performAnswerCallAction,告知你用户选择了接听(answer)

- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action

这里面你可以加载出你的通话界面

这类action的相关回调中,如果回调处理函数执行一切正常,要调用一下[action fulfill];有错误,则调用[action fail]


(3)上个回调后,系统立刻会回调

- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
此处,你就可以应答服务器并建立起话音通道,进行会话了

(4)挂断电话
(4.1)在系统通话界面挂断的电话,会触发performEndCallAction:回调,在这里你可以添加相关挂断处理代码
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
(4.2)被对方挂断或在应用内挂断电话,调用CXProvider的reportCallWithUUID:endedAtDate:reason:方法,不同的CXCallEndedReason可以区分出不同的通话结束原因(被挂、主动挂断等)
[_provider reportCallWithUUID:_callInfo.uuid endedAtDate:nil reason:reason];

2.主叫流程

未加入callkit前:

向服务器发起主叫请求->显示通话界面->对方回应 ->建立话音通道,进行会话->挂断电话或被对方挂断

加入callkit框架后:

利用CXCallController告知系统发起一个主叫动作->系统回调performStartCallAction,进入通话界面,向服务器发起主叫请求->对方回应->建立话音通道,进行会话->挂断电话或被对方挂断

主叫流程代码示例

(1)利用CXCallController告知系统发起一个主叫动作,通过CXCallController的requestTransaction发起一个startCall动作

        CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callInfo.phoneNum];
        CXStartCallAction *action = [[CXStartCallAction alloc] initWithCallUUID:callInfo.uuid handle:handle];
        CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
        
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
        
        [_callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
            if (!error)
            {
                NRS_Log(@"0|============================     reportOutGoingCall success");
                _callInfo = callInfo;
                _callInfo.status = CallOuting;
            }
            else
            {
                NRS_Log(@"0|============================     reportOutGoingCall fail");
                _callInfo = nil;
            }
            
            completion(error);
        }];


(2) 系统触发回调performStartCallAction

-(void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
这里面你可以加载出你的通话界面,并向服务器发起主叫请求了。


(3)向服务器发起主叫请求,并等待对方回应

-(void)callFrom:(NSString *)number to:(NSString *)toNumber
   callback:(void (^)(BOOL isSuc)) callback
{
    if(iOS10)
        [_callKitManager reportStartingConnection];
    
    [CallService callFrom:number to:toNumber callback:^(BOOL isSuc) {
        if (iOS10)
        {
            if(isSuc)
            {
                [_callKitManager reportConnectionConnected];
            }
            else
            {
                [_callKitManager callByeForReason:CXCallEndedReasonFailed];
            }
        }

        callback(isSuc);
    }];
}

-(void)reportStartingConnection
{
    if(_callInfo && _callInfo.status == CallOuting)
    {
        [_provider reportOutgoingCallWithUUID:_callInfo.uuid startedConnectingAtDate:nil];
        
        //在系统通话记录中更新出具体名字
        CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
        callUpdate.localizedCallerName = _callInfo.name;
        [_provider reportCallWithUUID:_callInfo.uuid updated:callUpdate];
    }
}

-(void)reportConnectionConnected
{
    if(_callInfo && _callInfo.status == CallOuting)
    {
        [_provider reportOutgoingCallWithUUID:_callInfo.uuid connectedAtDate:nil];
    }
}

(3.1)发起主叫请求,并等待对方回应过程中,会调用

reportOutgoingCallWithUUID:startedConnectingAtDate:(这个方法并不是必须调用的)

reportOutgoingCallWithUUID:connectedAtDate:

这两个函数主要是为了更精确的告知系统实际有效的通话时长(连接过程,不应算在通话时长中),以便更好的在通话记录中显示

(3.2)回看步骤(1)这部分代码时,你会发现没有方式去告知系统被叫方的名称,此处通过CXCallUpdate,将被叫方姓名告知系统,以便在通话记录中更好显示


(4)建立话音通道,进行会话


(5)挂断电话或被对方挂断,调用CXProvider的reportCallWithUUID:endedAtDate:reason:方法。

3.关于挂断处理的一些说明

在callkit中还有一个对应的endCall动作,我看demo或别的资料里在结束通话后都向系统发出了endCall动作,但在我的最终整合中,我并未主动去抛出过这个动作(都是通过reportCallWithUUID:endedAtDate:reason方法告知系统通话结束的),主要原因如下:

挂断分以下几种情况:

1.被叫时在系统通话界面点击挂断,这种情况发生在程序外部,系统先于app得知挂断的产生,然后系统会回调performEndCallAction:方法反馈给app,app再做挂断处理

2.其余情况,被对方挂断,或是在app内点击挂断按钮这些情况,都是app先于系统得知挂断的产生,app直接就可以立刻对挂断进行处理了,没有必要再去抛出一个action,然后又通过回调返回客户端再去做处理

注:action抛出后,都会触发相应的回调。

3.当然,从流程尽量统一的设计原则上讲,抛出这个endAction动作,然后统一在performEndCallAction:中做挂断处理是比较合理的,但就我的项目实际而言,这部分改动相对有些麻烦,于是我就没有去做相应调整。

目前来看,我没有去使用endCallAction,也未出现任何的问题。

3.通讯录对接

callkit嵌入后,在系统通讯录中就会出现用你的app产生的通话记录,如果用户点击了相应记录,你的app应该可以被启动,并发出拨叫,要实现这个功能,只需要进行以下工作:

实现appdelegate的方法

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler
    INInteraction *interaction = [userActivity interaction];
    INIntent *intent = interaction.intent;
    if ([intent isKindOfClass:[INStartAudioCallIntent class]])
    {
        INStartAudioCallIntent *audioIntent = (INStartAudioCallIntent *)intent;
        INPerson *person = audioIntent.contacts.firstObject;
        NSString *phoneNum = person.personHandle.value;    
        [self reportOutGoingCall:phoneNum];
    }
解析出对应的电话号码,发起主叫流程即可

4.其他功能对接与支持

系统通话界面里的分组、静音、dtmf、免提(不用代码处理)等等功能,callkit都提供了相应的对接方式,这里不再赘述。









你可能感兴趣的:(ObjC,iOS)