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 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
while attempts < 200 && self.isRunning && self.audioTap == nil { // 20秒
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
attempts += 1
// 阶段1:尝试 playerItem.tracks(最多 3 秒)
while attempts < 30 {
guard self.isRunning else { return }
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()