Skip to content

Instantly share code, notes, and snippets.

@samsonjs
Created June 21, 2024 05:39
Show Gist options
  • Save samsonjs/b0d2362e7db5bd8f0bef934978144edf to your computer and use it in GitHub Desktop.
Save samsonjs/b0d2362e7db5bd8f0bef934978144edf to your computer and use it in GitHub Desktop.
Weird AVAssetExportSession sendable error in Swift 6
import AVFoundation
import Combine
import OSLog
private let log = Logger.forType(MovieExportSession.self)
actor MovieExportSession {
let composition: AVComposition
let audioMix: AVAudioMix?
let videoComposition: AVVideoComposition
// Optional doesn't have any conformance to Sendable so instead of having an initializer that
// accepts an optional we instead have 2 initializers, one with an audio mix and one without.
init(composition: AVComposition, audioMix: AVAudioMix, videoComposition: AVVideoComposition) {
self.composition = composition
self.audioMix = audioMix
self.videoComposition = videoComposition
}
init(composition: AVComposition, videoComposition: AVVideoComposition) {
self.composition = composition
self.audioMix = nil
self.videoComposition = videoComposition
}
init(movie: ComposedMovie) {
if let audioMix = movie.audioMix {
self.init(composition: movie.composition, audioMix: audioMix, videoComposition: movie.videoComposition)
} else {
self.init(composition: movie.composition, videoComposition: movie.videoComposition)
}
}
func export(
to url: URL,
updateProgress: (@Sendable @MainActor (Float) -> Void)? = nil
) async throws {
let presetName = presetName(for: videoComposition.renderSize)
return try await export(to: url, presetName: presetName, updateProgress: updateProgress)
}
func exportOriginal(
to url: URL,
updateProgress: (@Sendable @MainActor (Float) -> Void)? = nil
) async throws {
try await export(to: url, presetName: AVAssetExportPresetPassthrough, updateProgress: updateProgress)
}
private func export(
to url: URL,
presetName: String,
updateProgress: (@Sendable @MainActor (Float) -> Void)? = nil
) async throws {
guard let session = AVAssetExportSession(asset: composition, presetName: presetName) else {
throw Error.invalidPresetName(videoComposition.renderSize, presetName)
}
session.audioMix = audioMix
session.outputFileType = .mp4
session.outputURL = url
session.shouldOptimizeForNetworkUse = true
session.videoComposition = videoComposition
await updateProgress?(0)
let states = session.states(updateInterval: 1)
Task.detached {
for await state in states {
switch state {
case .pending, .waiting:
break
case let .exporting(progress):
await updateProgress?(Float(progress.fractionCompleted))
@unknown default:
break
}
}
}
// ERROR in Swift 6: Sending 'session' risks causing data races
// Sending 'self'-isolated 'session' to nonisolated callee risks causing
// data races between nonisolated and 'self'-isolated uses
//
// How on earth is this isolated to self?! Makes no sense 🤨
try await session.export(to: url, as: .mp4)
}
// MARK: - Private API
private func presetName(for size: CGSize) -> String {
switch size {
case .x720, .x1080:
return AVAssetExportPresetHEVC1920x1080
case .x2160:
return AVAssetExportPresetHEVC3840x2160
default:
log.warning("Unsupported resolution \(size.debugDescription), defaulting to AVAssetExportPresetHEVCHighestQuality")
return AVAssetExportPresetHEVCHighestQuality
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment