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 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 tracks(HLS 可能通过这个路径加载)
|
// 方式2: 每3秒重试一次 async load asset tracks(HLS 关键路径)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
// 没找到音频,尝试非视频非音频 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 {
|
} 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 流,多路 fallback:playerItem.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.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user