fix: 修复HLS流媒体录制无音频问题

- 存储tapPrepareCallback实际采样率,替代不可靠的timeRange.duration.timescale
- findAudioTrackID增加3路fallback: playerItem.tracks → asset.tracks → 非视频推断
- async load(.tracks)从单次尝试改为每3秒重试
- 增加muxed track fallback(HLS可能返回非标准mediaType)
This commit is contained in:
yumoqing 2026-06-26 08:51:10 +08:00
parent d4a39dd7c3
commit 339664ebc1

View File

@ -15,6 +15,8 @@ class AudioCaptureContext {
nonisolated(unsafe) var appendCount: Int = 0
nonisolated(unsafe) var firstAppendLogged: Bool = false
nonisolated(unsafe) var processCallCount: Int = 0
nonisolated(unsafe) var sampleRate: Double = 44100.0
nonisolated(unsafe) var channelsPerFrame: UInt32 = 2
}
// MARK: - MTAudioProcessingTap C 回调
@ -39,6 +41,8 @@ private func tapPrepareCallback(
) {
guard let ctx = gAudioContext else { return }
var asbd = processingFormat.pointee
ctx.sampleRate = Double(asbd.mSampleRate)
ctx.channelsPerFrame = asbd.mChannelsPerFrame
var fmtDesc: CMAudioFormatDescription?
CMAudioFormatDescriptionCreate(
allocator: kCFAllocatorDefault,
@ -49,7 +53,7 @@ private func tapPrepareCallback(
formatDescriptionOut: &fmtDesc
)
ctx.formatDescription = fmtDesc
NSLog("[Recorder] tapPrepare: %.1fHz, %uch", asbd.mSampleRate, asbd.mChannelsPerFrame)
NSLog("[Recorder] tapPrepare: %.1fHz, %uch, mFormatID=%u", asbd.mSampleRate, asbd.mChannelsPerFrame, asbd.mFormatID)
}
private func tapUnprepareCallback(_ tap: MTAudioProcessingTap) {
@ -127,8 +131,7 @@ private func tapProcessCallback(
}
// PTS 基于累积帧数(与视频一样从0开始)
let sampleRate = Double(timeRange.duration.timescale) > 0
? Double(timeRange.duration.timescale) : 44100.0
let sampleRate = ctx.sampleRate
let ptsValue = Double(ctx.appendCount) / sampleRate
let pts = CMTime(seconds: ptsValue, preferredTimescale: Int32(sampleRate))
@ -408,12 +411,12 @@ final class PlayerRecorder: NSObject {
}
self.tracksObservation = observation
// 保底轮询:含 async asset.load(.tracks) 备选
// 超时保护:30秒后放弃
// 保底轮询:含 async asset.load(.tracks) 多次重试
// 超时保护:20秒后放弃
Task {
var attempts = 0
var didTryAsyncLoad = false
while attempts < 300 && self.isRunning && self.audioTap == nil { // 30秒
var lastAsyncAttempt = -100 // 确保首次就能尝试
while attempts < 200 && self.isRunning && self.audioTap == nil { // 20秒
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
attempts += 1
@ -428,10 +431,10 @@ final class PlayerRecorder: NSObject {
return
}
// 方式2: 5秒后尝试 async load asset tracks(HLS 可能通过这个路径加载)
if !didTryAsyncLoad && attempts >= 50 {
didTryAsyncLoad = true
NSLog("[Recorder] Trying async asset.load(.tracks)...")
// 方式2: 每3秒重试一次 async load asset tracks(HLS 关键路径)
if attempts - lastAsyncAttempt >= 30 {
lastAsyncAttempt = attempts
NSLog("[Recorder] Trying async asset.load(.tracks) (attempt at %d)...", attempts)
do {
let assetTracks = try await playerItem.asset.load(.tracks)
NSLog("[Recorder] async load returned %d tracks", assetTracks.count)
@ -439,6 +442,7 @@ final class PlayerRecorder: NSObject {
NSLog("[Recorder] assetTrack[%d]: mediaType=%@, trackID=%d",
i, t.mediaType.rawValue as NSString, t.trackID)
}
// 找音频 track
if let audioAssetTrack = assetTracks.first(where: { $0.mediaType == .audio }) {
let trackID = audioAssetTrack.trackID
NSLog("[Recorder] ✓ Audio track via async load: trackID=%d", trackID)
@ -449,6 +453,17 @@ final class PlayerRecorder: NSObject {
self.tracksObservation = nil
return
}
// 没找到音频,尝试非视频非音频 track(muxed)
if let muxedTrack = assetTracks.first(where: { $0.mediaType != .video && $0.mediaType != .audio }) {
let trackID = muxedTrack.trackID
NSLog("[Recorder] ✓ Muxed track via async load: trackID=%d, trying as audio", trackID)
await MainActor.run {
guard self.isRunning, self.audioTap == nil else { return }
self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput)
}
self.tracksObservation = nil
return
}
} catch {
NSLog("[Recorder] async load failed: %@", error.localizedDescription)
}
@ -462,29 +477,43 @@ final class PlayerRecorder: NSObject {
}
}
if self.audioTap == nil {
NSLog("[Recorder] ❌ No audio track found after %d polling attempts (30s timeout)", attempts)
NSLog("[Recorder] ❌ No audio track found after %d polling attempts (20s timeout)", attempts)
self.tracksObservation = nil
}
}
}
/// 从 playerItem.tracks 查找音频 trackID
/// 对 HLS 流,assetTrack 可能延迟加载或始终为 nil
/// 对 HLS 流,多路 fallback:playerItem.tracks → asset.tracks → 非视频推断
private func findAudioTrackID(in playerItem: AVPlayerItem) -> CMPersistentTrackID? {
// 方式1: playerItem.tracks + assetTrack(本地文件 / 已加载的HLS)
let itemTracks = playerItem.tracks
guard !itemTracks.isEmpty else { return nil }
for (i, track) in itemTracks.enumerated() {
guard let assetTrack = track.assetTrack else {
NSLog("[Recorder] findTrack: track[%d] assetTrack=nil (not loaded)", i)
continue
}
NSLog("[Recorder] findTrack: track[%d] mediaType=%@, trackID=%d, enabled=%d",
i, assetTrack.mediaType.rawValue as NSString, assetTrack.trackID, track.isEnabled)
if assetTrack.mediaType == .audio {
for track in itemTracks {
if let assetTrack = track.assetTrack, assetTrack.mediaType == .audio {
NSLog("[Recorder] findTrack: audio via playerItem.tracks, trackID=%d", assetTrack.trackID)
return assetTrack.trackID
}
}
// 方式2: asset.tracks(HLS 流可能通过这个路径能拿到)
let assetTracks = playerItem.asset.tracks
for track in assetTracks {
if track.mediaType == .audio {
NSLog("[Recorder] findTrack: audio via asset.tracks, trackID=%d", track.trackID)
return track.trackID
}
}
// 方式3: 如果有 track 但不是音频,找非视频的 track(可能是 muxed 或音频)
for track in itemTracks {
if let assetTrack = track.assetTrack,
assetTrack.mediaType != .video, assetTrack.mediaType != .audio {
NSLog("[Recorder] findTrack: non-video track (mediaType=%@, trackID=%d), trying as audio",
assetTrack.mediaType.rawValue as NSString, assetTrack.trackID)
return assetTrack.trackID
}
}
return nil
}