| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
NextLevelSessionExporter is an export and transcode media library for iOS written in Swift.
The library provides customizable audio and video encoding options unlike AVAssetExportSession and without having to learn the intricacies of AVFoundation. It was a port of SDAVAssetExportSession with inspiration from SCAssetExportSession – which are great obj-c alternatives.
Add the following to your Package.swift:
dependencies: [
.package(url: "https://github.com/nextlevel/NextLevelSessionExporter", from: "1.0.1")
]Or add it directly in Xcode: File → Add Package Dependencies...
pod "NextLevelSessionExporter", "~> 1.0.1"Alternatively, drop the source files into your Xcode project.
The modern Swift 6 async/await API provides clean, cancellable exports with progress updates:
let exporter = NextLevelSessionExporter(withAsset: asset)
exporter.outputFileType = .mp4
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(ProcessInfo().globallyUniqueString)
.appendingPathExtension("mp4")
exporter.outputURL = tmpURL
let compressionDict: [String: Any] = [
AVVideoAverageBitRateKey: NSNumber(integerLiteral: 6000000),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String,
]
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodec.h264,
AVVideoWidthKey: NSNumber(integerLiteral: 1920),
AVVideoHeightKey: NSNumber(integerLiteral: 1080),
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: compressionDict
]
exporter.audioOutputConfiguration = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderBitRateKey: NSNumber(integerLiteral: 128000),
AVNumberOfChannelsKey: NSNumber(integerLiteral: 2),
AVSampleRateKey: NSNumber(value: Float(44100))
]
// Option 1: Simple async export with progress callback
do {
let outputURL = try await exporter.export { progress in
print("Progress: \(progress * 100)%")
}
print("Export completed: \(outputURL)")
} catch {
print("Export failed: \(error)")
}
// Option 2: AsyncSequence for real-time progress updates
Task {
do {
for try await event in exporter.exportAsync() {
switch event {
case .progress(let progress):
await MainActor.run {
progressBar.progress = progress
}
case .completed(let url):
print("Export completed: \(url)")
}
}
} catch {
print("Export failed: \(error)")
}
}For compatibility with older iOS versions, you can use the completion handler API.
let exporter = NextLevelSessionExporter(withAsset: asset)
exporter.outputFileType = AVFileType.mp4
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(ProcessInfo().globallyUniqueString)
.appendingPathExtension("mp4")
exporter.outputURL = tmpURL
let compressionDict: [String: Any] = [
AVVideoAverageBitRateKey: NSNumber(integerLiteral: 6000000),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String,
]
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodec.h264,
AVVideoWidthKey: NSNumber(integerLiteral: 1920),
AVVideoHeightKey: NSNumber(integerLiteral: 1080),
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: compressionDict
]
exporter.audioOutputConfiguration = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderBitRateKey: NSNumber(integerLiteral: 128000),
AVNumberOfChannelsKey: NSNumber(integerLiteral: 2),
AVSampleRateKey: NSNumber(value: Float(44100))
]
exporter.export(progressHandler: { (progress) in
print(progress)
}, completionHandler: { result in
switch result {
case .success(let status):
switch status {
case .completed:
print("NextLevelSessionExporter, export completed, \(exporter.outputURL?.description ?? "")")
break
default:
print("NextLevelSessionExporter, did not complete")
break
}
break
case .failure(let error):
print("NextLevelSessionExporter, failed to export \(error)")
break
}
})Use videoTransform to apply a custom affine transform to video content without building a full AVVideoComposition manually. The exporter applies it as the base orientation transform, then handles centering and scaling on top.
This is useful when the raw encoded pixels are in an unexpected orientation — for example, a video whose naturalSize.height > naturalSize.width that must be delivered in landscape:
// Rotate portrait-encoded video 90° clockwise to landscape
let isPortrait = try await videoTrack.load(.naturalSize).height > videoTrack.load(.naturalSize).width
if isPortrait {
exporter.videoTransform = CGAffineTransform(rotationAngle: -.pi / 2)
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: 1920, // landscape width
AVVideoHeightKey: 1080, // landscape height
]
}Any CGAffineTransform works — rotations, flips, or combinations. When videoComposition is set directly, videoTransform is ignored.
The 1.0 release introduces Swift 6 with modern async/await APIs while maintaining full backward compatibility. Here's how to migrate:
Before (0.x):
exporter.export(progressHandler: { progress in
print("Progress: \(progress)")
}, completionHandler: { result in
switch result {
case .success:
print("Export completed")
case .failure(let error):
print("Export failed: \(error)")
}
})After (1.0):
do {
let outputURL = try await exporter.export { progress in
print("Progress: \(progress)")
}
print("Export completed: \(outputURL)")
} catch {
print("Export failed: \(error)")
}No changes required! The completion handler API works exactly the same. However, note that error cases now include descriptive messages:
// Errors now have helpful context
case .failure(let error):
print(error.localizedDescription) // e.g., "Failed to read media: Asset is corrupted"
print(error.recoverySuggestion) // e.g., "Verify the source asset is not corrupted"None! The 1.0 release is fully backward compatible. New async/await APIs are additive.
Unlike AVAssetExportSession, NextLevelSessionExporter gives you complete control over encoding parameters:
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.hevc, // H.265 for better compression
AVVideoWidthKey: 1920,
AVVideoHeightKey: 1080,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: 6_000_000, // 6 Mbps
AVVideoMaxKeyFrameIntervalKey: 30, // Keyframe every 30 frames
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
]
]Control how videos are scaled to target dimensions using AVVideoScalingModeKey (Fixed in 1.0.1 - Issue #33):
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: 720,
AVVideoHeightKey: 1280,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill // Choose your scaling mode
]Available Scaling Modes:
AVVideoScalingModeResizeAspectFill (Recommended)
AVVideoScalingModeResize
AVVideoScalingModeResizeAspect (Default if not specified)
Example: Landscape → Portrait Conversion
// Convert 1920x1080 landscape video to 720x1280 portrait
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: 720,
AVVideoHeightKey: 1280,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill // Crops sides, fills frame
]Fine-tune audio settings for optimal file size and quality:
exporter.audioOutputConfiguration = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderBitRateKey: 128_000, // 128 kbps
AVNumberOfChannelsKey: 2, // Stereo
AVSampleRateKey: 44100 // 44.1 kHz
]Apply complex video compositions and audio mixing:
// Custom video composition
let composition = AVMutableVideoComposition()
composition.instructions = [/* your instructions */]
exporter.videoComposition = composition
// Custom audio mix
let audioMix = AVMutableAudioMix()
audioMix.inputParameters = [/* your parameters */]
exporter.audioMix = audioMixProcess each video frame during export with a render handler:
exporter.export { renderFrame, presentationTime, resultBuffer in
// Apply custom effects, filters, overlays, etc.
// Process renderFrame and write to resultBuffer
applyWatermark(to: resultBuffer)
} progress: { progress in
print("Progress: \(progress)")
}NextLevelSessionExporter automatically detects and preserves HDR content (HLG and HDR10) from source videos:
// Automatic HDR preservation (default behavior)
let exporter = NextLevelSessionExporter(withAsset: hdrAsset)
exporter.outputURL = outputURL
exporter.videoOutputConfiguration = [
AVVideoWidthKey: 1920,
AVVideoHeightKey: 1080
]
// HDR properties automatically detected and preserved ✨
let result = try await exporter.export()
// Output maintains HDR color space, transfer function, and 10-bit encodingFeatures:
To convert HDR to SDR, disable HDR preservation:
exporter.preserveHDR = false
// Output will be 8-bit SDRForce HDR encoding even for SDR source, or override detected transfer function:
// Configure for HLG HDR
exporter.configureForHDR(transferFunction: .hlg)
// Or configure for HDR10 (PQ)
exporter.configureForHDR(transferFunction: .hdr10)
// Note: HEVC codec and appropriate dimensions required
exporter.videoOutputConfiguration = [
AVVideoCodecKey: AVVideoCodecType.hevc,
AVVideoWidthKey: 1920,
AVVideoHeightKey: 1080
]Requirements:
Supported HDR Formats:
Export only a portion of the video:
let startTime = CMTime(seconds: 10, preferredTimescale: 600)
let endTime = CMTime(seconds: 30, preferredTimescale: 600)
exporter.timeRange = CMTimeRange(start: startTime, end: endTime)Embed custom metadata in exported videos:
let metadata: [AVMetadataItem] = [
createMetadataItem(key: .commonKeyTitle, value: "My Video"),
createMetadataItem(key: .commonKeyDescription, value: "Exported with NextLevelSessionExporter"),
]
exporter.metadata = metadataControl the priority of export operations to prevent thread priority inversion and optimize performance:
// High priority for user-initiated exports (default)
let exporter = NextLevelSessionExporter(withAsset: asset, qos: .userInitiated)
// Medium priority for background processing
let exporter = NextLevelSessionExporter(withAsset: asset, qos: .utility)
// Low priority for deferrable work
let exporter = NextLevelSessionExporter(withAsset: asset, qos: .background)When to use different QoS levels:
This resolves thread priority inversion warnings (Issues #48, #41) and is especially important when calling from async/await contexts.
The library automatically manages memory during export using autoreleasepool, preventing memory accumulation during long exports. This fix resolved Issue #56 where exports would crash after ~10 minutes.
With the modern async API, exports are properly cancelled when the Task is cancelled:
let exportTask = Task {
try await exporter.export()
}
// Cancel export
exportTask.cancel() // Properly stops export and cleans up resourcesFor optimal UI responsiveness, update progress on the main actor:
for try await event in exporter.exportAsync() {
switch event {
case .progress(let progress):
await MainActor.run {
progressView.progress = progress
}
case .completed(let url):
await handleCompletion(url)
}
}For long exports, consider using background tasks:
let taskID = await UIApplication.shared.beginBackgroundTask()
defer { await UIApplication.shared.endBackgroundTask(taskID) }
try await exporter.export()When exporting videos from the user's photo library, copy the file to your app's directory first to avoid permission issues:
// ⚠️ NOT RECOMMENDED: Direct PHAsset access may cause cancelled errors
let phAsset = // ... from photo library
let avAsset = AVAsset(url: phAsset.url) // May fail!
// ✅ RECOMMENDED: Copy to app directory first
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent("video.mov")
// Export PHAsset to temp file, then create AVAsset
let avAsset = AVAsset(url: tempURL)
let exporter = NextLevelSessionExporter(withAsset: avAsset)See the Troubleshooting section for complete implementation.
Problem: Export fails with AVFoundationErrorDomain Code=-11819 "Cannot Complete Action", especially on iOS 14.5.
Cause: This is an iOS system-level bug where media daemons crash during export operations. It's not a library issue but an Apple bug that affects AVAssetReader/AVAssetWriter operations.
Solutions:
func exportWithRetry(maxAttempts: Int = 3) async throws -> URL {
var lastError: Error?
for attempt in 1...maxAttempts {
do {
let url = try await exporter.export()
return url
} catch let error as NSError where error.code == -11819 {
lastError = error
print("Attempt \(attempt) failed with -11819, retrying...")
try await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay
continue
} catch {
throw error // Other errors, don't retry
}
}
throw lastError ?? NextLevelSessionExporterError.writingFailure("Export failed after \(maxAttempts) attempts")
}Reduce Complexity: Lower resolution, bitrate, or remove video composition if using CoreAnimation tools
Update iOS: The issue is less frequent on iOS 15+
Report to Apple: File a Feedback Assistant report with sysdiagnose if this occurs frequently
References:
Problem: Some videos fail to compress with a cancelled/canceled error message, especially when selecting videos directly from the photo library.
Cause: File access permissions or buffering issues when reading from certain storage locations.
Solution: Copy the video to your app's writable directory before exporting:
func exportVideoFromLibrary(asset: PHAsset) async throws -> URL {
// 1. Export to temporary file first
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mov")
// 2. Request video resource from Photos library
let options = PHVideoRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
try await withCheckedThrowingContinuation { continuation in
PHImageManager.default().requestExportSession(
forVideo: asset,
options: options,
exportPreset: AVAssetExportPresetPassthrough
) { exportSession, _ in
guard let session = exportSession else {
continuation.resume(throwing: NSError(domain: "Export", code: -1))
return
}
session.outputURL = tempURL
session.outputFileType = .mov
session.exportAsynchronously {
if session.status == .completed {
continuation.resume(returning: ())
} else {
continuation.resume(throwing: session.error ?? NSError(domain: "Export", code: -1))
}
}
}
}
// 3. Now export with NextLevelSessionExporter
let avAsset = AVAsset(url: tempURL)
let exporter = NextLevelSessionExporter(withAsset: avAsset)
let outputURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
exporter.outputURL = outputURL
exporter.videoOutputConfiguration = [/* your config */]
exporter.audioOutputConfiguration = [/* your config */]
let result = try await exporter.export()
// 4. Clean up temp file
try? FileManager.default.removeItem(at: tempURL)
return result
}Alternative (simpler): Use AVAsset(url:) with a file URL rather than PHAsset directly:
// Copy to caches directory first
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent("video.mov")
// ... copy file to cacheURL ...
let asset = AVAsset(url: cacheURL)
let exporter = NextLevelSessionExporter(withAsset: asset)Problem: Export fails when reading the source asset.
Solutions:
Fixed in 1.0! Previous versions had a memory leak causing crashes on videos longer than 10 minutes. Update to 1.0 or later.
Tips:
The library automatically handles video orientation and transforms. If you're experiencing issues:
Orientation Problems:
Scaling Not Working (Fixed in 1.0.1):
Issue: Some videos export without audio.
Solution: This was fixed in 1.0. The library now properly filters APAC audio tracks that cause export failures. Update to the latest version.
You can find the docs here. Documentation is generated with jazzy and hosted on GitHub-Pages.
NextLevelSessionExporter is available under the MIT license, see the LICENSE file for more information.
| Back | FazBrowse Home | New Git URL |