资讯专栏INFORMATION COLUMN

如何结合 CallKit 和 Agora SDK 实现视频 VoIP 通话应用

happyhuangjinjin / 624人阅读

摘要:主要负责通话流程的控制,向系统注册通话和更新通话的连接状态等。主要负责执行对通话的操作。和结合下面我们看下怎么在一个使用的视频通话应用中集成。当发起通话时,在使用向系统注册通话后,系统会启动应用的,并将其优先级提高到运营商通话的级别。

作者简介:龚宇华,声网 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 最重要的类有两个,CXProviderCXCallController。这两个类是 CallKit 框架的核心。

CXProvider

CXProvider 主要负责通话流程的控制,向系统注册通话和更新通话的连接状态等。重要的 api 有下面这些:

open class CXProvider : NSObject {
    
    /// 初始化方法
    public init(configuration: CXProviderConfiguration)

    /// 设置回调对象
    open func setDelegate(_ delegate: CXProviderDelegate?, queue: DispatchQueue?)

    /// 向系统注册一个来电。如果注册成功,系统就会根据 CXCallUpdate 中的信息弹出来电画面
    open func reportNewIncomingCall(with UUID: UUID, update: CXCallUpdate, completion: @escaping (Error?) -> Swift.Void)

    /// 更新一个通话的信息
    open func reportCall(with UUID: UUID, updated update: CXCallUpdate)

    /// 告诉系统通话开始连接
    open func reportOutgoingCall(with UUID: UUID, startedConnectingAt dateStartedConnecting: Date?)

    /// 告诉系统通话连接成功
    open func reportOutgoingCall(with UUID: UUID, connectedAt dateConnected: Date?)

    /// 告诉系统通话结束
    open func reportCall(with UUID: UUID, endedAt dateEnded: Date?, reason endedReason: CXCallEndedReason)
}

可以看到,CXProvider 使用 UUID 来标识一个通话,使用 CXCallUpdate 类来设置通话的属性。开发者可以使用正确格式的字符串为每个通话创建对应的 UUID;也可以直接使用系统创建的 UUID

用户在系统界面上对通话进行的操作都通过 CXProviderDelegate 中的回调方法通知应用。

CXCallController

CXCallController 主要负责执行对通话的操作。

open class CXCallController : NSObject {

    /// 初始化方法
    public convenience init()
    
    /// 获取 callObserver,通过 callObserver 可以得到系统所有进行中的通话的 uuid 和通话状态
    open var callObserver: CXCallObserver { get }

    /// 执行对一个通话的操作
    open func request(_ transaction: CXTransaction, completion: @escaping (Error?) -> Swift.Void)
}

其中 CXTransaction 是一个操作的封装,包含了动作 CXAction 和通话 UUID。发起通话、接听通话、挂断通话、静音通话等动作都有对应的 CXAction 子类。

和 Agora SDK 结合

下面我们看下怎么在一个使用 Agora SDK 的视频通话应用中集成 CallKit。Demo 的完整代码见 Github 地址

实现视频通话

首先快速实现一个视频通话的功能。

使用 AppId 创建 AgoraRtcEngineKit 实例:

private lazy var rtcEngine: AgoraRtcEngineKit = AgoraRtcEngineKit.sharedEngine(withAppId: <#Your AppId#>, delegate: self)

设置 ChannelProfile 和本地预览视图:

override func viewDidLoad() {
    super.viewDidLoad()
        
    rtcEngine.setChannelProfile(.communication)
    
    let canvas = AgoraRtcVideoCanvas()
    canvas.uid = 0
    canvas.view = localVideoView
    canvas.renderMode = .hidden
    rtcEngine.setupLocalVideo(canvas)
}

AgoraRtcEngineDelegate 的远端用户加入频道事件中设置远端视图:

extension ViewController: AgoraRtcEngineDelegate {
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
        let canvas = AgoraRtcVideoCanvas()
        canvas.uid = uid
        canvas.view = remoteVideoView
        canvas.renderMode = .hidden
        engine.setupRemoteVideo(canvas)
        
        remoteUid = uid
        remoteVideoView.isHidden = false
    }
}

