Processing spatial video with a custom video compositor
At a glance
| Item |
Summary |
| Purpose |
Preview and export mono or stereo transformations of spatial video using custom video compositors. |
| App architecture |
SwiftUI owns one observable feature model; the model builds compositions and selects exporters through explicit protocol/factory boundaries. |
| Main patterns |
Observable model, Strategy/Factory, Builder, instruction decorator, and AVFoundation protocol implementations. |
| Project style |
Feature folders separate Compositing, Export, Model, and Views; files match their primary type. |
Project structure
AVFSpatialCustomVideoCompositorSample/
├── AVFSpatialCustomVideoCompositorSample.swift # App root
├── ContentView.swift # Feature UI and file flow
├── SampleModel.swift # Playback/export owner
├── Model/UserSettings.swift # UserDefaults-backed selection
├── Compositing/
│ ├── VideoCompositionBuilder.swift
│ ├── MonoOutputCompositor.swift
│ ├── StereoOutputCompositor.swift
│ └── PixelBufferHelper.swift
├── Export/
│ ├── Exporter.swift # Protocol, factory enum, status/errors
│ ├── ExportSessionExporter.swift
│ ├── ReaderWriterExporter.swift
│ └── FileManagerExtensions.swift
└── Views/PlayerView.swift / SettingsView.swift
Structure observations
- Folder boundaries follow capabilities rather than UI screens.
SampleModel is the feature facade and only long-lived owner of playback/export state.
- Export has an app-defined protocol; compositing implements Apple’s protocol and uses an enum to choose concrete classes.
Overall architecture
flowchart LR
App["App"] --> View["ContentView"]
View --> Model["SampleModel"]
Model --> Player["AVPlayer"]
Model --> Builder["VideoCompositionBuilder"]
Builder --> Mono["MonoOutputCompositor"]
Builder --> Stereo["StereoOutputCompositor"]
Model --> Factory["ExporterType factory"]
Factory --> Session["ExportSessionExporter"]
Factory --> RW["ReaderWriterExporter"]
Settings["UserSettings.shared"] --> Model
Reference code
AVFSpatialCustomVideoCompositorSample/SampleModel.swift:80 — model selects export and composition collaborators
func export(using exporterType: ExporterType) async throws {
exporter = exporterType.createExporter()
isExportInProgress = true
let asset = AVURLAsset(url: movieFile)
let videoComposition = try await makeVideoComposition(for: asset)
try await exporter?.export(asset: asset, videoComposition: videoComposition)
// publish output, then release exporter
}
Interpretation
ContentView owns presentation state and sends intents to SampleModel. The model is a concrete observable facade: it owns the player, turns settings into a video composition, and retains the chosen exporter only for the active operation.
Ownership and state
classDiagram
ContentView *-- SampleModel : creates as State
SampleModel *-- AVPlayer : creates and replaces
SampleModel *-- Exporter : retains during export
SampleModel ..> VideoCompositionBuilder : creates per operation
UserSettings o-- UserDefaults : shared storage
AVVideoComposition ..> AVVideoCompositing : framework creates selected class
Ownership evidence
AVFSpatialCustomVideoCompositorSample/ContentView.swift:14 and AVFSpatialCustomVideoCompositorSample/SampleModel.swift:14 — view/model ownership
@State private var sampleModel = SampleModel()
private(set) var player = AVPlayer()
private var movieFile: URL? = nil
private var exporter: Exporter?
private(set) var isExportInProgress = false
private(set) var exportTempURL: URL? = nil
| Owner |
Object or state |
Relationship |
Mutation authority |
ContentView |
SampleModel and modal/import/export presentation flags |
Creates as @State |
View for presentation; model for feature state |
SampleModel |
Player, security-scoped movie URL, export state, active exporter |
Creates/replaces/retains |
SampleModel methods only for restricted properties |
VideoCompositionBuilder |
Asset and compositor selection |
Receives immutable dependencies per build |
Builder operation |
UserSettings.shared |
UserDefaults key/value |
Singleton value wrapper |
Computed property setter |
| AVFoundation |
Concrete custom compositor instance |
Instantiates from configured class |
Framework request lifecycle |
Class and protocol design
classDiagram
class Exporter {
<<protocol>>
status
export
}
ExportSessionExporter ..|> Exporter
ReaderWriterExporter ..|> Exporter
ExporterType ..> Exporter : factory
MonoOutputCompositor ..|> AVVideoCompositing
StereoOutputCompositor ..|> AVVideoCompositing
SpatialVideoCompositionInstruction ..|> AVVideoCompositionInstructionProtocol
Reference code
AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:27 — protocol factory
func createExporter() -> Exporter {
switch self {
case .exportSession: return ExportSessionExporter()
case .readerWriter: return ReaderWriterExporter()
}
}
protocol Exporter {
var status: ExporterStatus { get }
func export(asset: AVAsset, videoComposition: AVVideoComposition?) async throws
}
| Type |
Responsibility |
Depends on |
SampleModel |
Coordinate security-scoped loading, playback composition, export, and observable state |
AVFoundation, settings, exporter abstraction |
VideoCompositionBuilder |
Preserve color/projection/spatial configuration and select compositor class |
AVFoundation |
MonoOutputCompositor / StereoOutputCompositor |
Process mono or tagged stereo source buffers |
AVVideoCompositing, pixel helpers |
SpatialVideoCompositionInstruction |
Wrap a framework instruction while adding spatial configuration and projection tag |
AVVideoCompositionInstructionProtocol |
| Exporter implementations |
Export through high-level session or low-level reader/writer pipeline |
Exporter, AVFoundation |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
SampleModel.player, loaded/export flags, output URL, stereo-preview flag |
private(set) |
Views can observe values but only SampleModel can assign them. |
Enforces the model as mutation authority. |
movieFile, active exporter, makePlayer, makeVideoComposition |
private |
UI cannot bypass security-scope, composition, or export lifecycle. |
Protects feature invariants behind commands. |
Exporter status implementations |
private(set) |
Model can read progress/result but exporters alone transition status. |
Gives each exporter ownership of its state machine. |
| Reader/writer AVFoundation objects and helper methods |
private |
No caller can partially configure the low-level pipeline. |
Keeps reader/writer ordering internal. |
UserSettings initializer, defaults object, and key |
private |
Callers must use shared and the typed property. |
Centralizes persistence key and default behavior. |
| Builder asset, compositor type, and derived async helpers |
private |
Only build() exposes composition construction. |
Presents a narrow builder API. |
| App protocols/types otherwise |
internal by default |
Reusable across this app target, not exported as a framework. |
Appropriate application boundary. |
Reference code
AVFSpatialCustomVideoCompositorSample/SampleModel.swift:14 and AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift:10 — enforced mutation and singleton boundary
private(set) var player = AVPlayer()
private(set) var isMovieFileLoaded = false
private var exporter: Exporter?
struct UserSettings {
static let shared = UserSettings()
private let userDefaults = UserDefaults.standard
private init() {}
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Import UI, modal state, and file movement |
ContentView |
Platform presentation concerns |
| Security-scoped URL and player replacement |
SampleModel |
Long-lived feature lifecycle |
| Translate setting into composition |
SampleModel.makeVideoComposition |
Joins user preference to builder invocation |
| Color/spatial configuration and instruction wrapping |
VideoCompositionBuilder |
Composition-construction concern |
| Pixel transformations |
compositor types and PixelBufferHelper |
Per-frame AVFoundation callback path |
| Export state and mechanics |
exporter implementations |
Each strategy owns its AV resources and progress |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Observable feature model |
AVFSpatialCustomVideoCompositorSample/SampleModel.swift:11, AVFSpatialCustomVideoCompositorSample/ContentView.swift:14 |
Keeps SwiftUI declarative while centralizing playback/export state. |
| Strategy + Factory |
AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:11, AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:28 |
UI chooses an enum; model depends on the common Exporter contract. |
| Builder |
AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:10, AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:26 |
Constructs a correctly tagged/color-configured composition in one operation. |
| Decorator |
AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:113 |
Forwards the framework instruction contract and attaches spatial metadata. |
| Framework strategy |
AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:153 |
Supplies mono or stereo AVVideoCompositing classes to AVFoundation. |
| Singleton settings wrapper |
AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift:10 |
Central typed access, with the usual global-state/testability tradeoff. |
Naming conventions
- Capability roles use precise suffixes:
Builder, Exporter, Compositor, Instruction, and Helper.
- Concrete strategies state mechanism/output:
ExportSessionExporter, ReaderWriterExporter, MonoOutputCompositor, StereoOutputCompositor.
- Boolean names state conditions:
isMovieFileLoaded, isExportInProgress, previewRequiresStereo, outputsStereo.
- Commands use clear verbs:
loadMovieFile, applyUserSettings, createExporter, build, and export.
- Files mirror primary types and capability folders.
Architecture takeaways
- Use
private(set) when views need observation but one model must retain mutation authority.
- A small protocol plus enum factory cleanly supports materially different export mechanisms.
- Wrap framework instructions when custom compositors need metadata beyond the base protocol.
- Keep video-composition construction separate from both UI and frame processing.
- This sample demonstrates meaningful protocol-oriented design at both app (
Exporter) and framework (AVVideoCompositing) boundaries.
Source map
| Source file |
Relevant symbols |
AVFSpatialCustomVideoCompositorSample/ContentView.swift |
view-owned model and file/export UI |
AVFSpatialCustomVideoCompositorSample/SampleModel.swift |
SampleModel |
AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift |
UserSettings |
AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift |
builder, instruction wrapper, CompositorType |
AVFSpatialCustomVideoCompositorSample/Compositing/MonoOutputCompositor.swift |
mono compositor |
AVFSpatialCustomVideoCompositorSample/Compositing/StereoOutputCompositor.swift |
stereo compositor |
AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift |
protocol, factory enum, status, errors |
AVFSpatialCustomVideoCompositorSample/Export/ExportSessionExporter.swift |
high-level exporter |
AVFSpatialCustomVideoCompositorSample/Export/ReaderWriterExporter.swift |
low-level exporter |