fix: 录制停止时音频未同步停止导致输出文件超长

根因: readAudioLoop 从文件读音频速度远快于实时,每帧 dispatch 到
MainActor 造成大量 Task 积压。用户点停止时,积压的 Task 在
isRunning=false 生效前全部执行,导致整个源音频被写入。

修复:
- 音频直接在后台队列写入,不再 dispatch 到 MainActor
- 增加 nonisolated(unsafe) audioStopped 标志,stopAudioCapture
  先设标志让 loop 立即退出
- 增加 nonisolated(unsafe) audioFirstFrameTime 供后台线程读取
- audioInput 作为参数传入 readAudioLoop,避免跨 actor 访问
This commit is contained in:
yumoqing 2026-06-24 21:01:39 +08:00
parent cbc03b40b4
commit 60a3369d82

View File

@ -17,6 +17,10 @@ final class PlayerRecorder: NSObject {
private var audioReaderOutput: AVAssetReaderTrackOutput? private var audioReaderOutput: AVAssetReaderTrackOutput?
private var audioCaptureQueue: DispatchQueue? private var audioCaptureQueue: DispatchQueue?
private var isRunning = false private var isRunning = false
/// 线
nonisolated(unsafe) private var audioStopped: Bool = false
/// 线 firstFrameTime
nonisolated(unsafe) private var audioFirstFrameTime: CMTime?
@Published var isRecording = false @Published var isRecording = false
@Published var durationText = "00:00" @Published var durationText = "00:00"
@ -40,6 +44,7 @@ final class PlayerRecorder: NSObject {
weakOutput = output weakOutput = output
firstFrameTime = nil firstFrameTime = nil
isRunning = false isRunning = false
audioStopped = false
// //
let formatter = DateFormatter() let formatter = DateFormatter()
@ -143,6 +148,7 @@ final class PlayerRecorder: NSObject {
// session // session
if firstFrameTime == nil { if firstFrameTime == nil {
firstFrameTime = itemTime firstFrameTime = itemTime
audioFirstFrameTime = itemTime // 线
writer?.startSession(atSourceTime: itemTime) writer?.startSession(atSourceTime: itemTime)
isRunning = true isRunning = true
} }
@ -213,7 +219,8 @@ final class PlayerRecorder: NSObject {
let queue = DispatchQueue(label: "recorder.audio.capture") let queue = DispatchQueue(label: "recorder.audio.capture")
self.audioCaptureQueue = queue self.audioCaptureQueue = queue
queue.async { [weak self] in 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 { } catch {
@ -222,47 +229,45 @@ final class PlayerRecorder: NSObject {
} }
} }
private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput) { private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput, audioInput: AVAssetWriterInput) {
while reader.status == .reading { while !audioStopped && reader.status == .reading {
guard let sampleBuffer = output.copyNextSampleBuffer() else { guard let sampleBuffer = output.copyNextSampleBuffer() else {
break break
} }
// MainActor // MainActor
Task { @MainActor [weak self] in guard let firstTime = audioFirstFrameTime else {
guard let self = self, self.isRunning else { return } // buffer
guard let firstTime = self.firstFrameTime else { return } continue
guard let input = self.audioInput, input.isReadyForMoreMediaData else { return } }
guard audioInput.isReadyForMoreMediaData else { continue }
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let relativeTime = CMTimeSubtract(pts, firstTime) let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
guard relativeTime.seconds >= 0 else { return } let relativeTime = CMTimeSubtract(pts, firstTime)
guard relativeTime.seconds >= 0 else { continue }
// sample buffer with new time
var count: CMItemCount = 0 // sample buffer with new time
CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &count) var count: CMItemCount = 0
var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: count) CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &count)
CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &timingInfo, entriesNeededOut: nil) var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: count)
CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: count, arrayToFill: &timingInfo, entriesNeededOut: nil)
for i in 0..<count {
timingInfo[i].decodeTimeStamp = .invalid for i in 0..<count {
timingInfo[i].presentationTimeStamp = CMTimeSubtract(timingInfo[i].presentationTimeStamp, firstTime) timingInfo[i].decodeTimeStamp = .invalid
} timingInfo[i].presentationTimeStamp = CMTimeSubtract(timingInfo[i].presentationTimeStamp, firstTime)
var newBuffer: CMSampleBuffer?
CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: count, sampleTimingArray: &timingInfo, sampleBufferOut: &newBuffer)
if let nb = newBuffer {
_ = input.append(nb)
}
} }
// MainActor var newBuffer: CMSampleBuffer?
Thread.sleep(forTimeInterval: 0.005) CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: count, sampleTimingArray: &timingInfo, sampleBufferOut: &newBuffer)
if let nb = newBuffer {
_ = audioInput.append(nb)
}
} }
} }
private func stopAudioCapture() { private func stopAudioCapture() {
audioStopped = true // loop 退
audioReader?.cancelReading() audioReader?.cancelReading()
audioReader = nil audioReader = nil
audioReaderOutput = nil audioReaderOutput = nil