diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 7ae1b93..33e90b9 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -36,6 +36,7 @@ class AudioTapContext { nonisolated(unsafe) var notReadyCount: Int = 0 // writerInput not ready 次数 nonisolated(unsafe) var lastActiveTimestamp: Double = 0 // 最后一次有声音的时间(秒) nonisolated(unsafe) var recordStartTime: Date? // 录制开始时间 + nonisolated(unsafe) var mixTrackID: Int32 = -1 // audioMix 绑定的 trackID let audioQueue = DispatchQueue(label: "miniplayer.audioTap") } @@ -274,6 +275,7 @@ struct AudioDiagStats { let lastActiveTimestamp: Double let sampleRate: Double let channelsPerFrame: UInt32 + let mixTrackID: Int32 } // MARK: - PlayerRecorder @@ -316,58 +318,109 @@ final class PlayerRecorder: NSObject { // MARK: - Audio Tap Setup - /// 在录制开始时安装音频 Tap 到 playerItem + /// 异步安装音频 Tap(HLS 流需要异步加载 tracks) private func installAudioTap(on playerItem: AVPlayerItem) { // 确保全局上下文存在 if gTapContext == nil { gTapContext = AudioTapContext() } - guard let ctx = gTapContext else { return } - // 同步获取音频 track(录制时 asset 已经加载完毕) - let audioTracks = playerItem.asset.tracks(withMediaType: .audio) - guard let audioTrack = audioTracks.first else { - NSLog("[Recorder] ⚠️ No audio track found (tried %d tracks)", audioTracks.count) - 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 + } + + 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) } - - NSLog("[Recorder] Found audio track: ID=%d, formatDescriptions=%d", - audioTrack.trackID, audioTrack.formatDescriptions.count) - - // 创建 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 status = MTAudioProcessingTapCreate( - kCFAllocatorDefault, &callbacks, - kMTAudioProcessingTapCreationFlag_PreEffects, - &tap - ) - guard status == noErr, let tapRef = tap else { - NSLog("[Recorder] ⚠️ MTAudioProcessingTapCreate failed: %d", status) - return + } + + private func writeDiagLog(_ content: String) { + if let moviesDir = FileManager.default.urls(for: .moviesDirectory, in: .userDomainMask).first { + let logPath = moviesDir.appendingPathComponent("MiniPlayer/tap_install_log.txt") + try? content.write(to: logPath, atomically: true, encoding: .utf8) + print("[Recorder:DIAG] Log written to: \(logPath.path)") } - - // 创建 AudioMix — 必须用实际 trackID - // kCMPersistentTrackID_Invalid (=0) 实测不会触发 tap - let inputParams = AVMutableAudioMixInputParameters() - inputParams.trackID = audioTrack.trackID - inputParams.audioTapProcessor = tapRef - - let audioMix = AVMutableAudioMix() - audioMix.inputParameters = [inputParams] - - playerItem.audioMix = audioMix - NSLog("[Recorder] ✓ Audio tap installed: trackID=%d, asset tracks=%d, playerItem.audioMix set", - audioTrack.trackID, playerItem.asset.tracks.count) } // MARK: - 开始录制 @@ -527,7 +580,8 @@ final class PlayerRecorder: NSObject { notReadyCount: tapContext?.notReadyCount ?? 0, lastActiveTimestamp: tapContext?.lastActiveTimestamp ?? 0, sampleRate: tapContext?.sampleRate ?? 0, - channelsPerFrame: tapContext?.channelsPerFrame ?? 0 + channelsPerFrame: tapContext?.channelsPerFrame ?? 0, + mixTrackID: tapContext?.mixTrackID ?? -1 ) NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d, peak=%.4f, active=%d, silent=%d", @@ -718,6 +772,7 @@ final class PlayerRecorder: NSObject { "--- Audio Format ---", "sampleRate: \(String(format: "%.0f", stats.sampleRate)) Hz", "channelsPerFrame: \(stats.channelsPerFrame)", + "mixTrackID: \(stats.mixTrackID) (bound in audioMix, -1=not set)", "peakAmplitude: \(String(format: "%.6f", stats.peakAmplitude)) (\(String(format: "%.1f", peakDB)) dB)", "", "--- Skip/Error Counts ---",