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 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user