AVCamFilter: Applying filters to a capture stream
At a glance
| Item |
Summary |
| Purpose |
Apply Core Image or Metal rose filters and optional depth visualization to live preview and captured photos. |
| App architecture |
Controller-centered UIKit capture app with pluggable renderer objects and a custom Metal preview. |
| Main patterns |
Renderer strategy protocol, delegate-driven media pipeline, queue confinement, prepare/render/reset lifecycle. |
| Project style |
Flat camera target with the main controller, renderer classes, a Metal preview, shaders, and a small Objective-C bridge utility. |
Project structure
AVCamFilter/
├── CameraViewController.swift # Capture graph, UI state, frame/photo coordination
├── FilterRenderer.swift # Renderer capability and buffer-pool helpers
├── RosyMetalRenderer.swift
├── RosyCIRenderer.swift
├── DepthToGrayscaleConverter.swift
├── VideoMixer.swift
├── PreviewMetalView.swift
├── Shaders/ # Metal kernels
├── minMaxFromBuffer.{h,m} # Bridged depth utility
├── Base.lproj/Main.storyboard
└── AppDelegate.swift
Structure observations
- Capture and screen coordination remain in a large
CameraViewController; GPU/Core Image algorithms are separate concrete objects.
- Each renderer owns its own buffer pool, format descriptions, and framework resources.
- Live and photo paths use separate renderer/mixer instances, avoiding shared mutable GPU preparation state across those paths.
Overall architecture
flowchart LR
UI["CameraViewController"] --> Session["AVCaptureSession"]
Session --> Video["Video data output"]
Session --> Depth["Depth data output"]
Session --> Photo["Photo output"]
Video --> Filter["FilterRenderer strategy"]
Depth --> Converter["DepthToGrayscaleConverter"]
Filter --> Mixer["VideoMixer"]
Converter --> Mixer
Mixer --> Preview["PreviewMetalView"]
Photo --> PhotoPipeline["Photo filter + depth mixer"]
PhotoPipeline --> Photos["PhotoKit"]
Reference code
AVCamFilter/CameraViewController.swift:1040 — live-frame pipeline
var finalVideoPixelBuffer = videoPixelBuffer
if let filter = videoFilter {
filter.prepare(with: formatDescription, outputRetainedBufferCountHint: 3)
finalVideoPixelBuffer = filter.render(pixelBuffer: finalVideoPixelBuffer)!
}
// The optional depth mixer updates finalVideoPixelBuffer.
previewView.pixelBuffer = finalVideoPixelBuffer
Interpretation
AVFoundation delivers frames to the controller, which chooses and sequences renderer objects before handing the final buffer to a custom Metal view. The forced unwrap above abbreviates the source’s guarded result; the source handles failure before assignment.
Ownership and state
classDiagram
CameraViewController *-- AVCaptureSession : creates
CameraViewController *-- AVCaptureVideoDataOutput : creates
CameraViewController *-- AVCaptureDepthDataOutput : creates
CameraViewController *-- AVCapturePhotoOutput : creates
CameraViewController *-- RosyMetalRenderer : live and photo instances
CameraViewController *-- RosyCIRenderer : live and photo instances
CameraViewController *-- DepthToGrayscaleConverter : live and photo instances
CameraViewController *-- VideoMixer : live and photo instances
CameraViewController o-- PreviewMetalView : storyboard outlet
Ownership evidence
AVCamFilter/CameraViewController.swift:62 — retained pipeline objects
private let session = AVCaptureSession()
private let videoDataOutput = AVCaptureVideoDataOutput()
private let depthDataOutput = AVCaptureDepthDataOutput()
private let photoOutput = AVCapturePhotoOutput()
private let filterRenderers: [FilterRenderer] = [RosyMetalRenderer(), RosyCIRenderer()]
private let videoDepthMixer = VideoMixer()
private let videoDepthConverter = DepthToGrayscaleConverter()
| Owner |
Object or state |
Relationship |
Mutation authority |
CameraViewController |
Session, outputs, queues, active renderer selection |
Creates and retains the entire capture/processing graph |
Controller, dispatched to the appropriate queue. |
| Each renderer |
Buffer pool, format descriptions, Metal/Core Image resources |
Creates resources during prepare, releases during reset |
Concrete renderer only. |
PreviewMetalView |
Current buffer and Metal presentation resources |
Receives buffers; retains synchronized internal copies |
View, guarded by syncQueue. |
| Storyboard |
PreviewMetalView and controls |
Injects outlets into controller |
UIKit lifecycle owns view instances. |
Class and protocol design
classDiagram
class FilterRenderer {
<<protocol>>
prepare()
render()
reset()
}
RosyMetalRenderer ..|> FilterRenderer
RosyCIRenderer ..|> FilterRenderer
DepthToGrayscaleConverter ..|> FilterRenderer
CameraViewController --> FilterRenderer : selects
CameraViewController --> VideoMixer : composes depth
CameraViewController --> PreviewMetalView : presents final buffer
Reference code
AVCamFilter/FilterRenderer.swift:10 — renderer capability
protocol FilterRenderer: AnyObject {
var isPrepared: Bool { get }
func prepare(with inputFormatDescription: CMFormatDescription,
outputRetainedBufferCountHint: Int)
func reset()
func render(pixelBuffer: CVPixelBuffer) -> CVPixelBuffer?
}
| Type |
Responsibility |
Depends on |
CameraViewController |
Capture setup, feature selection, delegate processing, save flow |
AVFoundation, renderer/mixer objects, UIKit, PhotoKit |
FilterRenderer |
Normalize lifecycle and pixel-buffer transformation |
Core Media/Core Video |
RosyMetalRenderer |
Compute-shader rose filter |
Metal |
RosyCIRenderer |
Core Image rose filter |
Core Image |
DepthToGrayscaleConverter |
Convert depth to a grayscale pixel buffer |
Metal and Objective-C min/max helper |
VideoMixer |
Blend video and processed depth buffers |
Metal |
PreviewMetalView |
Present final buffers with rotation/mirroring |
MetalKit |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Controller outlets, session, outputs, queues, and renderer instances |
private |
Other files cannot rewire the capture or processing graph |
Centralizes order-sensitive mutation in the controller. |
| Renderer input/output format descriptions |
private(set) |
Pipeline code may inspect formats but only the renderer updates them |
Keeps prepare/reset invariants local. |
| Renderer GPU resources and buffer pools |
private |
Only the concrete algorithm accesses its resources |
Prevents invalid cross-renderer sharing. |
FilterRenderer and concrete types |
implicit internal |
Usable within the app module only |
The protocol supports app-local strategies, not a framework API. |
Reference code
AVCamFilter/RosyMetalRenderer.swift:18 — externally readable renderer state
private(set) var inputFormatDescription: CMFormatDescription?
private(set) var outputFormatDescription: CMFormatDescription?
private var outputPixelBufferPool: CVPixelBufferPool?
private var computePipelineState: MTLComputePipelineState?
No public, open, or fileprivate declaration appears in the Swift source.
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Session/device lifecycle and feature toggles |
CameraViewController |
Coordinates storyboard controls with capture reconfiguration. |
| Live/video sequencing |
CameraViewController.processVideo |
Chooses active strategy and optional depth mix for each frame. |
| Photo processing/save |
Photo delegate in CameraViewController |
Coordinates captured photo, renderer instances, depth, JPEG, and PhotoKit. |
| Filter algorithms/resources |
Renderer types |
Swappable implementations behind one protocol. |
| Display transforms and draw loop |
PreviewMetalView |
Presentation-specific Metal state stays inside the view. |
| Shared pixel-buffer allocation |
FilterRenderer.swift free functions |
Reused by multiple concrete renderers. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Strategy |
AVCamFilter/FilterRenderer.swift:10 and renderer arrays at AVCamFilter/CameraViewController.swift:81 |
Switches Core Image and Metal implementations through one contract. |
| Explicit renderer lifecycle |
AVCamFilter/FilterRenderer.swift:16 |
Makes expensive preparation and reset visible; callers must uphold ordering. |
| Delegate-driven pipeline |
AVCamFilter/CameraViewController.swift:14 and output callbacks at line 1026 |
Accepts video, depth, synchronized, and photo deliveries. |
| Queue confinement |
AVCamFilter/CameraViewController.swift:67, line 71, and line 105 |
Separates session mutation, realtime data, and photo processing work. |
| Adapter/view bridge |
AVCamFilter/PreviewMetalView.swift:12 |
Replaces the standard preview layer so the app can transform frames before display. |
Naming conventions
- Types: operation-plus-role names (
DepthToGrayscaleConverter, RosyMetalRenderer, VideoMixer).
- Protocol: capability role
FilterRenderer; concrete types name technology where alternatives matter (Metal, CI).
- Methods: lifecycle verbs (
prepare, render, reset) and event processing (processVideo, processDepth).
- State:
...Enabled, ...On, and isPrepared make Boolean scope explicit.
- Files: one renderer or visual component per file; shader filenames match their effects.
Architecture takeaways
- Keep capture coordination concrete while moving expensive transformation algorithms behind a small renderer protocol.
- Separate live and photo renderer instances when preparation state and buffer pools must not race.
- Use a custom render view when frames need modification before presentation.
private(set) is useful for renderer lifecycle state that callers inspect but must not mutate.
- The controller remains large; the source optimizes sample visibility over a fully layered application architecture.
Source map
| Source file |
Relevant symbols |
AVCamFilter/CameraViewController.swift |
Capture ownership, delegates, live/photo pipelines |
AVCamFilter/FilterRenderer.swift |
FilterRenderer, allocation helpers |
AVCamFilter/RosyMetalRenderer.swift |
Metal strategy |
AVCamFilter/RosyCIRenderer.swift |
Core Image strategy |
AVCamFilter/DepthToGrayscaleConverter.swift |
Depth conversion strategy |
AVCamFilter/VideoMixer.swift |
Depth/video composition |
AVCamFilter/PreviewMetalView.swift |
Metal preview and transforms |