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:
parent
cbc03b40b4
commit
60a3369d82
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user