diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 64361f5..3a6d6d3 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -184,6 +184,7 @@ final class PlayerRecorder: NSObject { private var audioTap: MTAudioProcessingTap? private var audioContext: AudioCaptureContext? private weak var currentPlayerItem: AVPlayerItem? + private var tracksObservation: NSKeyValueObservation? // 录制起始时间(用于计算视频相对时间戳) nonisolated(unsafe) private var recordStartTime: CMTime = .zero @@ -292,6 +293,7 @@ final class PlayerRecorder: NSObject { // 停止音频 tap audioContext?.isRunning = false + tracksObservation = nil let totalAudioFrames = audioContext?.appendCount ?? 0 let processCalls = audioContext?.processCallCount ?? 0 NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d", @@ -336,52 +338,74 @@ final class PlayerRecorder: NSObject { // MARK: - 音频 Tap 设置 private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) { - // 两步策略查找音频 track: - // 1) playerItem.tracks(已加载的 track,trackID 与 audioMix 匹配) - // 2) asset.loadTracks(fallback,HLS 可能 tracks 尚未填充) + // HLS 流的 tracks 是异步加载的 + // 策略:先同步检查 → KVO 观察 → 定时轮询保底 + // 先尝试直接获取(本地文件场景) + if let audioTrack = self.findAudioTrackID(in: playerItem) { + NSLog("[Recorder] ✓ Audio track immediately available: trackID=%d", audioTrack) + self.installTap(on: playerItem, trackID: audioTrack, audioWriterInput: audioWriterInput) + return + } + + // tracks 尚未加载 — 用 KVO 观察变化 + NSLog("[Recorder] ⏳ Audio tracks not yet loaded, setting up KVO + polling...") + + let observation = playerItem.observe(\.tracks, options: [.new]) { [weak self] item, _ in + guard let self, self.isRunning, self.audioTap == nil else { return } + + if let trackID = self.findAudioTrackID(in: item) { + NSLog("[Recorder] ✓ Audio track via KVO: trackID=%d", trackID) + Task { @MainActor in + guard self.isRunning, self.audioTap == nil else { return } + self.installTap(on: item, trackID: trackID, audioWriterInput: audioWriterInput) + } + self.tracksObservation = nil + } + } + self.tracksObservation = observation + + // 保底:在视频抓帧循环中定期检查(HLS KVO 可能不触发) + // 超时保护:20秒后放弃 Task { var attempts = 0 - - // 阶段1:尝试 playerItem.tracks(最多 3 秒) - while attempts < 30 { - guard self.isRunning else { return } + while attempts < 200 && self.isRunning && self.audioTap == nil { // 20秒 + try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒 + attempts += 1 - let itemTracks = playerItem.tracks - if let audioItemTrack = itemTracks.first(where: { $0.assetTrack?.mediaType == .audio }), - let assetTrack = audioItemTrack.assetTrack { - let trackID = assetTrack.trackID - NSLog("[Recorder] ✓ Audio track via playerItem.tracks: trackID=%d", trackID) - self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput) + if let trackID = self.findAudioTrackID(in: playerItem) { + NSLog("[Recorder] ✓ Audio track via polling (attempt %d): trackID=%d", attempts, trackID) + await MainActor.run { + guard self.isRunning, self.audioTap == nil else { return } + self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput) + } + self.tracksObservation = nil return } - - attempts += 1 - try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒 } - - // 阶段2:fallback — asset.loadTracks(最多再等 5 秒) - NSLog("[Recorder] ⚠️ playerItem.tracks empty, falling back to asset.loadTracks") - attempts = 0 - while attempts < 50 { - guard self.isRunning else { return } - - if let assetTracks = try? await playerItem.asset.loadTracks(withMediaType: .audio), - let audioTrack = assetTracks.first { - let trackID = audioTrack.trackID - NSLog("[Recorder] ✓ Audio track via asset.loadTracks: trackID=%d", trackID) - self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput) - return - } - - attempts += 1 - try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒 + if self.audioTap == nil { + NSLog("[Recorder] ❌ No audio track found after %d polling attempts", attempts) + self.tracksObservation = nil } - - NSLog("[Recorder] ❌ No audio track found after both attempts") } } + /// 从 playerItem.tracks 查找音频 trackID + private func findAudioTrackID(in playerItem: AVPlayerItem) -> CMPersistentTrackID? { + let itemTracks = playerItem.tracks + guard !itemTracks.isEmpty else { return nil } + + for (i, track) in itemTracks.enumerated() { + guard let assetTrack = track.assetTrack else { continue } + NSLog("[Recorder] track[%d]: mediaType=%@, trackID=%d, isEnabled=%d", + i, assetTrack.mediaType.rawValue as NSString, assetTrack.trackID, track.isEnabled) + if assetTrack.mediaType == .audio { + return assetTrack.trackID + } + } + return nil + } + private func installTap(on playerItem: AVPlayerItem, trackID: CMPersistentTrackID, audioWriterInput: AVAssetWriterInput) { // 创建音频捕获上下文 let context = AudioCaptureContext()