iOS Call Kit for VOIP

最近苹果 iOS 10 新发布了一个新的框架Call Kit,使第三方VOIP类型语音通话类APP有了更好的展现方式和用户体验的提升,想深入学习一下。基于Apple Audio Unit及开源GCDAsyncSocket做了一个类似’VOIP’的语音通话Demo APP,实现了位于同一局域网下的两台苹果手机之间基础的语音通话功能(拔出、接听、拒接、挂断、双向对讲),然后翻看了下苹果官网SpeakerBox Demo及国外的少部分Call Kit相关技术资料,将Call Kit新特性加入到了这个通话Demo中,下面大概讲解一下实现思路及要点,示例图片基本都来自Apple的WWDC16的Call Kit Session里。

Call Kit 能做什么

在苹果官方WWDC16 Enhancing VoIP Apps with CallKit(Session 230)中,苹果的工程师为我们展示了集成Call Kit后的VOIP 通话APP的效果,例如在iPhone锁屏状态下APP来电时,通过Call Kit可以像iOS原生电话来电一样展现全屏的来电及接听界面,VOIP APP与系统Call有着相同的通话优先级别,而且在通讯录中的拔号记录,Siri唤起,勿扰模式等都有着很好的支持。

incoming call.png

下面这张图简略的描述了原生APP、第三方APP在Call Kit框架下的关系,Call Kit在系统中提供了一种独有的服务,在需要的时刻,原生或者第三方APP通过Call Kit提供的API向系统请求诸如来电、拔出等展现服务,由Call Service统一安排调度这些请求以达成统一的交互响应。

callkit architecture.png

如何使用Call Kit

简单来说,Call Kit就是提供了统一的壳(语音通话UI)及与该壳交互的API而已,实际的通话链路监听、搭建和管理还是APP原有的实现思路,下面以通话的来电、拔出等几个场景描述下APP与Call Kit的交互流程

Prepare

  1. 创建CXProvider,指定Call Kit展现UI中的APP名称和图标,通话数量,来电铃声等Configuration
  2. 实现CXProviderDelegate协议,以接收来自Call Service发来的更新状态,例如用户点击了接听或者挂断等动作,决定网络链路的处理动作;还有AudioSession的激活状态,决定Audio播放录制的启动关闭时机
  3. 创建CXCallController,使APP可以发送更新状态给Call Service,例如用户拔出电话或者对方挂断电话等状态
  4. 如果需要锁屏界面以及APP未启动状态下显示来电界面,要搭建PushKit通路,与APNS差不多,只是APP端处理方式有些不同。文章末尾有一些资料,这里不详细展开了。
prepare callkit.png

Incoming Call

  1. APP前台时收到来自网络Server端的连接请求,或者APP后台时收到来自网络Push Server的PushEvent
  2. APP收到连接请求或者Push消息后,创建CXCallUpdate对象,指定此次来电的号码等属性
  3. 将上面创建的CXCallUpdate对象通过CXProvider的reportNewIncomingCallWithUUID方法报告给iOS系统
  4. Call Service收到新的来电请求后根据当前的状态,展现原生来电UI
incoming call architecture 1.png

Answer Call

  1. 用户点击来电界面上的接听按钮
  2. Call Service通过CXProvider的Delegate协议performAnswerCallAction方法告知APP
  3. APP将接听命令通过网络传给对端,开始实际音频数据传输,进行通话
incoming call architecture 2.png

End Call

  1. 用户点击APP UI中的挂断按钮
  2. 创建CXEndCallAction对象,指定此次通话的UUID属性
  3. 创建CXTransaction,将刚刚的Action指定给它
  4. 通过CXCallController,调用requestTransaction将这个挂断事件通知给Call Service
  5. Call Service通过当前通话状态,通过CXProvider将挂断动作通知回给APP
  6. APP在CXProviderDelegate中的performEndCallAction中结束此次通话的网络链路,停止音频录制播放Loop
incoming call architecture 4.png

Outgoing Call

  1. 用户通过APP UI、通话录、Siri发起一个拔出请求
  2. 创建CXHandle指定拨出的电话号码,创建CXStartCallAction并将CXHandle指定给它
  3. 创建CXTransaction,将刚刚的Action指定给它
  4. 通过CXCallController,调用requestTransaction将这个拔出事件通知给Call Service
  5. Call Service收到新的拨出请求后根据当前的状态,通过CXProvider将拨出动作通知回APP,
  6. APP在CXProviderDelegate中的performStartCallAction中开启此次呼叫的网络链路,通过CXProvider的reportOutgoingCallWithUUID设置呼叫与接通时间,根据链路建立的结果来展现原生呼叫UI的状态(呼叫成功或者失败)
outgoing call.png

Demo APP (MyCall)

正如前面说的,我实现的这个Demo主要是为了实践Call Kit这个新Framework的功能,因此只具备最简单的点对点通话功能,没有很完善的网络通信协议、APP模块结构划分、通话低时延、路由切换、容错、PushKit通知等处理

Demo结构图

MyCall architecture.png

APP中总共封装有四个socket实例对象用来网络通信,其中两个负责监听来电的Server Player和Server Recorder,两个负责呼出的Client Player和Client Recorder,实际通话中只有两路socket处于收发状态。这四个链路对象统一由CallManager对象调度管理。AudioController基于Audio Unit封装,提供音频流的播放和采集功能。CallControoler封装了CallKit的CXCallController对象,ProviderDelegate封装了CXProvider对象,这两个对象负责与Call Service交互。UI比较简单,就是几个通话控制的按钮,拨号地址,通话状态显示这几个控件。

一些说明

  1. Call Kit开关,在Define中我加入了Call Kit开关,不使用Call Kit这个Demo的基本功能也可以正常运行,方便参照对比。
  2. 通信协议,其实基本上没有什么协议,APP启动后就开始监听Socket连接,没有身份注册验证等,有连接就接受开始准备收发音频PCM裸数据。只是为了实现一端拨出,对端选择接听或者拒接这一流程,增加了一个Accept标志位。
  3. 通话标识,由于此demo并不是真正的VOIP,不存在Server注册及身份号码标识,我以局域网的IP地址及固定端口号为"Phone Number",进行Socket连接。

Tips

  1. 开发者通过Call Kit与Service进行交互时需要形成闭环,例如你向Call Service提交了一个StartCallAction请求后,在某些条件下(例如对方挂断或者拒接)必须发送EndCallAction请求,来告诉Call Service此次通话已经结束,否则原生通话UI会显示呼叫失败
  2. 在CXProviderDelegate协议中收到Action并且处理完自己的逻辑后,要调用fulfill或者fail告诉Call Service用户引发的这个Action是成功还是失败
  3. Audio播放录制loop的启动一定要放在CXProviderDelegate的didActivateAudioSession中,而不是performStartCall或者performAnswerCall中,大概是因为Call Service本身也要播放铃声的关系
  4. 在Call Kit使用中,主要面对的就是CXProvider与CXCallController这两个类,尽量以两个层面看待问题,一个是UI层,用户通过APP UI或者APP自主发起的电话状态更新,基本都是通过CXCallController;另一个是Model层,即实际数据传输链接的搭建,甚至Audio Loop,都是在CXProvider的Delegate里完成。Call Kit的原生UI该怎样展现,取决于APP内网络链路的处理方式和结果

demo代码地址
download

参考资料
XAMARIN Introduction to CallKit
WWDC16 Session 230 (Enhancing VoIP Apps with CallKit)
Apple Call Kit Speakerbox Demo
PushKit Practice

推荐阅读更多精彩内容