diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 274f31b..bdb2434 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -122,8 +122,7 @@ final class PlayerBridge: ObservableObject { } guard !queue.isEmpty else { return } let sourceURL = queue[currentIndex].url - let startTime = player.currentTime() - screenRecorder.startRecording(from: output, sourceURL: sourceURL, startTime: startTime) + screenRecorder.startRecording(from: output, sourceURL: sourceURL) } #endif } diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 7b30345..ff7e638 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -22,6 +22,10 @@ final class PlayerRecorder: NSObject { /// 音频线程读取 firstFrameTime 的镜像 nonisolated(unsafe) private var audioFirstFrameTime: CMTime? + // 音频预加载状态 + private var pendingAudioAsset: AVURLAsset? + private var pendingAudioTrack: AVAssetTrack? + @Published var isRecording = false @Published var durationText = "00:00" @@ -38,7 +42,7 @@ final class PlayerRecorder: NSObject { } /// 开始录制 - func startRecording(from output: AVPlayerItemVideoOutput, sourceURL: URL, startTime: CMTime) { + func startRecording(from output: AVPlayerItemVideoOutput, sourceURL: URL) { guard !isRecording else { return } weakOutput = output @@ -97,8 +101,8 @@ final class PlayerRecorder: NSObject { startTimer() startCaptureLoop() - // 启动音频读取(从源 URL) - startAudioCapture(sourceURL: sourceURL, startTime: startTime, audioWriterInput: aInput) + // 准备音频读取(等待 firstFrameTime 后才真正开始) + prepareAudioCapture(sourceURL: sourceURL) } /// 停止录制 @@ -151,6 +155,8 @@ final class PlayerRecorder: NSObject { audioFirstFrameTime = itemTime // 同步给音频线程 writer?.startSession(atSourceTime: itemTime) isRunning = true + // 音频预加载完成后,用 firstFrameTime 作为起始时间开始读取 + beginAudioReading(startTime: itemTime) } // 相对时间戳 @@ -182,8 +188,12 @@ final class PlayerRecorder: NSObject { } // MARK: - 音频读取(从源 URL) - private func startAudioCapture(sourceURL: URL, startTime: CMTime, audioWriterInput: AVAssetWriterInput) { + + /// 预加载音频轨道(异步),不立即开始读取 + private func prepareAudioCapture(sourceURL: URL) { let asset = AVURLAsset(url: sourceURL) + pendingAudioAsset = asset + pendingAudioTrack = nil Task { do { @@ -192,43 +202,66 @@ final class PlayerRecorder: NSObject { print("[Recorder] No audio track in source") return } - - let reader = try AVAssetReader(asset: asset) - let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [ - AVFormatIDKey: kAudioFormatLinearPCM, - AVSampleRateKey: 44100, - AVNumberOfChannelsKey: 2, - AVLinearPCMBitDepthKey: 16, - AVLinearPCMIsFloatKey: false, - AVLinearPCMIsBigEndianKey: false, - AVLinearPCMIsNonInterleaved: false - ]) - output.alwaysCopiesSampleData = false - - // 从播放位置开始读取 - reader.timeRange = CMTimeRange(start: startTime, duration: .positiveInfinity) - - if reader.canAdd(output) { - reader.add(output) - reader.startReading() - - self.audioReader = reader - self.audioReaderOutput = output - - // 后台线程读取音频 - let queue = DispatchQueue(label: "recorder.audio.capture") - self.audioCaptureQueue = queue - queue.async { [weak self] in - guard let self = self, let aInput = self.audioInput else { return } - self.readAudioLoop(reader: reader, output: output, audioInput: aInput) - } + self.pendingAudioTrack = audioTrack + // 如果 firstFrameTime 已经设置好了,立即开始读取 + if let fft = self.firstFrameTime { + self.beginAudioReading(startTime: fft) } } catch { - print("[Recorder] Audio capture setup failed: \(error)") + print("[Recorder] Audio track load failed: \(error)") } } } + /// 用 firstFrameTime 作为起始时间,创建 reader 并开始后台读取 + private func beginAudioReading(startTime: CMTime) { + guard let asset = pendingAudioAsset, let audioTrack = pendingAudioTrack else { + return // 轨道还没加载好,prepareAudioCapture 的回调会再次调用 + } + guard let aInput = audioInput else { return } + // 防止重复启动 + guard audioReader == nil else { return } + + do { + let reader = try AVAssetReader(asset: asset) + let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [ + AVFormatIDKey: kAudioFormatLinearPCM, + AVSampleRateKey: 44100, + AVNumberOfChannelsKey: 2, + AVLinearPCMBitDepthKey: 16, + AVLinearPCMIsFloatKey: false, + AVLinearPCMIsBigEndianKey: false, + AVLinearPCMIsNonInterleaved: false + ]) + output.alwaysCopiesSampleData = false + + // 从视频第一帧时间开始读取,确保音视频同步 + reader.timeRange = CMTimeRange(start: startTime, duration: .positiveInfinity) + + if reader.canAdd(output) { + reader.add(output) + reader.startReading() + + self.audioReader = reader + self.audioReaderOutput = output + + // 后台线程读取音频 + let queue = DispatchQueue(label: "recorder.audio.capture") + self.audioCaptureQueue = queue + queue.async { [weak self] in + guard let self = self else { return } + self.readAudioLoop(reader: reader, output: output, audioInput: aInput) + } + } + } catch { + print("[Recorder] Audio capture setup failed: \(error)") + } + + // 清理预加载状态 + pendingAudioAsset = nil + pendingAudioTrack = nil + } + private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput, audioInput: AVAssetWriterInput) { while !audioStopped && reader.status == .reading { guard let sampleBuffer = output.copyNextSampleBuffer() else {