Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2025 Photos & CameraAudio & Video

WWDC25 · 18 min · Photos & Camera / Audio & Video

Capture cinematic video in your app

Discover how the Cinematic Video API enables your app to effortlessly capture cinema-style videos. We’ll cover how to configure a Cinematic capture session and introduce the fundamentals of building a video capture UI. We’ll also explore advanced Cinematic features such as applying a depth of field effect to achieve both tracking and rack focus.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:00 — Introduction
  • 0:33 — Cinematic video
  • 3:44 — Build a great cinematic capture experience

Code shown on screen · 28 snippets

Select a video device swift · at 4:26 ↗
// Select a video device

let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualWideCamera], mediaType: .video, position: .back)
        
guard let camera = deviceDiscoverySession.devices.first else {
    print("Failed to find the capture device")
    return
}
Select a format that supports Cinematic Video capture swift · at 5:07 ↗
// Select a format that supports Cinematic Video capture

for format in camera.formats {

    if format.isCinematicVideoCaptureSupported {

       try! camera.lockForConfiguration()
       camera.activeFormat = format
       camera.unlockForConfiguration()

       break
    }

}
Select a microphone swift · at 5:51 ↗
// Select a microphone

let audioDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes [.microphone], mediaType: .audio, position: .unspecified)

guard let microphone = audioDeviceDiscoverySession.devices.first else {
    print("Failed to find a microphone")
    return
}
Add devices to input & add inputs to the capture session & enable Cinematic Video capture swift · at 6:00 ↗
// Add devices to inputs

let videoInput = try! AVCaptureDeviceInput(device: camera)
guard captureSession.canAddInput(videoInput) else {
    print("Can't add the video input to the session")
    return
}

let audioInput = try! AVCaptureDeviceInput(device: microphone)
guard captureSession.canAddInput(audioInput) else {
    print("Can't add the audio input to the session")
    return
}

// Add inputs to the capture session

captureSession.addInput(videoInput)
captureSession.addInput(audioInput)

// Enable Cinematic Video capture

if (videoInput.isCinematicVideoCaptureSupported) {
  videoInput.isCinematicVideoCaptureEnabled = true
}
Capture spatial audio swift · at 6:17 ↗
// Configure spatial audio

if audioInput.isMultichannelAudioModeSupported(.firstOrderAmbisonics) {
    audioInput.multichannelAudioMode = .firstOrderAmbisonics
}
Add outputs to the session & configure video stabilization & associate the preview layer with the capture session swift · at 6:33 ↗
// Add outputs to the session

let movieFileOutput = AVCaptureMovieFileOutput()
guard captureSession.canAddOutput(movieFileOutput) else {
    print("Can't add the movie file output to the session")
    return
}
captureSession.addOutput(movieFileOutput)
        

// Configure video stabilization

if let connection = movieFileOutput.connection(with: .video), 
    connection.isVideoStabilizationSupported {
    connection.preferredVideoStabilizationMode = .cinematicExtendedEnhanced
}

// Add a preview layer as the view finder

let previewLayer = AVCaptureVideoPreviewLayer()
previewLayer.session = captureSession
Display the preview layer with SwiftUI swift · at 7:11 ↗
// Display the preview layer with SwiftUI

struct CameraPreviewView: UIViewRepresentable {

    func makeUIView(context: Context) -> PreviewView {
        return PreviewView()
    }

    class CameraPreviewUIView: UIView {
	
			override class var layerClass: AnyClass {
    		AVCaptureVideoPreviewLayer.self
			}

			var previewLayer: AVCaptureVideoPreviewLayer {
  	  	layer as! AVCaptureVideoPreviewLayer
			}

			...
		}

...
}
Display the preview layer with SwiftUI swift · at 7:54 ↗
// Display the preview layer with SwiftUI

@MainActor
struct CameraView: View {       

    var body: some View {
        ZStack {
            CameraPreviewView()  
          	CameraControlsView()
        }
    }
}
Adjust bokeh strength with simulated aperture swift · at 8:05 ↗
// Adjust bokeh strength with simulated aperture


open class AVCaptureDeviceInput : AVCaptureInput {

	open var simulatedAperture: Float

	...

}
Find min, max, and default simulated aperture swift · at 8:40 ↗
// Adjust bokeh strength with simulated aperture


extension AVCaptureDeviceFormat {

	open var minSimulatedAperture: Float { get }

