From 60a3369d82605a522719d47a20cbc9a15b9991d5 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 24 Jun 2026 21:01:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BD=95=E5=88=B6=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E6=97=B6=E9=9F=B3=E9=A2=91=E6=9C=AA=E5=90=8C=E6=AD=A5=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E5=AF=BC=E8=87=B4=E8=BE=93=E5=87=BA=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=B6=85=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: readAudioLoop 从文件读音频速度远快于实时,每帧 dispatch 到 MainActor 造成大量 Task 积压。用户点停止时,积压的 Task 在 isRunning=false 生效前全部执行,导致整个源音频被写入。 修复: - 音频直接在后台队列写入,不再 dispatch 到 MainActor - 增加 nonisolated(unsafe) audioStopped 标志,stopAudioCapture 先设标志让 loop 立即退出 - 增加 nonisolated(unsafe) audioFirstFrameTime 供后台线程读取 - audioInput 作为参数传入 readAudioLoop,避免跨 actor 访问 --- Sources/PlayerRecorder.swift | 69 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index df83195..7b30345 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -17,6 +17,10 @@ final class PlayerRecorder: NSObject { private var audioReaderOutput: AVAssetReaderTrackOutput? private var audioCaptureQueue: DispatchQueue? private var isRunning = false + /// 线程安全的音频停止标志,后台队列直接读取 + nonisolated(unsafe) private var audioStopped: Bool = false + /// 音频线程读取 firstFrameTime 的镜像 + nonisolated(unsafe) private var audioFirstFrameTime: CMTime? @Published var isRecording = false @Published var durationText = "00:00" @@ -40,6 +44,7 @@ final class PlayerRecorder: NSObject { weakOutput = output firstFrameTime = nil isRunning = false + audioStopped = false // 临时文件 let formatter = DateFormatter() @@ -143,6 +148,7 @@ final class PlayerRecorder: NSObject { // 第一帧:用它的时间作为 session 起点 if firstFrameTime == nil { firstFrameTime = itemTime + audioFirstFrameTime = itemTime // 同步给音频线程 writer?.startSession(atSourceTime: itemTime) isRunning = true } @@ -213,7 +219,8 @@ final class PlayerRecorder: NSObject { let queue = DispatchQueue(label: "recorder.audio.capture") self.audioCaptureQueue = queue queue.async { [weak self] in - self?.readAudioLoop(reader: reader, output: output) + guard let self = self, let aInput = self.audioInput else { return } + self.readAudioLoop(reader: reader, output: output, audioInput: aInput) } } } catch { @@ -222,47 +229,45 @@ final class PlayerRecorder: NSObject { } } - private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput) { - while reader.status == .reading { + private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput, audioInput: AVAssetWriterInput) { + while !audioStopped && reader.status == .reading { guard let sampleBuffer = output.copyNextSampleBuffer() else { break } - // 在 MainActor 上调整时间戳并写入 - Task { @MainActor [weak self] in - guard let self = self, self.isRunning else { return } - guard let firstTime = self.firstFrameTime else { return } - guard let input = self.audioInput, input.isReadyForMoreMediaData else { return } - - let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - let relativeTime = CMTimeSubtract(pts, firstTime) - guard relativeTime.seconds >= 0 else { return } - - // 重建 sample buffer with new time - var count: CMItemCount = 0 - CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &count) - var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: count) - CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &timingInfo, entriesNeededOut: nil) - - for i in 0..= 0 else { continue } + + // 重建 sample buffer with new time + var count: CMItemCount = 0 + CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &count) + var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: count) + CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &timingInfo, entriesNeededOut: nil) + + for i in 0..