实现通话开始、静音、结束的方法:

extension ViewController {
    func startSession(_ session: String) {
        rtcEngine.startPreview()
        rtcEngine.joinChannel(byToken: nil, channelId: session, info: nil, uid: 0, joinSuccess: nil)
    }
    
    func muteAudio(_ mute: Bool) {
        rtcEngine.muteLocalAudioStream(mute)
    }
    
    func stopSession() {
        remoteVideoView.isHidden = true
        
        rtcEngine.leaveChannel(nil)
        rtcEngine.stopPreview()
    }
}

至此,一个简单的视频通话应用搭建就完成了。双方只要调用 startSession(_:) 方法加入同一个频道,就可以进行视频通话。

来电显示

我们首先创建一个专门的类 CallCenter 来统一管理 CXProviderCXCallController

class CallCenter: NSObject {
        
    fileprivate let controller = CXCallController()
    private let provider = CXProvider(configuration: CallCenter.providerConfiguration)
    
    private static var providerConfiguration: CXProviderConfiguration {
        let appName = "AgoraRTCWithCallKit"
        let providerConfiguration = CXProviderConfiguration(localizedName: appName)
        providerConfiguration.supportsVideo = true
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.maximumCallGroups = 1
        providerConfiguration.supportedHandleTypes = [.phoneNumber]
        
        if let iconMaskImage = UIImage(named: <#Icon file name#>) {
            providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage)
        }
        providerConfiguration.ringtoneSound = <#Ringtone file name#>
        
        return providerConfiguration
    }
}

其中 providerConfiguration 设置了 CallKit 向系统注册通话时需要的一些基本属性。比如 localizedName 告诉系统向用户显示应用的名称。iconTemplateImage 给系统提供一张图片,以在锁屏的通话界面中显示。ringtoneSound 是自定义来电响铃文件。

接着,我们创建一个接收到呼叫后把呼叫通过 CallKit 注册给系统的方法。

func showIncomingCall(of session: String) {
    let callUpdate = CXCallUpdate()
    callUpdate.remoteHandle = CXHandle(type: .phoneNumber, value: session)
    callUpdate.localizedCallerName = session
    callUpdate.hasVideo = true
    callUpdate.supportsDTMF = false
    
    let uuid = pairedUUID(of: session)
    
    provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
        if let error = error {
            print("reportNewIncomingCall error: (error.localizedDescription)")
        }
    })
}

简单起见,我们用对方的手机号码字符串做为通话 session 标示,并构造一个简单的 session 和 UUID 匹配查询系统。最后在调用了 CXProviderreportNewIncomingCall(with:update:completion:) 方法后,系统就会根据 CXCallUpdate 中的信息,弹出和运营商电话类似的界面提醒用户。用户可以接听或者拒接,也可以点击第六个按钮打开 app。

接听 /挂断通话

用户在系统界面上点击“接受”或“拒绝”按钮后,CallKit 会通过 CXProviderDelegate 的相关回调通知 app。

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    guard let session = pairedSession(of:action.callUUID) else {
        action.fail()
        return
    }
    
    delegate?.callCenter(self, answerCall: session)
    action.fulfill()
}

func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    guard let session = pairedSession(of:action.callUUID) else {
        action.fail()
        return
    }
    
    delegate?.callCenter(self, declineCall: session)
    action.fulfill()
}

通过回调传入的 CXAction 对象,我们可以知道用户的操作类型以及通话对应的 UUID。最后通过我们自己定义的 CallCenterDelegate 回调通知到 app 的 ViewController 中。

发起通话 /静音 /结束通话

使用 CXStartCallAction 构造一个 CXTransaction,我们就可以用 CXCallControllerrequest(_:completion:) 方法向系统注册一个发起的通话。

func startOutgoingCall(of session: String) {
    let handle = CXHandle(type: .phoneNumber, value: session)
    let uuid = pairedUUID(of: session)
    let startCallAction = CXStartCallAction(call: uuid, handle: handle)
    startCallAction.isVideo = true
    
    let transaction = CXTransaction(action: startCallAction)
    controller.request(transaction) { (error) in
        if let error = error {
            print("startOutgoingSession failed: (error.localizedDescription)")
        }
    }
}

