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:
parent
d4a39dd7c3
commit
339664ebc1
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user