Flesh out documentation for most of the public API

This commit is contained in:
Sami Samhuri 2024-08-18 13:08:45 -07:00
parent 16599d638c
commit b60032f15f
No known key found for this signature in database
9 changed files with 128 additions and 39 deletions

View file

@ -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. `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 [AV]: https://developer.apple.com/documentation/avfoundation/avassetexportsession
[SDAV]: https://github.com/rs/SDAVAssetExportSession [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. 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 [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+. 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. 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: 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")) .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. 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: 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. 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: 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: 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: [ AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoAverageBitRateKey: NSNumber(value: 1_000_000), AVVideoAverageBitRateKey: NSNumber(value: 1_000_000),
] as [String: (any Sendable)], ] as [String: any Sendable],
AVVideoColorPropertiesKey: [ AVVideoColorPropertiesKey: [
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
AVVideoTransferFunctionKey: AVVideoTransferFunction_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 ### Mix and Match
[video settings]: https://developer.apple.com/documentation/avfoundation/video_settings
## Mix and Match
`AudioOutputSettings` and `VideoOutputSettings` have a property named `settingsDictionary` and you can use that to bootstrap your own custom settings. `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 <sami@samhuri.net>. Released under the terms of the [MIT License][MIT]. Copyright © 2024 Sami Samhuri, https://samhuri.net <sami@samhuri.net>. Released under the terms of the [MIT License][MIT].

View file

