mirror of
https://github.com/samsonjs/SJSAssetExportSession.git
synced 2026-03-25 08:45:50 +00:00
Trim fat, improve code comments
This commit is contained in:
parent
462694bb85
commit
bf386ffe7b
14 changed files with 296 additions and 2843 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue