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

Swift中WebRTC直播音频异常:AURemoteIO启动失败问题求助

问题:WebRTC直播中Push to Talk功能偶现音频失效问题

在集成Push to Talk功能的Swift WebRTC直播类中,偶尔出现音频无法正常工作(无声音)的情况,同时收到错误提示:AURemoteIO.cpp:1679 AUIOClient_StartIO failed。但当启动直播无声音后,将应用切换至后台再切回前台,音频即可恢复正常。已在plist文件配置相机、麦克风权限,相关代码如下:

class WebRTCManager: NSObject {
    
    private  let peerConnectionFactory: RTCPeerConnectionFactory = {
        RTCInitializeSSL()
        
        let videoEncoderFactory = RTCVideoEncoderFactoryH264()
        let videoDecoderFactory = RTCVideoDecoderFactoryH264()
        return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
    }()
    
    private var peerConnection: RTCPeerConnection?
    private var audioSource: RTCAudioSource?
    private var audioTrack: RTCAudioTrack?
    private var audioSender: RTCRtpSender?
    private var localStream: RTCMediaStream?
    
    private var remoteStream: RTCMediaStream?
    private var dataChannel: RTCDataChannel?
    private var remoteDataChannel: RTCDataChannel?
    private let audioQueue = DispatchQueue(label: "audio")
    private let rtcAudioSession =  RTCAudioSession.sharedInstance()
    var socket: WebSocket!
    
    public private(set) var isConnected: Bool = false
    
    // enableMicrophone
    override init( ) {
        super.init()
        
        let audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
        self.audioSource = self.peerConnectionFactory.audioSource(with: audioConstraints)
        self.audioTrack  = self.peerConnectionFactory.audioTrack(with: self.audioSource!, trackId: "audio0")
        self.localStream = self.peerConnectionFactory.mediaStream(withStreamId: "stream")
        localStream!.addAudioTrack(self.audioTrack!)
    }
    
    deinit {
        self.peerConnection = nil
        self.socket = nil
    }
    
    func reset(){
        self.peerConnection = nil
        self.socket = nil
    }
    
    func initLive(socketURL:String) {
        
        self.socket = WebSocket(url: URL(string: socketURL)!)
        self.socket.delegate = self
        self.socket.connect()
    }
    
    // MARK: Connect
    func connect(onSuccess: @escaping (RTCSessionDescription) -> Void){
        setupPeerConnection()
        
    }
    
    func createAndConnectSocket(socketURL:String){
        self.socket = WebSocket(url: URL(string: socketURL)!)
        self.socket.delegate = self
        self.socket.connect()
    }
    
    // MARK: - setupPeerConnection
    private func setupPeerConnection() {
        let rtcConf = RTCConfiguration()
        rtcConf.sdpSemantics = .unifiedPlan
        
        rtcConf.iceServers = [ RTCIceServer(urlStrings: ["turn:" + self.coturnHost + ":" + String(self.coturnPort)] ,username: self.userName ,credential: self.password )]
          
        let mediaConstraints = RTCMediaConstraints.init(mandatoryConstraints: nil, optionalConstraints: nil)
        self.peerConnection = self.peerConnectionFactory.peerConnection(with: rtcConf, constraints: mediaConstraints, delegate: nil)
        self.peerConnection?.delegate = self
        
        for track in localStream!.audioTracks {
            audioSender = peerConnection?.add(audioTrack!, streamIds: [localStream!.streamId])
        }
    }
    
    // MARK: Fonctionnalité Push to Talk
    
    func pushToTalk(isPressed: Bool) {
        self.audioTrack?.isEnabled = isPressed
    }
    
    // MARK: the speaker function
    
    func setSpeaker(isOn: Bool) {
        audioQueue.async { [weak self] in
            guard let self = self else {
                return
            }
            
            self.rtcAudioSession.lockForConfiguration()
            
            do {
                try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue)
                
                if isOn {
                    try self.rtcAudioSession.overrideOutputAudioPort(.speaker)
                    try self.rtcAudioSession.setActive(true)
                } else {
                    try self.rtcAudioSession.overrideOutputAudioPort(.none)
                }
            } catch let error {
                debugPrint("Error setting AVAudioSession category: \(error)")
            }
            self.rtcAudioSession.unlockForConfiguration()
        }
    }
    
}

