fix: use async loadTracks for HLS stream audio tracks - HLS AVURLAsset.tracks returns empty array synchronously - must use asset.loadTracks(withMediaType: .audio)

This commit is contained in:
yumoqing 2026-06-28 09:15:50 +08:00
parent 70b264bcc8
commit 1880eeb3d3

View File

@ -36,6 +36,7 @@ class AudioTapContext {
nonisolated(unsafe) var notReadyCount: Int = 0 // writerInput not ready nonisolated(unsafe) var notReadyCount: Int = 0 // writerInput not ready
nonisolated(unsafe) var lastActiveTimestamp: Double = 0 // () nonisolated(unsafe) var lastActiveTimestamp: Double = 0 // ()
nonisolated(unsafe) var recordStartTime: Date? // nonisolated(unsafe) var recordStartTime: Date? //
nonisolated(unsafe) var mixTrackID: Int32 = -1 // audioMix trackID
let audioQueue = DispatchQueue(label: "miniplayer.audioTap") let audioQueue = DispatchQueue(label: "miniplayer.audioTap")
} }
@ -274,6 +275,7 @@ struct AudioDiagStats {
let lastActiveTimestamp: Double let lastActiveTimestamp: Double
let sampleRate: Double let sampleRate: Double
let channelsPerFrame: UInt32 let channelsPerFrame: UInt32
let mixTrackID: Int32
} }
// MARK: - PlayerRecorder // MARK: - PlayerRecorder
@ -316,58 +318,109 @@ final class PlayerRecorder: NSObject {
// MARK: - Audio Tap Setup // MARK: - Audio Tap Setup
/// Tap playerItem /// TapHLS tracks
private func installAudioTap(on playerItem: AVPlayerItem) { private func installAudioTap(on playerItem: AVPlayerItem) {
// //
if gTapContext == nil { if gTapContext == nil {
gTapContext = AudioTapContext() gTapContext = AudioTapContext()
} }
guard let ctx = gTapContext else { return }
// track asset Task { @MainActor in
let audioTracks = playerItem.asset.tracks(withMediaType: .audio) guard let ctx = gTapContext else { return }
guard let audioTrack = audioTracks.first else {
NSLog("[Recorder] ⚠️ No audio track found (tried %d tracks)", audioTracks.count) print("[Recorder:DIAG] === installAudioTap START (async) ===")
return print("[Recorder:DIAG] asset class: \(type(of: playerItem.asset))")
// HLS tracks loadTracks
let audioTracks: [AVAssetTrack]
do {
audioTracks = try await playerItem.asset.loadTracks(withMediaType: .audio)
} catch {
print("[Recorder:DIAG] ⚠️ loadTracks failed: \(error)")
//
writeDiagLog("loadTracks error: \(error)")
return
}
guard let audioTrack = audioTracks.first else {
print("[Recorder:DIAG] ⚠️ No audio track found after async load!")
writeDiagLog("No audio tracks found")
return
}
print("[Recorder:DIAG] Using audio trackID=\(audioTrack.trackID)")
// tracks
let allTracks = try? await playerItem.asset.load(.tracks)
if let tracks = allTracks {
for (i, t) in tracks.enumerated() {
print("[Recorder:DIAG] track[\(i)]: ID=\(t.trackID) mediaType=\(t.mediaType.rawValue)")
}
}
// MTAudioProcessingTap
var callbacks = MTAudioProcessingTapCallbacks(
version: kMTAudioProcessingTapCallbacksVersion_0,
clientInfo: Unmanaged.passUnretained(ctx).toOpaque(),
init: tapInit,
finalize: tapFinalize,
prepare: tapPrepare,
unprepare: tapUnprepare,
process: tapProcess
)
var tap: MTAudioProcessingTap?
let tapStatus = MTAudioProcessingTapCreate(
kCFAllocatorDefault, &callbacks,
kMTAudioProcessingTapCreationFlag_PreEffects,
&tap
)
print("[Recorder:DIAG] MTAudioProcessingTapCreate status=\(tapStatus) (noErr=\(noErr))")
guard tapStatus == noErr, let tapRef = tap else {
print("[Recorder:DIAG] ⚠️ Tap creation failed!")
return
}
// AudioMix
let inputParams = AVMutableAudioMixInputParameters()
inputParams.trackID = audioTrack.trackID
inputParams.audioTapProcessor = tapRef
let audioMix = AVMutableAudioMix()
audioMix.inputParameters = [inputParams]
playerItem.audioMix = audioMix
//
let verifyMix = playerItem.audioMix
print("[Recorder:DIAG] playerItem.audioMix set=\(verifyMix != nil)")
if let vMix = verifyMix, let params = vMix.inputParameters.first {
print("[Recorder:DIAG] verified trackID=\(params.trackID)")
}
print("[Recorder:DIAG] === installAudioTap DONE ===")
ctx.mixTrackID = audioTrack.trackID
//
var diagLog = "=== installAudioTap Log ===\n"
diagLog += "Time: \(Date())\n"
diagLog += "Asset class: \(type(of: playerItem.asset))\n"
diagLog += "Audio trackID used: \(audioTrack.trackID)\n"
diagLog += "TapCreate status: \(tapStatus)\n"
diagLog += "audioMix set: \(verifyMix != nil)\n"
if let vMix = verifyMix, let params = vMix.inputParameters.first {
diagLog += "Verified trackID: \(params.trackID)\n"
}
writeDiagLog(diagLog)
} }
}
NSLog("[Recorder] Found audio track: ID=%d, formatDescriptions=%d",
audioTrack.trackID, audioTrack.formatDescriptions.count) private func writeDiagLog(_ content: String) {
if let moviesDir = FileManager.default.urls(for: .moviesDirectory, in: .userDomainMask).first {
// MTAudioProcessingTap let logPath = moviesDir.appendingPathComponent("MiniPlayer/tap_install_log.txt")
var callbacks = MTAudioProcessingTapCallbacks( try? content.write(to: logPath, atomically: true, encoding: .utf8)
version: kMTAudioProcessingTapCallbacksVersion_0, print("[Recorder:DIAG] Log written to: \(logPath.path)")
clientInfo: Unmanaged.passUnretained(ctx).toOpaque(),
init: tapInit,
finalize: tapFinalize,
prepare: tapPrepare,
unprepare: tapUnprepare,
process: tapProcess
)
var tap: MTAudioProcessingTap?
let status = MTAudioProcessingTapCreate(
kCFAllocatorDefault, &callbacks,
kMTAudioProcessingTapCreationFlag_PreEffects,
&tap
)
guard status == noErr, let tapRef = tap else {
NSLog("[Recorder] ⚠️ MTAudioProcessingTapCreate failed: %d", status)
return
} }
// AudioMix trackID
// kCMPersistentTrackID_Invalid (=0) tap
let inputParams = AVMutableAudioMixInputParameters()
inputParams.trackID = audioTrack.trackID
inputParams.audioTapProcessor = tapRef
let audioMix = AVMutableAudioMix()
audioMix.inputParameters = [inputParams]
playerItem.audioMix = audioMix
NSLog("[Recorder] ✓ Audio tap installed: trackID=%d, asset tracks=%d, playerItem.audioMix set",
audioTrack.trackID, playerItem.asset.tracks.count)
} }
// MARK: - // MARK: -
@ -527,7 +580,8 @@ final class PlayerRecorder: NSObject {
notReadyCount: tapContext?.notReadyCount ?? 0, notReadyCount: tapContext?.notReadyCount ?? 0,
lastActiveTimestamp: tapContext?.lastActiveTimestamp ?? 0, lastActiveTimestamp: tapContext?.lastActiveTimestamp ?? 0,
sampleRate: tapContext?.sampleRate ?? 0, sampleRate: tapContext?.sampleRate ?? 0,
channelsPerFrame: tapContext?.channelsPerFrame ?? 0 channelsPerFrame: tapContext?.channelsPerFrame ?? 0,
mixTrackID: tapContext?.mixTrackID ?? -1
) )
NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d, peak=%.4f, active=%d, silent=%d", NSLog("[Recorder] Stopping: videoFrames=%d, audioProcessCalls=%d, audioAppended=%d, peak=%.4f, active=%d, silent=%d",
@ -718,6 +772,7 @@ final class PlayerRecorder: NSObject {
"--- Audio Format ---", "--- Audio Format ---",
"sampleRate: \(String(format: "%.0f", stats.sampleRate)) Hz", "sampleRate: \(String(format: "%.0f", stats.sampleRate)) Hz",
"channelsPerFrame: \(stats.channelsPerFrame)", "channelsPerFrame: \(stats.channelsPerFrame)",
"mixTrackID: \(stats.mixTrackID) (bound in audioMix, -1=not set)",
"peakAmplitude: \(String(format: "%.6f", stats.peakAmplitude)) (\(String(format: "%.1f", peakDB)) dB)", "peakAmplitude: \(String(format: "%.6f", stats.peakAmplitude)) (\(String(format: "%.1f", peakDB)) dB)",
"", "",
"--- Skip/Error Counts ---", "--- Skip/Error Counts ---",