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:
parent
70b264bcc8
commit
1880eeb3d3
@ -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
|
||||
/// 异步安装音频 Tap(HLS 流需要异步加载 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 ---",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user