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 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(已加载的 track,trackID 与 audioMix 匹配)
|
// 策略:先同步检查 → KVO 观察 → 定时轮询保底
|
||||||
// 2) asset.loadTracks(fallback,HLS 可能 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 {
|
||||||
// 阶段2:fallback — 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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user