diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 33e90b9..8608bd3 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -318,101 +318,95 @@ final class PlayerRecorder: NSObject { // MARK: - Audio Tap Setup - /// 异步安装音频 Tap(HLS 流需要异步加载 tracks) + /// 安装音频 Tap(使用 playerItem.tracks 获取轨道,支持 HLS 虚拟 asset) private func installAudioTap(on playerItem: AVPlayerItem) { // 确保全局上下文存在 if gTapContext == nil { gTapContext = AudioTapContext() } + guard let ctx = gTapContext else { return } - Task { @MainActor in - guard let ctx = gTapContext else { return } - - print("[Recorder:DIAG] === installAudioTap START (async) ===") - print("[Recorder:DIAG] asset class: \(type(of: playerItem.asset))") - - // HLS 流的 tracks 是异步加载的,必须用 loadTracks - let audioTracks: [AVAssetTrack] - do { - audioTracks = try await playerItem.asset.loadTracks(withMediaType: .audio) - } catch { - print("[Recorder:DIAG] ⚠️ loadTracks failed: \(error)") - // 写诊断日志 - writeDiagLog("loadTracks error: \(error)") - return + print("[Recorder:DIAG] === installAudioTap START ===") + + // HLS 虚拟 asset 的 asset.tracks 为空,必须用 playerItem.tracks + let itemTracks = playerItem.tracks + print("[Recorder:DIAG] playerItem.tracks count: \(itemTracks.count)") + + // 找到音频轨道 + var audioTrackID: CMPersistentTrackID = kCMPersistentTrackID_Invalid + for (i, t) in itemTracks.enumerated() { + let tid = t.assetTrack?.trackID ?? -1 + let mediaType = t.assetTrack?.mediaType.rawValue ?? "?" + let enabled = t.isEnabled + print("[Recorder:DIAG] itemTrack[\(i)]: trackID=\(tid) mediaType=\(mediaType) enabled=\(enabled)") + if t.assetTrack?.mediaType == .audio && t.isEnabled { + audioTrackID = tid } - - guard let audioTrack = audioTracks.first else { - print("[Recorder:DIAG] ⚠️ No audio track found after async load!") - writeDiagLog("No audio tracks found") - return - } - - print("[Recorder:DIAG] Using audio trackID=\(audioTrack.trackID)") - - // 列出所有 tracks 用于诊断 - let allTracks = try? await playerItem.asset.load(.tracks) - if let tracks = allTracks { - for (i, t) in tracks.enumerated() { - print("[Recorder:DIAG] track[\(i)]: ID=\(t.trackID) mediaType=\(t.mediaType.rawValue)") - } - } - - // 创建 MTAudioProcessingTap - var callbacks = MTAudioProcessingTapCallbacks( - version: kMTAudioProcessingTapCallbacksVersion_0, - clientInfo: Unmanaged.passUnretained(ctx).toOpaque(), - init: tapInit, - finalize: tapFinalize, - prepare: tapPrepare, - unprepare: tapUnprepare, - process: tapProcess - ) - - var tap: MTAudioProcessingTap? - let tapStatus = MTAudioProcessingTapCreate( - kCFAllocatorDefault, &callbacks, - kMTAudioProcessingTapCreationFlag_PreEffects, - &tap - ) - print("[Recorder:DIAG] MTAudioProcessingTapCreate status=\(tapStatus) (noErr=\(noErr))") - guard tapStatus == noErr, let tapRef = tap else { - print("[Recorder:DIAG] ⚠️ Tap creation failed!") - return - } - - // 创建 AudioMix - let inputParams = AVMutableAudioMixInputParameters() - inputParams.trackID = audioTrack.trackID - inputParams.audioTapProcessor = tapRef - - let audioMix = AVMutableAudioMix() - audioMix.inputParameters = [inputParams] - - playerItem.audioMix = audioMix - - // 验证 - let verifyMix = playerItem.audioMix - print("[Recorder:DIAG] playerItem.audioMix set=\(verifyMix != nil)") - if let vMix = verifyMix, let params = vMix.inputParameters.first { - print("[Recorder:DIAG] verified trackID=\(params.trackID)") - } - print("[Recorder:DIAG] === installAudioTap DONE ===") - - ctx.mixTrackID = audioTrack.trackID - - // 写诊断日志 - var diagLog = "=== installAudioTap Log ===\n" - diagLog += "Time: \(Date())\n" - diagLog += "Asset class: \(type(of: playerItem.asset))\n" - diagLog += "Audio trackID used: \(audioTrack.trackID)\n" - diagLog += "TapCreate status: \(tapStatus)\n" - diagLog += "audioMix set: \(verifyMix != nil)\n" - if let vMix = verifyMix, let params = vMix.inputParameters.first { - diagLog += "Verified trackID: \(params.trackID)\n" - } - writeDiagLog(diagLog) } + + guard audioTrackID != kCMPersistentTrackID_Invalid else { + print("[Recorder:DIAG] ⚠️ No enabled audio track in playerItem.tracks!") + writeDiagLog("No audio track in playerItem.tracks (count=\(itemTracks.count))") + return + } + + print("[Recorder:DIAG] Using audio trackID=\(audioTrackID)") + + // 创建 MTAudioProcessingTap + var callbacks = MTAudioProcessingTapCallbacks( + version: kMTAudioProcessingTapCallbacksVersion_0, + clientInfo: Unmanaged.passUnretained(ctx).toOpaque(), + init: tapInit, + finalize: tapFinalize, + prepare: tapPrepare, + unprepare: tapUnprepare, + process: tapProcess + ) + + var tap: MTAudioProcessingTap? + let tapStatus = MTAudioProcessingTapCreate( + kCFAllocatorDefault, &callbacks, + kMTAudioProcessingTapCreationFlag_PreEffects, + &tap + ) + print("[Recorder:DIAG] MTAudioProcessingTapCreate status=\(tapStatus) (noErr=\(noErr))") + guard tapStatus == noErr, let tapRef = tap else { + print("[Recorder:DIAG] ⚠️ Tap creation failed!") + return + } + + // 创建 AudioMix + let inputParams = AVMutableAudioMixInputParameters() + inputParams.trackID = audioTrackID + inputParams.audioTapProcessor = tapRef + + let audioMix = AVMutableAudioMix() + audioMix.inputParameters = [inputParams] + + playerItem.audioMix = audioMix + + // 验证 + let verifyMix = playerItem.audioMix + print("[Recorder:DIAG] playerItem.audioMix set=\(verifyMix != nil)") + if let vMix = verifyMix, let params = vMix.inputParameters.first { + print("[Recorder:DIAG] verified trackID=\(params.trackID)") + } + print("[Recorder:DIAG] === installAudioTap DONE ===") + + ctx.mixTrackID = audioTrackID + + // 写诊断日志 + var diagLog = "=== installAudioTap Log ===\n" + diagLog += "Time: \(Date())\n" + diagLog += "Source: playerItem.tracks\n" + diagLog += "playerItem.tracks count: \(itemTracks.count)\n" + for (i, t) in itemTracks.enumerated() { + diagLog += " itemTrack[\(i)]: trackID=\(t.assetTrack?.trackID ?? -1) mediaType=\(t.assetTrack?.mediaType.rawValue ?? "?") enabled=\(t.isEnabled)\n" + } + diagLog += "Audio trackID used: \(audioTrackID)\n" + diagLog += "TapCreate status: \(tapStatus)\n" + diagLog += "audioMix set: \(verifyMix != nil)\n" + writeDiagLog(diagLog) } private func writeDiagLog(_ content: String) {