-
Notifications
You must be signed in to change notification settings - Fork 0
fix(record_ios): Prevent M4A silent frame drops when encoder buffer is full #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,17 @@ import CoreMedia | |
| import Foundation | ||
|
|
||
| /// Output writer that encodes PCM audio to M4A/AAC format. | ||
| /// Uses a queue-and-drain pattern to avoid dropping frames when the AAC encoder | ||
| /// buffer is full (isReadyForMoreMediaData == false), e.g. under CPU/memory pressure. | ||
| class M4aFileOutputWriter: AudioOutputWriter { | ||
| private let outputPath: String | ||
| private var writer: AVAssetWriter? | ||
| private var input: AVAssetWriterInput? | ||
| private var errorMessage: String? | ||
| private var pcmFormat: AVAudioFormat? | ||
|
|
||
| private let drainQueue = DispatchQueue(label: "M4aFileOutputWriter.drain") | ||
| private var pendingSampleBuffers: [CMSampleBuffer] = [] | ||
|
|
||
| init(outputPath: String) { | ||
| self.outputPath = outputPath | ||
| } | ||
|
|
@@ -81,53 +85,87 @@ class M4aFileOutputWriter: AudioOutputWriter { | |
| let input = input, | ||
| let writer = writer, | ||
| let pcmFormat = pcmFormat else { return } | ||
|
|
||
| guard input.isReadyForMoreMediaData else { return } | ||
|
|
||
|
|
||
| let pts = CMTimeMake(value: framePosition, timescale: Int32(pcmFormat.sampleRate)) | ||
|
|
||
| guard let sampleBuffer = buffer.toCMSampleBuffer(presentationTime: pts) else { | ||
| if errorMessage == nil { | ||
| errorMessage = "Failed to create CMSampleBuffer" | ||
| } | ||
| return | ||
| } | ||
|
|
||
| let success = input.append(sampleBuffer) | ||
| if !success { | ||
| if writer.status == .failed { | ||
| errorMessage = writer.error?.localizedDescription ?? "Writer failed" | ||
| } else if errorMessage == nil { | ||
| errorMessage = "Failed to append sample buffer" | ||
|
|
||
| drainQueue.async { [weak self] in | ||
| self?.enqueueAndDrain(sampleBuffer: sampleBuffer) | ||
| } | ||
| } | ||
|
|
||
| private func enqueueAndDrain(sampleBuffer: CMSampleBuffer) { | ||
| guard errorMessage == nil, | ||
| let input = input, | ||
| let writer = writer else { return } | ||
|
|
||
| pendingSampleBuffers.append(sampleBuffer) | ||
| drainPendingBuffers() | ||
|
Comment on lines
+103
to
+109
|
||
| } | ||
|
|
||
| private func drainPendingBuffers() { | ||
| guard errorMessage == nil, | ||
| let input = input, | ||
| let writer = writer else { return } | ||
|
|
||
| while !pendingSampleBuffers.isEmpty && input.isReadyForMoreMediaData && writer.status != .failed { | ||
| let sampleBuffer = pendingSampleBuffers.removeFirst() | ||
| let success = input.append(sampleBuffer) | ||
| if !success { | ||
| if writer.status == .failed { | ||
| errorMessage = writer.error?.localizedDescription ?? "Writer failed" | ||
| } else if errorMessage == nil { | ||
| errorMessage = "Failed to append sample buffer" | ||
| } | ||
| pendingSampleBuffers.insert(sampleBuffer, at: 0) | ||
| return | ||
| } | ||
|
Comment on lines
+117
to
128
|
||
| } | ||
| } | ||
|
|
||
| func stop(completion: @escaping () -> Void) { | ||
| guard let writer = writer, let input = input else { | ||
| guard writer != nil, input != nil else { | ||
| completion() | ||
| return | ||
| } | ||
|
|
||
| input.markAsFinished() | ||
| writer.finishWriting { [weak self] in | ||
| guard let self = self else { | ||
|
|
||
| drainQueue.async { [weak self] in | ||
| guard let self = self, | ||
| let writer = self.writer, | ||
| let input = self.input else { | ||
| completion() | ||
| return | ||
| } | ||
|
|
||
| if writer.status == .failed { | ||
| self.errorMessage = writer.error?.localizedDescription ?? "Unknown error" | ||
|
|
||
| self.drainPendingBuffers() | ||
| input.markAsFinished() | ||
| writer.finishWriting { [weak self] in | ||
|
Comment on lines
+146
to
+148
|
||
| guard let self = self else { | ||
| completion() | ||
| return | ||
| } | ||
| if writer.status == .failed { | ||
| self.errorMessage = writer.error?.localizedDescription ?? "Unknown error" | ||
| } | ||
| self.pendingSampleBuffers.removeAll() | ||
| completion() | ||
| } | ||
|
|
||
| completion() | ||
| } | ||
| } | ||
|
|
||
| func release() { | ||
| writer = nil | ||
| input = nil | ||
| pcmFormat = nil | ||
| drainQueue.sync { | ||
| writer = nil | ||
| input = nil | ||
| pcmFormat = nil | ||
| pendingSampleBuffers.removeAll() | ||
| } | ||
| } | ||
|
|
||
| func getOutputPath() -> String? { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
write(...),inputandwriterare unwrapped but never used anymore (they’re only used for existence checks). This will produce unused-variable warnings; consider changing the guard to boolean nil checks or remove these bindings and rely on the drainQueue-side guard.