@ -7,9 +7,15 @@
public import AVFoundation 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 { public struct AudioOutputSettings {
/// Describes the output file format.
public enum Format { public enum Format {
/// Advanced Audio Codec. The audio format typically used for MPEG-4 audio.
case aac case aac
/// The MPEG Layer 3 audio format.
case mp3 case mp3
var formatID: AudioFormatID { var formatID: AudioFormatID {
@ -25,10 +31,12 @@ public struct AudioOutputSettings {
let sampleRate: Int? let sampleRate: Int?
let mix: AVAudioMix? let mix: AVAudioMix?
/// Specifies the AAC format with 2 channels at a 44.1 KHz sample rate.
public static var `default`: AudioOutputSettings { public static var `default`: AudioOutputSettings {
.format(.aac).channels(2).sampleRate(44_100) .format(.aac).channels(2).sampleRate(44_100)
} }
/// Specifies the given format with 2 channels.
public static func format(_ format: Format) -> AudioOutputSettings { public static func format(_ format: Format) -> AudioOutputSettings {
.init(format: format.formatID, channels: 2, sampleRate: nil, mix: nil) .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) .init(format: format, channels: channels, sampleRate: sampleRate, mix: mix)
} }
var settingsDictionary: [String: any Sendable] { public var settingsDictionary: [String: any Sendable] {
if let sampleRate { if let sampleRate {
[ [
AVFormatIDKey: format, AVFormatIDKey: format,

View file

@ -16,6 +16,28 @@ public final class ExportSession: Sendable {
(progressStream, progressContinuation) = AsyncStream<Float>.makeStream() (progressStream, progressContinuation) = AsyncStream<Float>.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( public func export(
asset: sending AVAsset, asset: sending AVAsset,
optimizeForNetworkUse: Bool = false, 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: - Parameters:
- asset: The source asset to export. This can be any kind of `AVAsset` including subclasses such as `AVComposition`. - 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. - 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`. - 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( public func export(
asset: sending AVAsset, asset: sending AVAsset,
optimizeForNetworkUse: Bool = false, optimizeForNetworkUse: Bool = false,
metadata: sending [AVMetadataItem] = [], metadata: sending [AVMetadataItem] = [],
timeRange: CMTimeRange? = nil, timeRange: CMTimeRange? = nil,
audioOutputSettings: [String: (any Sendable)], audioOutputSettings: [String: any Sendable],
mix: sending AVAudioMix? = nil, mix: sending AVAudioMix? = nil,
videoOutputSettings: [String: (any Sendable)], videoOutputSettings: [String: any Sendable],
composition: sending AVVideoComposition? = nil, composition: sending AVVideoComposition? = nil,
to outputURL: URL, to outputURL: URL,
as fileType: AVFileType as fileType: AVFileType

View file

@ -1,13 +1,49 @@
# ``SJSAssetExportSession`` # ``SJSAssetExportSession``
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@--> `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.
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@--> 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 ## Topics
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@--> ### Exporting
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@--> - ``ExportSession``
- ``ExportSession/Error``
- ``ExportSession/SetupFailureReason``
### Audio Output Settings
- ``AudioOutputSettings``
- ``AudioOutputSettings/Format``
### Video Output Settings
- ``VideoOutputSettings``
- ``VideoOutputSettings/Codec``
- ``VideoOutputSettings/H264Profile``
- ``VideoOutputSettings/Color``

View file

@ -32,9 +32,9 @@ actor SampleWriter {
} }
private var progressContinuation: AsyncStream<Float>.Continuation? private var progressContinuation: AsyncStream<Float>.Continuation?
private let audioOutputSettings: [String: (any Sendable)] private let audioOutputSettings: [String: any Sendable]
private let audioMix: AVAudioMix? private let audioMix: AVAudioMix?
private let videoOutputSettings: [String: (any Sendable)] private let videoOutputSettings: [String: any Sendable]
private let videoComposition: AVVideoComposition? private let videoComposition: AVVideoComposition?
private let reader: AVAssetReader private let reader: AVAssetReader
private let writer: AVAssetWriter private let writer: AVAssetWriter
@ -48,9 +48,9 @@ actor SampleWriter {
nonisolated init( nonisolated init(
asset: sending AVAsset, asset: sending AVAsset,
audioOutputSettings: sending [String: (any Sendable)], audioOutputSettings: sending [String: any Sendable],
audioMix: sending AVAudioMix?, audioMix: sending AVAudioMix?,
videoOutputSettings: sending [String: (any Sendable)], videoOutputSettings: sending [String: any Sendable],
videoComposition: sending AVVideoComposition, videoComposition: sending AVVideoComposition,
timeRange: CMTimeRange? = nil, timeRange: CMTimeRange? = nil,
optimizeForNetworkUse: Bool = false, optimizeForNetworkUse: Bool = false,

View file

@ -7,7 +7,13 @@
import AVFoundation 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 { public struct VideoOutputSettings {
/// Describes an H.264 encoding profile.
public enum H264Profile { public enum H264Profile {
case baselineAuto, baseline30, baseline31, baseline41 case baselineAuto, baseline30, baseline31, baseline41
case mainAuto, main31, main32, main41 case mainAuto, main31, main32, main41
@ -30,11 +36,15 @@ public struct VideoOutputSettings {
} }
} }
/// Specifies the output codec.
public enum Codec { public enum Codec {
/// H.264 using the associated encoding profile.
case h264(H264Profile) case h264(H264Profile)
/// HEVC / H.265
case hevc case hevc
static var h264: Codec { /// Construct Codec.h264 using the default profile `H264Profile.highAuto`.
public static var h264: Codec {
.h264(.highAuto) .h264(.highAuto)
} }
@ -53,8 +63,12 @@ public struct VideoOutputSettings {
} }
} }
/// Specifies whether to use Standard Dynamic Range or High Dynamic Range colours.
public enum Color { 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] { var properties: [String: any Sendable] {
switch self { switch self {
@ -100,7 +114,7 @@ public struct VideoOutputSettings {
.init(codec: codec, size: size, fps: fps, bitrate: bitrate, color: color) .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] = [ var result: [String: any Sendable] = [
AVVideoCodecKey: codec.stringValue, AVVideoCodecKey: codec.stringValue,
AVVideoWidthKey: NSNumber(value: Int(size.width)), AVVideoWidthKey: NSNumber(value: Int(size.width)),
@ -121,6 +135,11 @@ public struct VideoOutputSettings {
} }
return result 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 { extension AVMutableVideoComposition {

View file

@ -5,7 +5,7 @@
// Created by Sami Samhuri on 2024-07-07. // Created by Sami Samhuri on 2024-07-07.
// //
internal import AVFoundation import AVFoundation
extension AVAsset { extension AVAsset {
func sendTracks(withMediaType mediaType: AVMediaType) async throws -> sending [AVAssetTrack] { func sendTracks(withMediaType mediaType: AVMediaType) async throws -> sending [AVAssetTrack] {

View file

@ -5,8 +5,8 @@
// Created by Sami Samhuri on 2024-08-18. // Created by Sami Samhuri on 2024-08-18.
// //
internal import AVFoundation import AVFoundation
@testable import SJSAssetExportSession import SJSAssetExportSession
private func readmeNiceExample() async throws { private func readmeNiceExample() async throws {
let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov") let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov")
@ -101,7 +101,7 @@ private func readmeFlexibleExample() async throws {
AVVideoCompressionPropertiesKey: [ AVVideoCompressionPropertiesKey: [
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoAverageBitRateKey: NSNumber(value: 1_000_000), AVVideoAverageBitRateKey: NSNumber(value: 1_000_000),
] as [String: (any Sendable)], ] as [String: any Sendable],
AVVideoColorPropertiesKey: [ AVVideoColorPropertiesKey: [
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,

View file

@ -5,9 +5,9 @@
// Created by Sami Samhuri on 2024-06-29. // Created by Sami Samhuri on 2024-06-29.
// //
internal import AVFoundation import AVFoundation
import CoreLocation import CoreLocation
@testable import SJSAssetExportSession import SJSAssetExportSession
import Testing import Testing
final class ExportSessionTests { final class ExportSessionTests {