	open var maxSimulatedAperture: Float { get }

	open var defaultSimulatedAperture: Float { get }

	...

}
Add a metadata output swift · at 9:12 ↗
// Add a metadata output

let metadataOutput = AVCaptureMetadataOutput()

guard captureSession.canAddOutput(metadataOutput) else {
    print("Can't add the metadata output to the session")
    return
}
captureSession.addOutput(metadataOutput)

metadataOutput.metadataObjectTypes = metadataOutput.requiredMetadataObjectTypesForCinematicVideoCapture

metadataOutput.setMetadataObjectsDelegate(self, queue: sessionQueue)
Update the observed manager object swift · at 9:50 ↗
// Update the observed manager object

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {

   self.metadataManager.metadataObjects = metadataObjects

}

// Pass metadata to SwiftUI

@Observable
class CinematicMetadataManager {
    
    var metadataObjects: [AVMetadataObject] = []
    
}
Observe changes and update the view swift · at 10:12 ↗
// Observe changes and update the view

struct FocusOverlayView : View {

    var body: some View {

	        ForEach(
	      metadataManager.metadataObjects, id:\.objectID)
		  	{ metadataObject in

    		  rectangle(for: metadataObject)

			  }
		}
}
Make a rectangle for a metadata swift · at 10:18 ↗
// Make a rectangle for a metadata

private func rectangle(for metadata: AVMetadataObjects) -> some View {
    
    let transformedRect = previewLayer.layerRectConverted(fromMetadataOutputRect: metadata.bounds)
    
    return Rectangle()
        .frame(width:transformedRect.width,
               height:transformedRect.height)
        .position(
            x:transformedRect.midX,
            y:transformedRect.midY)
}
Focus methods swift · at 10:53 ↗
open func setCinematicVideoTrackingFocus(detectedObjectID: Int, focusMode: AVCaptureDevice.CinematicVideoFocusMode)

open func setCinematicVideoTrackingFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode)

open func setCinematicVideoFixedFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode)
Focus method 1 & CinematicVideoFocusMode swift · at 10:59 ↗
// Focus methods

open func setCinematicVideoTrackingFocus(detectedObjectID: Int, focusMode: AVCaptureDevice.CinematicVideoFocusMode)


public enum CinematicVideoFocusMode : Int, @unchecked Sendable {

    case none = 0

    case strong = 1

    case weak = 2
}

extension AVMetadataObject {

   open var cinematicVideoFocusMode: Int32 { get }

}
Focus method no.2 swift · at 12:19 ↗
// Focus method no.2

open func setCinematicVideoTrackingFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode)
Focus method no.3 swift · at 12:41 ↗
// Focus method no.3

open func setCinematicVideoFixedFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode)
Create the spatial tap gesture swift · at 13:54 ↗
var body: some View {

let spatialTapGesture = SpatialTapGesture()
    .onEnded { event in
        Task {
            await camera.focusTap(at: event.location)
        }
     }

...
}
Simulate a long press gesture with a drag gesture swift · at 14:15 ↗
@State private var pressLocation: CGPoint = .zero
@State private var isPressing = false
private let longPressDuration: TimeInterval = 0.3

var body: some View {
  
  ...
  
	let longPressGesture = DragGesture(minimumDistance: 0).onChanged { value in
		if !isPressing {
			isPressing = true
			pressLocation = value.location
			startLoopPressTimer()
		}
	}.onEnded { _ in
		isPressing = false
	}
  
	...
  
}