同样的,我们可以用 CXSetMutedCallActionCXEndCallAction 来静音 /结束通话。

func muteAudio(of session: String, muted: Bool) {
    let muteCallAction = CXSetMutedCallAction(call: pairedUUID(of: session), muted: muted)
    let transaction = CXTransaction(action: muteCallAction)
    controller.request(transaction) { (error) in
        if let error = error {
            print("muteSession (muted) failed: (error.localizedDescription)")
        }
    }
}

func endCall(of session: String) {
    let endCallAction = CXEndCallAction(call: pairedUUID(of: session))
    let transaction = CXTransaction(action: endCallAction)
    controller.request(transaction) { error in
        if let error = error {
            print("endSession failed: (error.localizedDescription)")
        }
    }
}
模拟来电和呼叫

真实的 VoIP 应用需要使用信令系统或者 iOS 的 PushKit 推送,来实现通话呼叫。为了简单起见,我们在 Demo 上添加了两个按钮,直接模拟收到了新的通话呼叫和呼出新的通话。

private lazy var callCenter = CallCenter(delegate: self)

@IBAction func doCallOutPressed(_ sender: UIButton) {
    callCenter.startOutgoingCall(of: session)
}

@IBAction func doCallInPressed(_ sender: UIButton) {
    callCenter.showIncomingCall(of: session)
}

接着通过实现 CallCenterDelegate 回调,调用我们前面已经预先实现了的使用 Agora SDK 进行视频通话功能,一个完整的 CallKit 视频应用就完成了。

extension ViewController: CallCenterDelegate {
    func callCenter(_ callCenter: CallCenter, startCall session: String) {
        startSession(session)
    }
    
    func callCenter(_ callCenter: CallCenter, answerCall session: String) {
        startSession(session)
        callCenter.setCallConnected(of: session)
    }
    
    func callCenter(_ callCenter: CallCenter, declineCall session: String) {
        print("call declined")
    }
    
    func callCenter(_ callCenter: CallCenter, muteCall muted: Bool, session: String) {
        muteAudio(muted)
    }
    
    func callCenter(_ callCenter: CallCenter, endCall session: String) {
        stopSession()
    }
}

通话过程中在音频外放的状态下锁屏,会显示类似运营商电话的通话界面。不过可惜的是,目前 CallKit 还不支持像 FaceTime 那样的在锁屏下显示视频的功能。

通讯录 /系统通话记录

使用了 CallKit 的 VoIP 通话会出现在用户系统的通话记录中,用户可以像运营商电话一样直接点击通话记录发起新的 VoIP 呼叫。同时用户通讯录中也会有对应的选项让用户直接使用支持 CallKit 的应用发起呼叫。

实现这个功能并不复杂。无论用户是点击通信录中按钮,还是点击通话记录,系统都会启动打开对应 app,并触发 UIApplicationDelegateapplication(_:continue:restorationHandler:) 回调。我们可以在这个回调方法中获取到被用户点击的电话号码,并开始 VoIP 通话。

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
    guard let interaction = userActivity.interaction else {
        return false
    }
    
    var phoneNumber: String?
    if let callIntent = interaction.intent as? INStartVideoCallIntent {
        phoneNumber = callIntent.contacts?.first?.personHandle?.value
    } else if let callIntent = interaction.intent as? INStartAudioCallIntent {
        phoneNumber = callIntent.contacts?.first?.personHandle?.value
    }
    
    let callVC = window?.rootViewController as? ViewController
    callVC?.applyContinueUserActivity(toCall:phoneNumber)
    
    return true
}

extension ViewController {
    func applyContinueUserActivity(toCall phoneNumber: String?) {
        guard let phoneNumber = phoneNumber, !phoneNumber.isEmpty else {
            return
        }
        phoneNumberTextField.text = phoneNumber
        callCenter.startOutgoingCall(of: session)
    }
}
一些注意点

必需在项目的后台模式设置中启用 VoIP 模式,才可以正常使用 CallKit 的相关功能。这个模式需要在 Info.plist 文件的 UIBackgroundModes 字段下添加 voip 项来开启。 如果没有开启后台 VoIP 模式,调用 reportNewIncomingCall(with:update:completion:) 等方法不会有效果。

