在 iOS 上使用 CallKit 和 WebRTC 掌握 Voip 音频

大家好!我叫 Kostya,是一名 iOS 开发人员。在过去的几个月里,我一直在使用以下协议栈开发点对点 (P2P) 通话功能: WebRTC、PushKit 和 CallKit。今天,我将不介绍如何使用 WebRTC 实现 VoIP 通话,而只介绍如何使用 CallKit 和 WebRTC 处理音频。

我花了大约两周时间解决与音频和 AVAudioSession 相关的问题,希望这篇文章能帮助您在使用音频进行 VoIP 通话时节省时间。让我们开始吧!

在 iOS 上使用 CallKit 和 WebRTC 掌握 Voip 音频

关键概念

CallKit

CallKit 是苹果公司的框架,可让 VoIP 应用程序与本地电话用户界面紧密集成。它有助于管理与通话相关的交互,如接听电话、管理通话记录和处理通话中断。使用 CallKit 可以让 VoIP 通话感觉像普通电话一样,从而增强用户体验。

WebRTC

WebRTC 是一个开源项目,通过简单的 API 提供实时通信功能。它广泛用于构建需要通过点对点连接进行音频、视频和数据通信的应用程序。

CallKit 提供商概述

让我们先来概述一下 CallKit 提供的与音频相关的委托方法:

// 当提供程序执行
指定的开始呼叫操作时调用。
func  provider(
  _  provider:CXProvider,
  perform  action:CXStartCallAction
)

// 当提供程序执行
指定的应答呼叫操作时调用。
func  provider(
  _  provider:CXProvider,
  perform  action:CXAnswerCallAction
)

// 当提供程序执行
指定的设置静音呼叫操作时调用。
func  provider(
  _  provider:CXProvider,
  perform  action:CXSetMutedCallAction
)

// 当提供程序的
音频会话激活状态改变时调用
func  provider(
  _  provider:CXProvider,
  didActivate  audioSession:AVAudioSession
)

// 当提供程序的
音频会话停用状态改变时调用
func  provider(
  _  provider:CXProvider,
  didDeactivate  audioSession:AVAudioSession
)

本文我们只关注这些方法。不会对每种方法进行详细介绍,只是在我们的音频工作中对它们有所触及。

配置音频

首先,我们需要配置音频会话,以便在通话时正常工作。此时我们会发现一些问题。那么,我们应该在哪里以及如何配置音频会话呢?

最初,我尝试使用 AVAudioSession 的 setCategory 方法进行一些配置:

func setupIncorrectAudioConfiguration() {
  let audioSession = AVAudioSession.sharedInstance()
  do {
    try audioSession.setCategory(
      .playbackAndRecord,
      mode: .voiceChat,
      options: [.mixWithOthers]
    )
  } catch {
    print(error)
  }
}

虽然这种配置可以正常工作,但我在 CallKit UI 屏幕上的扬声器按钮上遇到了一个问题。点击扬声器按钮后,它一会儿变为选定状态(扬声器输出),一会儿又重置为未选定状态(接收器输出)。这让人很沮丧。我花了很多时间寻找适合我这种情况的解决方案,最后终于找到了。

解决方案是使用 WebRTC 的音频配置。在底层,它使用.playAndRecord类别和.voiceChat模式。这与我使用 AVAudioSession的方法类似,但这种方法对我有效:

func  setupCorrectAudioConfiguration () { 
  let rtcAudioSession: RTCAudioSession .sharedInstance() 
  rtcAudioSession.lockForConfiguration() 

  let configuration =  RTCAudioSessionConfiguration .webRTC() 
  configuration.categoryOptions = [ 
      .allowBluetoothA2DP, 
      .duckOthers, 
      .allowBluetooth, 
      .mixWithOthers 
    ] 

  do { 
    try rtcAudioSession.setConfiguration(configuration) 
  } catch { 
    print (error) 
  } 

  rtcAudioSession.unlockForConfiguration() 
}

要应用配置,需要激活音频会话。我是通过 WebRTC 的音频会话来实现这一点的:

func setAudioSessionActive(_ active: Bool) {
  let rtcAudioSession: RTCAudioSession.sharedInstance()
  rtcAudioSession.lockForConfiguration()

  do {
    try rtcAudioSession.setActive(active)
  } catch {
    print(error)
  }

  rtcAudioSession.unlockForConfiguration()
}

激活音频会话的方法如下:

func  activateAudioSession () { 
  setupCorrectAudioConfiguration() 
  setAudioSessionActive( true ) 
}

但我应该在哪里调用 activateAudioSession 方法呢?让我们参考 Apple CallKit 文档

对于呼出电话,请在 CXStartCallAction 的执行方法中配置音频会话:

func provider(_: CXProvider, perform action: CXStartCallAction) {
  activateAudioSession()
  ...
}

对于来电,请在CXAnswerCallActionperform方法中配置音频会话:

func provider(_: CXProvider, perform action: CXAnswerCallAction) {
  activateAudioSession()
  ...
}

但这对我来说根本不起作用。于是,我求助于 Google 寻找解决方案。我发现应该在 didActivate 委托方法中配置音频会话:

func  provider(_:CXProvider,didActivate  audioSession:AVAudioSession){ 
  activateAudioSession()
}

成功了!不过,在测试过程中,我遇到了另一个问题。当我点击 CallKit UI 上的扬声器按钮(切换到扬声器),然后建立了两个用户之间的连接时,扬声器按钮重置为接收器。

以下是问题的解决方案:

在 iOS 上使用 CallKit 和 WebRTC 掌握 Voip 音频

