Kealdish's Studio.

CallKit学习笔记

字数统计: 2.2k阅读时长: 8 min
2016/07/30 Share

概览

近几年4G大规模的普及使得VoIP(Voice over Internet Protocol)变得可靠和稳定。iOS 10开始全面支持VOIP,于是推出了CallKit框架。

CallKit框架能让你的第三方VoIP应用程序获得原声应用程序的体验效果。为了能更好得讲述CallKit新特性,我们以一个第三方VoIP应用程序——“Speakerbox”举例来说明。

在iOS 10之前,当用户收到“Speakerbox”的电话时的界面如同下图。在锁屏状态下,我们无法区分iMessage和VoIP电话的区别,它只是一则通知。如果想要接听“Speakerbox”中的来电,我们需要先右滑通知,输入密码,进入app才能接听电话。这是非常糟糕的体验。而在解锁状态下,来电会显示为一条横幅,我们有时候可能会错过这些通知,这样的体验也是很糟糕的。

在iOS 10中,“Speakerbox”可以像原生电话UI那样接听和显示VoIP通话。

Architecture

在iOS 10以前,像系统服务如蓝牙、Siri、CarPlay与VoIP的应用程序如“Speakerbox”是相互独立的两部分。在iOS 10中,通过CallKit框架可以将两者连接起来,现在在“Speakerbox”上发起的通信电话可以被系统获取然后系统会与目的设备之间建立起通信。

现在我们进一步来聊聊“Speakerbox”。在下图中,我们有“Speakerbox”以及它所有的代码,它与网络进行通信并有自己的UI。接下来,我们要与CallKit连接。这里有两个类需要我们着重去注意,分别是CXProviderCXCallController

CXProvider

Out-of-band nontifications
Not user actions
External events

  • incoming calls
CXCallController

Requests from app
Local user actions
Internal events

  • Start call

CXProvider的作用是告知系统所有的带外(out-of-band)通知信息。带外通知的意思是指非用户操作而是外在真实发生的事件,比如来电(incoming call)。

CXCallController的作用是让系统感知来自于app自身的请求。这里的请求是真实的用户操作就像内部事件,如开始通话操作。CXCallController会与系统上其他的通话产生相互影响。例如,用户已经处于通话状态了,而他还想通过“Speakerbox”再开启一个通话。这时候CXCallController会告诉系统用户的开启通话的操作,系统会告知通话提供者保持当前的通话,这样可以让“Speakerbox”开始它的通话。

所以,当CXProvider想与系统进行通信时它会使用CSXCallUpdate类,当系统想让“Speakerbox”接收用户操作,它会使用CXAction类来让“Speakerbox”知道。CXCallController会将这些用户操作装进CSTransaction对象中来告知系统用户产生的操作。

现在我们来看看来电的整体工作流程。现在比如说一个叫做Jane的人收到了来自他妈妈的来电通话请求。“Speakerbox”收到了来电,然后会创建CXCallUpdate对象并通过CXProvider发送给系统。接着,系统将来电告知包括UI的所有服务。如果Jane想应答,Jane可以通过通话界面的应答按钮接通通话。这时候系统会告诉“Speakerbox”只要它需要随时可以接通来电通话。如果Jane想通过操作app的UI来结束通话,CXCallController会将结束action打包进一个CXTransaction对象中并将其发送给系统。如果一切ok,系统会通过CXProvider将其发送给“Speakerbox”,这样“Speakerbox”就可以随时结束通话。

Incoming Call

下面以代码的形式简要概述来电发生的过程:

1.将来电信息通知到系统

1
provider.reportNewIncomingCall(with: UUID, update: CXCallUpdate) { error in /* ... */ }

2.用户点击通话按钮进行通话,ProviderDelegate会调用PerformAnswerCallAction方法。

1
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { /* ... */ }

3.当用户真正地在通话时,CXAnswerCallAction会执行fulfill方法。

1
answerCallAction.fulfill()

Call Actions

以下列出了所有的callAction类型:

  • CXAnswerCallAction
  • CXStartCallAction
  • CXEndCallAction
  • CXSetHeldCallAction
  • CXSetGroupCallAction
  • CXPlayDTMFCallAction
  • CXSetMutedCallAction
Multiple Call

接下来花点时间讲讲多来电通话时的管理。例如,现在“Speakerbox”已经处于通话状态了,这时候又有一个通话请求进来。

