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

WebRTC iOS:接收端录制远端音频流问题(耳机录制失效)

看起来你在实现音频流接收端的录制功能时碰到了麻烦,尤其是使用耳机的场景。我帮你梳理下代码里的几个关键问题,以及对应的修复方案:

核心问题分析

  1. 错误的录制源:你的代码当前在录制麦克风输入(engine.inputNode),但你需要的是录制接收并播放的音频流——这是最关键的偏差。
  2. 缺失音频会话配置:使用耳机时,系统需要明确的音频会话规则来路由音频,否则可能不会把音频输入/输出导向耳机。
  3. 未实现createRecordingFile方法:你的startRecording调用了这个方法但没提供实现,文件路径错误、无写入权限都会导致录制失败。
  4. Player节点未接入混音器:你attach了player但没连接到主混音器,接收的音频流根本不会被播放,更无法被录制。

修正后的完整实现

下面是调整后的代码,解决了上述问题,适配耳机场景:

import AVFoundation

class AudioStreamRecorder: NSObject {
    var engine = AVAudioEngine()
    var recordingFile: AVAudioFile?
    let player = AVAudioPlayerNode()
    var isRecording: Bool = false
    
    override init() {
        super.init()
        setupAudioSession()
        initializeAudioEngine()
    }
    
    // 配置音频会话,适配耳机/蓝牙设备
    private func setupAudioSession() {
        let session = AVAudioSession.sharedInstance()
        do {
            // 设置为播放+录制模式,允许蓝牙A2DP设备,无耳机时默认用扬声器
            try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetoothA2DP, .defaultToSpeaker])
            try session.setActive(true)
        } catch {
            print("音频会话配置失败: \(error)")
        }
    }
    
    func initializeAudioEngine() {
        // 把Player节点连接到主混音器,确保播放的音频流进入混音链路
        engine.attach(player)
        let mainMixer = engine.mainMixerNode
        let playerOutputFormat = player.outputFormat(forBus: 0)
        engine.connect(player, to: mainMixer, format: playerOutputFormat)
        
        // 如果你需要同时录制麦克风+播放的音频,取消下面的注释
        // let input = engine.inputNode
        // let inputFormat = input.inputFormat(forBus: 0)
        // engine.connect(input, to: mainMixer, format: inputFormat)
        
        engine.prepare()
        do {
            try engine.start()
        } catch {
            print("AudioEngine启动失败: \(error)")
        }
    }
    
    func startRecording() {
        guard !isRecording else { return }
        
        guard createRecordingFile() else {
            print("无法创建录制文件")
            return
        }
        
        isRecording = true
        // 给主混音器添加Tap,录制经过混音器的所有音频(包括Player播放的流)
        let mixerFormat = engine.mainMixerNode.outputFormat(forBus: 0)
        engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: mixerFormat) { [weak self] buffer, time in
            guard let self = self, self.isRecording else { return }
            do {
                try self.recordingFile?.write(from: buffer)
            } catch {
                print("录制写入错误: \(error)")
            }
        }
    }
    
    // 创建可写入的录制文件,存在Documents目录
    private func createRecordingFile() -> Bool {
        guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return false
        }
        let recordingURL = docsDir.appendingPathComponent("recorded_stream.caf")
        
        // 删除已存在的旧文件
        if FileManager.default.fileExists(atPath: recordingURL.path) {
            do {
                try FileManager.default.removeItem(at: recordingURL)
            } catch {
                print("删除旧录制文件失败: \(error)")
                return false
            }
        }
        
        // 用混音器的输出格式创建文件
        let format = engine.mainMixerNode.outputFormat(forBus: 0)
        do {
            recordingFile = try AVAudioFile(forWriting: recordingURL, settings: format.settings, commonFormat: format.commonFormat, interleaved: format.isInterleaved)
            return true
        } catch {
            print("创建录制文件失败: \(error)")
            return false
        }
    }
    
    func stopRecording() {
        guard isRecording else { return }
        
        isRecording = false
        engine.mainMixerNode.removeTap(onBus: 0)
        // 释放文件引用,确保写入完成
        recordingFile = nil
    }
    
    // 示例:播放接收到的音频流
    func playReceivedBuffer(_ buffer: AVAudioPCMBuffer) {
        if !engine.isRunning {
            do {
                try engine.start()
            } catch {
                print("重启AudioEngine失败: \(error)")
            }
        }
        player.scheduleBuffer(buffer)
        if !player.isPlaying {
            player.play()
        }
    }
}

额外注意事项

  • 权限配置:在Info.plist中添加NSMicrophoneUsageDescription(如果需要录制麦克风)和NSAppleMusicUsageDescription,否则应用会因权限不足崩溃。
  • 耳机路由:配置会话时的.allowBluetoothA2DP选项确保蓝牙耳机会被识别,有线耳机系统会自动路由,但正确的会话配置是前提。
  • 避免内存泄漏:Tap闭包中使用[weak self],防止循环引用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 07:47:47