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:
parent
be0cf09941
commit
8cfb072370
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user