Simplify cancellation and fix memory leak

This commit is contained in:
Sami Samhuri 2025-01-19 16:17:50 -08:00
parent 865e524be6
commit 49d41080bb
No known key found for this signature in database
2 changed files with 25 additions and 34 deletions

View file

@ -34,7 +34,7 @@ When you're integrating this into an app with Xcode then go to your project's Pa
When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file: When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file:
```swift ```swift
.package(url: "https://github.com/samsonjs/SJSAssetExportSession.git", .upToNextMajor(from: "0.3.5")) .package(url: "https://github.com/samsonjs/SJSAssetExportSession.git", .upToNextMajor(from: "0.3.7"))
``` ```
and then add `"SJSAssetExportSession"` to the list of dependencies in your target as well. and then add `"SJSAssetExportSession"` to the list of dependencies in your target as well.

View file

@ -41,13 +41,12 @@ actor SampleWriter {
// MARK: Internal state // MARK: Internal state
private let reader: AVAssetReader private var reader: AVAssetReader?
private let writer: AVAssetWriter private var writer: AVAssetWriter?
private var audioOutput: AVAssetReaderAudioMixOutput? private var audioOutput: AVAssetReaderAudioMixOutput?
private var audioInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput?
private var videoOutput: AVAssetReaderVideoCompositionOutput? private var videoOutput: AVAssetReaderVideoCompositionOutput?
private var videoInput: AVAssetWriterInput? private var videoInput: AVAssetWriterInput?
private var isCancelled = false
nonisolated init( nonisolated init(
asset: sending AVAsset, asset: sending AVAsset,
@ -107,34 +106,39 @@ actor SampleWriter {
} }
func writeSamples() async throws { func writeSamples() async throws {
guard let reader, let writer else { throw CancellationError() }
try Task.checkCancellation() try Task.checkCancellation()
// Clear all of these properties otherwise when we get cancelled then we leak a bunch of
// pixel buffers.
defer {
if Task.isCancelled {
reader.cancelReading()
writer.cancelWriting()
}
self.reader = nil
self.writer = nil
audioInput = nil
audioOutput = nil
videoInput = nil
videoOutput = nil
}
progressContinuation.yield(0.0) progressContinuation.yield(0.0)
writer.startWriting() writer.startWriting()
writer.startSession(atSourceTime: timeRange.start) writer.startSession(atSourceTime: timeRange.start)
reader.startReading() reader.startReading()
try Task.checkCancellation() try Task.checkCancellation()
startEncodingAudioTracks() startEncodingAudioTracks()
startEncodingVideoTracks() startEncodingVideoTracks()
while reader.status == .reading, writer.status == .writing { while reader.status == .reading, writer.status == .writing {
guard !Task.isCancelled else {
// Flag so that we stop writing samples
isCancelled = true
throw CancellationError()
}
try await Task.sleep(for: .milliseconds(10)) try await Task.sleep(for: .milliseconds(10))
} }
guard !isCancelled, reader.status != .cancelled, writer.status != .cancelled else {
log.debug("Cancelled before writing samples")
reader.cancelReading()
writer.cancelWriting()
throw CancellationError()
}
guard writer.status != .failed else { guard writer.status != .failed else {
reader.cancelReading() reader.cancelReading()
throw Error.writeFailure(writer.error) throw Error.writeFailure(writer.error)
@ -169,7 +173,7 @@ actor SampleWriter {
let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
audioOutput.alwaysCopiesSampleData = false audioOutput.alwaysCopiesSampleData = false
audioOutput.audioMix = audioMix audioOutput.audioMix = audioMix
guard reader.canAdd(audioOutput) else { guard let reader, reader.canAdd(audioOutput) else {
throw Error.setupFailure(.cannotAddAudioOutput) throw Error.setupFailure(.cannotAddAudioOutput)
} }
reader.add(audioOutput) reader.add(audioOutput)
@ -177,7 +181,7 @@ actor SampleWriter {
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings) let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings)
audioInput.expectsMediaDataInRealTime = false audioInput.expectsMediaDataInRealTime = false
guard writer.canAdd(audioInput) else { guard let writer, writer.canAdd(audioInput) else {
throw Error.setupFailure(.cannotAddAudioInput) throw Error.setupFailure(.cannotAddAudioInput)
} }
writer.add(audioInput) writer.add(audioInput)
@ -193,7 +197,7 @@ actor SampleWriter {
) )
videoOutput.alwaysCopiesSampleData = false videoOutput.alwaysCopiesSampleData = false
videoOutput.videoComposition = videoComposition videoOutput.videoComposition = videoComposition
guard reader.canAdd(videoOutput) else { guard let reader, reader.canAdd(videoOutput) else {
throw Error.setupFailure(.cannotAddVideoOutput) throw Error.setupFailure(.cannotAddVideoOutput)
} }
reader.add(videoOutput) reader.add(videoOutput)
@ -201,7 +205,7 @@ actor SampleWriter {
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings) let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
videoInput.expectsMediaDataInRealTime = false videoInput.expectsMediaDataInRealTime = false
guard writer.canAdd(videoInput) else { guard let writer, writer.canAdd(videoInput) else {
throw Error.setupFailure(.cannotAddVideoInput) throw Error.setupFailure(.cannotAddVideoInput)
} }
writer.add(videoInput) writer.add(videoInput)
@ -234,13 +238,6 @@ actor SampleWriter {
} }
private func writeAllReadySamples() { private func writeAllReadySamples() {
guard !isCancelled else {
log.debug("Cancelled while writing samples")
reader.cancelReading()
writer.cancelWriting()
return
}
if let audioInput, let audioOutput { if let audioInput, let audioOutput {
let hasMoreAudio = writeReadySamples(output: audioOutput, input: audioInput) let hasMoreAudio = writeReadySamples(output: audioOutput, input: audioInput)
if !hasMoreAudio { log.debug("Finished encoding audio") } if !hasMoreAudio { log.debug("Finished encoding audio") }
@ -252,13 +249,7 @@ actor SampleWriter {
private func writeReadySamples(output: AVAssetReaderOutput, input: AVAssetWriterInput) -> Bool { private func writeReadySamples(output: AVAssetReaderOutput, input: AVAssetWriterInput) -> Bool {
while input.isReadyForMoreMediaData { while input.isReadyForMoreMediaData {
guard !isCancelled else { guard reader?.status == .reading && writer?.status == .writing,
log.debug("Cancelled while writing samples")
reader.cancelReading()
writer.cancelWriting()
return false
}
guard reader.status == .reading && writer.status == .writing,
let sampleBuffer = output.copyNextSampleBuffer() else { let sampleBuffer = output.copyNextSampleBuffer() else {
input.markAsFinished() input.markAsFinished()
return false return false