NextLevelSessionExporter
🔄 Export and transcode media in Swift
Install / Use
/learn @NextLevel/NextLevelSessionExporterREADME
NextLevelSessionExporter 🔄
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.
✨ What's New in Swift 6
- 🚀 Modern Async/Await API - Native Swift concurrency support with
async/awaitandAsyncSequence - 🌈 HDR Video Support - Automatic detection and preservation of HLG and HDR10 content with 10-bit HEVC
- 📐 Scaling Mode Fixes - AVVideoScalingModeKey now works correctly for aspect-fill and resize (#33)
- ⚡ Better Performance - Proper memory management with autoreleasepool in encoding loop
- 🎯 QoS Configuration - Control export priority to prevent thread priority inversion (PR #44)
- 🔒 Swift 6 Strict Concurrency - Full
Sendableconformance and thread-safety - 📝 Enhanced Error Messages - Contextual error descriptions with recovery suggestions
- ♻️ Task Cancellation - Proper cancellation support for modern Swift concurrency
- 🛡️ Better Error Handling - Fixed silent failures causing audio-only exports (#38)
- 🔙 Backwards Compatible - Legacy completion handler API still works for iOS 13+
Requirements
- iOS 15.0+ for async/await APIs (iOS 13.0+ for legacy completion handler API)
- Swift 6.0
- Xcode 16.0+
Related Projects
Quick Start
Swift Package Manager (Recommended)
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...
CocoaPods
pod "NextLevelSessionExporter", "~> 1.0.1"
Manual Integration
Alternatively, drop the source files into your Xcode project.
Example
Modern Async/Await API (iOS 15+)
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)")
}
}
Legacy Completion Handler API
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
}
})
Migration Guide
Migrating from 0.x to 1.0 (Swift 6)
The 1.0 release introduces Swift 6 with modern async/await APIs while maintaining full backward compatibility. Here's how to migrate:
Option 1: Adopt Modern Async/Await (Recommended)
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)")
}
Option 2: Keep Using Completion Handlers
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"
Breaking Changes
None! The 1.0 release is fully backward compatible. New async/await APIs are additive.
Behavioral Changes
- Memory Management - Fixed memory leak in long video exports (no code changes needed)
- Error Messages - Errors now include contextual information and recovery suggestions
- Safety - Removed force unwraps; fallback to safe defaults
Features
Custom Video Encoding
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
]
]
Video Scaling Modes
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)- Scales video to fill the target dimensions while maintaining aspect ratio
- May crop content to fill the entire frame
- Ideal for converting landscape → portrait or vice versa
-
AVVideoScalingModeResize- Stretches video to exact target dimensions
- Does not maintain aspect ratio
- Use when you want non-uniform scaling
-
AVVideoScalingModeResizeAspect(Default if not specified)- Fits entire video within target dimensions while maintaining aspect ratio
- May add letterboxing/pillarboxing (black bars)
- Legacy behavior for backward compa
