From 339664ebc1bbc3515255a2b34ef21f9f913cc891 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 26 Jun 2026 08:51:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DHLS=E6=B5=81=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E5=BD=95=E5=88=B6=E6=97=A0=E9=9F=B3=E9=A2=91=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 存储tapPrepareCallback实际采样率,替代不可靠的timeRange.duration.timescale - findAudioTrackID增加3路fallback: playerItem.tracks → asset.tracks → 非视频推断 - async load(.tracks)从单次尝试改为每3秒重试 - 增加muxed track fallback(HLS可能返回非标准mediaType) --- Sources/PlayerRecorder.swift | 75 +++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 6ed9a26..e9e7d9b 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -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 }