如何结合 CallKit 和 Agora SDK 实现视频 VoIP 通话应用
作者简介:龚宇华,声网Agora.io 首席 iOS 研发工程师,负责 iOS 端移动应用产品设计和技术架构。
CallKit 是苹果在 iOS10 中推出的,专为 VoIP 通话场景设计的系统框架,在 iOS 上为 VoIP 通话提供了系统级的支持。在 iOS10 以前,VoIP 场景的体验存在很多局限。比如没有专门的来电呼叫通知方式,App 在后台接收到来电呼叫后,只能使用一般的系统通知方式提示用户。如果用户关掉了通知权限,就会错过来电。VoIP 通话本身也很容易被打断。比如用户在通话过程中打开了另一个使用音频设备的应用,或者接到了一个运营商电话,VoIP 通话就会被打断。为了改善 VoIP 通话的用户体验问题,CallKit 框架在系统层面把 VoIP 通话提高到了和运营商通话一样的级别。当 App 收到来电呼叫后,可以通过 CallKit 把 VoIP 通话注册给系统,让系统使用和运营商电话一样的界面提示用户。在通话过程中,app 的音视频权限也变成和运营商电话一样,不会被其他应用打断。VoIP 通话过程中接到运营商电话时,在界面上由用户自己选择是否挂起/挂断当前的 VoIP 通话。另外,使用了 CallKit 框架的 VoIP 通话也会和运营商电话一样出现在系统的电话记录中。用户可以直接在通讯录和电话记录中发起新的 VoIP 呼叫。因此,一个有 VoIP 通话场景的应用应该尽快集成 CallKit,以大幅提高用户体验和使用便捷性。下面我们就来看下 CallKit 的使用方法,并且把它集成到一个使用 Agora SDK 的视频通话应用中。
CallKit 基本类介绍
CallKit 最重要的类有两个,
CXProvider和
CXCallController。这两个类是 CallKit 框架的核心。
CXProvider
CXProvider主要负责通话流程的控制,向系统注册通话和更新通话的连接状态等。重要的 api 有下面这些:
1open class CXProvider : NSObject {可以看到,
2 /// 初始化方法
3 public init(configuration: CXProviderConfiguration)
4 /// 设置回调对象
5 open func setDelegate(_ delegate: CXProviderDelegate?, queue: DispatchQueue?)
6 /// 向系统注册一个来电。如果注册成功,系统就会根据 CXCallUpdate 中的信息弹出来电画面
7 open func reportNewIncomingCall(with UUID: UUID, update: CXCallUpdate, completion: @escaping (Error?) -> Swift.Void)
8 /// 更新一个通话的信息
9 open func reportCall(with UUID: UUID, updated update: CXCallUpdate)
10 /// 告诉系统通话开始连接
11 open func reportOutgoingCall(with UUID: UUID, startedConnectingAt d 3ff7 ateStartedConnecting: Date?)
12 /// 告诉系统通话连接成功
13 open func reportOutgoingCall(with UUID: UUID, connectedAt dateConnected: Date?)
14 /// 告诉系统通话结束
15 open func reportCall(with UUID: UUID, endedAt dateEnded: Date?, reason endedReason: CXCallEndedReason)
16}
CXProvider使用
UUID来标识一个通话,使用
CXCallUpdate类来设置通话的属性。开发者可以使用正确格式的字符串为每个通话创建对应的
UUID;也可以直接使用系统创建的
UUID。用户在系统界面上对通话进行的操作都通过
CXProviderDelegate中的回调方法通知应用。
CXCallController
CXCallController主要负责执行对通话的操作。
1open class CXCallController : NSObject {其中
2 /// 初始化方法
3 public convenience init()
4 /// 获取 callObserver,通过 callObserver 可以得到系统所有进行中的通话的 uuid 和通话状态
5 open var callObserver: CXCallObserver { get }
6 /// 执行对一个通话的操作
7 open func request(_ transaction: CXTransaction, completion: @escaping (Error?) -> Swift.Void)
8}
CXTransaction是一个操作的封装,包含了动作
CXAction和通话
UUID。发起通话、接听通话、挂断通话、静音通话等动作都有对应的
CXAction子类。
和 Agora SDK 结合
下面我们看下怎么在一个使用 Agora SDK 的视频通话应用中集成 CallKit。
实现视频通话
首先快速实现一个视频通话的功能。使用 AppId 创建
AgoraRtcEngineKit实例:
1private lazy var rtcEngine: AgoraRtcEngineKit = AgoraRtcEngineKit.sharedEngine(withAppId: <#Your AppId#>, delegate: self)设置 ChannelProfile 和本地预览视图:
1override func viewDidLoad() {在
2 super.viewDidLoad()
3 rtcEngine.setChannelProfile(.communication)
4 let canvas = AgoraRtcVideoCanvas()
5 canvas.uid = 0
6 canvas.view = localVideoView
7 canvas.renderMode = .hidden
8 rtcEngine.setupLocalVideo(canvas)
9}
AgoraRtcEngineDelegate的远端用户加入频道事件中设置远端视图:
1extension ViewController: AgoraRtcEngineDelegate {实现通话开始、静音、结束的方法:
2 func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
3 let canvas = AgoraRtcVideoCanvas()
4 canvas.uid = uid
5 canvas.view = remoteVideoView
6 canvas.renderMode = .hidden
7 engine.setupRemoteVideo(canvas)
8 remoteUid = uid
9 remoteVideoView.isHidden = false
10 }
11}
1extension ViewController {至此,一个简单的视频通话应用搭建就完成了。双方只要调用
2 func startSession(_ session: String) {
3 rtcEngine.startPreview()
4 rtcEngine.joinChannel(byToken: nil, channelId: session, info: nil, uid: 0, joinSuccess: nil)
5 }
6 func muteAudio(_ mute: Bool) {
7 rtcEngine.muteLocalAudioStream(mute)
8 }
9 func stopSession() {
10 remoteVideoView.isHidden = true
11 rtcEngine.leaveChannel(nil)
12 rtcEngine.stopPreview()
13 }
14}
startSession(_:)方法加入同一个频道,就可以进行视频通话。
来电显示
我们首先创建一个专门的类
CallCenter来统一管理
CXProvider和
CXCallController。
1class CallCenter: NSObject {其中
2 fileprivate let controller = CXCallController()
3 private let provider = CXProvider(configuration: CallCenter.providerConfiguration)
4 private static var providerConfiguration: CXProviderConfiguration {
5 let appName = "AgoraRTCWithCallKit"
6 let providerConfiguration = CXProviderConfiguration(localizedName: appName)
7 providerConfiguration.supportsVideo = true
8 providerConfiguration.maximumCallsPerCallGroup = 1
9 providerConfiguration.maximumCallGroups = 1
10 providerConfiguration.supportedHandleTypes = [.phoneNumber]
11 if let iconMaskImage = UIImage(named: <#Icon file name#>) {
12 providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage)
13 }
14 providerConfiguration.ringtoneSound = <#Ringtone file name#>
15 return providerConfiguration
16 }
17}
providerConfiguration设置了 CallKit 向系统注册通话时需要的一些基本属性。比如
localizedName告诉系统向用户显示应用的名称。
iconTemplateImage给系统提供一张图片,以在锁屏的通话界面中显示。
ringtoneSound是自定义来电响铃文件。接着,我们创建一个接收到呼叫后把呼叫通过 CallKit 注册给系统的方法。
1func showIncomingCall(of session: String) {简单起见,我们用对方的手机号码字符串做为通话 session 标示,并构造一个简单的 session 和 UUID 匹配查询系统。最后在调用了
2 let callUpdate = CXCallUpdate()
3 callUpdate.remoteHandle = CXHandle(type: .phoneNumber, value: session)
4 callUpdate.localizedCallerName = session
5 callUpdate.hasVideo = true
6 callUpdate.supportsDTMF = false
7 let uuid = pairedUUID(of: session)
8 provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
9 if let error = error {
10 print("reportNewIncomingCall error: \(error.localizedDescription)")
11 }
12 })
13}
CXProvider的
reportNewIncomingCall(with:update:completion:)方法后,系统就会根据
CXCallUpdate中的信息,弹出和运营商电话类似的界面提醒用户。用户可以接听或者拒接,也可以点击第六个按钮打开 app。
接听/挂断通话
用户在系统界面上点击“接受”或“拒绝”按钮后,CallKit 会通过
CXProviderDelegate的相关回调通知 app。
1func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {通过回调传入的
2 guard let session = pairedSession(of:action.callUUID) else {
3 action.fail()
4 return
5 }
6 delegate?.callCenter(self, answerCall: session)
7 action.fulfill()
8}
9func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
10 guard let session = pairedSession(of:action.callUUID) else {
11 action.fail()
12 return
13 }
14 delegate?.callCenter(self, declineCall: session)
15 action.fulfill()
16}
CXAction对象,我们可以知道用户的操作类型以及通话对应的 UUID。最后通过我们自己定义的
CallCenterDelegate回调通知到 app 的
ViewController中。
发起通话/静音/结束通话
使用
CXStartCallAction构造一个
CXTransaction,我们就可以用
CXCallController的
request(_:completion:)方法向系统注册一个发起的通话。
1func startOutgoingCall(of session: String) {同样的,我们可以用
2 let handle = CXHandle(type: .phoneNumber, value: session)
3 let uuid = pairedUUID(of: session)
4 let startCallAction = CXStartCallAction(call: uuid, handle: handle)
5 startCallAction.isVideo = true
6 let transaction = CXTransaction(action: startCallAction)
7 controller.request(transaction) { (error) in
8 if let error = error {
9 print("startOutgoingSession failed: \(error.localizedDescription)")
10 }
11 }
12}
CXSetMutedCallAction和
CXEndCallAction来静音/结束通话。
1func muteAudio(of session: String, muted: Bool) {
2 let muteCallAction = CXSetMutedCallAction(call: pairedUUID(of: session), muted: muted)
3 let transaction = CXTransaction(action: muteCallAction)
4 controller.request(transaction) { (error) in
5 if let error = error {
6 print("muteSession \(muted) failed: \(error.localizedDescription)")
7 }
8 }
9}
10func endCall(of session: String) {
11 let endCallAction = CXEndCallAction(call: pairedUUID(of: session))
12 let transaction = CXTransaction(action: endCallAction)
13 controller.request(transaction) { error in
14 if let error = error {
15 print("endSession failed: \(error.localizedDescription)")
16 }
17 }
18}
模拟来电和呼叫
真实的 VoIP 应用需要使用信令系统或者 iOS 的 PushKit 推送,来实现通话呼叫。为了简单起见,我们在 Demo 上添加了两个按钮,直接模拟收到了新的通话呼叫和呼出新的通话。1private lazy var callCenter = CallCenter(delegate: self)接着通过实现
2@IBAction func doCallOutPressed(_ sender: UIButton) {
3 callCenter.startOutgoingCall(of: session)
4}
5@IBAction func doCallInPressed(_ sender: UIButton) {
6 callCenter.showIncomingCall(of: session)
7}
CallCenterDelegate回调,调用我们前面已经预先实现了的使用 Agora SDK 进行视频通话功能,一个完整的 CallKit 视频应用就完成了。
1extension ViewController: CallCenterDelegate {
2 func callCenter(_ callCenter: CallCenter, startCall session: String) {
3 startSession(session)
4 }
5 func callCenter(_ callCenter: CallCenter, answerCall session: String) {
6 startSession(session)
7 callCenter.setCallConnected(of: session)
8 }
9 func callCenter(_ callCenter: CallCenter, declineCall session: String) {
10 print("call declined")
11 }
12 func callCenter(_ callCenter: CallCenter, muteCall muted: Bool, session: String) {
13 muteAudio(muted)
14 }
15 func callCenter(_ callCenter: CallCenter, endCall session: String) {
16 stopSession()
17 }
18}
通话过程中在音频外放的状态下锁屏,会显示类似运营商电话的通话界面。不过可惜的是,目前 CallKit 还不支持像 FaceTime 那样的在锁屏下显示视频的功能。
通讯录/系统通话记录
使用了 CallKit 的 VoIP 通话会出现在用户系统的通话记录中,用户可以像运营商电话一样直接点击通话记录发起新的 VoIP 呼叫。同时用户通讯录中也会有对应的选项让用户直接使用支持 CallKit 的应用发起呼叫。 实现这个功能并不复杂。无论用户是点击通信录中按钮,还是点击通话记录,系统都会启动打开对应 app,并触发
UIApplicationDelegate的
application(_:continue:restorationHandler:)回调。我们可以在这个回调方法中获取到被用户点击的电话号码,并开始 VoIP 通话。
1func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
2 guard let interaction = userActivity.interaction else {
3 return false
4 }
5 var phoneNumber: String?
6 if let callIntent = interaction.intent as? INStartVideoCallIntent {
7 phoneNumber = callIntent.contacts?.first?.personHandle?.value
8 } else if let callIntent = interaction.intent as? INStartAudioCallIntent {
9 phoneNumber = callIntent.contacts?.first?.personHandle?.value
10 }
11 let callVC = window?.rootViewController as? ViewController
12 callVC?.applyContinueUserActivity(toCall:phoneNumber)
13 return true
14}
15extension ViewController {
16 func applyContinueUserActivity(toCall phoneNumber: String?) {
17 guard let phoneNumber = phoneNumber, !phoneNumber.isEmpty else {
18 return
19 }
20 phoneNumberTextField.text = phoneNumber
21 callCenter.startOutgoingCall(of: session)
22 }
23}
一些注意点
- 必需在项目的后台模式设置中启用 VoIP 模式,才可以正常使用 CallKit 的相关功能。这个模式需要在
Info.plist
文件的UIBackgroundModes
字段下添加voip
项来开启。 如果没有开启后台 VoIP 模式,调用reportNewIncomingCall(with:update:completion:)
等方法不会有效果。 当发起通话时,在使用
CXStartCallAction
向系统注册通话后,系统会启动应用的 AudioSession,并将其优先级提高到运营商通话的级别。如果应用在这个过程中自己对 AudioSession 进行设置操作,很可能会导致 AudioSession 启动失败。所以应用需要等系统启动 AudioSession 完成,在收到CXProviderDelegate
的provider(_:didActive:)
回调后,再进行 AudioSession 相关的设置。我们在 Demo 中是通过 Agora SDK 的disableAudio()
和enableAudio()
等接口来处理这部分逻辑的。集成 CallKit 后,VoIP 来电也会和运营商电话一样受到用户系统 “勿扰” 等设置的影响。
在听筒模式下按锁屏键,系统会按照挂断处理。这个行为也和运营商电话一致。
进入 Github 查看完整代码:https://github.com/AgoraIO/Agora-RTC-With-CallKit
- 如何结合 CallKit 和 Agora SDK 实现视频 VoIP 通话应用
- 实时语音视频通话SDK如何实现立体声(二)
- 实时语音视频通话SDK如何实现听声辨位
- 使用 Agora SDK 实现视频对话应用 HouseParty-附 Android 源码
- 实时语音视频通话SDK如何实现立体声(一)
- Agora 教程 | 如何使用 Qt 开发视频通话应用
- 如何实现安全的音视频通话
- C#-接入声网SDK实现网页版1对1视频通话以及边录制边上传
- AnyChatSDK 实现视频通话
- PHP如何实现阿里云短信sdk灵活应用在项目中的方法
- Android 8.0 中如何实现视频通话的画中画模式的示例
- 如何实现Linux平台的视频通话
- 语音视频SDK如何实现超低延迟优化?
- 语音视频SDK的回声消除技术是如何实现的
- Android利用环信SDK 3.x实现1对1视频通话
- 如何实现webwork+spring+hibernate框架的结合应用
- 【FFMpeg视频开发与应用基础】六、调用FFMpeg SDK实现视频文件的转封装
- 飞信的SDK应用与PowerTalk的结合(带视频演示和代码下载),IM,asp.net,客服,聊天示例
- Sipdroid中的视频通话是如何实现的?
- 【FFMpeg视频开发与应用基础】七、 调用FFMpeg SDK实现视频水印