From b60032f15fbb2d1b3531eedc347c821aecc6863e Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sun, 18 Aug 2024 13:08:45 -0700 Subject: [PATCH] Flesh out documentation for most of the public API --- Readme.md | 36 ++++++++------- .../AudioOutputSettings.swift | 10 +++- SJSAssetExportSession/ExportSession.swift | 30 ++++++++++-- .../SJSAssetExportSession.md | 46 +++++++++++++++++-- SJSAssetExportSession/SampleWriter.swift | 8 ++-- .../VideoOutputSettings.swift | 25 ++++++++-- .../AVAsset+sending.swift | 2 +- .../ReadmeExamples.swift | 6 +-- .../SJSAssetExportSessionTests.swift | 4 +- 9 files changed, 128 insertions(+), 39 deletions(-) diff --git a/Readme.md b/Readme.md index 01657b1..86b2e54 100644 --- a/Readme.md +++ b/Readme.md @@ -1,25 +1,31 @@ -# Overview +# SJSAssetExportSession + +## Overview `SJSAssetExportSession` is an alternative to [`AVAssetExportSession`][AV] that lets you provide custom audio and video settings, without dropping down into the world of `AVAssetReader` and `AVAssetWriter`. It has similar capabilites to [SDAVAssetExportSession][SDAV] but the API is completely different, the code is written in Swift, and it's ready for the world of strict concurrency. +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. + [AV]: https://developer.apple.com/documentation/avfoundation/avassetexportsession [SDAV]: https://github.com/rs/SDAVAssetExportSession +[audio settings]: https://developer.apple.com/documentation/avfoundation/audio_settings +[video settings]: https://developer.apple.com/documentation/avfoundation/video_settings -# Installation +## Installation The only way to install this package is with Swift Package Manager (SPM). Please [file a new issue][] or submit a pull-request if you want to use something else. [file a new issue]: https://github.com/samsonjs/SJSAssetExportSession/issues/new -## Supported Platforms +### Supported Platforms This package is supported on iOS 17.0+, macOS Sonoma 14.0+, and visionOS 1.3+. -## Xcode +### Xcode When you're integrating this into an app with Xcode then go to your project's Package Dependencies and enter the URL `https://github.com/samsonjs/SJSAssetExportSession` and then go through the usual flow for adding packages. -## Swift Package Manager (SPM) +### Swift Package Manager (SPM) When you're integrating this using SPM on its own then add this to your Package.swift file: @@ -27,11 +33,11 @@ When you're integrating this using SPM on its own then add this to your Package. .package(url: "https://github.com/samsonjs/SJSAssetExportSession.git", .upToNextMajor(from: "1.0")) ``` -# Usage +## Usage There are two ways of exporting assets: one using dictionaries for audio and video settings just like with `SDAVAssetExportSession`, and the other using a builder-like API with data structures for commonly used settings. -## The Nice Way +### The Nice Way This should be fairly self-explanatory: @@ -58,7 +64,7 @@ try await exporter.export( Most of the audio and video configuration is optional which is why there are no audio settings specified here. By default you get AAC with 2 channels at a 44.1 KHz sample rate. -## All Nice Parameters +### All Nice Parameters Here are all of the parameters you can pass into the nice export method: @@ -94,7 +100,7 @@ try await exporter.export( ) ``` -## The Most Flexible Way +### The Most Flexible Way When you need all the control you can get down to the nitty gritty details. This code does the exact same thing as the code above: @@ -138,7 +144,7 @@ try await exporter.export( AVVideoCompressionPropertiesKey: [ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoAverageBitRateKey: NSNumber(value: 1_000_000), - ] as [String: (any Sendable)], + ] as [String: any Sendable], AVVideoColorPropertiesKey: [ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, @@ -151,13 +157,9 @@ try await exporter.export( ) ``` -It's an effective illustration of why the nicer API exists right? 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. But when you need this flexibility then it's available for you. +It's an effective illustration of why the nicer API exists right? But when you need this flexibility then it's available for you. -[audio settings]: https://developer.apple.com/documentation/avfoundation/audio_settings - -[video settings]: https://developer.apple.com/documentation/avfoundation/video_settings - -## Mix and Match +### Mix and Match `AudioOutputSettings` and `VideoOutputSettings` have a property named `settingsDictionary` and you can use that to bootstrap your own custom settings. @@ -188,7 +190,7 @@ try await exporter.export( ) ``` -# License +## License Copyright © 2024 Sami Samhuri, https://samhuri.net . Released under the terms of the [MIT License][MIT]. diff --git a/SJSAssetExportSession/AudioOutputSettings.swift b/SJSAssetExportSession/AudioOutputSettings.swift index 41aff2b..7e9d863 100644 --- a/SJSAssetExportSession/AudioOutputSettings.swift +++ b/SJSAssetExportSession/AudioOutputSettings.swift @@ -7,9 +7,15 @@ public import AVFoundation +/// A convenient API for constructing audio settings dictionaries. +/// +/// Construct this by starting with ``AudioOutputSettings/default`` or ``AudioOutputSettings/format(_:)`` and then chain calls to further customize it, if desired, using ``channels(_:)``, ``sampleRate(_:)``, and ``mix(_:)``. public struct AudioOutputSettings { + /// Describes the output file format. public enum Format { + /// Advanced Audio Codec. The audio format typically used for MPEG-4 audio. case aac + /// The MPEG Layer 3 audio format. case mp3 var formatID: AudioFormatID { @@ -25,10 +31,12 @@ public struct AudioOutputSettings { let sampleRate: Int? let mix: AVAudioMix? + /// Specifies the AAC format with 2 channels at a 44.1 KHz sample rate. public static var `default`: AudioOutputSettings { .format(.aac).channels(2).sampleRate(44_100) } + /// Specifies the given format with 2 channels. public static func format(_ format: Format) -> AudioOutputSettings { .init(format: format.formatID, channels: 2, sampleRate: nil, mix: nil) } @@ -45,7 +53,7 @@ public struct AudioOutputSettings { .init(format: format, channels: channels, sampleRate: sampleRate, mix: mix) } - var settingsDictionary: [String: any Sendable] { + public var settingsDictionary: [String: any Sendable] { if let sampleRate { [ AVFormatIDKey: format, diff --git a/SJSAssetExportSession/ExportSession.swift b/SJSAssetExportSession/ExportSession.swift index 6f639da..f844da2 100644 --- a/SJSAssetExportSession/ExportSession.swift +++ b/SJSAssetExportSession/ExportSession.swift @@ -16,6 +16,28 @@ public final class ExportSession: Sendable { (progressStream, progressContinuation) = AsyncStream.makeStream() } + /** + Exports the given asset using all of the other parameters to transform it in some way. This method uses code to build up audio and video settings with a nice API instead of diving into the nitty gritty settings dictionaries. Monitor progress using ``progressStream``. + + - Parameters: + - asset: The source asset to export. This can be any kind of `AVAsset` including subclasses such as `AVComposition`. + + - optimizeForNetworkUse: Setting this value to `true` writes the output file in a form that enables a player to begin playing the media after downloading only a small portion of it. Defaults to `false`. + + - metadata: Optional array of `AVMetadataItem`s to be written out with the exported asset. + + - timeRange: Providing a time range exports a subset of the asset instead of the entire duration, which is the default behaviour. + + - audio: Optional audio settings using ``AudioOutputSettings``. Defaults to ``AudioOutputSettings/default``. + + - video: Video settings using ``VideoOutputSettings``. + + - outputURL: The file `URL` where the exported video will be written. + + - fileType: The type of of video file to export. This will typically be one of `AVFileType.mp4`, `AVFileType.m4v`, or `AVFileType.mov`. + + - Throws: One of the cases in the ``ExportSession/Error`` enum when the export fails. See ``ExportSession/Error`` for possible failures. + */ public func export( asset: sending AVAsset, optimizeForNetworkUse: Bool = false, @@ -50,7 +72,7 @@ public final class ExportSession: Sendable { } /** - Exports the given asset using all of the other parameters to transform it in some way. + Exports the given asset using all of the other parameters to transform it in some way. This method provides the most control over the export using audio and video settings dictionaries, in addition to an optionial audio mix and optional video composition. Monitor progress using ``progressStream``. - Parameters: - asset: The source asset to export. This can be any kind of `AVAsset` including subclasses such as `AVComposition`. @@ -77,15 +99,17 @@ public final class ExportSession: Sendable { - outputURL: The file URL where the exported video will be written. - fileType: The type of of video file to export. This will typically be one of `AVFileType.mp4`, `AVFileType.m4v`, or `AVFileType.mov`. + + - Throws: One of the cases in the ``ExportSession/Error`` enum when the export fails. See ``ExportSession/Error`` for possible failures. */ public func export( asset: sending AVAsset, optimizeForNetworkUse: Bool = false, metadata: sending [AVMetadataItem] = [], timeRange: CMTimeRange? = nil, - audioOutputSettings: [String: (any Sendable)], + audioOutputSettings: [String: any Sendable], mix: sending AVAudioMix? = nil, - videoOutputSettings: [String: (any Sendable)], + videoOutputSettings: [String: any Sendable], composition: sending AVVideoComposition? = nil, to outputURL: URL, as fileType: AVFileType diff --git a/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md b/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md index 0d8aace..9d19a20 100644 --- a/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md +++ b/SJSAssetExportSession/SJSAssetExportSession.docc/SJSAssetExportSession.md @@ -1,13 +1,49 @@ # ``SJSAssetExportSession`` -Summary +`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`. -## Overview +[`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. -Text +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. + +[AV]: https://developer.apple.com/documentation/avfoundation/avassetexportsession +[SDAV]: https://github.com/rs/SDAVAssetExportSession +[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)") + } +} +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"), + as: .mp4 +) +``` ## Topics -### Group +### Exporting -- ``Symbol`` \ No newline at end of file +- ``ExportSession`` +- ``ExportSession/Error`` +- ``ExportSession/SetupFailureReason`` + +### Audio Output Settings + +- ``AudioOutputSettings`` +- ``AudioOutputSettings/Format`` + +### Video Output Settings + +- ``VideoOutputSettings`` +- ``VideoOutputSettings/Codec`` +- ``VideoOutputSettings/H264Profile`` +- ``VideoOutputSettings/Color`` diff --git a/SJSAssetExportSession/SampleWriter.swift b/SJSAssetExportSession/SampleWriter.swift index 5d11425..589dda9 100644 --- a/SJSAssetExportSession/SampleWriter.swift +++ b/SJSAssetExportSession/SampleWriter.swift @@ -32,9 +32,9 @@ actor SampleWriter { } private var progressContinuation: AsyncStream.Continuation? - private let audioOutputSettings: [String: (any Sendable)] + private let audioOutputSettings: [String: any Sendable] private let audioMix: AVAudioMix? - private let videoOutputSettings: [String: (any Sendable)] + private let videoOutputSettings: [String: any Sendable] private let videoComposition: AVVideoComposition? private let reader: AVAssetReader private let writer: AVAssetWriter @@ -48,9 +48,9 @@ actor SampleWriter { nonisolated init( asset: sending AVAsset, - audioOutputSettings: sending [String: (any Sendable)], + audioOutputSettings: sending [String: any Sendable], audioMix: sending AVAudioMix?, - videoOutputSettings: sending [String: (any Sendable)], + videoOutputSettings: sending [String: any Sendable], videoComposition: sending AVVideoComposition, timeRange: CMTimeRange? = nil, optimizeForNetworkUse: Bool = false, diff --git a/SJSAssetExportSession/VideoOutputSettings.swift b/SJSAssetExportSession/VideoOutputSettings.swift index d6256ae..079df0f 100644 --- a/SJSAssetExportSession/VideoOutputSettings.swift +++ b/SJSAssetExportSession/VideoOutputSettings.swift @@ -7,7 +7,13 @@ import AVFoundation +/// A convenient API for constructing video settings dictionaries. +/// +/// Construct this by starting with ``VideoOutputSettings/codec(_:size:)`` or ``VideoOutputSettings/codec(_:width:height:)`` and then chaining calls to further customize it, if desired, using ``fps(_:)``, ``bitrate(_:)``, and ``color(_:)``. +/// +/// Setting the fps and colour also needs support from the `AVVideoComposition` and these settings can be applied to them with ``VideoOutputSettings/apply(to:)``. public struct VideoOutputSettings { + /// Describes an H.264 encoding profile. public enum H264Profile { case baselineAuto, baseline30, baseline31, baseline41 case mainAuto, main31, main32, main41 @@ -30,11 +36,15 @@ public struct VideoOutputSettings { } } + /// Specifies the output codec. public enum Codec { + /// H.264 using the associated encoding profile. case h264(H264Profile) + /// HEVC / H.265 case hevc - static var h264: Codec { + /// Construct Codec.h264 using the default profile `H264Profile.highAuto`. + public static var h264: Codec { .h264(.highAuto) } @@ -53,8 +63,12 @@ public struct VideoOutputSettings { } } + /// Specifies whether to use Standard Dynamic Range or High Dynamic Range colours. public enum Color { - case sdr, hdr + /// Standard dynamic range colours (BT.709 which roughly corresponds to SRGB) + case sdr + /// High dynamic range colours (BT.2020) + case hdr var properties: [String: any Sendable] { switch self { @@ -100,7 +114,7 @@ public struct VideoOutputSettings { .init(codec: codec, size: size, fps: fps, bitrate: bitrate, color: color) } - var settingsDictionary: [String: any Sendable] { + public var settingsDictionary: [String: any Sendable] { var result: [String: any Sendable] = [ AVVideoCodecKey: codec.stringValue, AVVideoWidthKey: NSNumber(value: Int(size.width)), @@ -121,6 +135,11 @@ public struct VideoOutputSettings { } return result } + + /// Applies the subset of relevant settings to the given video composition, namely fps and colour. + public func apply(to videoComposition: AVMutableVideoComposition) { + _ = videoComposition.applyingSettings(self) + } } extension AVMutableVideoComposition { diff --git a/SJSAssetExportSessionTests/AVAsset+sending.swift b/SJSAssetExportSessionTests/AVAsset+sending.swift index af4a601..ad2545c 100644 --- a/SJSAssetExportSessionTests/AVAsset+sending.swift +++ b/SJSAssetExportSessionTests/AVAsset+sending.swift @@ -5,7 +5,7 @@ // Created by Sami Samhuri on 2024-07-07. // -internal import AVFoundation +import AVFoundation extension AVAsset { func sendTracks(withMediaType mediaType: AVMediaType) async throws -> sending [AVAssetTrack] { diff --git a/SJSAssetExportSessionTests/ReadmeExamples.swift b/SJSAssetExportSessionTests/ReadmeExamples.swift index 749c350..5d757e9 100644 --- a/SJSAssetExportSessionTests/ReadmeExamples.swift +++ b/SJSAssetExportSessionTests/ReadmeExamples.swift @@ -5,8 +5,8 @@ // Created by Sami Samhuri on 2024-08-18. // -internal import AVFoundation -@testable import SJSAssetExportSession +import AVFoundation +import SJSAssetExportSession private func readmeNiceExample() async throws { let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov") @@ -101,7 +101,7 @@ private func readmeFlexibleExample() async throws { AVVideoCompressionPropertiesKey: [ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoAverageBitRateKey: NSNumber(value: 1_000_000), - ] as [String: (any Sendable)], + ] as [String: any Sendable], AVVideoColorPropertiesKey: [ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, diff --git a/SJSAssetExportSessionTests/SJSAssetExportSessionTests.swift b/SJSAssetExportSessionTests/SJSAssetExportSessionTests.swift index 8a4c639..5c5687a 100644 --- a/SJSAssetExportSessionTests/SJSAssetExportSessionTests.swift +++ b/SJSAssetExportSessionTests/SJSAssetExportSessionTests.swift @@ -5,9 +5,9 @@ // Created by Sami Samhuri on 2024-06-29. // -internal import AVFoundation +import AVFoundation import CoreLocation -@testable import SJSAssetExportSession +import SJSAssetExportSession import Testing final class ExportSessionTests {