Trim fat, improve code comments

This commit is contained in:
Sami Samhuri 2025-06-13 12:23:46 -07:00
parent 462694bb85
commit bf386ffe7b
No known key found for this signature in database
14 changed files with 296 additions and 2843 deletions

View file

@ -6,6 +6,10 @@
//
extension Array {
/// Filters the array using an async predicate function.
///
/// - Parameter isIncluded: Async predicate to test each element.
/// - Returns: Array containing only elements where the predicate returned true.
func filterAsync(_ isIncluded: (Element) async throws -> Bool) async rethrows -> [Element] {
var result: [Element] = []
for element in self {

View file

@ -40,14 +40,25 @@ public struct AudioOutputSettings: Hashable, Sendable, Codable {
.init(format: format.formatID, channels: 2, sampleRate: nil)
}
/// Sets the number of output channels.
///
/// - Parameter channels: Number of channels (1 for mono, 2 for stereo, etc.).
/// - Returns: A new AudioOutputSettings with the specified channel count.
public func channels(_ channels: Int) -> AudioOutputSettings {
.init(format: format, channels: channels, sampleRate: sampleRate)
}
/// Sets the sample rate in Hz.
///
/// - Parameter sampleRate: Sample rate in Hz, or nil to use default for format.
/// - Returns: A new AudioOutputSettings with the specified sample rate.
public func sampleRate(_ sampleRate: Int?) -> AudioOutputSettings {
.init(format: format, channels: channels, sampleRate: sampleRate)
}
/// Converts these settings to an AVFoundation audio settings dictionary.
///
/// - Returns: Dictionary suitable for use with AVAssetWriter.
public var settingsDictionary: [String: any Sendable] {
if let sampleRate {
[

View file

@ -8,6 +8,12 @@
public import CoreMedia
public extension CMTime {
/// Creates a CMTime with the specified duration in seconds using a timescale of 600.
///
/// The timescale of 600 provides good precision for typical video frame rates.
///
/// - Parameter seconds: The duration in seconds.
/// - Returns: A CMTime representing the specified duration.
static func seconds(_ seconds: TimeInterval) -> CMTime {
CMTime(seconds: seconds, preferredTimescale: 600)
}

View file

@ -8,14 +8,23 @@
import Foundation
extension ExportSession {
/// Specific reasons why export setup can fail.
public enum SetupFailureReason: String, Sendable, CustomStringConvertible {
/// Audio settings were required but not provided.
case audioSettingsEmpty
/// The provided audio settings are invalid or unsupported.
case audioSettingsInvalid
/// Could not add audio input to the asset writer.
case cannotAddAudioInput
/// Could not add audio output to the asset reader.
case cannotAddAudioOutput
/// Could not add video input to the asset writer.
case cannotAddVideoInput
/// Could not add video output to the asset reader.
case cannotAddVideoOutput
/// The provided video settings are invalid or unsupported.
case videoSettingsInvalid
/// The source asset has no video tracks to export.
case videoTracksEmpty
public var description: String {
@ -40,9 +49,13 @@ extension ExportSession {
}
}
/// Errors that can occur during export operations.
public enum Error: LocalizedError, Equatable {
/// Export failed during initial setup phase.
case setupFailure(SetupFailureReason)
/// Export failed while reading from the source asset.
case readFailure((any Swift.Error)?)
/// Export failed while writing to the destination file.
case writeFailure((any Swift.Error)?)
public var errorDescription: String? {

View file

@ -1,162 +1,51 @@
# Audio Configuration
Learn how to configure audio settings for your video exports.
Configure audio export settings using the ``AudioOutputSettings`` builder pattern.
## Overview
SJSAssetExportSession provides flexible audio configuration through the ``AudioOutputSettings`` builder pattern. You can easily specify format, channels, sample rate, and more advanced options.
## Basic Audio Settings
### Default AAC Configuration
The simplest approach uses the default AAC settings:
## Basic Configuration
```swift
try await exporter.export(
asset: sourceAsset,
audio: .default, // AAC, 2 channels, 44.1 kHz
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// Default AAC settings (2 channels, 44.1 kHz)
.default
// Specify format
.format(.aac) // Recommended for MP4/MOV
.format(.mp3) // Legacy compatibility
// Full configuration
.format(.aac)
.channels(2)
.sampleRate(48_000)
```
The default configuration provides:
- Format: AAC (kAudioFormatMPEG4AAC)
- Channels: 2 (stereo)
- Sample Rate: 44,100 Hz
### Specifying Audio Format
Choose between supported audio formats:
## Common Configurations
```swift
// AAC format (recommended for MP4)
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// High quality stereo
.format(.aac).channels(2).sampleRate(48_000)
// MP3 format
try await exporter.export(
asset: sourceAsset,
audio: .format(.mp3),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// Voice/podcast (mono, lower sample rate)
.format(.aac).channels(1).sampleRate(22_050)
// Music/professional
.format(.aac).channels(2).sampleRate(96_000)
// 5.1 surround
.format(.aac).channels(6).sampleRate(48_000)
```
## Channel Configuration
## Sample Rates
### Mono Audio
- 22,050 Hz - Voice, low bandwidth
- 44,100 Hz - CD quality, general use
- 48,000 Hz - Professional video production
- 96,000 Hz - High-resolution audio
For voice recordings or to reduce file size:
## Using with Exports
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(1),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
let exporter = ExportSession()
### Stereo Audio
Standard stereo configuration:
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(2),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
### Multi-Channel Audio
For surround sound or complex audio setups:
```swift
// 5.1 surround sound
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(6),
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mov
)
```
## Sample Rate Configuration
### Common Sample Rates
Choose the appropriate sample rate for your content:
```swift
// CD quality (44.1 kHz)
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).sampleRate(44_100),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// Professional audio (48 kHz)
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).sampleRate(48_000),
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mov
)
// High-resolution audio (96 kHz)
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).sampleRate(96_000),
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mov
)
```
### Reduced Quality for Web
For web streaming or mobile apps:
```swift
// Lower quality for smaller file size
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(1).sampleRate(22_050),
video: .codec(.h264, width: 854, height: 480),
to: destinationURL,
as: .mp4
)
```
## Audio Format Guidelines
### AAC Format
Best for:
- MP4/MOV containers
- Streaming applications
- Mobile devices
- General compatibility
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(2).sampleRate(48_000),
@ -166,49 +55,10 @@ try await exporter.export(
)
```
### MP3 Format
Best for:
- Legacy compatibility
- Audio-focused applications
- When file size is critical
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.mp3).channels(2).sampleRate(44_100),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
## Builder Pattern Chaining
Combine multiple audio settings using method chaining:
```swift
// Complete audio configuration
let audioSettings = AudioOutputSettings
.format(.aac)
.channels(2)
.sampleRate(48_000)
try await exporter.export(
asset: sourceAsset,
audio: audioSettings,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
## Audio Mix Integration
Combine audio settings with audio mix for advanced processing:
```swift
// Create an audio mix for volume control
// Create audio mix for volume control
let audioMix = AVMutableAudioMix()
let inputParameters = AVMutableAudioMixInputParameters(track: audioTrack)
inputParameters.setVolume(0.5, at: .zero) // 50% volume
@ -216,111 +66,22 @@ audioMix.inputParameters = [inputParameters]
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(2).sampleRate(48_000),
audio: .format(.aac).channels(2),
mix: audioMix,
video: .codec(.h264, width: 1920, height: 1080),
video: videoSettings,
to: destinationURL,
as: .mp4
)
```
## Audio-Only Exports
## Troubleshooting
Export audio without video:
```swift
// Note: You still need to provide video settings, but the output will be audio-only
// if the source asset has no video tracks
try await exporter.export(
asset: audioOnlyAsset,
audio: .format(.aac).channels(2).sampleRate(44_100),
video: .codec(.h264, width: 1, height: 1), // Minimal video settings
to: destinationURL,
as: .m4a
)
```
## Common Audio Configurations
### Podcast Export
Optimized for speech content:
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(1).sampleRate(22_050),
video: .codec(.h264, width: 640, height: 360),
to: destinationURL,
as: .mp4
)
```
### Music Video Export
High-quality audio for music content:
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(2).sampleRate(48_000),
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
### Social Media Export
Balanced quality for social platforms:
```swift
try await exporter.export(
asset: sourceAsset,
audio: .format(.aac).channels(2).sampleRate(44_100),
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
## Troubleshooting Audio Issues
### No Audio in Output
If your exported video has no audio:
1. Check that the source asset has audio tracks
2. Ensure audio settings are properly configured
3. Verify the output container supports your audio format
```swift
// Check for audio tracks
let audioTracks = try await sourceAsset.loadTracks(withMediaType: .audio)
if audioTracks.isEmpty {
print("Source asset has no audio tracks")
}
```
### Audio Quality Issues
For better audio quality:
- Use higher sample rates (48 kHz or higher)
- Choose AAC over MP3 when possible
- Ensure sufficient bitrate for your channel configuration
### Compatibility Issues
For maximum compatibility:
- Use AAC format with MP4 container
- Stick to standard sample rates (44.1 kHz, 48 kHz)
- Use stereo (2 channels) for general content
If no audio in output:
- Verify source has audio tracks: `asset.loadTracks(withMediaType: .audio)`
- Check container supports format (AAC for MP4/MOV)
- Ensure audio settings are specified
## See Also
- ``AudioOutputSettings`` - Audio settings builder
- ``AudioOutputSettings/Format`` - Supported audio formats
- <doc:VideoConfiguration> - Configuring video settings
- <doc:CustomSettings> - Using raw audio settings dictionaries
- ``AudioOutputSettings/Format`` - Supported formats

View file

@ -1,374 +0,0 @@
# Custom Settings
Learn how to use raw settings dictionaries for maximum control over export parameters.
## Overview
While SJSAssetExportSession provides convenient builder patterns through ``AudioOutputSettings`` and ``VideoOutputSettings``, you can also use raw settings dictionaries for complete control over export parameters. This approach gives you access to every AVFoundation setting while maintaining the benefits of the export session's architecture.
## Raw Settings API
### Using Raw Settings Dictionaries
The flexible export method accepts raw settings dictionaries:
```swift
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: audioSettingsDict,
videoOutputSettings: videoSettingsDict,
to: destinationURL,
as: .mp4
)
```
This method provides the most control over the export process and allows you to specify any settings supported by AVFoundation.
## Audio Settings Dictionaries
### Basic Audio Settings
```swift
let audioSettings: [String: any Sendable] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: NSNumber(value: 2),
AVSampleRateKey: NSNumber(value: 48_000)
]
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: audioSettings,
videoOutputSettings: videoSettings,
to: destinationURL,
as: .mp4
)
```
### Advanced Audio Settings
```swift
let advancedAudioSettings: [String: any Sendable] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: NSNumber(value: 2),
AVSampleRateKey: NSNumber(value: 48_000),
AVEncoderBitRateKey: NSNumber(value: 128_000), // 128 kbps
AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.high.rawValue),
AVChannelLayoutKey: AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_Stereo)!.asData()
]
```
### Multi-Channel Audio
For surround sound configurations:
```swift
let surroundAudioSettings: [String: any Sendable] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVChannelLayoutKey: AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_5_1)!.asData(),
AVSampleRateKey: NSNumber(value: 48_000),
AVEncoderBitRateKey: NSNumber(value: 384_000) // Higher bitrate for 5.1
]
```
## Video Settings Dictionaries
### Basic Video Settings
```swift
let videoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1920),
AVVideoHeightKey: NSNumber(value: 1080)
]
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: audioSettings,
videoOutputSettings: videoSettings,
to: destinationURL,
as: .mp4
)
```
### Advanced Video Settings
```swift
let advancedVideoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1920),
AVVideoHeightKey: NSNumber(value: 1080),
AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoAverageBitRateKey: NSNumber(value: 5_000_000),
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 30),
AVVideoAllowFrameReorderingKey: NSNumber(value: true),
AVVideoExpectedSourceFrameRateKey: NSNumber(value: 30),
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
] as [String: any Sendable],
AVVideoColorPropertiesKey: [
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
]
]
```
### HEVC Settings
```swift
let hevcSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.hevc.rawValue,
AVVideoWidthKey: NSNumber(value: 3840),
AVVideoHeightKey: NSNumber(value: 2160),
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: NSNumber(value: 20_000_000),
AVVideoQualityKey: NSNumber(value: 0.8),
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 60)
] as [String: any Sendable],
AVVideoColorPropertiesKey: [
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_2020,
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_2100_HLG,
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_2020
]
]
```
## Mix-and-Match Approach
### Bootstrap from Builder Patterns
Start with builder patterns and customize as needed:
```swift
// Start with builder pattern
var audioSettings = AudioOutputSettings
.format(.aac)
.channels(2)
.sampleRate(48_000)
.settingsDictionary
// Add custom settings
audioSettings[AVEncoderBitRateKey] = NSNumber(value: 192_000)
audioSettings[AVEncoderAudioQualityKey] = NSNumber(value: AVAudioQuality.max.rawValue)
var videoSettings = VideoOutputSettings
.codec(.h264, width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000)
.settingsDictionary
// Add advanced H.264 settings
if var compressionProps = videoSettings[AVVideoCompressionPropertiesKey] as? [String: any Sendable] {
compressionProps[AVVideoH264EntropyModeKey] = AVVideoH264EntropyModeCABAC
compressionProps[AVVideoAllowFrameReorderingKey] = NSNumber(value: true)
videoSettings[AVVideoCompressionPropertiesKey] = compressionProps
}
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: audioSettings,
videoOutputSettings: videoSettings,
to: destinationURL,
as: .mp4
)
```
## Video Composition Integration
### Custom Video Composition
When using raw settings, you can provide your own video composition:
```swift
let videoComposition = try await AVMutableVideoComposition.videoComposition(withPropertiesOf: sourceAsset)
videoComposition.renderSize = CGSize(width: 1920, height: 1080)
videoComposition.frameDuration = CMTime(value: 1, timescale: 30) // 30 fps
// Add filters or effects
let instruction = videoComposition.instructions.first as? AVMutableVideoCompositionInstruction
let layerInstruction = instruction?.layerInstructions.first as? AVMutableVideoCompositionLayerInstruction
// Apply transform or filters here...
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: audioSettings,
videoOutputSettings: videoSettings,
composition: videoComposition,
to: destinationURL,
as: .mp4
)
```
## Advanced Use Cases
### Variable Bitrate Encoding
```swift
let vbrVideoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1920),
AVVideoHeightKey: NSNumber(value: 1080),
AVVideoCompressionPropertiesKey: [
AVVideoQualityKey: NSNumber(value: 0.7), // Use quality instead of bitrate
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 60),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
] as [String: any Sendable]
]
```
### Custom Audio Channel Layout
```swift
// Create custom channel layout for 7.1 surround
let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_7_1)!
let customAudioSettings: [String: any Sendable] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVChannelLayoutKey: channelLayout.asData(),
AVSampleRateKey: NSNumber(value: 48_000),
AVEncoderBitRateKey: NSNumber(value: 512_000) // Higher bitrate for 7.1
]
```
### Low-Latency Encoding
```swift
let lowLatencySettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1280),
AVVideoHeightKey: NSNumber(value: 720),
AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel,
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 1), // I-frame only
AVVideoAllowFrameReorderingKey: NSNumber(value: false),
AVVideoRealTimeKey: NSNumber(value: true)
] as [String: any Sendable]
]
```
## Settings Validation
### Validating Custom Settings
```swift
func validateSettings(
audioSettings: [String: any Sendable],
videoSettings: [String: any Sendable],
fileType: AVFileType
) throws {
// Create temporary writer to validate settings
let tempURL = URL.temporaryDirectory.appending(component: UUID().uuidString)
let writer = try AVAssetWriter(outputURL: tempURL, fileType: fileType)
// Validate audio settings if provided
if !audioSettings.isEmpty {
guard writer.canApply(outputSettings: audioSettings, forMediaType: .audio) else {
throw ExportSession.Error.setupFailure(.audioSettingsInvalid)
}
}
// Validate video settings
guard writer.canApply(outputSettings: videoSettings, forMediaType: .video) else {
throw ExportSession.Error.setupFailure(.videoSettingsInvalid)
}
// Clean up
try? FileManager.default.removeItem(at: tempURL)
}
```
## Common Settings References
### H.264 Compression Properties
| Key | Type | Description |
|-----|------|-------------|
| `AVVideoAverageBitRateKey` | NSNumber | Average bitrate in bits per second |
| `AVVideoQualityKey` | NSNumber | Quality factor (0.0-1.0) |
| `AVVideoMaxKeyFrameIntervalKey` | NSNumber | Maximum keyframe interval |
| `AVVideoProfileLevelKey` | String | H.264 profile and level |
| `AVVideoAllowFrameReorderingKey` | NSNumber (Bool) | Enable B-frames |
| `AVVideoH264EntropyModeKey` | String | CAVLC or CABAC entropy mode |
### Audio Format Properties
| Key | Type | Description |
|-----|------|-------------|
| `AVFormatIDKey` | AudioFormatID | Audio codec identifier |
| `AVSampleRateKey` | NSNumber | Sample rate in Hz |
| `AVNumberOfChannelsKey` | NSNumber | Number of audio channels |
| `AVChannelLayoutKey` | Data | Channel layout information |
| `AVEncoderBitRateKey` | NSNumber | Audio bitrate in bits per second |
| `AVEncoderAudioQualityKey` | NSNumber | Audio quality setting |
## Performance Considerations
### Optimizing Custom Settings
```swift
// For fast encoding (lower quality)
let fastVideoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1280),
AVVideoHeightKey: NSNumber(value: 720),
AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel,
AVVideoAverageBitRateKey: NSNumber(value: 2_000_000),
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 12),
AVVideoAllowFrameReorderingKey: NSNumber(value: false)
] as [String: any Sendable]
]
// For high quality (slower encoding)
let qualityVideoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1920),
AVVideoHeightKey: NSNumber(value: 1080),
AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoQualityKey: NSNumber(value: 0.9),
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 30),
AVVideoAllowFrameReorderingKey: NSNumber(value: true)
] as [String: any Sendable]
]
```
## Troubleshooting Custom Settings
### Common Issues
1. **Invalid Settings**: Always validate settings with `AVAssetWriter.canApply(outputSettings:forMediaType:)`
2. **Missing Required Keys**: Ensure `AVFormatIDKey` for audio and `AVVideoCodecKey` for video
3. **Type Mismatches**: Use `NSNumber` for numeric values, not Swift native types
4. **Channel Layout Data**: Convert `AVAudioChannelLayout` to `Data` using `asData()`
### Debug Settings
```swift
func debugSettings(_ settings: [String: any Sendable], mediaType: AVMediaType) {
print("Settings for \(mediaType.rawValue):")
for (key, value) in settings {
print(" \(key): \(value)")
}
// Test with a temporary writer
do {
let tempURL = URL.temporaryDirectory.appending(component: "test")
let writer = try AVAssetWriter(outputURL: tempURL, fileType: .mp4)
let canApply = writer.canApply(outputSettings: settings, forMediaType: mediaType)
print(" Can apply: \(canApply)")
try? FileManager.default.removeItem(at: tempURL)
} catch {
print(" Validation error: \(error)")
}
}
```
## See Also
- ``AudioOutputSettings`` - Audio settings builder
- ``VideoOutputSettings`` - Video settings builder
- <doc:AudioConfiguration> - Builder pattern for audio
- <doc:VideoConfiguration> - Builder pattern for video
- <doc:ErrorHandling> - Handling settings validation errors

View file

@ -1,446 +0,0 @@
# Error Handling
Learn how to properly handle errors and troubleshoot common issues with SJSAssetExportSession.
## Overview
SJSAssetExportSession provides comprehensive error reporting through the ``ExportSession/Error`` enum. This guide covers how to handle different types of errors and recover from common failure scenarios.
## Error Types
### ExportSession.Error
The main error type with three categories:
```swift
public enum Error: LocalizedError, Equatable {
case setupFailure(SetupFailureReason)
case readFailure((any Swift.Error)?)
case writeFailure((any Swift.Error)?)
}
```
## Setup Failures
Setup failures occur during export initialization, before any media processing begins.
### Common Setup Failures
```swift
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
} catch ExportSession.Error.setupFailure(let reason) {
switch reason {
case .videoTracksEmpty:
print("Source asset has no video tracks")
case .audioSettingsEmpty:
print("Audio settings dictionary is empty")
case .audioSettingsInvalid:
print("Audio settings are not valid for this format")
case .videoSettingsInvalid:
print("Video settings are not valid for this format")
case .cannotAddAudioInput:
print("Cannot add audio input to the writer")
case .cannotAddAudioOutput:
print("Cannot add audio output to the reader")
case .cannotAddVideoInput:
print("Cannot add video input to the writer")
case .cannotAddVideoOutput:
print("Cannot add video output to the reader")
}
}
```
### Handling Setup Failures
#### No Video Tracks
```swift
// Check for video tracks before export
let videoTracks = try await sourceAsset.loadTracks(withMediaType: .video)
guard !videoTracks.isEmpty else {
print("Asset has no video tracks - cannot export")
return
}
```
#### Invalid Settings
```swift
// Validate settings compatibility
do {
let writer = try AVAssetWriter(outputURL: tempURL, fileType: .mp4)
let videoSettings = VideoOutputSettings.codec(.h264, width: 1920, height: 1080).settingsDictionary
guard writer.canApply(outputSettings: videoSettings, forMediaType: .video) else {
print("Video settings are not compatible with MP4 format")
return
}
} catch {
print("Failed to create writer: \(error)")
}
```
## Read Failures
Read failures occur when the asset reader encounters problems reading the source media.
### Handling Read Failures
```swift
do {
try await exporter.export(/* ... */)
} catch ExportSession.Error.readFailure(let underlyingError) {
if let error = underlyingError {
print("Read failed: \(error.localizedDescription)")
// Handle specific AVFoundation errors
if let avError = error as? AVError {
switch avError.code {
case .fileFormatNotRecognized:
print("File format not supported")
case .mediaServicesWereReset:
print("Media services were reset - retry may succeed")
case .diskFull:
print("Not enough disk space")
default:
print("AVFoundation error: \(avError.localizedDescription)")
}
}
} else {
print("Unknown read failure")
}
}
```
### Common Read Failure Causes
- Corrupted source files
- Unsupported file formats
- Permission issues
- Network interruption (for remote assets)
- Media services restart
## Write Failures
Write failures occur when the asset writer cannot write to the destination.
### Handling Write Failures
```swift
do {
try await exporter.export(/* ... */)
} catch ExportSession.Error.writeFailure(let underlyingError) {
if let error = underlyingError {
print("Write failed: \(error.localizedDescription)")
if let avError = error as? AVError {
switch avError.code {
case .diskFull:
print("Not enough disk space for export")
case .fileAlreadyExists:
print("Destination file already exists")
case .noPermission:
print("No permission to write to destination")
default:
print("Write error: \(avError.localizedDescription)")
}
}
} else {
print("Unknown write failure")
}
}
```
### Common Write Failure Causes
- Insufficient disk space
- File permissions
- Destination file already exists
- Invalid destination path
- Unsupported format combination
## Comprehensive Error Handling
### Complete Error Handling Pattern
```swift
func exportVideoWithErrorHandling() async {
do {
let exporter = ExportSession()
// Optional: Monitor progress
Task {
for await progress in exporter.progressStream {
print("Progress: \(Int(progress * 100))%")
}
}
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
print("Export completed successfully!")
} catch let error as ExportSession.Error {
handleExportError(error)
} catch {
print("Unexpected error: \(error.localizedDescription)")
}
}
private func handleExportError(_ error: ExportSession.Error) {
switch error {
case .setupFailure(let reason):
print("Setup failed: \(reason.description)")
// Could show user-friendly message based on reason
case .readFailure(let underlyingError):
print("Failed to read source: \(underlyingError?.localizedDescription ?? "Unknown")")
// Could suggest checking source file
case .writeFailure(let underlyingError):
print("Failed to write output: \(underlyingError?.localizedDescription ?? "Unknown")")
// Could suggest checking disk space or permissions
}
}
```
## Retry Strategies
### Automatic Retry with Backoff
```swift
func exportWithRetry(maxAttempts: Int = 3) async throws {
var lastError: Error?
for attempt in 1...maxAttempts {
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
return // Success!
} catch let error as ExportSession.Error {
lastError = error
// Only retry for certain types of failures
switch error {
case .readFailure(let underlyingError):
if let avError = underlyingError as? AVError,
avError.code == .mediaServicesWereReset {
print("Media services reset, retrying... (attempt \(attempt))")
try await Task.sleep(for: .seconds(1))
continue
}
throw error
case .writeFailure(let underlyingError):
if let avError = underlyingError as? AVError,
avError.code == .diskFull {
print("Disk full - cannot retry")
throw error
}
// Other write failures might be transient
print("Write failed, retrying... (attempt \(attempt))")
try await Task.sleep(for: .seconds(2))
continue
case .setupFailure:
// Setup failures are usually permanent
throw error
}
}
}
throw lastError ?? ExportSession.Error.setupFailure(.videoTracksEmpty)
}
```
## Cancellation Handling
### Handling Task Cancellation
```swift
func exportWithCancellation() async throws {
let exporter = ExportSession()
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
} catch is CancellationError {
print("Export was cancelled")
// Clean up partial files if needed
try? FileManager.default.removeItem(at: destinationURL)
}
}
// Cancel from another task
let exportTask = Task {
try await exportWithCancellation()
}
// Later...
exportTask.cancel()
```
## Validation Before Export
### Pre-Export Validation
```swift
func validateBeforeExport(asset: AVAsset, destinationURL: URL) async throws {
// Check video tracks
let videoTracks = try await asset.loadTracks(withMediaType: .video)
guard !videoTracks.isEmpty else {
throw ExportSession.Error.setupFailure(.videoTracksEmpty)
}
// Check disk space
let resourceValues = try destinationURL.resourceValues(forKeys: [.volumeAvailableCapacityKey])
if let availableCapacity = resourceValues.volumeAvailableCapacity {
let estimatedSize = try await estimateOutputSize(for: asset)
guard availableCapacity > estimatedSize else {
throw ExportSession.Error.writeFailure(AVError(.diskFull))
}
}
// Check destination directory exists
let destinationDir = destinationURL.deletingLastPathComponent()
guard FileManager.default.fileExists(atPath: destinationDir.path) else {
throw ExportSession.Error.writeFailure(AVError(.fileNotFound))
}
// Check write permissions
guard FileManager.default.isWritableFile(atPath: destinationDir.path) else {
throw ExportSession.Error.writeFailure(AVError(.noPermission))
}
}
private func estimateOutputSize(for asset: AVAsset) async throws -> Int64 {
let duration = try await asset.load(.duration)
let durationSeconds = duration.seconds
// Rough estimate: 5 Mbps for 1080p H.264
let estimatedBitrate = 5_000_000 // bits per second
let estimatedBytes = Int64(durationSeconds * Double(estimatedBitrate) / 8)
return estimatedBytes
}
```
## User-Friendly Error Messages
### Providing Helpful Messages
```swift
func userFriendlyErrorMessage(for error: ExportSession.Error) -> String {
switch error {
case .setupFailure(.videoTracksEmpty):
return "The selected file doesn't contain any video content."
case .setupFailure(.audioSettingsInvalid):
return "The audio settings are not compatible with the selected format."
case .setupFailure(.videoSettingsInvalid):
return "The video settings are not compatible with the selected format."
case .readFailure(let underlyingError):
if let avError = underlyingError as? AVError {
switch avError.code {
case .fileFormatNotRecognized:
return "The video file format is not supported."
case .mediaServicesWereReset:
return "Media services were interrupted. Please try again."
default:
return "Unable to read the source video file."
}
}
return "Unable to read the source video file."
case .writeFailure(let underlyingError):
if let avError = underlyingError as? AVError {
switch avError.code {
case .diskFull:
return "Not enough storage space to complete the export."
case .noPermission:
return "Permission denied. Check that you can write to the destination folder."
default:
return "Unable to save the exported video file."
}
}
return "Unable to save the exported video file."
}
}
```
## Debugging Tips
### Enable Detailed Logging
```swift
// Add logging to track export progress
let exporter = ExportSession()
Task {
for await progress in exporter.progressStream {
print("Export progress: \(String(format: "%.1f", progress * 100))%")
}
}
print("Starting export...")
print("Source: \(sourceAsset)")
print("Destination: \(destinationURL)")
do {
try await exporter.export(/* ... */)
print("Export completed successfully")
} catch {
print("Export failed: \(error)")
}
```
### Check Asset Properties
```swift
func debugAssetProperties(asset: AVAsset) async {
do {
let duration = try await asset.load(.duration)
let videoTracks = try await asset.loadTracks(withMediaType: .video)
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
print("Asset duration: \(duration.seconds) seconds")
print("Video tracks: \(videoTracks.count)")
print("Audio tracks: \(audioTracks.count)")
for (index, track) in videoTracks.enumerated() {
let naturalSize = try await track.load(.naturalSize)
let nominalFrameRate = try await track.load(.nominalFrameRate)
print("Video track \(index): \(naturalSize) @ \(nominalFrameRate) fps")
}
} catch {
print("Failed to load asset properties: \(error)")
}
}
```
## See Also
- ``ExportSession/Error`` - Main error enum
- ``ExportSession/SetupFailureReason`` - Setup failure details
- <doc:GettingStarted> - Basic usage examples
- <doc:PerformanceOptimization> - Avoiding common performance issues

View file

@ -1,132 +1,36 @@
# Exporting Videos
Comprehensive guide to video export scenarios and best practices.
Export videos with custom settings and format conversion.
## Overview
This guide covers various video export scenarios, from simple conversions to complex multi-track compositions with custom settings.
## Basic Video Export
### Simple Format Conversion
Convert between video formats while maintaining quality:
## Basic Export
```swift
let exporter = ExportSession()
// Simple format conversion
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
### Changing Resolution
Scale video to different resolutions:
```swift
// 4K to 1080p
// Change resolution and codec
try await exporter.export(
asset: source4KAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
// 1080p to 720p
try await exporter.export(
asset: source1080pAsset,
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
## Advanced Video Configuration
### High-Quality Exports
For maximum quality, use HEVC with high bitrates:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.fps(60)
.bitrate(20_000_000) // 20 Mbps
.color(.hdr),
video: .codec(.hevc, width: 1920, height: 1080)
.fps(30)
.bitrate(4_000_000),
to: destinationURL,
as: .mov
)
```
### Optimized for Social Media
## Time Ranges
Twitter-optimized export:
Extract clips or segments:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1280, height: 720)
.fps(30)
.bitrate(2_000_000), // 2 Mbps
to: destinationURL,
as: .mp4
)
```
Instagram-optimized export:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1080, height: 1080) // Square aspect ratio
.fps(30)
.bitrate(3_500_000), // 3.5 Mbps
to: destinationURL,
as: .mp4
)
```
### Web Streaming
Optimize for web playback with multiple bitrates:
```swift
// Low bitrate for mobile
try await exporter.export(
asset: sourceAsset,
optimizeForNetworkUse: true,
video: .codec(.h264, width: 854, height: 480)
.fps(24)
.bitrate(800_000), // 800 Kbps
to: lowQualityURL,
as: .mp4
)
// High bitrate for desktop
try await exporter.export(
asset: sourceAsset,
optimizeForNetworkUse: true,
video: .codec(.h264, width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000), // 5 Mbps
to: highQualityURL,
as: .mp4
)
```
## Working with Time Ranges
### Creating Clips
Extract specific segments from longer videos:
```swift
// First 30 seconds
let clipRange = CMTimeRange(
start: .zero,
duration: CMTime(seconds: 30, preferredTimescale: 600)
@ -141,196 +45,58 @@ try await exporter.export(
)
```
### Removing Sections
Skip a middle section by exporting multiple clips:
## Color Spaces
```swift
// Export first part (0-60 seconds)
let part1Range = CMTimeRange(
start: .zero,
duration: CMTime(seconds: 60, preferredTimescale: 600)
)
// SDR for compatibility
.color(.sdr)
// Export second part (120 seconds to end)
let part2Start = CMTime(seconds: 120, preferredTimescale: 600)
let totalDuration = try await sourceAsset.load(.duration)
let part2Range = CMTimeRange(
start: part2Start,
duration: totalDuration - part2Start
)
// Export each part separately
try await exporter.export(
asset: sourceAsset,
timeRange: part1Range,
video: .codec(.h264, width: 1280, height: 720),
to: part1URL,
as: .mp4
)
try await exporter.export(
asset: sourceAsset,
timeRange: part2Range,
video: .codec(.h264, width: 1280, height: 720),
to: part2URL,
as: .mp4
)
// HDR for supported displays
.color(.hdr)
```
## Color Management
### Standard Dynamic Range (SDR)
For compatibility with most devices:
## H.264 Profiles
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.color(.sdr),
to: destinationURL,
as: .mp4
)
// Fast encoding, lower compression
.codec(.h264(.baseline30), width: 1280, height: 720)
// Balanced (default)
.codec(.h264(.main32), width: 1920, height: 1080)
// Best compression, slower
.codec(.h264(.high41), width: 1920, height: 1080)
```
### High Dynamic Range (HDR)
Preserve HDR content for compatible displays:
## Progress Tracking
```swift
try await exporter.export(
asset: hdrSourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.color(.hdr),
to: destinationURL,
as: .mov
)
```
## File Format Considerations
### MP4 vs MOV
**MP4** - Best for:
- Web streaming
- Mobile devices
- General compatibility
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
**MOV** - Best for:
- Professional workflows
- HDR content
- Apple ecosystem
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160),
to: destinationURL,
as: .mov
)
```
## Performance Optimization
### Choosing Appropriate Settings
Balance quality and performance based on your use case:
```swift
// Fast export (lower quality)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baseline30), width: 1280, height: 720)
.fps(24)
.bitrate(1_500_000),
to: destinationURL,
as: .mp4
)
// Balanced export
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.main32), width: 1920, height: 1080)
.fps(30)
.bitrate(4_000_000),
to: destinationURL,
as: .mp4
)
// High quality export (slower)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.high41), width: 1920, height: 1080)
.fps(60)
.bitrate(8_000_000),
to: destinationURL,
as: .mp4
)
```
### Progress Monitoring
Provide user feedback during long exports:
```swift
let exporter = ExportSession()
Task {
for await progress in exporter.progressStream {
await MainActor.run {
progressView.progress = progress
progressLabel.text = "\(Int(progress * 100))%"
}
// Update UI with progress (0.0 to 1.0)
}
}
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
try await exporter.export(...)
```
## Error Handling
Always wrap exports in proper error handling:
```swift
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
print("Export completed successfully")
try await exporter.export(...)
} catch let error as ExportSession.Error {
switch error {
case .setupFailure(let reason):
print("Setup failed: \(reason)")
// Handle setup errors
case .readFailure(let underlyingError):
print("Read failed: \(underlyingError?.localizedDescription ?? "Unknown")")
// Handle read errors
case .writeFailure(let underlyingError):
print("Write failed: \(underlyingError?.localizedDescription ?? "Unknown")")
// Handle write errors
}
} catch {
print("Unexpected error: \(error)")
}
```
## See Also
- <doc:AudioConfiguration> - Adding audio to your exports
- <doc:CustomSettings> - Using raw settings dictionaries
- <doc:PerformanceOptimization> - Optimizing export performance
- <doc:VideoConfiguration> - Video settings in detail

View file

@ -103,7 +103,7 @@ func exportVideo() async throws {
- Learn about <doc:AudioConfiguration> to customize audio settings
- Explore <doc:VideoConfiguration> for advanced video options
- Check out <doc:ExportingVideos> for more complex scenarios
- Read about <doc:ErrorHandling> to handle export failures gracefully
- See <doc:ProgressTracking> for progress monitoring patterns
## Common Patterns

View file

@ -1,581 +1,78 @@
# Performance Optimization
Learn how to optimize export performance and handle large video files efficiently.
Practical tips for faster exports and efficient resource usage.
## Overview
## Codec Selection
Export performance depends on many factors including source file characteristics, output settings, device capabilities, and system resources. This guide covers strategies to optimize export speed while maintaining quality.
## Understanding Performance Factors
### Key Performance Variables
1. **Source Resolution**: Higher resolution sources require more processing
2. **Output Resolution**: Scaling affects performance
3. **Codec Choice**: H.264 vs HEVC vs other codecs
4. **Bitrate**: Higher bitrates require more processing
5. **Frame Rate**: Higher frame rates increase workload
6. **Device Capabilities**: CPU, GPU, and available memory
### Performance vs Quality Trade-offs
**Fastest to slowest:**
1. H.264 Baseline - Fast encoding, larger files
2. H.264 Main - Balanced (recommended default)
3. H.264 High - Better compression, slower
4. HEVC - Best compression, slowest
```swift
// Fast export (lower quality)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baseline30), width: 1280, height: 720)
.fps(24)
.bitrate(1_500_000),
to: destinationURL,
as: .mp4
)
// Fast export
.codec(.h264(.baseline30), width: 1280, height: 720)
// Balanced export
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.main32), width: 1920, height: 1080)
.fps(30)
.bitrate(4_000_000),
to: destinationURL,
as: .mp4
)
// Balanced
.codec(.h264(.main32), width: 1920, height: 1080)
// High quality (slower)
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.fps(60)
.bitrate(20_000_000),
to: destinationURL,
as: .mov
)
// High compression
.codec(.hevc, width: 1920, height: 1080)
```
## Codec Optimization
## Resolution Guidelines
### H.264 Profile Selection
Choose the appropriate H.264 profile for your performance needs:
- **Don't upscale** - Export at source resolution or smaller
- **Use standard resolutions** - 720p, 1080p, 4K
- **Consider device targets** - Mobile apps rarely need 4K
```swift
// Fastest encoding - Baseline profile
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baselineAuto), width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// Get source resolution first
let tracks = try await asset.loadTracks(withMediaType: .video)
let naturalSize = try await tracks.first?.load(.naturalSize)
// Balanced - Main profile
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.mainAuto), width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
// Best compression (slower) - High profile
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.highAuto), width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
// Export at source resolution or smaller
.codec(.h264, width: min(1920, naturalSize.width), height: min(1080, naturalSize.height))
```
### HEVC Considerations
## Frame Rate
HEVC provides better compression but requires more processing power:
Match or reduce source frame rate:
```swift
// Use HEVC only when:
// 1. Target devices support it
// 2. File size is more important than encoding speed
// 3. You have sufficient processing power
let sourceFrameRate = try await videoTrack.load(.nominalFrameRate)
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 1920, height: 1080)
.bitrate(3_000_000), // Lower bitrate than H.264 for same quality
to: destinationURL,
as: .mp4
)
```
## Resolution and Scaling Optimization
### Intelligent Resolution Selection
```swift
func optimizedResolution(for sourceAsset: AVAsset) async throws -> CGSize {
let videoTracks = try await sourceAsset.loadTracks(withMediaType: .video)
guard let firstTrack = videoTracks.first else {
throw ExportSession.Error.setupFailure(.videoTracksEmpty)
}
let naturalSize = try await firstTrack.load(.naturalSize)
// Don't upscale - only downscale for performance
if naturalSize.width <= 1280 && naturalSize.height <= 720 {
return naturalSize
} else if naturalSize.width <= 1920 && naturalSize.height <= 1080 {
return CGSize(width: 1280, height: 720) // Downscale to 720p
} else {
return CGSize(width: 1920, height: 1080) // Downscale to 1080p
}
}
// Usage
let optimalSize = try await optimizedResolution(for: sourceAsset)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, size: optimalSize),
to: destinationURL,
as: .mp4
)
```
### Avoiding Unnecessary Scaling
```swift
// Check if scaling is needed
func needsScaling(sourceSize: CGSize, targetSize: CGSize) -> Bool {
return sourceSize.width != targetSize.width || sourceSize.height != targetSize.height
}
// Match source resolution when possible
let videoTracks = try await sourceAsset.loadTracks(withMediaType: .video)
if let track = videoTracks.first {
let naturalSize = try await track.load(.naturalSize)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, size: naturalSize), // No scaling needed
to: destinationURL,
as: .mp4
)
}
```
## Frame Rate Optimization
### Source-Based Frame Rate Selection
```swift
func optimizedFrameRate(for asset: AVAsset) async throws -> Int? {
let videoTracks = try await asset.loadTracks(withMediaType: .video)
guard let track = videoTracks.first else { return nil }
let nominalFrameRate = try await track.load(.nominalFrameRate)
// Use source frame rate or a common divisor
switch nominalFrameRate {
case 0..<25:
return 24
case 25..<30:
return 25
case 30..<50:
return 30
case 50..<60:
return 50
default:
return 60
}
}
// Usage
let optimalFPS = try await optimizedFrameRate(for: sourceAsset)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(optimalFPS ?? 30),
to: destinationURL,
as: .mp4
)
// Don't increase frame rate
.fps(min(30, Int(sourceFrameRate)))
```
## Memory Management
### Handling Large Files
For large files, reduce concurrent operations and use lower settings:
```swift
func exportLargeFile(asset: AVAsset) async throws {
// Check available memory
let processInfo = ProcessInfo.processInfo
let physicalMemory = processInfo.physicalMemory
// Adjust settings based on available memory
let videoSettings: VideoOutputSettings
if physicalMemory < 4_000_000_000 { // Less than 4GB
videoSettings = .codec(.h264(.baseline31), width: 1280, height: 720)
.fps(24)
.bitrate(2_000_000)
} else if physicalMemory < 8_000_000_000 { // Less than 8GB
videoSettings = .codec(.h264(.main32), width: 1920, height: 1080)
.fps(30)
.bitrate(4_000_000)
} else {
videoSettings = .codec(.h264(.high40), width: 1920, height: 1080)
.fps(30)
.bitrate(6_000_000)
}
try await exporter.export(
asset: asset,
video: videoSettings,
to: destinationURL,
as: .mp4
)
// For 4K+ sources, consider reducing output resolution
if sourceWidth > 3840 {
.codec(.h264(.main32), width: 1920, height: 1080)
} else {
.codec(.h264(.main32), width: sourceWidth, height: sourceHeight)
}
```
### Memory-Efficient Settings
## Common Pitfalls
```swift
// Use raw settings for fine-grained memory control
let memoryEfficientVideoSettings: [String: any Sendable] = [
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
AVVideoWidthKey: NSNumber(value: 1280),
AVVideoHeightKey: NSNumber(value: 720),
AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel,
AVVideoAverageBitRateKey: NSNumber(value: 2_000_000),
AVVideoMaxKeyFrameIntervalKey: NSNumber(value: 30),
AVVideoAllowFrameReorderingKey: NSNumber(value: false), // Reduces memory usage
AVVideoExpectedSourceFrameRateKey: NSNumber(value: 24)
] as [String: any Sendable]
]
**Avoid:**
- Upscaling resolution (1080p → 4K)
- Increasing frame rate (24fps → 60fps)
- Using HEVC for time-critical exports
- Extremely high bitrates (>50Mbps for most use cases)
try await exporter.export(
asset: sourceAsset,
audioOutputSettings: AudioOutputSettings.default.settingsDictionary,
videoOutputSettings: memoryEfficientVideoSettings,
to: destinationURL,
as: .mp4
)
```
## Parallel Processing
### Batch Export Optimization
```swift
class OptimizedBatchExporter {
private let maxConcurrentExports: Int
init(maxConcurrentExports: Int = 2) {
// Limit concurrent exports based on system capabilities
let processorCount = ProcessInfo.processInfo.processorCount
self.maxConcurrentExports = min(maxConcurrentExports, max(1, processorCount / 2))
}
func exportFiles(_ files: [(asset: AVAsset, url: URL)]) async throws {
// Process files in chunks to avoid overwhelming the system
for chunk in files.chunked(into: maxConcurrentExports) {
try await withThrowingTaskGroup(of: Void.self) { group in
for file in chunk {
group.addTask {
let exporter = ExportSession()
try await exporter.export(
asset: file.asset,
video: .codec(.h264, width: 1280, height: 720),
to: file.url,
as: .mp4
)
}
}
try await group.waitForAll()
}
}
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0..<min($0 + size, count)])
}
}
}
```
## Device-Specific Optimization
### iOS Device Capabilities
```swift
import UIKit
func deviceOptimizedSettings() -> VideoOutputSettings {
let device = UIDevice.current
// Check device capabilities
if device.userInterfaceIdiom == .pad {
// iPad - more processing power
return .codec(.h264(.high40), width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000)
} else {
// iPhone - optimize for battery and heat
return .codec(.h264(.main32), width: 1280, height: 720)
.fps(30)
.bitrate(3_000_000)
}
}
```
### macOS Optimization
```swift
#if os(macOS)
import IOKit
func macOptimizedSettings() -> VideoOutputSettings {
// Check for discrete GPU
let hasDiscreteGPU = hasDiscreteGraphics()
if hasDiscreteGPU {
// Use higher settings with discrete GPU
return .codec(.hevc, width: 3840, height: 2160)
.fps(30)
.bitrate(15_000_000)
} else {
// Conservative settings for integrated graphics
return .codec(.h264(.main32), width: 1920, height: 1080)
.fps(30)
.bitrate(4_000_000)
}
}
private func hasDiscreteGraphics() -> Bool {
// Implementation to check for discrete GPU
// This is a simplified check - actual implementation would be more complex
return false
}
#endif
```
## Monitoring and Profiling
### Performance Metrics
```swift
class PerformanceMonitor {
private var startTime: Date?
private var startMemory: UInt64?
func startMonitoring() {
startTime = Date()
startMemory = getCurrentMemoryUsage()
}
func endMonitoring(fileSize: UInt64) -> PerformanceReport {
let endTime = Date()
let endMemory = getCurrentMemoryUsage()
let duration = endTime.timeIntervalSince(startTime ?? endTime)
let memoryDelta = endMemory - (startMemory ?? 0)
let processingSpeed = Double(fileSize) / duration // bytes per second
return PerformanceReport(
duration: duration,
memoryUsed: memoryDelta,
processingSpeed: processingSpeed
)
}
private func getCurrentMemoryUsage() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count)
}
}
if kerr == KERN_SUCCESS {
return info.resident_size
} else {
return 0
}
}
}
struct PerformanceReport {
let duration: TimeInterval
let memoryUsed: UInt64
let processingSpeed: Double // bytes per second
var mbPerSecond: Double {
return processingSpeed / 1_000_000
}
}
```
### Usage Example
```swift
func monitoredExport() async throws {
let monitor = PerformanceMonitor()
monitor.startMonitoring()
let exporter = ExportSession()
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? UInt64 ?? 0
let report = monitor.endMonitoring(fileSize: fileSize)
print("Export completed in \(report.duration)s")
print("Processing speed: \(report.mbPerSecond) MB/s")
print("Memory used: \(report.memoryUsed / 1_000_000) MB")
}
```
## Common Performance Issues
### Avoiding Common Pitfalls
1. **Don't upscale unnecessarily**:
```swift
// Bad: Upscaling from 720p to 4K
try await exporter.export(
asset: sourceAsset720p,
video: .codec(.h264, width: 3840, height: 2160), // Unnecessary upscaling
to: destinationURL,
as: .mp4
)
// Good: Maintain source resolution
try await exporter.export(
asset: sourceAsset720p,
video: .codec(.h264, width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
```
2. **Choose appropriate bitrates**:
```swift
// Bad: Excessive bitrate for resolution
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1280, height: 720)
.bitrate(20_000_000), // Too high for 720p
to: destinationURL,
as: .mp4
)
// Good: Appropriate bitrate
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1280, height: 720)
.bitrate(2_500_000), // Suitable for 720p
to: destinationURL,
as: .mp4
)
```
3. **Consider frame rate needs**:
```swift
// Bad: Unnecessary high frame rate
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(120), // Overkill for most content
to: destinationURL,
as: .mp4
)
// Good: Standard frame rate
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(30), // Standard and efficient
to: destinationURL,
as: .mp4
)
```
## Adaptive Quality Selection
### Dynamic Settings Based on Source
```swift
func adaptiveExportSettings(for asset: AVAsset) async throws -> VideoOutputSettings {
let videoTracks = try await asset.loadTracks(withMediaType: .video)
guard let track = videoTracks.first else {
throw ExportSession.Error.setupFailure(.videoTracksEmpty)
}
let naturalSize = try await track.load(.naturalSize)
let nominalFrameRate = try await track.load(.nominalFrameRate)
let estimatedDataRate = try await track.load(.estimatedDataRate)
// Calculate appropriate output settings
let outputWidth = min(Int(naturalSize.width), 1920)
let outputHeight = min(Int(naturalSize.height), 1080)
let outputFPS = min(Int(nominalFrameRate), 30)
let outputBitrate = min(Int(estimatedDataRate), 5_000_000)
return .codec(.h264(.mainAuto), width: outputWidth, height: outputHeight)
.fps(outputFPS)
.bitrate(outputBitrate)
}
// Usage
let settings = try await adaptiveExportSettings(for: sourceAsset)
try await exporter.export(
asset: sourceAsset,
video: settings,
to: destinationURL,
as: .mp4
)
```
## Testing Performance
### Benchmarking Different Settings
```swift
func benchmarkSettings() async throws {
let testCases: [(name: String, settings: VideoOutputSettings)] = [
("Fast", .codec(.h264(.baseline30), width: 1280, height: 720).fps(24).bitrate(1_500_000)),
("Balanced", .codec(.h264(.main32), width: 1920, height: 1080).fps(30).bitrate(4_000_000)),
("Quality", .codec(.h264(.high40), width: 1920, height: 1080).fps(30).bitrate(8_000_000)),
("HEVC", .codec(.hevc, width: 1920, height: 1080).fps(30).bitrate(3_000_000))
]
for testCase in testCases {
let startTime = Date()
let exporter = ExportSession()
try await exporter.export(
asset: sourceAsset,
video: testCase.settings,
to: URL.temporaryDirectory.appending(component: "\(testCase.name).mp4"),
as: .mp4
)
let duration = Date().timeIntervalSince(startTime)
print("\(testCase.name): \(duration)s")
}
}
```
**Do:**
- Test with representative content
- Monitor memory usage with large files
- Use network optimization for streaming: `optimizeForNetworkUse: true`
## See Also
- <doc:VideoConfiguration> - Video settings options
- <doc:AudioConfiguration> - Audio settings optimization
- <doc:ProgressTracking> - Monitoring export progress
- <doc:ErrorHandling> - Handling performance-related errors
- <doc:VideoConfiguration> - Video settings reference

View file

@ -1,566 +1,124 @@
# Progress Tracking
Learn how to monitor export progress and provide user feedback during video processing.
Monitor export progress using `AsyncStream<Float>` for real-time feedback.
## Overview
SJSAssetExportSession provides real-time progress tracking through an `AsyncStream<Float>`. This allows you to create responsive user interfaces that show export progress, estimated time remaining, and handle user cancellation.
## Basic Progress Monitoring
### Simple Progress Tracking
## Basic Usage
```swift
let exporter = ExportSession()
Task {
for await progress in exporter.progressStream {
// Progress ranges from 0.0 to 1.0
print("Export progress: \(Int(progress * 100))%")
}
}
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
try await exporter.export(...)
```
### Progress Values
The progress stream yields `Float` values between `0.0` and `1.0`:
- `0.0`: Export has just started
- `0.5`: Export is halfway complete
- `1.0`: Export has finished successfully
## UI Integration
### UIKit Progress View
### UIKit
```swift
import UIKit
class ExportViewController: UIViewController {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var progressLabel: UILabel!
@IBOutlet weak var cancelButton: UIButton!
private var exportTask: Task<Void, Error>?
func startExport() {
let exporter = ExportSession()
exportTask = Task {
// Monitor progress on main thread
Task { @MainActor in
for await progress in exporter.progressStream {
progressView.progress = progress
progressLabel.text = "\(Int(progress * 100))%"
}
}
// Perform export
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
await MainActor.run {
progressLabel.text = "Complete!"
cancelButton.isEnabled = false
}
}
}
@IBAction func cancelExport() {
exportTask?.cancel()
exportTask = nil
}
}
```
### SwiftUI Progress View
```swift
import SwiftUI
struct ExportView: View {
@State private var progress: Float = 0.0
@State private var isExporting = false
@State private var exportTask: Task<Void, Error>?
var body: some View {
VStack(spacing: 20) {
if isExporting {
ProgressView("Exporting...", value: progress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
Text("\(Int(progress * 100))%")
.font(.headline)
Button("Cancel") {
exportTask?.cancel()
exportTask = nil
isExporting = false
}
.foregroundColor(.red)
} else {
Button("Start Export") {
startExport()
}
}
}
.padding()
}
private func startExport() {
isExporting = true
progress = 0.0
let exporter = ExportSession()
exportTask = Task {
// Monitor progress
Task { @MainActor in
for await progressValue in exporter.progressStream {
progress = progressValue
}
}
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
await MainActor.run {
isExporting = false
progress = 1.0
}
} catch {
await MainActor.run {
isExporting = false
// Handle error
}
}
}
}
}
```
## Advanced Progress Features
### Time Estimation
```swift
class ExportProgressTracker {
private var startTime: Date?
private var lastProgressTime: Date?
private var lastProgress: Float = 0.0
func trackProgress(_ progress: Float) -> (timeElapsed: TimeInterval, timeRemaining: TimeInterval?) {
let now = Date()
if startTime == nil {
startTime = now
}
let timeElapsed = now.timeIntervalSince(startTime!)
// Calculate time remaining based on progress rate
let timeRemaining: TimeInterval?
if progress > 0 && progress != lastProgress {
let progressRate = progress / Float(timeElapsed)
let remainingProgress = 1.0 - progress
timeRemaining = TimeInterval(remainingProgress / progressRate)
} else {
timeRemaining = nil
}
lastProgressTime = now
lastProgress = progress
return (timeElapsed, timeRemaining)
}
}
// Usage
let progressTracker = ExportProgressTracker()
Task {
Task { @MainActor in
for await progress in exporter.progressStream {
let (elapsed, remaining) = progressTracker.trackProgress(progress)
await MainActor.run {
progressView.progress = progress
progressLabel.text = "\(Int(progress * 100))%"
if let remaining = remaining {
timeLabel.text = "Time remaining: \(Int(remaining))s"
}
}
progressView.progress = progress
progressLabel.text = "\(Int(progress * 100))%"
}
}
```
### Progress with Detailed Status
### SwiftUI
```swift
enum ExportStatus {
case preparing
case encoding(Float)
case finalizing
case completed
case failed(Error)
case cancelled
}
@State private var progress: Float = 0.0
class DetailedExportTracker: ObservableObject {
@Published var status: ExportStatus = .preparing
// In your view
ProgressView("Exporting...", value: progress, total: 1.0)
// Update progress
Task { @MainActor in
for await progressValue in exporter.progressStream {
progress = progressValue
}
}
```
## Time Estimation
```swift
class ProgressTracker {
private let startTime = Date()
func startExport() {
status = .preparing
func timeRemaining(for progress: Float) -> TimeInterval? {
guard progress > 0 else { return nil }
let elapsed = Date().timeIntervalSince(startTime)
return elapsed * Double(1.0 - progress) / Double(progress)
}
}
```
## Cancellation
```swift
private var exportTask: Task<Void, Error>?
func startExport() {
exportTask = Task {
let exporter = ExportSession()
Task {
// Monitor progress
Task { @MainActor in
for await progress in exporter.progressStream {
if progress < 1.0 {
status = .encoding(progress)
} else {
status = .finalizing
}
}
}
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
await MainActor.run {
status = .completed
}
} catch is CancellationError {
await MainActor.run {
status = .cancelled
}
} catch {
await MainActor.run {
status = .failed(error)
}
Task { @MainActor in
for await progress in exporter.progressStream {
updateProgress(progress)
}
}
try await exporter.export(...)
}
}
func cancelExport() {
exportTask?.cancel()
exportTask = nil
}
```
## Throttling Updates
```swift
Task {
var lastUpdate = Date()
for await progress in exporter.progressStream {
let now = Date()
if now.timeIntervalSince(lastUpdate) > 0.1 { // 100ms throttle
await MainActor.run { updateUI(progress) }
lastUpdate = now
}
}
}
```
## Batch Export Progress
### Multiple File Export
```swift
class BatchExportManager: ObservableObject {
@Published var overallProgress: Float = 0.0
@Published var currentFileProgress: Float = 0.0
@Published var currentFileName: String = ""
@Published var filesCompleted: Int = 0
func exportFiles(_ files: [(asset: AVAsset, url: URL, name: String)]) async throws {
let totalFiles = files.count
for (index, file) in files.enumerated() {
await MainActor.run {
currentFileName = file.name
currentFileProgress = 0.0
filesCompleted = index
}
let exporter = ExportSession()
// Track individual file progress
Task { @MainActor in
for await progress in exporter.progressStream {
currentFileProgress = progress
// Calculate overall progress
let completedPortion = Float(index) / Float(totalFiles)
let currentPortion = progress / Float(totalFiles)
overallProgress = completedPortion + currentPortion
}
}
try await exporter.export(
asset: file.asset,
video: .codec(.h264, width: 1920, height: 1080),
to: file.url,
as: .mp4
)
}
await MainActor.run {
overallProgress = 1.0
filesCompleted = totalFiles
}
}
}
```
## Background Export Progress
### Handling Background Tasks
```swift
import BackgroundTasks
class BackgroundExportManager {
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
func startBackgroundExport() {
// Request background time
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "VideoExport") {
// Background time expired
self.endBackgroundTask()
}
func exportMultipleFiles(_ files: [AVAsset]) async throws {
for (index, asset) in files.enumerated() {
let exporter = ExportSession()
Task {
// Monitor progress with reduced frequency for background
Task {
var lastUpdate = Date()
for await progress in exporter.progressStream {
let now = Date()
if now.timeIntervalSince(lastUpdate) > 1.0 { // Update every second
print("Background export progress: \(Int(progress * 100))%")
lastUpdate = now
}
}
}
do {
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
print("Background export completed")
} catch {
print("Background export failed: \(error)")
}
endBackgroundTask()
}
}
private func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
```
## Progress Persistence
### Saving Progress State
```swift
class PersistentExportManager {
private let userDefaults = UserDefaults.standard
private let progressKey = "export_progress"
func saveProgress(_ progress: Float, for exportID: String) {
userDefaults.set(progress, forKey: "\(progressKey)_\(exportID)")
}
func loadProgress(for exportID: String) -> Float {
return userDefaults.float(forKey: "\(progressKey)_\(exportID)")
}
func clearProgress(for exportID: String) {
userDefaults.removeObject(forKey: "\(progressKey)_\(exportID)")
}
func resumableExport(exportID: String) async throws {
let savedProgress = loadProgress(for: exportID)
if savedProgress > 0 {
print("Resuming export from \(Int(savedProgress * 100))%")
}
let exporter = ExportSession()
Task {
Task { @MainActor in
for await progress in exporter.progressStream {
saveProgress(progress, for: exportID)
await MainActor.run {
// Update UI
}
let overallProgress = (Float(index) + progress) / Float(files.count)
updateOverallProgress(overallProgress)
}
}
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
clearProgress(for: exportID)
try await exporter.export(asset: asset, ...)
}
}
```
## Performance Considerations
### Throttling Progress Updates
```swift
func throttledProgressTracking() {
let exporter = ExportSession()
Task {
var lastUpdate = Date()
let updateInterval: TimeInterval = 0.1 // Update every 100ms
for await progress in exporter.progressStream {
let now = Date()
if now.timeIntervalSince(lastUpdate) >= updateInterval {
await MainActor.run {
progressView.progress = progress
progressLabel.text = "\(Int(progress * 100))%"
}
lastUpdate = now
}
}
}
try await exporter.export(/* ... */)
}
```
### Memory-Efficient Progress Tracking
```swift
func efficientProgressTracking() {
let exporter = ExportSession()
Task {
// Use AsyncSequence operations to process progress efficiently
for await progress in exporter.progressStream
.compactMap { progress in
// Only emit significant progress changes
progress.isMultiple(of: 0.01) ? progress : nil
} {
await MainActor.run {
updateProgress(progress)
}
}
}
}
```
## Testing Progress Tracking
### Mock Progress Stream
```swift
extension ExportSession {
static func mockProgressStream(duration: TimeInterval = 5.0) -> AsyncStream<Float> {
AsyncStream { continuation in
Task {
let steps = 100
let interval = duration / Double(steps)
for step in 0...steps {
let progress = Float(step) / Float(steps)
continuation.yield(progress)
if step < steps {
try await Task.sleep(for: .seconds(interval))
}
}
continuation.finish()
}
}
}
}
// Usage in tests
func testProgressTracking() async {
var progressValues: [Float] = []
for await progress in ExportSession.mockProgressStream(duration: 1.0) {
progressValues.append(progress)
}
XCTAssertEqual(progressValues.first, 0.0)
XCTAssertEqual(progressValues.last, 1.0)
XCTAssertTrue(progressValues.count > 50) // Should have many progress updates
}
```
## Common Patterns
### Progress with User Feedback
```swift
func exportWithFeedback() async throws {
let exporter = ExportSession()
let startTime = Date()
Task { @MainActor in
for await progress in exporter.progressStream {
let elapsed = Date().timeIntervalSince(startTime)
if progress > 0 {
let estimatedTotal = elapsed / Double(progress)
let remaining = estimatedTotal - elapsed
progressLabel.text = """
\(Int(progress * 100))% complete
Time remaining: \(formatTime(remaining))
"""
} else {
progressLabel.text = "Starting export..."
}
progressView.progress = progress
}
}
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
}
private func formatTime(_ seconds: TimeInterval) -> String {
let minutes = Int(seconds) / 60
let seconds = Int(seconds) % 60
return String(format: "%d:%02d", minutes, seconds)
}
```
## See Also
- ``ExportSession`` - Main export class with progress stream
- <doc:ErrorHandling> - Handling errors during progress tracking
- <doc:PerformanceOptimization> - Optimizing export performance
- <doc:GettingStarted> - Basic usage examples
- ``ExportSession`` - Main export class

View file

@ -8,12 +8,11 @@ A Swift-first alternative to AVAssetExportSession with custom audio/video settin
### Key Features
- **Two-tier API Design**: Choose between a simple builder pattern or raw settings dictionaries for maximum flexibility
- **Swift 6 Strict Concurrency**: Built from the ground up with `Sendable` types and async/await
- **Builder Pattern API**: Intuitive, type-safe configuration with method chaining
- **Swift 6 Strict Concurrency**: Built with `Sendable` types and async/await
- **Real-time Progress Reporting**: Monitor export progress via `AsyncStream<Float>`
- **Comprehensive Format Support**: H.264, HEVC, AAC, MP3, and more
- **Advanced Color Management**: Support for both SDR (BT.709) and HDR (BT.2020) workflows
- **Mix-and-Match Approach**: Bootstrap custom settings from builder patterns for ultimate flexibility
- **Format Support**: H.264, HEVC, AAC, MP3 with custom settings
- **Color Management**: Support for both SDR (BT.709) and HDR (BT.2020) workflows
### Why SJSAssetExportSession?
@ -124,10 +123,8 @@ try await exporter.export(
- ``ExportSession/Error``
- ``ExportSession/SetupFailureReason``
- <doc:ErrorHandling>
### Advanced Topics
- <doc:CustomSettings>
- <doc:ProgressTracking>
- <doc:PerformanceOptimization>

View file

@ -1,452 +1,97 @@
# Video Configuration
Comprehensive guide to configuring video settings for optimal export results.
Configure video export settings using the ``VideoOutputSettings`` builder pattern.
## Overview
SJSAssetExportSession provides extensive video configuration options through the ``VideoOutputSettings`` builder pattern. Configure codecs, resolution, frame rates, bitrates, and color properties to achieve the perfect balance of quality and file size for your use case.
## Basic Video Settings
### Choosing a Codec
Select the appropriate codec for your target platform and quality requirements:
## Basic Configuration
```swift
// H.264 - Maximum compatibility
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
.codec(.h264, width: 1920, height: 1080)
// HEVC - Better compression, newer devices
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160),
to: destinationURL,
as: .mov
)
// HEVC - Better compression
.codec(.hevc, width: 3840, height: 2160)
// With additional settings
.codec(.h264, width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000)
.color(.sdr)
```
### Setting Resolution
Specify exact dimensions for your output video:
## H.264 Profiles
```swift
// Common resolutions
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080), // 1080p
to: destinationURL,
as: .mp4
)
// Auto-select appropriate level
.codec(.h264(.baselineAuto), width: 1280, height: 720) // Maximum compatibility
.codec(.h264(.mainAuto), width: 1920, height: 1080) // Balanced (default)
.codec(.h264(.highAuto), width: 1920, height: 1080) // Best compression
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1280, height: 720), // 720p
to: destinationURL,
as: .mp4
)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 3840, height: 2160), // 4K
to: destinationURL,
as: .mov
)
// Specific levels for precise control
.codec(.h264(.baseline31), width: 1280, height: 720) // Web compatibility
.codec(.h264(.high41), width: 1920, height: 1080) // Modern devices
```
You can also use `CGSize` for resolution:
## Frame Rates
```swift
let resolution = CGSize(width: 1920, height: 1080)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, size: resolution),
to: destinationURL,
as: .mp4
)
.fps(24) // Cinematic
.fps(30) // Standard video
.fps(60) // High motion content
```
## Frame Rate Configuration
## Bitrates
### Standard Frame Rates
Rough guidelines for quality vs file size:
Set the output frame rate to match your content or target platform:
| Resolution | 30fps | 60fps |
|------------|-------|-------|
| 720p | 2-4 Mbps | 4-6 Mbps |
| 1080p | 4-8 Mbps | 8-12 Mbps |
| 4K | 15-25 Mbps | 25-40 Mbps |
## Color Spaces
```swift
// 24 fps - Cinematic content
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(24),
to: destinationURL,
as: .mp4
)
// 30 fps - Standard video content
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(30),
to: destinationURL,
as: .mp4
)
// 60 fps - High motion content
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(60),
to: destinationURL,
as: .mp4
)
.color(.sdr) // Standard Dynamic Range (BT.709)
.color(.hdr) // High Dynamic Range (BT.2020 with HLG)
```
### Frame Rate Considerations
- **24 fps**: Film and cinematic content
- **25/30 fps**: Standard broadcast and web video
- **50/60 fps**: Sports, gaming, high-motion content
- **120+ fps**: Slow-motion source material
## Bitrate Configuration
### Quality vs. File Size
Balance video quality with file size using bitrate settings:
## Complete Examples
```swift
// Low bitrate - Smaller file, lower quality
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1280, height: 720)
.fps(30)
.bitrate(1_000_000), // 1 Mbps
to: destinationURL,
as: .mp4
)
// Social media optimized
.codec(.h264(.mainAuto), width: 1280, height: 720)
.fps(30)
.bitrate(2_000_000)
.color(.sdr)
// Medium bitrate - Balanced
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000), // 5 Mbps
to: destinationURL,
as: .mp4
)
// Professional quality
.codec(.hevc, width: 3840, height: 2160)
.fps(24)
.bitrate(20_000_000)
.color(.hdr)
// High bitrate - Maximum quality
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(30)
.bitrate(15_000_000), // 15 Mbps
to: destinationURL,
as: .mp4
)
```
### Recommended Bitrates
| Resolution | Frame Rate | Recommended Bitrate |
|------------|------------|-------------------|
| 720p | 30 fps | 2-4 Mbps |
| 1080p | 30 fps | 4-8 Mbps |
| 1080p | 60 fps | 8-12 Mbps |
| 4K | 30 fps | 15-25 Mbps |
| 4K | 60 fps | 25-40 Mbps |
## H.264 Profile Configuration
### Profile Selection
Choose the appropriate H.264 profile for your target devices:
```swift
// Baseline - Maximum compatibility (older devices)
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baselineAuto), width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// Main - Good compatibility with better compression
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.mainAuto), width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
// High - Best compression, modern devices
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.highAuto), width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
### Specific Profile Levels
For precise control over encoding parameters:
```swift
// Baseline Level 3.1 - Web compatibility
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baseline31), width: 1280, height: 720),
to: destinationURL,
as: .mp4
)
// High Level 4.1 - Modern devices
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.high41), width: 1920, height: 1080),
to: destinationURL,
as: .mp4
)
```
## Color Configuration
### Standard Dynamic Range (SDR)
For maximum compatibility across devices:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
SDR uses the BT.709 color space, which corresponds roughly to sRGB and is supported by virtually all displays.
### High Dynamic Range (HDR)
For premium content with enhanced color and brightness:
```swift
try await exporter.export(
asset: hdrSourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.color(.hdr),
to: destinationURL,
as: .mov
)
```
HDR uses the BT.2020 color space with HLG transfer function, providing wider color gamut and higher dynamic range.
## Complete Configuration Examples
### Social Media Optimized
Twitter/X optimized export:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.mainAuto), width: 1280, height: 720)
.fps(30)
.bitrate(2_000_000)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
Instagram optimized export:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.mainAuto), width: 1080, height: 1080) // Square
.fps(30)
.bitrate(3_500_000)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
### Professional Video
High-quality export for professional use:
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.fps(24)
.bitrate(20_000_000)
.color(.hdr),
to: destinationURL,
as: .mov
)
```
### Web Streaming
Optimized for web playback:
```swift
// Low quality variant
// Web streaming (with network optimization)
try await exporter.export(
asset: sourceAsset,
optimizeForNetworkUse: true,
video: .codec(.h264(.baseline31), width: 854, height: 480)
.fps(24)
.bitrate(800_000)
.color(.sdr),
to: lowQualityURL,
as: .mp4
)
// High quality variant
try await exporter.export(
asset: sourceAsset,
optimizeForNetworkUse: true,
video: .codec(.h264(.high40), width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000)
.color(.sdr),
to: highQualityURL,
as: .mp4
)
```
## Builder Pattern Chaining
Combine all video settings using method chaining:
```swift
let videoSettings = VideoOutputSettings
.codec(.h264(.highAuto), width: 1920, height: 1080)
.fps(30)
.bitrate(5_000_000)
.color(.sdr)
try await exporter.export(
asset: sourceAsset,
video: videoSettings,
.bitrate(800_000),
to: destinationURL,
as: .mp4
)
```
## Video Composition Integration
## Performance Notes
Video settings work seamlessly with AVVideoComposition:
```swift
let videoComposition = try await AVMutableVideoComposition.videoComposition(withPropertiesOf: sourceAsset)
// The video settings will be applied to the composition automatically
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264, width: 1920, height: 1080)
.fps(24)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
## Performance Considerations
### Encoding Speed vs. Quality
- **Baseline Profile**: Fastest encoding, largest file size
- **Main Profile**: Balanced encoding speed and compression
- **High Profile**: Slower encoding, best compression
- **HEVC**: Slowest encoding, best compression for modern devices
### Resolution and Performance
Higher resolutions require more processing power:
```swift
// Fast export - lower resolution
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.baseline31), width: 1280, height: 720)
.fps(24)
.bitrate(2_000_000),
to: destinationURL,
as: .mp4
)
// Slow export - higher resolution
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 3840, height: 2160)
.fps(60)
.bitrate(25_000_000),
to: destinationURL,
as: .mov
)
```
## Common Video Configurations
### YouTube Upload
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.highAuto), width: 1920, height: 1080)
.fps(30)
.bitrate(8_000_000)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
### Mobile App
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.h264(.mainAuto), width: 1280, height: 720)
.fps(30)
.bitrate(2_500_000)
.color(.sdr),
to: destinationURL,
as: .mp4
)
```
### Archive/Storage
```swift
try await exporter.export(
asset: sourceAsset,
video: .codec(.hevc, width: 1920, height: 1080)
.fps(24)
.bitrate(3_000_000)
.color(.sdr),
to: destinationURL,
as: .mov
)
```
- Baseline profile encodes fastest but produces larger files
- High profile encodes slower but achieves better compression
- HEVC provides best compression but requires more processing power
- Higher resolutions and frame rates significantly impact encoding time
## See Also
- ``VideoOutputSettings`` - Video settings builder
- ``VideoOutputSettings/Codec`` - Supported video codecs
- ``VideoOutputSettings/H264Profile`` - H.264 profile options
- ``VideoOutputSettings/Color`` - Color space configuration
- <doc:AudioConfiguration> - Configuring audio settings
- <doc:CustomSettings> - Using raw video settings dictionaries
- ``VideoOutputSettings/Codec`` - Supported codecs
- ``VideoOutputSettings/H264Profile`` - H.264 profiles

View file

@ -102,18 +102,33 @@ public struct VideoOutputSettings: Hashable, Sendable, Codable {
.codec(codec, size: CGSize(width: width, height: height))
}
/// Sets the output frame rate.
///
/// - Parameter fps: Target frame rate in frames per second, or nil for default.
/// - Returns: A new VideoOutputSettings with the specified frame rate.
public func fps(_ fps: Int?) -> VideoOutputSettings {
.init(codec: codec, size: size, fps: fps, bitrate: bitrate, color: color)
}
/// Sets the output bitrate.
///
/// - Parameter bitrate: Target bitrate in bits per second, or nil for default.
/// - Returns: A new VideoOutputSettings with the specified bitrate.
public func bitrate(_ bitrate: Int?) -> VideoOutputSettings {
.init(codec: codec, size: size, fps: fps, bitrate: bitrate, color: color)
}
/// Sets the color space configuration.
///
/// - Parameter color: Color space settings, or nil for default.
/// - Returns: A new VideoOutputSettings with the specified color configuration.
public func color(_ color: Color?) -> VideoOutputSettings {
.init(codec: codec, size: size, fps: fps, bitrate: bitrate, color: color)
}
/// Converts these settings to an AVFoundation video settings dictionary.
///
/// - Returns: Dictionary suitable for use with AVAssetWriter.
public var settingsDictionary: [String: any Sendable] {
var result: [String: any Sendable] = [
AVVideoCodecKey: codec.stringValue,