我再次求助于谷歌。我尝试了不同的音频会话配置,并试图在不同的地方配置音频会话,但这些想法都不适合我。最终,我找到了另一种解决方案。

首先,我在 WebRTC 中启用了手动音频配置。这样,我就可以在需要时管理音频会话,而不是在 WebRTC 框架决定这样做时。

func application(_, didFinishLaunchingWithOptions:) {
  RTCAudioSession.sharedInstance().useManualAudio = true
  ...
}

然后,我对音频会话的启动方式稍作修改:

func  setAudioSessionActive ( _  active : Bool ) { 
  let rtcAudioSession: RTCAudioSession .sharedInstance() 

  rtcAudioSession.lockForConfiguration() 
  do { 
    try rtcAudioSession.setActive(active) 
    rtcAudioSession.isAudioEnabled = active // 添加了此行
  } catch { 
    print (error) 
  } 
  rtcAudioSession.unlockForConfiguration() 
}

嘭,它如期工作了。轻按扬声器按钮后,音频端口从受话器切换到扬声器,并在所有通话状态下返回。

在通话过程中调用 AVAudioSession.sharedInstance().setActive() 可能会影响音频,导致其停止工作。为避免此问题,请确保正确管理音频会话状态。

重置音频

通话结束后,应重置音频配置。

为此,我定义了一个使用 AVAudioSession 重置音频配置的函数:

func  resetAudioConfiguration () { 
  let audioSession =  AVAudioSession .sharedInstance() 
  do { 
    try audioSession.setCategory( 
      .playback, 
      mode: .default, 
      options: [.mixWithOthers] 
    ) 
  } catch { 
    print (error) 
  } 
}

在触发 didDeactivate 委托方法时,应重置音频会话。

func deactivateAudioSession() {
  resetAudioConfiguration()
  setAudioSessionActive(false)
}

func provider(_: CXProvider, didDeactivate audioSession: AVAudioSession) {
  deactivateAudioSession()
}

执行 CXStartCallAction 和 CXAnswerCallAction

让我们来谈谈 CXStartCallAction 和 CXAnswerCallAction,以及它们与音频的关系。

CXStartCallAction 和 CXAnswerCallAction 都有两个方法:.fill() 和 .fail()。调用 .fail 会以 .failed 作为终止原因结束呼叫。调用 .fulfill() 会触发以下委托方法:

func provider(_: CXProvider, didActivate audioSession: AVAudioSession) {}

关于 .fulfill() 方法的一些说明:调用 CXAnswerCallAction 的 .fulfill() 将把 CallKit UI 上的文本从 “AppName Audio connecting… “更改为 “AppName Audio – timer”。

处理静音/取消静音

上面我提到了 CallKit 提供者委托方法,你可能会注意到这里有一个带有 mute(静音) 字样的方法。让我们看看 CXSetMutedCallAction perform 方法。

当你在 CallKit UI 上更改静音按钮状态时,就会触发该方法。下面是我的处理方法:

func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
  ...
  if action.isMuted {
      call.mute()
  } else {
      call.unmute()
  }
  
  action.fulfill()
  ...
}

但如何以编程方式更改静音按钮的状态呢?您只需创建一个 CXSetMutedCallAction,并在 CXCallController 对象上用此操作请求一个事务。如果事务请求成功,就会触发 CXSetMutedCallAction 的委托方法。

下面是一个执行静音操作的方法:

let controller = CXCallController()

func performMutedAction(
  for uuid: UUID, 
  muted: Bool, 
  completion: @escaping (Error?) -> Void
) {
  let action = CXSetMutedCallAction(call: uuid, muted: muted)
  controller.requestTransaction(with: action, completion: completion)
}

处理音频路由更改

最后一章是关于处理音频路由更改。如果再次查看 CallKit 的提供者委托方法,您将找不到任何可以帮助处理音频路由更改的方法。这是合乎逻辑的,因为处理这些更改并不是 CallKit 的职责。

但如何处理音频路由更改呢?需要使用 AVAudioSessionAVAudioSession.routeChangeNotification

首先,需要开始观察 routeChangeNotification

NotificationCenter.default.addObserver(
  self,
  selector: #selector(handleRouteChange),
  name: AVAudioSession.routeChangeNotification,
  object: nil
)

下一步是添加 handleRouteChange 实现:

let audioSession =  AVAudioSession .sharedInstance() 

@objc  func  handleRouteChange ( notification : Notification ) { 
  let currentRoute = audioSession.currentRoute 
  guard  let portDescription = currentRoute.outputs.first else { 
    return
   } 
  
  let portType = portDescription.portType 
  if portType == .builtInSpeaker { 
    // 处理扬声器
  } else  if portType == .builtInReceiver { 
    // 处理接收器
  } else  if portType.isHeadphonesOrBluetoothDevice { 
    // 处理耳机或蓝牙设备
  } 
}

要检查portType是否是耳机或蓝牙设备,可以使用以下代码:

extension AVAudioSessionPortDescription {
  var isHeadphonesOrBluetoothDevice: Bool {
    let types: [AVAudioSession.Port] = [
      .headphones,
      .bluetoothHFP,
      .bluetoothA2DP
    ]
    return types.contains(portType)
  }
}

结论

就是这样!我与大家分享了我在开发 P2P 通话时遇到的所有挑战,特别是音频管理方面的挑战。希望本文提供的解决方案和见解能对在项目中遇到类似问题的人有所帮助。

译自:https://medium.com/@tsivilko/mastering-voip-audio-with-callkit-and-webrtc-on-ios-0f2092402331

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/49336.html

(0)

相关推荐

发表回复

登录后才能评论