当发起通话时,在使用 CXStartCallAction 向系统注册通话后,系统会启动应用的 AudioSession,并将其优先级提高到运营商通话的级别。如果应用在这个过程中自己对 AudioSession 进行设置操作,很可能会导致 AudioSession 启动失败。所以应用需要等系统启动 AudioSession 完成,在收到 CXProviderDelegateprovider(_:didActive:) 回调后,再进行 AudioSession 相关的设置。我们在 Demo 中是通过 Agora SDK 的 disableAudio()enableAudio() 等接口来处理这部分逻辑的。

集成 CallKit 后,VoIP 来电也会和运营商电话一样受到用户系统 “勿扰” 等设置的影响。

在听筒模式下按锁屏键,系统会按照挂断处理。这个行为也和运营商电话一致。

最后再次附上完整 Demo Github 地址

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/16974.html

相关文章

  • Qcon 演讲纪实:详解如何在实时视频通话实现AR功能

    摘要:声网首席研发工程师,端移动应用产品设计和技术架构负责人龚宇华,受邀分享了基于和,在实时视频通话中实现功能,在演讲中剖析了与差异,的工作原理,以及逐步讲解如何基于与声网创建视频会议场景。实现视频通话功能我们可以通过声网来快速实现视频通话。 2018年4月20日-22日,由 infoQ 主办的 Qcon 2018全球软件开发大会在北京如期举行。声网首席 iOS 研发工程师,iOS 端移动应...

    gitmilk 评论0 收藏0
  • Qcon 演讲纪实:详解如何在实时视频通话实现AR功能

    摘要:声网首席研发工程师,端移动应用产品设计和技术架构负责人龚宇华,受邀分享了基于和,在实时视频通话中实现功能,在演讲中剖析了与差异,的工作原理,以及逐步讲解如何基于与声网创建视频会议场景。实现视频通话功能我们可以通过声网来快速实现视频通话。 2018年4月20日-22日,由 infoQ 主办的 Qcon 2018全球软件开发大会在北京如期举行。声网首席 iOS 研发工程师,iOS 端移动应...

    szysky 评论0 收藏0
  • 实践解析:Electron实现跨平台视频会议的几种思路

    摘要:而现在我们可以利用多种工具框架进行跨平台开发。实现视频会议的几种思路如何利用实现一个视频会议应用这主要取决于使用什么技术来实现作为业务核心的部分。通过与技术结合,实现了网页端多方音视频通讯,可以快速实现部分的开发。 作者简介:张乾泽,声网 Agora Web 研发工程师 对于在线教育、医疗、视频会议等场景来讲,开发面向 Windows、Mac 的跨平台客户端是必不可少的一步。在过去,每...

    xi4oh4o 评论0 收藏0
  • Android端实现多人音视频聊天应用(二):实现多人通话

    摘要:我们先实现一个瀑布流瀑布流的实现方式很多,本文采用结合的来实现。有了一个可用的瀑布流之后,下面我们就可以实现动态聊天窗了动态聊天窗的要点在于的大小由视频的宽高比决定,因此及其对应的就该注意不要写死尺寸。 作者:声网用户,资深Android工程师吴东洋本系列文章分享了基于Agora SDK 2.1实现多人视频通话的实践经验。 在上一篇《Android 多人视频聊天应用的开发(一)一对一聊...

    The question 评论0 收藏0
  • Android端实现多人音视频聊天应用(一)

    作者:声网Agora用户,资深Android开发者吴东洋。本系列文章分享了基于Agora SDK 2.1实现多人视频通话的实践经验。 自从2016年,鼓吹互联网寒冬的论调甚嚣尘上,2017年亦有愈演愈烈之势。但连麦直播、在线抓娃娃、直播问答、远程狼人杀等类型的项目却异军突起,成了投资人的风口,创业者的蓝海和用户的必装App,而这些方向的项目都有一个共同的特点——都依赖视频通话和全互动直播技术。 声...

    raoyi 评论0 收藏0

发表评论

0条评论

happyhuangjinjin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<