AVMultiCamPiP: Capturing from Multiple Cameras
At a glance
| Item |
Summary |
| Purpose |
Preview front and back cameras simultaneously and record a Metal-composited picture-in-picture movie. |
| App architecture |
Controller-centered UIKit app that manually wires a multi-camera capture graph, delegates mixing to PiPVideoMixer, and delegates writing to MovieRecorder. |
| Main patterns |
Manual graph construction, queue confinement, compositor object, recorder facade, progressive cost reduction. |
| Project style |
Flat target with a large camera controller and small concrete preview, mixer, recorder, and utility files. |
Project structure
AVMultiCamPiP/
├── CameraViewController.swift # Session graph, UI, routing, recording coordination
├── PiPVideoMixer.swift # Metal compositing
├── PiPMixer.metal # Compute kernel
├── MovieRecorder.swift # AVAssetWriter wrapper
├── PreviewView.swift # Preview-layer-backed UIView
├── Utilities.swift
├── Base.lproj/Main.storyboard
└── AppDelegate.swift
Structure observations
- The controller owns front/back video and microphone endpoints and their explicit connections.
- Rendering and file writing are extracted into concrete helper objects, but there is no app-defined protocol abstraction.
- Front/back preview views are storyboard-owned; recording uses mixed buffers rather than recording either preview layer.
Overall architecture
flowchart LR
Controller["ViewController"] --> Session["AVCaptureMultiCamSession"]
Session --> Front["Front video output"]
Session --> Back["Back video output"]
Session --> Audio["Front/back audio outputs"]
Front --> Router["Sample routing"]
Back --> Router
Router --> Mixer["PiPVideoMixer"]
Mixer --> Recorder["MovieRecorder"]
Audio --> Recorder
Recorder --> Writer["AVAssetWriter"]
Writer --> Photos["PhotoKit save"]
Reference code
AVMultiCamPiP/CameraViewController.swift:869 — frame routing and composition
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
if let videoDataOutput = output as? AVCaptureVideoDataOutput {
processVideoSampleBuffer(sampleBuffer, fromOutput: videoDataOutput)
} else if let audioDataOutput = output as? AVCaptureAudioDataOutput {
processsAudioSampleBuffer(sampleBuffer, fromOutput: audioDataOutput)
}
}
Interpretation
The multi-camera session produces independent streams. The controller classifies each frame as fullscreen or PiP, retains the latest PiP frame, and asks the mixer to composite when a fullscreen frame arrives. Only mixed video and the selected microphone stream reach the recorder.
Ownership and state
classDiagram
ViewController *-- AVCaptureMultiCamSession : creates
ViewController *-- AVCaptureVideoDataOutput : front and back
ViewController *-- AVCaptureAudioDataOutput : two connections
ViewController *-- PiPVideoMixer : creates
ViewController *-- MovieRecorder : creates per recording
ViewController o-- PreviewView : storyboard outlets
MovieRecorder *-- AVAssetWriter : creates per movie
MovieRecorder *-- AVAssetWriterInput : audio and video
Ownership evidence
AVMultiCamPiP/CameraViewController.swift:272 and :702 — core owned objects
private let session = AVCaptureMultiCamSession()
private let dataOutputQueue = DispatchQueue(label: "data output queue")
private var movieRecorder: MovieRecorder?
private var currentPiPSampleBuffer: CMSampleBuffer?
private var videoMixer = PiPVideoMixer()
| Owner |
Object or state |
Relationship |
Mutation authority |
ViewController |
Session, input/output objects, connection state |
Creates and manually wires the capture graph |
Controller on sessionQueue. |
ViewController |
PiP position, last PiP sample, mixer, recorder |
Retains composition/recording coordination state |
Controller on dataOutputQueue. |
PiPVideoMixer |
Metal pipeline, texture cache, output pool |
Creates and retains GPU resources |
Mixer methods. |
MovieRecorder |
Asset writer and writer inputs |
Creates per recording and appends samples |
Recorder methods called on data-output queue. |
| Storyboard |
Preview views |
Injects outlets; controller keeps weak layer references |
UIKit owns views/layers. |
Class and protocol design
No app-defined protocol separates the three main concrete collaborators. The controller conforms directly to AVFoundation’s video and audio sample-buffer delegate protocols.
| Type |
Responsibility |
Depends on |
ViewController |
Build capture graph, route samples, coordinate UI/recording, reduce session cost |
UIKit, AVFoundation, mixer, recorder, PhotoKit |
PiPVideoMixer |
Composite fullscreen and PiP buffers into a pooled output buffer |
Metal/Core Video |
MovieRecorder |
Convert audio/video settings into a real-time AVAssetWriter session |
AVFoundation |
PreviewView |
Expose an AVCaptureVideoPreviewLayer backing layer |
UIKit, AVFoundation |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Session, queues, outputs, mixer, recorder, and routing state |
private |
Only the controller can reconfigure and route the graph |
Keeps queue-affine capture state within its coordinator. |
backCameraDeviceInput |
private(set) plus @objc dynamic |
KVO can observe it, but only the controller assigns it |
Supports pressure observation without external mutation. |
MovieRecorder.isRecording |
private(set) |
Controller can choose start/stop UI state, but recorder controls transitions |
Preserves writer lifecycle consistency. |
| Preview-layer references |
private weak |
Controller doesn’t extend layer lifetime |
Storyboard view remains the owner. |
| Main classes |
implicit internal |
Available only inside the app module |
No reusable framework API is declared. |
Reference code
AVMultiCamPiP/MovieRecorder.swift:13 — recorder boundary
private var assetWriter: AVAssetWriter?
private var assetWriterVideoInput: AVAssetWriterInput?
private var assetWriterAudioInput: AVAssetWriterInput?
private(set) var isRecording = false
No public, open, or fileprivate declaration appears in the Swift source.
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Front/back/microphone graph construction |
ViewController private setup methods |
Requires shared session and explicit ports/connections. |
| Fullscreen/PiP stream classification |
ViewController.processVideoSampleBuffer |
Depends on current UI-selected PiP device. |
| Pixel compositing |
PiPVideoMixer |
Encapsulates Metal resources and buffer allocation. |
| Movie writer lifecycle/sample append |
MovieRecorder |
Hides asset writer/input state from UI code. |
| Session cost degradation |
ViewController.checkSystemCost and helpers |
Reconfigures session/device formats in priority order. |
| Preview backing layer |
PreviewView |
Minimal UIKit/AVFoundation view concern. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Manual capture graph |
AVMultiCamPiP/CameraViewController.swift:314 and addInputWithNoConnections setup |
Gives exact front/back preview/data connection control required by MultiCam. |
| Queue confinement |
AVMultiCamPiP/CameraViewController.swift:276 and line 278 |
Separates blocking graph mutation from realtime sample work. |
| Compositor object |
AVMultiCamPiP/PiPVideoMixer.swift:11 |
Keeps GPU pipeline and buffer-pool details out of controller flow. |
| Recorder facade |
AVMultiCamPiP/MovieRecorder.swift:33 |
Presents start/stop/append operations over AVAssetWriter. |
| Progressive fallback chain |
AVMultiCamPiP/CameraViewController.swift:1004 and checkSystemCost() |
Reduces resolution, ports, or frame rate until capture costs fit. |
Naming conventions
- Types: responsibility nouns (
PiPVideoMixer, MovieRecorder, PreviewView); the main type uses generic ViewController despite the filename’s camera context.
- Methods: configuration names identify device side (
configureBackCamera, configureFrontCamera) and processing stage (processFullScreenSampleBuffer).
- Properties:
front... and back... prefixes make parallel graph endpoints explicit.
- State: Boolean names use
is... or ...Enabled; queue names state their affinity.
- Files: one concrete helper per file; Metal kernel separated by implementation technology.
Architecture takeaways
- MultiCam requires explicit capture-port/connection ownership, which naturally concentrates graph coordination.
- Realtime routing, mixing, and recording share one data-output queue, making ordering assumptions visible.
- A small recorder wrapper prevents asset-writer lifecycle details from leaking through the controller.
private(set) exposes status for coordination while preserving the component that may transition it.
- The design is concrete and sample-focused; it does not introduce protocols where only one implementation exists.
Source map
| Source file |
Relevant symbols |
AVMultiCamPiP/CameraViewController.swift |
Session graph, routing, recording and cost coordination |
AVMultiCamPiP/PiPVideoMixer.swift |
PiPVideoMixer and Metal composition |
AVMultiCamPiP/MovieRecorder.swift |
MovieRecorder, AVAssetWriter lifecycle |
AVMultiCamPiP/PreviewView.swift |
Preview-layer-backed view |
AVMultiCamPiP/Utilities.swift |
Orientation/session helpers |