Created
June 21, 2024 05:39
-
-
Save samsonjs/b0d2362e7db5bd8f0bef934978144edf to your computer and use it in GitHub Desktop.
Weird AVAssetExportSession sendable error in Swift 6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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