diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/AudioConfiguration.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/AudioConfiguration.md new file mode 100644 index 0000000..d2df356 --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/AudioConfiguration.md @@ -0,0 +1,326 @@ +# Audio Configuration + +Learn how to configure audio settings for your video exports. + +## 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: + +```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 +) +``` + +The default configuration provides: +- Format: AAC (kAudioFormatMPEG4AAC) +- Channels: 2 (stereo) +- Sample Rate: 44,100 Hz + +### Specifying Audio Format + +Choose between supported audio formats: + +```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 +) + +// MP3 format +try await exporter.export( + asset: sourceAsset, + audio: .format(.mp3), + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +## Channel Configuration + +### Mono Audio + +For voice recordings or to reduce file size: + +```swift +try await exporter.export( + asset: sourceAsset, + audio: .format(.aac).channels(1), + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +### 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), + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 +) +``` + +### 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 +let audioMix = AVMutableAudioMix() +let inputParameters = AVMutableAudioMixInputParameters(track: audioTrack) +inputParameters.setVolume(0.5, at: .zero) // 50% volume +audioMix.inputParameters = [inputParameters] + +try await exporter.export( + asset: sourceAsset, + audio: .format(.aac).channels(2).sampleRate(48_000), + mix: audioMix, + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 +) +``` + +## Audio-Only Exports + +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 + +## See Also + +- ``AudioOutputSettings`` - Audio settings builder +- ``AudioOutputSettings/Format`` - Supported audio formats +- - Configuring video settings +- - Using raw audio settings dictionaries \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/CustomSettings.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/CustomSettings.md new file mode 100644 index 0000000..49a9969 --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/CustomSettings.md @@ -0,0 +1,374 @@ +# 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 +- - Builder pattern for audio +- - Builder pattern for video +- - Handling settings validation errors \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ErrorHandling.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ErrorHandling.md new file mode 100644 index 0000000..192d6ab --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ErrorHandling.md @@ -0,0 +1,446 @@ +# 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 +- - Basic usage examples +- - Avoiding common performance issues \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ExportingVideos.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ExportingVideos.md new file mode 100644 index 0000000..837381c --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ExportingVideos.md @@ -0,0 +1,336 @@ +# Exporting Videos + +Comprehensive guide to video export scenarios and best practices. + +## 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: + +```swift +let exporter = ExportSession() + +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 +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), + to: destinationURL, + as: .mov +) +``` + +### Optimized for Social Media + +Twitter-optimized export: + +```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) +) + +try await exporter.export( + asset: sourceAsset, + timeRange: clipRange, + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +### Removing Sections + +Skip a middle section by exporting multiple clips: + +```swift +// Export first part (0-60 seconds) +let part1Range = CMTimeRange( + start: .zero, + duration: CMTime(seconds: 60, preferredTimescale: 600) +) + +// 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 +) +``` + +## Color Management + +### Standard Dynamic Range (SDR) + +For compatibility with most devices: + +```swift +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080) + .color(.sdr), + to: destinationURL, + as: .mp4 +) +``` + +### High Dynamic Range (HDR) + +Preserve HDR content for compatible displays: + +```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))%" + } + } +} + +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 +) +``` + +## 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") +} catch let error as ExportSession.Error { + switch error { + case .setupFailure(let reason): + print("Setup failed: \(reason)") + case .readFailure(let underlyingError): + print("Read failed: \(underlyingError?.localizedDescription ?? "Unknown")") + case .writeFailure(let underlyingError): + print("Write failed: \(underlyingError?.localizedDescription ?? "Unknown")") + } +} catch { + print("Unexpected error: \(error)") +} +``` + +## See Also + +- - Adding audio to your exports +- - Using raw settings dictionaries +- - Optimizing export performance \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/GettingStarted.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/GettingStarted.md new file mode 100644 index 0000000..1e4200c --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/GettingStarted.md @@ -0,0 +1,158 @@ +# Getting Started + +Learn how to quickly set up and use SJSAssetExportSession for video exports. + +## Overview + +SJSAssetExportSession provides a simple yet powerful way to export videos with custom settings. This guide will walk you through the basic setup and your first export. + +## Installation + +Add SJSAssetExportSession to your project using Swift Package Manager: + +```swift +dependencies: [ + .package(url: "https://github.com/samhuri/SJSAssetExportSession.git", from: "0.3.0") +] +``` + +## Basic Usage + +### Step 1: Import the Framework + +```swift +import SJSAssetExportSession +import AVFoundation +``` + +### Step 2: Create an Export Session + +```swift +let exporter = ExportSession() +``` + +### Step 3: Prepare Your Asset + +```swift +let sourceURL = URL(fileURLWithPath: "path/to/your/video.mov") +let sourceAsset = AVURLAsset(url: sourceURL, options: [ + AVURLAssetPreferPreciseDurationAndTimingKey: true +]) +``` + +> Important: Always use `AVURLAssetPreferPreciseDurationAndTimingKey: true` for accurate duration and timing information. + +### Step 4: Define Your Output + +```swift +let destinationURL = URL.temporaryDirectory.appending(component: "exported-video.mp4") +``` + +### Step 5: Export with Basic Settings + +```swift +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +## Complete Example + +Here's a complete example that includes progress monitoring: + +```swift +import SJSAssetExportSession +import AVFoundation + +func exportVideo() async throws { + let sourceURL = URL(fileURLWithPath: "input.mov") + let sourceAsset = AVURLAsset(url: sourceURL, options: [ + AVURLAssetPreferPreciseDurationAndTimingKey: true + ]) + + let destinationURL = URL.temporaryDirectory.appending(component: "output.mp4") + + let exporter = ExportSession() + + // Monitor progress + Task { + for await progress in exporter.progressStream { + print("Export progress: \(Int(progress * 100))%") + } + } + + // Perform the export + try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080) + .fps(30) + .bitrate(5_000_000), + to: destinationURL, + as: .mp4 + ) + + print("Export completed successfully!") +} +``` + +## Next Steps + +- Learn about to customize audio settings +- Explore for advanced video options +- Check out for more complex scenarios +- Read about to handle export failures gracefully + +## Common Patterns + +### Exporting a Video Clip + +To export only a portion of a video: + +```swift +try await exporter.export( + asset: sourceAsset, + timeRange: CMTimeRange( + start: CMTime(seconds: 10, preferredTimescale: 600), + duration: CMTime(seconds: 30, preferredTimescale: 600) + ), + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +### Adding Metadata + +Include metadata in your exported video: + +```swift +let titleMetadata = AVMutableMetadataItem() +titleMetadata.key = AVMetadataKey.commonKeyTitle.rawValue as NSString +titleMetadata.keySpace = .common +titleMetadata.value = "My Video Title" as NSString + +try await exporter.export( + asset: sourceAsset, + metadata: [titleMetadata], + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +### Optimizing for Network Playback + +For videos that will be streamed or downloaded: + +```swift +try await exporter.export( + asset: sourceAsset, + optimizeForNetworkUse: true, + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/PerformanceOptimization.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/PerformanceOptimization.md new file mode 100644 index 0000000..7942525 --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/PerformanceOptimization.md @@ -0,0 +1,581 @@ +# Performance Optimization + +Learn how to optimize export performance and handle large video files efficiently. + +## Overview + +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 + +```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 (slower) +try await exporter.export( + asset: sourceAsset, + video: .codec(.hevc, width: 3840, height: 2160) + .fps(60) + .bitrate(20_000_000), + to: destinationURL, + as: .mov +) +``` + +## Codec Optimization + +### H.264 Profile Selection + +Choose the appropriate H.264 profile for your performance needs: + +```swift +// Fastest encoding - Baseline profile +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264(.baselineAuto), width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) + +// 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 +) +``` + +### HEVC Considerations + +HEVC provides better compression but requires more processing power: + +```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 + +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 +) +``` + +## Memory Management + +### Handling Large Files + +```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 + ) +} +``` + +### Memory-Efficient Settings + +```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] +] + +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.. 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.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") + } +} +``` + +## See Also + +- - Video settings options +- - Audio settings optimization +- - Monitoring export progress +- - Handling performance-related errors \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ProgressTracking.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ProgressTracking.md new file mode 100644 index 0000000..89d73c3 --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/ProgressTracking.md @@ -0,0 +1,566 @@ +# Progress Tracking + +Learn how to monitor export progress and provide user feedback during video processing. + +## Overview + +SJSAssetExportSession provides real-time progress tracking through an `AsyncStream`. 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 + +```swift +let exporter = ExportSession() + +Task { + for await progress in exporter.progressStream { + print("Export progress: \(Int(progress * 100))%") + } +} + +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 +) +``` + +### 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 + +```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? + + 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? + + 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 { + 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" + } + } + } +} +``` + +### Progress with Detailed Status + +```swift +enum ExportStatus { + case preparing + case encoding(Float) + case finalizing + case completed + case failed(Error) + case cancelled +} + +class DetailedExportTracker: ObservableObject { + @Published var status: ExportStatus = .preparing + + func startExport() { + status = .preparing + + 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) + } + } + } + } +} +``` + +## 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() + } + + 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 { + for await progress in exporter.progressStream { + saveProgress(progress, for: exportID) + + await MainActor.run { + // Update UI + } + } + } + + try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 + ) + + clearProgress(for: exportID) + } +} +``` + +## 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 { + 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 +- - Handling errors during progress tracking +- - Optimizing export performance +- - Basic usage examples \ No newline at end of file diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md index 9d19a20..f041276 100644 --- a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md @@ -1,49 +1,133 @@ # ``SJSAssetExportSession`` -`SJSAssetExportSession` is an alternative to `AVAssetExportSession` that lets you provide custom audio and video settings, without dropping down into the world of `AVAssetReader` and `AVAssetWriter`. +A Swift-first alternative to AVAssetExportSession with custom audio/video settings and strict concurrency support. -[`AVAssetExportSession`][AV] is fine for some things but it provides basically no way to customize the export settings, besides the couple of options on `AVVideoComposition` like render size and frame rate. This package has similar capabilites to the venerable [`SDAVAssetExportSession`][SDAV] but the API is completely different, the code is written in Swift, and it's ready for the world of strict concurrency. +## Overview -You shouldn't have to read through [audio settings][] and [video settings][] just to set the bitrate, and setting the frame rate can be tricky, so there's a nicer API that builds these settings dictionaries with some commonly used settings. +`SJSAssetExportSession` is a modern Swift package that provides an alternative to `AVAssetExportSession` with full control over audio and video export settings. Unlike the built-in `AVAssetExportSession`, this library allows you to specify custom codec settings, bitrates, frame rates, and color properties without having to work directly with `AVAssetReader` and `AVAssetWriter`. + +### 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 +- **Real-time Progress Reporting**: Monitor export progress via `AsyncStream` +- **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 + +### Why SJSAssetExportSession? + +[`AVAssetExportSession`][AV] provides limited customization options, essentially restricting you to the presets it offers. This package gives you the control you need while maintaining a simple, Swift-friendly API. [AV]: https://developer.apple.com/documentation/avfoundation/avassetexportsession -[SDAV]: https://github.com/rs/SDAVAssetExportSession + +Instead of wrestling with complex [audio settings][] and [video settings][] dictionaries, you can use the builder pattern to construct exactly what you need: + [audio settings]: https://developer.apple.com/documentation/avfoundation/audio_settings [video settings]: https://developer.apple.com/documentation/avfoundation/video_settings -The simplest usage is something like this: - ```swift let exporter = ExportSession() Task { for await progress in exporter.progressStream { - print("Progress: \(progress)") + print("Export progress: \(progress)") } } + try await exporter.export( - asset: AVURLAsset(url: sourceURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]), - video: .codec(.h264, width: 1280, height: 720), - to: URL.temporaryDirectory.appeding(component: "new-video.mp4"), + asset: sourceAsset, + audio: .format(.aac).channels(2).sampleRate(48_000), + video: .codec(.h264, width: 1920, height: 1080) + .fps(30) + .bitrate(5_000_000) + .color(.sdr), + to: destinationURL, as: .mp4 ) ``` +## Getting Started + +### Basic Export + +The simplest way to get started is with a basic video export: + +```swift +let exporter = ExportSession() +try await exporter.export( + asset: AVURLAsset(url: sourceURL), + video: .codec(.h264, width: 1280, height: 720), + to: destinationURL, + as: .mp4 +) +``` + +### Monitoring Progress + +Track export progress using the built-in progress stream: + +```swift +let exporter = ExportSession() +Task { + for await progress in exporter.progressStream { + DispatchQueue.main.async { + progressView.progress = progress + } + } +} +try await exporter.export(/* ... */) +``` + +### Advanced Configuration + +For more control, specify custom audio settings, metadata, and time ranges: + +```swift +try await exporter.export( + asset: sourceAsset, + optimizeForNetworkUse: true, + metadata: [locationMetadata], + timeRange: CMTimeRange(start: .zero, duration: .seconds(30)), + audio: .format(.mp3).channels(1).sampleRate(22_050), + video: .codec(.hevc, width: 3840, height: 2160) + .fps(60) + .bitrate(15_000_000) + .color(.hdr), + to: destinationURL, + as: .mov +) +``` + ## Topics -### Exporting +### Essentials - ``ExportSession`` -- ``ExportSession/Error`` -- ``ExportSession/SetupFailureReason`` +- +- -### Audio Output Settings +### Audio Configuration - ``AudioOutputSettings`` - ``AudioOutputSettings/Format`` +- -### Video Output Settings +### Video Configuration - ``VideoOutputSettings`` - ``VideoOutputSettings/Codec`` - ``VideoOutputSettings/H264Profile`` - ``VideoOutputSettings/Color`` +- + +### Error Handling + +- ``ExportSession/Error`` +- ``ExportSession/SetupFailureReason`` +- + +### Advanced Topics + +- +- +- diff --git a/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/VideoConfiguration.md b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/VideoConfiguration.md new file mode 100644 index 0000000..994dfe7 --- /dev/null +++ b/Sources/SJSAssetExportSession/SJSAssetExportSession.docc/VideoConfiguration.md @@ -0,0 +1,452 @@ +# Video Configuration + +Comprehensive guide to configuring video settings for optimal export results. + +## 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: + +```swift +// H.264 - Maximum compatibility +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080), + to: destinationURL, + as: .mp4 +) + +// HEVC - Better compression, newer devices +try await exporter.export( + asset: sourceAsset, + video: .codec(.hevc, width: 3840, height: 2160), + to: destinationURL, + as: .mov +) +``` + +### Setting Resolution + +Specify exact dimensions for your output video: + +```swift +// Common resolutions +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, width: 1920, height: 1080), // 1080p + to: destinationURL, + as: .mp4 +) + +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 +) +``` + +You can also use `CGSize` for resolution: + +```swift +let resolution = CGSize(width: 1920, height: 1080) +try await exporter.export( + asset: sourceAsset, + video: .codec(.h264, size: resolution), + to: destinationURL, + as: .mp4 +) +``` + +## Frame Rate Configuration + +### Standard Frame Rates + +Set the output frame rate to match your content or target platform: + +```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 +) +``` + +### 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: + +```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 +) + +// 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 +) + +// 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 +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, + to: destinationURL, + as: .mp4 +) +``` + +## Video Composition Integration + +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 +) +``` + +## See Also + +- ``VideoOutputSettings`` - Video settings builder +- ``VideoOutputSettings/Codec`` - Supported video codecs +- ``VideoOutputSettings/H264Profile`` - H.264 profile options +- ``VideoOutputSettings/Color`` - Color space configuration +- - Configuring audio settings +- - Using raw video settings dictionaries \ No newline at end of file