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 lastActiveTimestamp: Double = 0 // ()
nonisolated(unsafe) var recordStartTime: Date? //
nonisolated(unsafe) var mixTrackID: Int32 = -1 // audioMix trackID
let audioQueue = DispatchQueue(label: "miniplayer.audioTap")
}
@ -274,6 +275,7 @@ struct AudioDiagStats {
let lastActiveTimestamp: Double
let sampleRate: Double
let channelsPerFrame: UInt32
let mixTrackID: Int32
}
// MARK: - PlayerRecorder
@ -316,58 +318,109 @@ final class PlayerRecorder: NSObject {
// MARK: - Audio Tap Setup
/// Tap playerItem
/// TapHLS tracks
private func installAudioTap(on playerItem: AVPlayerItem) {
//
if gTapContext == nil {
gTapContext = AudioTapContext()
}
guard let ctx = gTapContext else { return }
// track asset
let audioTracks = playerItem.asset.tracks(withMediaType: .audio)
guard let audioTrack = audioTracks.first else {
NSLog("[Recorder] ⚠️ No audio track found (tried %d tracks)", audioTracks.count)
return
Task { @MainActor in
guard let ctx = gTapContext else { return }
print("[Recorder:DIAG] === installAudioTap START (async) ===")
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)
// 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 status = MTAudioProcessingTapCreate(
kCFAllocatorDefault, &callbacks,
kMTAudioProcessingTapCreationFlag_PreEffects,
&tap
)
guard status == noErr, let tapRef = tap else {
NSLog("[Recorder] ⚠️ MTAudioProcessingTapCreate failed: %d", status)
return
}
private func writeDiagLog(_ content: String) {
if let moviesDir = FileManager.default.urls(for: .moviesDirectory, in: .userDomainMask).first {
let logPath = moviesDir.appendingPathComponent("MiniPlayer/tap_install_log.txt")
try? content.write(to: logPath, atomically: true, encoding: .utf8)
print("[Recorder:DIAG] Log written to: \(logPath.path)")
}
// 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: -
@ -527,7 +580,8 @@ final class PlayerRecorder: NSObject {
notReadyCount: tapContext?.notReadyCount ?? 0,
lastActiveTimestamp: tapContext?.lastActiveTimestamp ?? 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",
@ -718,6 +772,7 @@ final class PlayerRecorder: NSObject {
"--- Audio Format ---",
"sampleRate: \(String(format: "%.0f", stats.sampleRate)) Hz",
"channelsPerFrame: \(stats.channelsPerFrame)",
"mixTrackID: \(stats.mixTrackID) (bound in audioMix, -1=not set)",
"peakAmplitude: \(String(format: "%.6f", stats.peakAmplitude)) (\(String(format: "%.1f", peakDB)) dB)",
"",
"--- Skip/Error Counts ---",