// MARK: - PeerConnection Delegates

extension WebRTCManager : RTCPeerConnectionDelegate {
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
        print("liveWRTC--did add stream")
        self.remoteStream = stream
        
        if let track = stream.videoTracks.first {
            print("liveWRTC--video track faund")
            track.add(remoteRenderView!)
        }
        
        if let audioTrack = stream.audioTracks.first{
            print("WebRTCManager: liveWRTC--audio track faund")
            
        }
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
        self.remoteDataChannel = dataChannel
        
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
        //peerConnection
    }
    
    func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection){
        //peerConnectionShouldNegotiate
    }
    
    func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
        //peerConnection   
        }
    }
}

==> 直播连接成功时,会执行以下代码:

if (liveStreamingStatus == .connected){
    WebRTCManager.setSpeaker(isOn: true)
    WebRTCManager.pushToTalk(isPressed: false)
}

修复方案

这个问题根源是音频会话状态冲突或WebRTC音频组件初始化时机错误,切换后台恢复是因为系统强制重置了音频会话。以下是针对性修复:

1. 修正音频会话配置逻辑

统一激活会话的时机,添加会话模式和选项,避免状态不一致:

func setSpeaker(isOn: Bool) {
    audioQueue.async { [weak self] in
        guard let self = self else { return }
        self.rtcAudioSession.lockForConfiguration()
        do {
            // 先设置会话类别、模式,再激活
            try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, 
                                                 mode: .voiceChat, 
                                                 options: [.mixWithOthers, .allowBluetooth])
            try self.rtcAudioSession.setActive(true, options: .notifyOthersOnDeactivation)
            
            if isOn {
                try self.rtcAudioSession.overrideOutputAudioPort(.speaker)
            } else {
                try self.rtcAudioSession.overrideOutputAudioPort(.none)
            }
        } catch let error {
            debugPrint("Error setting AVAudioSession: \(error)")
        }
        self.rtcAudioSession.unlockForConfiguration()
    }
}

2. 确保权限获取后再初始化音频组件

初始化音频轨道前先确认麦克风权限,避免组件在无权限状态下创建:

override init() {
    super.init()
    
    // 先请求麦克风权限
    AVCaptureDevice.requestAccess(for: .audio) { [weak self] granted in
        guard let self = self, granted else { return }
        DispatchQueue.main.async {
            let audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
            self.audioSource = self.peerConnectionFactory.audioSource(with: audioConstraints)
            self.audioTrack = self.peerConnectionFactory.audioTrack(with: self.audioSource!, trackId: "audio0")
            self.localStream = self.peerConnectionFactory.mediaStream(withStreamId: "stream")
            self.localStream!.addAudioTrack(self.audioTrack!)
        }
    }
    
    // 添加音频中断监听
    NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption(_:)), 
                                           name: AVAudioSession.interruptionNotification, object: nil)
}

3. 同步控制音频源与轨道状态

仅设置audioTrack.isEnabled可能无法触发音频流重启,需要同步控制音频源:

func pushToTalk(isPressed: Bool) {
    self.audioTrack?.isEnabled = isPressed
    self.audioSource?.setIsEnabled(isPressed)
}

4. 监听音频中断并恢复会话

当中断结束后重新激活音频会话,避免异常状态残留:

@objc private func handleAudioSessionInterruption(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
    
    if type == .ended {
        audioQueue.async { [weak self] in
            guard let self = self else { return }
            self.rtcAudioSession.lockForConfiguration()
            do {
                try self.rtcAudioSession.setActive(true)
            } catch {
                debugPrint("Failed to reactivate audio session: \(error)")
            }
            self.rtcAudioSession.unlockForConfiguration()
        }
    }
}

deinit {
    NotificationCenter.default.removeObserver(self)
    self.peerConnection = nil
    self.socket = nil
}

5. 延迟创建PeerConnection确保音频组件就绪

避免在音频轨道未初始化完成时创建PeerConnection:

func connect(onSuccess: @escaping (RTCSessionDescription) -> Void){
    guard self.audioTrack != nil else {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.connect(onSuccess: onSuccess)
        }
        return
    }
    setupPeerConnection()
    // 后续创建offer等逻辑补充
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 02:45:02