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 appendCount: Int = 0
nonisolated(unsafe) var firstAppendLogged: Bool = false nonisolated(unsafe) var firstAppendLogged: Bool = false
nonisolated(unsafe) var processCallCount: Int = 0 nonisolated(unsafe) var processCallCount: Int = 0
nonisolated(unsafe) var sampleRate: Double = 44100.0
nonisolated(unsafe) var channelsPerFrame: UInt32 = 2
} }
// MARK: - MTAudioProcessingTap C // MARK: - MTAudioProcessingTap C
@ -39,6 +41,8 @@ private func tapPrepareCallback(
) { ) {
guard let ctx = gAudioContext else { return } guard let ctx = gAudioContext else { return }
var asbd = processingFormat.pointee var asbd = processingFormat.pointee
ctx.sampleRate = Double(asbd.mSampleRate)
ctx.channelsPerFrame = asbd.mChannelsPerFrame
var fmtDesc: CMAudioFormatDescription? var fmtDesc: CMAudioFormatDescription?
CMAudioFormatDescriptionCreate( CMAudioFormatDescriptionCreate(
allocator: kCFAllocatorDefault, allocator: kCFAllocatorDefault,
@ -49,7 +53,7 @@ private func tapPrepareCallback(
formatDescriptionOut: &fmtDesc formatDescriptionOut: &fmtDesc
) )
ctx.formatDescription = 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) { private func tapUnprepareCallback(_ tap: MTAudioProcessingTap) {
@ -127,8 +131,7 @@ private func tapProcessCallback(
} }
// PTS 0 // PTS 0
let sampleRate = Double(timeRange.duration.timescale) > 0 let sampleRate = ctx.sampleRate
? Double(timeRange.duration.timescale) : 44100.0
let ptsValue = Double(ctx.appendCount) / sampleRate let ptsValue = Double(ctx.appendCount) / sampleRate
let pts = CMTime(seconds: ptsValue, preferredTimescale: Int32(sampleRate)) let pts = CMTime(seconds: ptsValue, preferredTimescale: Int32(sampleRate))
@ -408,12 +411,12 @@ final class PlayerRecorder: NSObject {
} }
self.tracksObservation = observation self.tracksObservation = observation
// async asset.load(.tracks) // async asset.load(.tracks)
// 30 // 20
Task { Task {
var attempts = 0 var attempts = 0
var didTryAsyncLoad = false var lastAsyncAttempt = -100 //
while attempts < 300 && self.isRunning && self.audioTap == nil { // 30 while attempts < 200 && self.isRunning && self.audioTap == nil { // 20
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 try await Task.sleep(nanoseconds: 100_000_000) // 0.1
attempts += 1 attempts += 1
@ -428,10 +431,10 @@ final class PlayerRecorder: NSObject {
return return
} }
// 2: 5 async load asset tracksHLS // 2: 3 async load asset tracksHLS
if !didTryAsyncLoad && attempts >= 50 { if attempts - lastAsyncAttempt >= 30 {
didTryAsyncLoad = true lastAsyncAttempt = attempts
NSLog("[Recorder] Trying async asset.load(.tracks)...") NSLog("[Recorder] Trying async asset.load(.tracks) (attempt at %d)...", attempts)
do { do {
let assetTracks = try await playerItem.asset.load(.tracks) let assetTracks = try await playerItem.asset.load(.tracks)
NSLog("[Recorder] async load returned %d tracks", assetTracks.count) NSLog("[Recorder] async load returned %d tracks", assetTracks.count)
@ -439,6 +442,7 @@ final class PlayerRecorder: NSObject {
NSLog("[Recorder] assetTrack[%d]: mediaType=%@, trackID=%d", NSLog("[Recorder] assetTrack[%d]: mediaType=%@, trackID=%d",
i, t.mediaType.rawValue as NSString, t.trackID) i, t.mediaType.rawValue as NSString, t.trackID)
} }
// track
if let audioAssetTrack = assetTracks.first(where: { $0.mediaType == .audio }) { if let audioAssetTrack = assetTracks.first(where: { $0.mediaType == .audio }) {
let trackID = audioAssetTrack.trackID let trackID = audioAssetTrack.trackID
NSLog("[Recorder] ✓ Audio track via async load: trackID=%d", trackID) NSLog("[Recorder] ✓ Audio track via async load: trackID=%d", trackID)
@ -449,6 +453,17 @@ final class PlayerRecorder: NSObject {
self.tracksObservation = nil self.tracksObservation = nil
return 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 { } catch {
NSLog("[Recorder] async load failed: %@", error.localizedDescription) NSLog("[Recorder] async load failed: %@", error.localizedDescription)
} }
@ -462,29 +477,43 @@ final class PlayerRecorder: NSObject {
} }
} }
if self.audioTap == nil { 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 self.tracksObservation = nil
} }
} }
} }
/// playerItem.tracks trackID /// playerItem.tracks trackID
/// HLS assetTrack nil /// HLS fallbackplayerItem.tracks asset.tracks
private func findAudioTrackID(in playerItem: AVPlayerItem) -> CMPersistentTrackID? { private func findAudioTrackID(in playerItem: AVPlayerItem) -> CMPersistentTrackID? {
// 1: playerItem.tracks + assetTrack / HLS
let itemTracks = playerItem.tracks let itemTracks = playerItem.tracks
guard !itemTracks.isEmpty else { return nil } for track in itemTracks {
if let assetTrack = track.assetTrack, assetTrack.mediaType == .audio {
for (i, track) in itemTracks.enumerated() { NSLog("[Recorder] findTrack: audio via playerItem.tracks, trackID=%d", assetTrack.trackID)
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 {
return 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 return nil
} }