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 tracksHLS
if !didTryAsyncLoad && attempts >= 50 {
didTryAsyncLoad = true
NSLog("[Recorder] Trying async asset.load(.tracks)...")
// 2: 3 async load asset tracksHLS
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
}
// trackmuxed
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 fallbackplayerItem.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.tracksHLS
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
}