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 ↗Chapters
Code shown on screen · 28 snippets
Select a video device
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// Display the preview layer with SwiftUI
struct CameraView: View {
var body: some View {
ZStack {
CameraPreviewView()
CameraControlsView()
}
}
} Adjust bokeh strength with simulated aperture
// Adjust bokeh strength with simulated aperture
open class AVCaptureDeviceInput : AVCaptureInput {
open var simulatedAperture: Float
...
} Find min, max, and default simulated aperture
// 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
// 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
// Update the observed manager object
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
self.metadataManager.metadataObjects = metadataObjects
}
// Pass metadata to SwiftUI
class CinematicMetadataManager {
var metadataObjects: [AVMetadataObject] = []
} Observe changes and update the view
// 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
// 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
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
// 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
// Focus method no.2
open func setCinematicVideoTrackingFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode) Focus method no.3
// Focus method no.3
open func setCinematicVideoFixedFocus(at point: CGPoint, focusMode: AVCaptureDevice.CinematicVideoFocusMode) Create the spatial tap gesture
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
private var pressLocation: CGPoint = .zero
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.
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
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
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
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
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
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
extension AVCaptureDevice {
open var cinematicVideoCaptureSceneMonitoringStatuses: Set<AVCaptureSceneMonitoringStatus> { get }
}
extension AVCaptureSceneMonitoringStatus {
public static let notEnoughLight: AVCaptureSceneMonitoringStatus
} KVO handler for cinematicVideoCaptureSceneMonitoringStatuses
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
Related sessions
-
19 min -
23 min -
35 min -
29 min -
33 min