WebRTC iOS:接收端录制远端音频流问题(耳机录制失效)
看起来你在实现音频流接收端的录制功能时碰到了麻烦,尤其是使用耳机的场景。我帮你梳理下代码里的几个关键问题,以及对应的修复方案:
核心问题分析
- 错误的录制源:你的代码当前在录制麦克风输入(
engine.inputNode),但你需要的是录制接收并播放的音频流——这是最关键的偏差。 - 缺失音频会话配置:使用耳机时,系统需要明确的音频会话规则来路由音频,否则可能不会把音频输入/输出导向耳机。
- 未实现
createRecordingFile方法:你的startRecording调用了这个方法但没提供实现,文件路径错误、无写入权限都会导致录制失败。 - 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
相关产品推荐
相关产品推荐

