Reading multiview 3D video files
At a glance
| Item | Summary |
|---|---|
| Purpose | Open an MV-HEVC asset, validate it, and render the left- and right-eye image for a selected frame. |
| App architecture | Small SwiftUI app with view-owned navigation state, a metadata view model, and a frame-decoding view model. |
| Main patterns | MVVM, observable state, and an explicit loading/ready/error state machine. |
| Project style | One app target; files are grouped by primary view or view-model type rather than feature folders. |
Project structure
StereoViewer/
├── StereoViewerApp.swift # Composition root
├── LaunchView.swift # File selection and first navigation state
├── MediaDetailView.swift # Asset-validation screen
├── MediaDetailViewModel.swift # Async metadata loading
├── StereoView.swift # Frame controls and image presentation
└── StereoViewModel.swift # Track inspection and frame decoding
Structure observations
- Each Swift file contains one primary type; view and view-model pairs share the same feature prefix.
- The project has no separate service, domain, or persistence layer. AVFoundation work lives directly in the two view models.
Configuration/and the Xcode project hold build configuration; they do not introduce runtime architecture.
Overall architecture
flowchart LR
App["StereoViewerApp"] --> Launch["LaunchView"]
Launch --> Detail["MediaDetailView"]
Detail --> Metadata["MediaDetailViewModel"]
Detail --> Stereo["StereoView"]
Stereo --> Decoder["StereoViewModel"]
Metadata --> Asset["AVURLAsset"]
Decoder --> Reader["AVAssetReader + track output"]
Reference code
StereoViewer/MediaDetailView.swift:18 — transition from validation to decoding
init(filename: URL) {
mediaDetailViewModel = MediaDetailViewModel(filename: filename)
self.filename = filename
}
// In body, after the user continues:
StereoView(asset: mediaDetailViewModel.asset)Interpretation
The SwiftUI views form the navigation and composition layer. MediaDetailViewModel first validates and describes the selected asset; StereoViewModel then owns the lower-level AVFoundation read pipeline. This is a source-explicit MVVM organization, although the view models also act as framework services because the sample is intentionally small.
Ownership and state
classDiagram
MediaDetailView *-- MediaDetailViewModel : creates and retains
MediaDetailViewModel *-- AVURLAsset : creates and retains
StereoView *-- StereoViewModel : creates and retains
StereoViewModel o-- AVURLAsset : receives and retains
StereoViewModel *-- AVAssetReader : creates per seek
StereoView --> StereoViewModel : observes state and images
Ownership evidence
StereoViewer/StereoView.swift:19 and StereoViewer/StereoViewModel.swift:26 — injected asset and owned decoder
init(asset: AVURLAsset) {
self.asset = asset
self.stereoViewModel = StereoViewModel(asset: asset)
}
private let asset: AVURLAsset
private var assetReader: AVAssetReader?| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
LaunchView |
Selected file URL | Owns @State |
LaunchView actions only |
MediaDetailView |
MediaDetailViewModel |
Creates and retains | View model mutates metadata on the main actor |
MediaDetailViewModel |
AVURLAsset and validation results |
Creates asset; publishes results | View model only for mutable properties |
StereoView |
Slider and local presentation state | Owns @State |
StereoView only |
StereoViewModel |
Track, timestamps, reader, output, and decoded images | Retains or creates | Decoder internals; image/state publication is main-actor isolated |
The same AVURLAsset reference passes from the metadata phase into the decoding phase. The diagram uses aggregation for that injected reference and composition for objects each owner creates itself.
Class and protocol design
There are no application-defined protocols or interchangeable implementations. The design favors two concrete observable reference types because each coordinates mutable asynchronous framework state.
| Type | Responsibility | Depends on |
|---|---|---|
MediaDetailViewModel |
Load duration, playability, and readability | AVURLAsset |
StereoViewModel |
Discover the stereo track, index frames, configure a reader, and publish eye images | AVFoundation, VideoToolbox, SwiftUI/AppKit image types |
StereoViewModelState |
Represent loading, ready(times:), or error(message:) |
CMTime |
LaunchView, MediaDetailView, StereoView |
File selection, flow decisions, and rendering | Their owned state/view models |
The ViewModel suffix, @Observable, and the direction of calls from views to model objects are direct evidence for MVVM. Calling the design protocol-oriented would not be supported by this source.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
LaunchView.filename |
private |
Only LaunchView can read or mutate the selected URL. |
Keeps view navigation state local. |
StereoView.stereoViewModel and UI state |
private |
Other files cannot replace the decoder or manipulate slider/presentation state. | Protects view-owned lifecycle and UI invariants. |
StereoViewModel.asset, reader, track, timestamps, and layer IDs |
private |
Consumers use commands and observable outputs instead of editing the read pipeline. | Preserves reader configuration and frame-index consistency. |
publishState, presentationTimesFor, loadVideoLayerIdsForTrack |
private |
Callable only inside their lexical type/file scope. | Marks orchestration helpers as implementation details. |
| App types and view-model outputs without modifiers | internal by default |
Available within the app module, not exported as a library API. | Appropriate for a single-target sample app. |
Reference code
StereoViewer/StereoViewModel.swift:22 — narrow public surface and private pipeline state
@MainActor var state: StereoViewModelState = .loading
@MainActor var leftEye = NSImage(...)
@MainActor var rightEye = NSImage(...)
private let asset: AVURLAsset
private var assetReader: AVAssetReader?
private var trackOutput: AVAssetReaderTrackOutput?The three observable outputs remain module-visible so StereoView can read them, while the framework machinery is private. @MainActor controls concurrency isolation, not visibility. @unchecked Sendable is also a concurrency assertion, not an access level; the code manually routes published UI mutations to the main actor.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Choose a file and switch screens | LaunchView |
Pure UI interaction and local navigation state |
| Load asset metadata | MediaDetailViewModel |
Asynchronous state consumed by MediaDetailView |
| Frame controls and keyboard shortcuts | StereoView |
Presentation-specific behavior |
| Track discovery and timestamp indexing | StereoViewModel plus same-file private helpers |
Closely coupled to decode orchestration but hidden from consumers |
| Reader creation, seek range, and buffer decoding | StereoViewModel |
The model owns all mutable AVFoundation reader state |
| Convert eye pixel buffers to images | StereoViewModel |
Part of the decoding pipeline, with final image publication on MainActor |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Model-View-ViewModel | StereoViewer/MediaDetailView.swift:18, StereoViewer/StereoView.swift:19 |
Moves asynchronous AVFoundation work out of view bodies; concrete construction keeps the sample simple but tightly coupled. |
| Observable state | StereoViewer/MediaDetailViewModel.swift:11, StereoViewer/StereoViewModel.swift:19 |
Lets SwiftUI update when metadata, state, or eye images change. |
| Explicit state machine | StereoViewer/StereoViewModel.swift:12, StereoViewer/StereoView.swift:29 |
Makes loading, successful readiness, and errors exhaustive in the UI. |
| Encapsulated framework adapter | StereoViewer/StereoViewModel.swift:65 |
Presents frame-oriented commands while hiding AVAssetReader setup and tagged-buffer parsing. |
Main application flow
sequenceDiagram
actor User
participant View as SwiftUI views
participant Metadata as MediaDetailViewModel
participant Decoder as StereoViewModel
participant AVF as AVFoundation
User->>View: Select MV-HEVC file
View->>Metadata: Create with URL
Metadata->>AVF: Load metadata
User->>View: Continue / choose frame
View->>Decoder: Create or readBuffer(at:)
Decoder->>AVF: Discover track and read tagged buffers
AVF-->>Decoder: Left/right pixel buffers
Decoder-->>View: Ready state and eye images
Reference code
StereoViewer/StereoViewModel.swift:39 and StereoViewer/StereoViewModel.swift:139 — track discovery to image publication
if let track = try await asset.loadTracks(
withMediaCharacteristic: .containsStereoMultiviewVideo
).first {
self.track = track
self.framePresentationTimes = try presentationTimesFor(track: track, asset: asset)
// ... configure and read the first frame
}
// For each tagged pixel buffer:
if tags.contains(.stereoView(.leftEye)) { leftEye = nsImage }
else if tags.contains(.stereoView(.rightEye)) { rightEye = nsImage }Naming conventions
- Types use feature-and-role names:
MediaDetailView,MediaDetailViewModel,StereoView, andStereoViewModel. - State names describe lifecycle cases (
loading,ready,error) and attach only the data relevant to a case. - Commands use verbs and reveal intent:
readBufferFromAsset(at:),readNextBufferFromAsset(), andpublishState(_:). - Loader/helper names describe returned data:
presentationTimesFor(track:asset:)andloadVideoLayerIdsForTrack(_:). - Files match their primary type. The two file-private free functions stay beside the only consumer instead of creating a generic utility layer.
Architecture takeaways
- Use a small composition chain when a sample has a linear workflow; separate metadata validation from expensive decoding state.
- Give one object ownership of the mutable
AVAssetReaderpipeline so views issue intent rather than coordinate framework pieces. - Publish UI-consumed values on the main actor while keeping decode internals hidden.
- An enum state makes asynchronous loading and failure paths explicit without several competing Boolean flags.
- Protocol abstraction is unnecessary here because the app has one concrete decoding implementation and no test seam demonstrated in the source.
Source map
| Source file | Relevant symbols |
|---|---|
StereoViewer/StereoViewerApp.swift |
StereoViewerApp |
StereoViewer/LaunchView.swift |
LaunchView |
StereoViewer/MediaDetailView.swift |
MediaDetailView |
StereoViewer/MediaDetailViewModel.swift |
MediaDetailViewModel |
StereoViewer/StereoView.swift |
StereoView |
StereoViewer/StereoViewModel.swift |
StereoViewModelState, StereoViewModel, timestamp and layer-ID helpers |