private func startLoopPressTimer() {
	DispatchQueue.main.asyncAfter(deadline: .now() + longPressDuration) {
		if isPressing {
			Task {
				await camera.focusLongPress(at: pressLocation)
			}
		}
	}
}
Create a rectangle view to receive gestures. swift · at 14:36 ↗
var body: some View {

let spatialTapGesture = ...
let longPressGesture = ...

ZStack {
  ForEach(
    metadataManager.metadataObjects,
    id:\.objectID)
  { metadataObject in

    rectangle(for: metadataObject)

  }
  Rectangle()
      .fill(Color.clear)
      .contentShape(Rectangle())
      .gesture(spatialTapGesture)
  .gesture(longPressGesture)}

  }
}
Create the rectangle view swift · at 15:03 ↗
private func rectangle(for metadata: AVMetadataObject) -> some View {
    
    let transformedRect = previewLayer.layerRectConverted(fromMetadataOutputRect: metadata.bounds)
    var color: Color
    var strokeStyle: StrokeStyle
    
    switch metadata.focusMode {
    case .weak:
        color = .yellow
        strokeStyle = StrokeStyle(lineWidth: 2, dash: [5,4])
    case .strong:
        color = .yellow
        strokeStyle = StrokeStyle(lineWidth: 2)
    case .none:
        color = .white
        strokeStyle = StrokeStyle(lineWidth: 2)
    }
    
    return Rectangle()
        .stroke(color, style: strokeStyle)
        .contentShape(Rectangle())
        .frame(width: transformedRect.width, height: transformedRect.height)
        .position(x: transformedRect.midX, 
                  y: transformedRect.midY)
}
Implement focusTap swift · at 15:30 ↗
func focusTap(at point:CGPoint) {
    
   try! camera.lockForConfiguration()
        
    if let metadataObject = findTappedMetadataObject(at: point) {
        if metadataObject.cinematicVideoFocusMode == .weak {
            camera.setCinematicVideoTrackingFocus(detectedObjectID: metadataObject.objectID, focusMode: .strong)
            
        }
        else {
            camera.setCinematicVideoTrackingFocus(detectedObjectID: metadataObject.objectID, focusMode: .weak)
        }
    }
    else {
        let transformedPoint = previewLayer.metadataOutputRectConverted(fromLayerRect: CGRect(origin:point, size:.zero)).origin
        camera.setCinematicVideoTrackingFocus(at: transformedPoint, focusMode: .weak)
    }
    
    camera.unlockForConfiguration()
}
Implement findTappedMetadataObject swift · at 15:42 ↗
private func findTappedMetadataObject(at point: CGPoint) -> AVMetadataObject? {
    
    var metadataObjectToReturn: AVMetadataObject?
    
    for metadataObject in metadataObjectsArray {
        let layerRect = previewLayer.layerRectConverted(fromMetadataOutputRect: metadataObject.bounds)
        if layerRect.contains(point) {
            metadataObjectToReturn = metadataObject
            break
        }
    }
    
    return metadataObjectToReturn
}
focusTap implementation continued swift · at 16:01 ↗
func focusTap(at point:CGPoint) {
    
   try! camera.lockForConfiguration()
        
    if let metadataObject = findTappedMetadataObject(at: point) {
        if metadataObject.cinematicVideoFocusMode == .weak {
            camera.setCinematicVideoTrackingFocus(detectedObjectID: metadataObject.objectID, focusMode: .strong)
            
        }
        else {
            camera.setCinematicVideoTrackingFocus(detectedObjectID: metadataObject.objectID, focusMode: .weak)
        }
    }
    else {
        let transformedPoint = previewLayer.metadataOutputRectConverted(fromLayerRect: CGRect(origin:point, size:.zero)).origin
        camera.setCinematicVideoTrackingFocus(at: transformedPoint, focusMode: .weak)
    }
    
    camera.unlockForConfiguration()
}
Implement focusLongPress swift · at 16:23 ↗
func focusLongPress(at point:CGPoint) {
    
   try! camera.lockForConfiguration()

   let transformedPoint = previewLayer.metadataOutputRectConverted(fromLayerRect:CGRect(origin: point, size: CGSizeZero)).origin
       camera.setCinematicVideoFixedFocus(at: pointInMetadataOutputSpace, focusMode: .strong)
   
    camera.unlockForConfiguration()
}
Introduce cinematicVideoCaptureSceneMonitoringStatuses swift · at 17:10 ↗
extension AVCaptureDevice {

   open var cinematicVideoCaptureSceneMonitoringStatuses: Set<AVCaptureSceneMonitoringStatus> { get }

}

extension AVCaptureSceneMonitoringStatus {

   public static let notEnoughLight: AVCaptureSceneMonitoringStatus

}
KVO handler for cinematicVideoCaptureSceneMonitoringStatuses swift · at 17:42 ↗
private var observation: NSKeyValueObservation?

observation = camera.observe(\.cinematicVideoCaptureSceneMonitoringStatuses, options: [.new, .old]) { _, value in
    
    if let newStatuses = value.newValue {
        if newStatuses.contains(.notEnoughLight) {
            // Update UI (e.g., "Not enough light")
        }
        else if newStatuses.count == 0 {
            // Back to normal.
        }
    }
}

Resources