iOS10适配之 CallKit

iOS10来了,iOS程序员们又有的忙了。

公司产品的核心功能是VoIP语/视频通话,为了与时俱进,就要适配iOS最新的CallKit。关于CallKit的介绍我就不详述了,大家可以去看看iOS开发文档WWDC或者直接Google。

总的来说,CallKit有三大优势:

1.提供系统通话界面,这一点在锁屏时体验最明显。

2.VoIP通话权限提升到系统级别,即不是随便被系统电话打断,而是可以选择拒接。

3.支持系统通讯记录沉淀与唤起。

从这三点“升级”可以看出苹果是非常看中VoIP的市场,现在我们可以像打系统电话一样使用VoIP了。

那么,我就开门见山的介绍一些API的使用吧。

CXProvider

The CXProvider class provides a programmatic interface to an object that represents a telephony provider. A CXProvider object is responsible for reporting out-of-band notifications that occur to the system.

我们首先要初始化一个单例的provider。其方法是

- (instancetype)initWithConfiguration:(CXProviderConfiguration *)configuration

这里的CXProviderConfiguration很重要,很多我们显式看到的信息都是在这里面配置好的。

@interface CXProviderConfiguration : NSObject <NSCopying>

//系统来电页面显示的app名称和系统通讯记录的信息
@property (nonatomic, readonly, copy) NSString *localizedName; 

//来电铃声
@property (nonatomic, strong, nullable) NSString *ringtoneSound;

//锁屏接听时,系统界面右下角的app图标,要求40 x 40大小
@property (nonatomic, copy, nullable) NSData *iconTemplateImageData; 

//最大通话组
@property (nonatomic) NSUInteger maximumCallGroups; // Default 2

//是否支持视频
@property (nonatomic) BOOL supportsVideo; // Default NO

//支持的Handle类型
@property (nonatomic, copy) NSSet<NSNumber *> *supportedHandleTypes;

@end

我们初始化provider之后还要设置它代理,以便执行CXProviderDelegate的方法。其方法是:

- (void)setDelegate:(nullable id<CXProviderDelegate>)delegate queue:(nullable dispatch_queue_t)queue;

queue一般直接指定为nil,即在main线程执行callback。

完成初始化之后,provider 就可以为我们服务了,这时候来了一个VoIP电话,那么它应该报告系统,好让系统按照它的配置弹出一个系统来电界面。其方法是:

- (void)reportNewIncomingCallWithUUID:(NSUUID *)UUID update:(CXCallUpdate *)update completion:(void (^)(NSError *_Nullable error))completion;

其中UUID是每次随机生成的,标记一次通话;CXCallUpdate有点类似CXConfiguration,也是一些配置信息。

@interface CXCallUpdate : NSObject <NSCopying>

//通话对方的Handle 信息
@property (nonatomic, copy, nullable) CXHandle *remoteHandle;

//对方的名字,可以设置为app注册的昵称
@property (nonatomic, copy, nullable) NSString *localizedCallerName;

//通话过程中再来电,是否支持保留并接听
@property (nonatomic) BOOL supportsHolding;

//是否支持键盘拨号
@property (nonatomic) BOOL supportsDTMF;

//本次通话是否有视频
@property (nonatomic) BOOL hasVideo;

@end

这些配置信息会影响锁屏时的接听界面上的按钮状态以及多个通话的选择界面。如果执行成功,completion中的error为nil, 否则,不会弹出系统界面。

由于非本地人为(文章最后解释)的因素导致的通话结束,需要报告系统通话结束的时间和原因。其方法是:

- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;

如果dateEnded为nil,则认为结束时间是现在。

我们还可以动态更改provider的配置信息CXCallUpdate,比如作为拨打方,开始没有地方配置通话的界面,就可以在通话开始时更新这些配置信息。 其方法是:

- (void)reportCallWithUUID:(NSUUID *)UUID updated:(CXCallUpdate *)update;

作为拨打方,我们还可以报告通话的状态,以便让系统知道我们app的VoIP真正的通话开始时间。

通话连接时:

- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID startedConnectingAtDate:(nullable NSDate *)dateStartedConnecting;

通话连接上:

- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID connectedAtDate:(nullable NSDate *)dateConnected;

CXCallController

The CXCallController class provides the programmatic interface for interacting with and observing calls.

初始化:

- (instancetype)initWithQueue:(dispatch_queue_t)queue

queue也是指定执行callback的线程,默认是main线程。

在开始或结束一次通话时,需要提交action事务请求,这些事务会交给上面的provider执行。

- (void)requestTransaction:(CXTransaction *)transaction completion:(void (^)(NSError *_Nullable error))completion;

Transaction可以通过三种方法添加Action:

