mirror of
https://github.com/samsonjs/SJSAssetExportSession.git
synced 2026-03-25 08:45:50 +00:00
199 lines
7.6 KiB
Markdown
199 lines
7.6 KiB
Markdown
# 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
|
|
|
|
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
|
|
|
|
This package is supported on iOS 17.0+, macOS Sonoma 14.0+, and visionOS 1.3+.
|
|
|
|
### 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)
|
|
|
|
When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file:
|
|
|
|
```swift
|
|
.package(url: "https://github.com/samsonjs/SJSAssetExportSession.git", .upToNextMajor(from: "0.3.0"))
|
|
```
|
|
|
|
and then add `"SJSAssetExportSession"` to the list of dependencies in your target as well.
|
|
|
|
## 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
|
|
|
|
This should be fairly self-explanatory:
|
|
|
|
```swift
|
|
let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov")
|
|
let sourceAsset = AVURLAsset(url: sourceURL, options: [
|
|
AVURLAssetPreferPreciseDurationAndTimingKey: true,
|
|
])
|
|
let destinationURL = URL.temporaryDirectory.appending(component: "shiny-new-video.mp4")
|
|
let exporter = ExportSession()
|
|
Task {
|
|
for await progress in exporter.progressStream {
|
|
print("Export progress: \(progress)")
|
|
}
|
|
}
|
|
|
|
try await exporter.export(
|
|
asset: sourceAsset,
|
|
video: .codec(.h264, width: 1280, height: 720),
|
|
to: destinationURL,
|
|
as: .mp4
|
|
)
|
|
```
|
|
|
|
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
|
|
|
|
Here are all of the parameters you can pass into the nice export method:
|
|
|
|
```swift
|
|
let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov")
|
|
let sourceAsset = AVURLAsset(url: sourceURL, options: [
|
|
AVURLAssetPreferPreciseDurationAndTimingKey: true,
|
|
])
|
|
let destinationURL = URL.temporaryDirectory.appending(component: "shiny-new-video.mp4")
|
|
let exporter = ExportSession()
|
|
Task {
|
|
for await progress in exporter.progressStream {
|
|
print("Export progress: \(progress)")
|
|
}
|
|
}
|
|
|
|
let locationMetadata = AVMutableMetadataItem()
|
|
locationMetadata.key = AVMetadataKey.commonKeyLocation.rawValue as NSString
|
|
locationMetadata.keySpace = .common
|
|
locationMetadata.value = "+48.50176+123.34368/" as NSString
|
|
try await exporter.export(
|
|
asset: sourceAsset,
|
|
optimizeForNetworkUse: true,
|
|
metadata: [locationMetadata],
|
|
timeRange: CMTimeRange(start: .zero, duration: .seconds(1)),
|
|
audio: .format(.mp3).channels(1).sampleRate(22_050),
|
|
video: .codec(.h264, width: 1280, height: 720)
|
|
.fps(24)
|
|
.bitrate(1_000_000)
|
|
.color(.sdr),
|
|
to: destinationURL,
|
|
as: .mp4
|
|
)
|
|
```
|
|
|
|
### 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:
|
|
|
|
```swift
|
|
let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov")
|
|
let sourceAsset = AVURLAsset(url: sourceURL, options: [
|
|
AVURLAssetPreferPreciseDurationAndTimingKey: true,
|
|
])
|
|
let destinationURL = URL.temporaryDirectory.appending(component: "shiny-new-video.mp4")
|
|
let exporter = ExportSession()
|
|
Task {
|
|
for await progress in exporter.progressStream {
|
|
print("Export progress: \(progress)")
|
|
}
|
|
}
|
|
|
|
let locationMetadata = AVMutableMetadataItem()
|
|
locationMetadata.key = AVMetadataKey.commonKeyLocation.rawValue as NSString
|
|
locationMetadata.keySpace = .common
|
|
locationMetadata.value = "+48.50176+123.34368/" as NSString
|
|
|
|
let videoComposition = try await AVMutableVideoComposition.videoComposition(withPropertiesOf: sourceAsset)
|
|
videoComposition.renderSize = CGSize(width: 1280, height: 720)
|
|
videoComposition.sourceTrackIDForFrameTiming = kCMPersistentTrackID_Invalid
|
|
videoComposition.frameDuration = CMTime(value: 600 / 24, timescale: 600) // 24 fps
|
|
videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2
|
|
videoComposition.colorTransferFunction = AVVideoTransferFunction_ITU_R_709_2
|
|
videoComposition.colorYCbCrMatrix = AVVideoYCbCrMatrix_ITU_R_709_2
|
|
try await exporter.export(
|
|
asset: sourceAsset,
|
|
optimizeForNetworkUse: true,
|
|
metadata: [locationMetadata],
|
|
timeRange: CMTimeRange(start: .zero, duration: .seconds(1)),
|
|
audioOutputSettings: [
|
|
AVFormatIDKey: kAudioFormatMPEGLayer3,
|
|
AVNumberOfChannelsKey: NSNumber(value: 1),
|
|
AVSampleRateKey: NSNumber(value: 22_050),
|
|
],
|
|
videoOutputSettings: [
|
|
AVVideoCodecKey: AVVideoCodecType.h264.rawValue,
|
|
AVVideoCompressionPropertiesKey: [
|
|
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
AVVideoAverageBitRateKey: NSNumber(value: 1_000_000),
|
|
] as [String: any Sendable],
|
|
AVVideoColorPropertiesKey: [
|
|
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
|
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
|
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2,
|
|
],
|
|
],
|
|
composition: videoComposition,
|
|
to: destinationURL,
|
|
as: .mp4
|
|
)
|
|
```
|
|
|
|
It's an effective illustration of why the nicer API exists right? But when you need this flexibility then it's available for you.
|
|
|
|
### Mix and Match
|
|
|
|
`AudioOutputSettings` and `VideoOutputSettings` have a property named `settingsDictionary` and you can use that to bootstrap your own custom settings.
|
|
|
|
```swift
|
|
let sourceURL = URL.documentsDirectory.appending(component: "some-video.mov")
|
|
let sourceAsset = AVURLAsset(url: sourceURL, options: [
|
|
AVURLAssetPreferPreciseDurationAndTimingKey: true,
|
|
])
|
|
let destinationURL = URL.temporaryDirectory.appending(component: "shiny-new-video.mp4")
|
|
let exporter = ExportSession()
|
|
Task {
|
|
for await progress in exporter.progressStream {
|
|
print("Export progress: \(progress)")
|
|
}
|
|
}
|
|
|
|
var audioSettings = AudioOutputSettings.default.settingsDictionary
|
|
audioSettings[AVVideoAverageBitRateKey] = 65_536
|
|
let videoSettings = VideoOutputSettings
|
|
.codec(.hevc, width: 1280, height: 720)
|
|
.settingsDictionary
|
|
try await exporter.export(
|
|
asset: sourceAsset,
|
|
audioOutputSettings: audioSettings,
|
|
videoOutputSettings: videoSettings,
|
|
to: destinationURL,
|
|
as: .mp4
|
|
)
|
|
```
|
|
|
|
## License
|
|
|
|
Copyright © 2024 Sami Samhuri, https://samhuri.net <sami@samhuri.net>. Released under the terms of the [MIT License][MIT].
|
|
|
|
[MIT]: https://sjs.mit-license.org
|