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 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..<count {
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
guard let firstTime = audioFirstFrameTime else {
// buffer
continue
}
guard audioInput.isReadyForMoreMediaData else { continue }
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let relativeTime = CMTimeSubtract(pts, firstTime)
guard relativeTime.seconds >= 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..<count {
timingInfo[i].decodeTimeStamp = .invalid
timingInfo[i].presentationTimeStamp = CMTimeSubtract(timingInfo[i].presentationTimeStamp, firstTime)
}
// MainActor
Thread.sleep(forTimeInterval: 0.005)
var newBuffer: CMSampleBuffer?
CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: count, sampleTimingArray: &timingInfo, sampleBufferOut: &newBuffer)
if let nb = newBuffer {
_ = audioInput.append(nb)
}
}
}
private func stopAudioCapture() {
audioStopped = true // loop 退
audioReader?.cancelReading()
audioReader = nil
audioReaderOutput = nil