- (instancetype)initWithActions:(NSArray<CXAction *> *)actions
- (instancetype)initWithAction:(CXAction *)action;
- (void)addAction:(CXAction *)action;

CXAction是CXCallAction的基类,常见的CXCallAction有:

CXCallAction Subclass Description
CXAnswerCallAction Answers an incoming call
CXStartCallAction Initiates an outgoing call
CXEndCallAction Ends a call
CXSetHeldCallAction Places a call on hold or removes a call from hold
CXSetGroupCallAction Groups a call with another call or removes a call from a group.
CXSetMutedCallAction Mutes or unmutes a call
CXPlayDTMFCallAction Plays a DTMF (dual tone multi frequency) tone sequence on a call

CXProviderDelegate

The CXProviderDelegate protocol defines methods that are called by a CXProvider object when a provider begins or reset, when a transaction is requested, when an action is performed, and when an audio session changes its activation state.

当拨打方成功发起一个通话后,会触发

- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;

当接听方成功接听一个电话时,会触发

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

当接听方拒接电话或者双方结束通话时,会触发

- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;

当点击系统通话界面的Mute按钮时,会触发

- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;

流程图

一个简单经典的CallKit 通话流程如下图:


CallKit经典通话流程

苹果官方现在还没有给出Callkit的完整文档,所以都是自己摸索,难免有很多坑。

  • 无声

刚开始做的时候,会偶然碰到无声的情况,这个时候发现可以在VoIP通话成功后直接结束系统的通话界面就有声音了。然后就这么很傻叉地做了,而且发现imo一开始也是这么做的。不过,这样肯定会带来问题,最简单的就是系统通话纪录的时长显示不对,因为它是按照callkit上报的开始和结束时间算的,这样毫无理由地结束当然显示错误。QQ最先写了一篇文章,讲到无声的处理方法是

在流程开始前setCategory为PlayAndRecord

突然发现自己的代码里也写了这句话,由于以前的代码逻辑就会处理这种音频问题,所以怀疑是冲突了,反正现在不是很懂,感觉小复杂,去掉就可以了。

  • 如何在系统通讯录中增加选项

既然可以沉淀到系统通话纪录中,就应该可以在通话纪录中直接呼出。那么长按系统通讯录中的“呼叫”如何显示我们自己的app名称呢?就像图中的Whatsup和SpeakerBox一样。


通话选项

其实这依赖于CXProviderConfiguration的一个配置项:

configuration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypePhoneNumber)];

为了支持安装app就生效,可以在AppDelegate.m的didFinishLaunchingWithOptions方法中去做这个配置。

  • 如何从系统通讯中直接呼出

上面解决了选项问题,那么为什么点击了app的名字没有任何反应呢?
这需要在AppDelegate.m的continueUserActivity方法中响应。

INInteraction *interaction = userActivity.interaction;
INIntent *intent = interaction.intent;
    
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"])
{
    INPerson *person = [(INStartAudioCallIntent *)intent contacts][0];
    CXHandle *handle = [[CXHandle alloc] initWithType:(CXHandleType)person.personHandle.type value:person.personHandle.value];
        
    [[CallKitManager sharedInstance] startCallAction:handle isVideo:NO];
    return YES;
} else if([userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
    INPerson *person = [(INStartVideoCallIntent *)intent contacts][0];
    CXHandle *handle = [[CXHandle alloc] initWithType:(CXHandleType)person.personHandle.type value:person.personHandle.value];
        
    [[CallKitManager sharedInstance] startCallAction:handle isVideo:YES];
    return YES;
}

另外,在reportNewIncomingCallWithUUID:update:completion:时要指定remoteHandle为对方的Handle。

  • 何种方式结束

上面的介绍,我们知道结束通话可以有两种方法:

//1
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
//2
requestTransaction:CXEndCallAction

那么它们有什么区别,该选择哪个呢?

这个问题我在stackoverflow上提问了,答案我觉得很清楚,在此感谢这位@user102008解惑!

You do requestTransactionwith a CXEndCallAction when the user actively chooses to end the call from your app's UI. You do
reportCallWithUUID:endedAtDate:reason:
when it ended not due to user action (i.e. not due to
provider:performEndCallAction:). If you take a look at the allowed
CXCallEndedReasons (failed, remote ended, unanswered, answered elsewhere, and declined elsewhere), they are all reasons not due to the user's action.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 国庆节过完了,回家好好休息一天,今天好好分享一下CallKit开发。最近发现好多吃瓜问CallKit的VoIP开发...
    井悟空阅读 9,170评论 68 21
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,201评论 0 17
  • 近日部门老大自费换电脑,最终在京东买了行货T470P,电脑、ssd、内存我俩一块选的。到货后,包装都没拆,直接让我...
    赵东伟同学阅读 5,724评论 0 1