fix: HLS audio via KVO+polling for playerItem.tracks, diagnostic logging

- KVO观察playerItem.tracks变化(HLS tracks异步加载)
- 轮询保底(20秒超时),KVO不触发时仍能捕获
- 诊断日志:打印每个track的mediaType/trackID
- tap回调改进:始终先GetSourceAudio保证管道不阻塞
- 窗口标题随toolbar联动
This commit is contained in:
yumoqing 2026-06-25 09:12:02 +08:00
parent be0cf09941
commit 8cfb072370

View File

@ -184,6 +184,7 @@ final class PlayerRecorder: NSObject {
private var audioTap: MTAudioProcessingTap? private var audioTap: MTAudioProcessingTap?
private var audioContext: AudioCaptureContext? private var audioContext: AudioCaptureContext?
private weak var currentPlayerItem: AVPlayerItem? private weak var currentPlayerItem: AVPlayerItem?
private var tracksObservation: NSKeyValueObservation?
// //
nonisolated(unsafe) private var recordStartTime: CMTime = .zero nonisolated(unsafe) private var recordStartTime: CMTime = .zero
@ -292,6 +293,7 @@ final class PlayerRecorder: NSObject {
// tap // tap
audioContext?.isRunning = false audioContext?.isRunning = false
tracksObservation = nil
let totalAudioFrames = audioContext?.appendCount ?? 0 let totalAudioFrames = audioContext?.appendCount ?? 0
let processCalls = audioContext?.processCallCount ?? 0 let processCalls = audioContext?.processCallCount ?? 0
NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d", NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d",
@ -336,52 +338,74 @@ final class PlayerRecorder: NSObject {
// MARK: - Tap // MARK: - Tap
private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) { private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) {
// track // HLS tracks
// 1) playerItem.tracks tracktrackID audioMix // KVO
// 2) asset.loadTracksfallbackHLS tracks
//
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 { Task {
var attempts = 0 var attempts = 0
while attempts < 200 && self.isRunning && self.audioTap == nil { // 20
// 1 playerItem.tracks 3 try await Task.sleep(nanoseconds: 100_000_000) // 0.1
while attempts < 30 { attempts += 1
guard self.isRunning else { return }
let itemTracks = playerItem.tracks if let trackID = self.findAudioTrackID(in: playerItem) {
if let audioItemTrack = itemTracks.first(where: { $0.assetTrack?.mediaType == .audio }), NSLog("[Recorder] ✓ Audio track via polling (attempt %d): trackID=%d", attempts, trackID)
let assetTrack = audioItemTrack.assetTrack { await MainActor.run {
let trackID = assetTrack.trackID guard self.isRunning, self.audioTap == nil else { return }
NSLog("[Recorder] ✓ Audio track via playerItem.tracks: trackID=%d", trackID) self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput)
self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput) }
self.tracksObservation = nil
return return
} }
attempts += 1
try await Task.sleep(nanoseconds: 100_000_000) // 0.1
} }
if self.audioTap == nil {
// 2fallback asset.loadTracks 5 NSLog("[Recorder] ❌ No audio track found after %d polling attempts", attempts)
NSLog("[Recorder] ⚠️ playerItem.tracks empty, falling back to asset.loadTracks") self.tracksObservation = nil
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
} }
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) { private func installTap(on playerItem: AVPlayerItem, trackID: CMPersistentTrackID, audioWriterInput: AVAssetWriterInput) {
// //
let context = AudioCaptureContext() let context = AudioCaptureContext()