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 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,23 +318,45 @@ final class PlayerRecorder: NSObject {
|
|||||||
|
|
||||||
// MARK: - Audio Tap Setup
|
// MARK: - Audio Tap Setup
|
||||||
|
|
||||||
/// 在录制开始时安装音频 Tap 到 playerItem
|
/// 异步安装音频 Tap(HLS 流需要异步加载 tracks)
|
||||||
private func installAudioTap(on playerItem: AVPlayerItem) {
|
private func installAudioTap(on playerItem: AVPlayerItem) {
|
||||||
// 确保全局上下文存在
|
// 确保全局上下文存在
|
||||||
if gTapContext == nil {
|
if gTapContext == nil {
|
||||||
gTapContext = AudioTapContext()
|
gTapContext = AudioTapContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
guard let ctx = gTapContext else { return }
|
guard let ctx = gTapContext else { return }
|
||||||
|
|
||||||
// 同步获取音频 track(录制时 asset 已经加载完毕)
|
print("[Recorder:DIAG] === installAudioTap START (async) ===")
|
||||||
let audioTracks = playerItem.asset.tracks(withMediaType: .audio)
|
print("[Recorder:DIAG] asset class: \(type(of: playerItem.asset))")
|
||||||
guard let audioTrack = audioTracks.first else {
|
|
||||||
NSLog("[Recorder] ⚠️ No audio track found (tried %d tracks)", audioTracks.count)
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog("[Recorder] Found audio track: ID=%d, formatDescriptions=%d",
|
guard let audioTrack = audioTracks.first else {
|
||||||
audioTrack.trackID, audioTrack.formatDescriptions.count)
|
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
|
// 创建 MTAudioProcessingTap
|
||||||
var callbacks = MTAudioProcessingTapCallbacks(
|
var callbacks = MTAudioProcessingTapCallbacks(
|
||||||
@ -346,18 +370,18 @@ final class PlayerRecorder: NSObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var tap: MTAudioProcessingTap?
|
var tap: MTAudioProcessingTap?
|
||||||
let status = MTAudioProcessingTapCreate(
|
let tapStatus = MTAudioProcessingTapCreate(
|
||||||
kCFAllocatorDefault, &callbacks,
|
kCFAllocatorDefault, &callbacks,
|
||||||
kMTAudioProcessingTapCreationFlag_PreEffects,
|
kMTAudioProcessingTapCreationFlag_PreEffects,
|
||||||
&tap
|
&tap
|
||||||
)
|
)
|
||||||
guard status == noErr, let tapRef = tap else {
|
print("[Recorder:DIAG] MTAudioProcessingTapCreate status=\(tapStatus) (noErr=\(noErr))")
|
||||||
NSLog("[Recorder] ⚠️ MTAudioProcessingTapCreate failed: %d", status)
|
guard tapStatus == noErr, let tapRef = tap else {
|
||||||
|
print("[Recorder:DIAG] ⚠️ Tap creation failed!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 AudioMix — 必须用实际 trackID
|
// 创建 AudioMix
|
||||||
// kCMPersistentTrackID_Invalid (=0) 实测不会触发 tap
|
|
||||||
let inputParams = AVMutableAudioMixInputParameters()
|
let inputParams = AVMutableAudioMixInputParameters()
|
||||||
inputParams.trackID = audioTrack.trackID
|
inputParams.trackID = audioTrack.trackID
|
||||||
inputParams.audioTapProcessor = tapRef
|
inputParams.audioTapProcessor = tapRef
|
||||||
@ -366,8 +390,37 @@ final class PlayerRecorder: NSObject {
|
|||||||
audioMix.inputParameters = [inputParams]
|
audioMix.inputParameters = [inputParams]
|
||||||
|
|
||||||
playerItem.audioMix = audioMix
|
playerItem.audioMix = audioMix
|
||||||
NSLog("[Recorder] ✓ Audio tap installed: trackID=%d, asset tracks=%d, playerItem.audioMix set",
|
|
||||||
audioTrack.trackID, playerItem.asset.tracks.count)
|
// 验证
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ---",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user