You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AVFoundation通话结束后重复触发audioDeviceInUseByAnotherClient问题咨询

问题分析与解决方案

咱们直接拆解你遇到的核心问题:通话断开后,明明已经触发了call.hasEnded重启捕获会话,却立刻又被.audioDeviceInUseByAnotherClient中断导致会话停止,主要原因有两个:

1. 回调时机不匹配

CXCallObserver的call.hasEnded只是通话状态变更的通知,这个时机比系统真正释放音频硬件资源的时机要早很多。当你在这个回调里立刻启动AVCaptureSession时,音频设备其实还没完全从通话进程中释放出来,AVFoundation自然会检测到设备仍被占用,触发中断。

2. 重复重启的资源竞争

你的代码里同时在两个地方调用了restartCaptureSession():

  • CXCallObserver的call.hasEnded回调
  • AVCaptureSession的sessionInterruptionEnded通知

短时间内两次尝试启动会话,会引发系统音频资源的竞争,进而再次触发.audioDeviceInUseByAnotherClient中断。


解决方案

方案一:依赖AVCaptureSession原生中断通知(推荐)

AVCaptureSession本身已经提供了完整的中断生命周期管理,sessionInterruptionEnded就是系统明确告诉你「音频设备已可用」的信号,完全不需要依赖CXCallObserver来重启会话。你可以直接移除call.hasEnded里的重启逻辑:

func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
    if call.hasEnded == true {
        print("\nCXCallState :Disconnected")
        // 移除这里的restartCaptureSession()调用
    }
    if call.hasConnected == true && call.hasEnded == false {
        print("\nCXCallState : Connected")
        // 这里可以保留通话连接时的提示逻辑,比如告知用户当前无法录制
    }
}

这样只有当系统真正释放音频设备后,sessionInterruptionEnded才会触发,此时重启会话就不会再触发设备占用的中断了。

方案二:优化重启逻辑,避免竞争与过早操作

如果你确实需要保留CXCallObserver的监听(比如做额外业务逻辑),可以做以下优化:

  1. 增加标志位防止重复启动:
private var isRestartingSession = false

func restartCaptureSession() {
    guard !captureSession.isRunning, !isRestartingSession else { return }
    isRestartingSession = true
    
    // 捕获会话必须在专属串行队列操作,避免线程冲突
    let captureQueue = DispatchQueue(label: "com.yourapp.capture.queue")
    captureQueue.async { [weak self] in
        guard let self = self else { return }
        self.captureSession.startRunning()
        
        DispatchQueue.main.async {
            if self.previewLayer == nil {
                self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
                self.previewLayer?.videoGravity = .resizeAspectFill
                guard let previewLayer = self.previewLayer else { 
                    self.isRestartingSession = false
                    return 
                }
                previewLayer.frame = self.containerViewForPreviewLayer.bounds
                self.containerViewForPreviewLayer.layer.insertSublayer(previewLayer, at: 0)
            }
            self.isRestartingSession = false
        }
    }
}
  1. 在call.hasEnded里延迟重启,给系统足够时间释放音频设备:
func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
    if call.hasEnded == true {
        print("\nCXCallState :Disconnected")
        // 延迟1.5秒再重启,等待音频设备完全释放
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
            self?.restartCaptureSession()
        }
    }
}

额外注意点

  • AVCaptureSession的所有操作(启动/停止、配置输入输出)必须在同一个串行队列执行,你之前代码里用DispatchQueue.global().async嵌套main.sync存在死锁风险,一定要避免。
  • 可以提前检查音频设备可用性,在重启前确认设备未被占用:
private func isAudioDeviceAvailable() -> Bool {
    let audioDevice = AVCaptureDevice.default(.builtInMicrophone, for: .audio, position: .unspecified)
    return audioDevice?.isAvailable ?? false
}

内容的提问来源于stack exchange,提问作者Lance Samaria

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:28:54