如果用户想要结束现在进行的通话并且回应刚进来的通话时,系统会给“Speakerbox”发送CXTransactionCXTransaction只是一个或多个action组合起来的集合,在现在这种情况下它就是endanswer操作的集合。一旦“Speakerbox”处理并执行集合中的action,系统就会了解到并且换UI。

Outgoing Call

再来看看去电的流程。要拨出电话首先要找到你需要联系的联系人再进行操作发起通话请求(start call intent)。intent是代表用户意愿操作的对象,被打包成NSUser activity后传回你的app中。app接收到开始通话的intent然后在intent信息的基础上构建开始通话action。我们通过CallController来执行和请求action,之后,CallController会将action传递给系统。如果通话请求被接受了,action会通过ProviderDelegate返回你的app中。最后,app通过网络执行必要的命令建立起通话连接。

我们来看下整个去电的生命周期。我们以执行开始通话操作为起始点。这时候通话处于starting状态,之后,我们会执行完action操作并将通话状态变成started。当通话的另一端响应通话时,我们会通知provider通话已经开始连接(connecting)。最后,我们告诉provider通话已经连接上了(connected)来通知系统两端可以开始互相说话。

最后我们以代码的形式来简要回顾一下去电的流程:

1.通过CallController请求开始通话action

1
callController.request(startCallTransaction) { error in /* ... */ }

2.action通过ProviderDelegate被接受,然后执行。

1
startCallAction.fulfill()

3.app通知call将其状态从connecting转变成connected。

1
2
provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate)
provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate)

API Details

Authorization

跟其他如contacts、core location的API类似,CallKit也需要向用户请求准许的权限。正因为如此,每次app启动之后,你的app要做的第一件事就是检查当前的授权状态。因为可能在上次启动app之后用户进入到设置中修改了权限。如果你发现你的app的授权状态是不允许的,你应该为你的app请求授权。系统会弹框向用户请求允许权限。最后,当你的app启动时,你应该去观察并监听任何授权状态的改变,这样你才能一直将最新的UI展示给你的用户。

Provider Configuration

Provider Configuration可以帮助你的app自定义通话界面。比如说,你的app在通话界面显示的名称,还有是否支持视频通话,指定显示在通话UI最后的button图片,点击这个button可以直接跳转到你的app中。

Actions Error

我们已经知道了在不发生错误的情况下action发生的过程。那么要是在执行过程中发生错误了呢?比如,网络连接发生异常,我们无法建立起通话连接。在这种情况下,action会执行失败。

其中的过程是这样的:action通知系统发生错误,系统转而通过像通话失败UI来告诉用户通话请求失败。与这些action错误连同一起的是action timeouts。系统的每个action都有其特定的过期时间,这些时间很重要,它们确保了被用户启动的action执行的顺畅。如果action执行超时,provider的代理方法会通知app来正确响应此时的操作。

System Restrictions

现实中你可能会遇到来电被拒。来电被拒绝的原因可能是用户禁用了你的app并且不再给予授权,可能是你的来电被用户列入黑名单中,可能是用户开启勿扰模式。在这些情况下,API中的competition handler会通知你的app。例如reportNewIncomingCallAPI会在它的completion handler传入一个error。

1
2
3
4
5
6
provider.reportNewIncomingCall(with: UUID(), update: CXCallUpdate()) { error in
if let incomingCallError = error,
incomingCallErrorCode = CXErrorCodeIncomingCallError(rawValue: incomingCallError.code) where incomingCallErrorCode == .filteredByDoNotDisturb {
// handle do not disturb
}
}
Audio

由于有CallKit,你的app的通话音频得到了很多好处。其中最大的好处就是你的音频会话的优先级比系统通话和Facetime更高,这意味着系统其它的app不能够打断你的app的音频通话。

现在以来电流程举例。我们知道当有来电时,app会收到应答action然后执行action。在接收action后是配置音频会话最佳的时间点,这是因为我们知道这时候通话即将变成connected状态。当我们执行应答action后,系统会优先自动为app开启音频会话,然后通过音频会话提供者的代理方法的回调来告诉app发生了什么。

CATALOG
  1. 1. 概览
  2. 2. Architecture
    1. 2.0.1. CXProvider
    2. 2.0.2. CXCallController
    3. 2.0.3. Incoming Call
    4. 2.0.4. Call Actions
    5. 2.0.5. Multiple Call
    6. 2.0.6. Outgoing Call
  • 3. API Details
    1. 3.0.1. Authorization
    2. 3.0.2. Provider Configuration
    3. 3.0.3. Actions Error
    4. 3.0.4. System Restrictions
    5. 3.0.5. Audio