--- name: axiom-camera-capture-ref description: Reference — AVCaptureSession, AVCapturePhotoSettings, AVCapturePhotoOutput, RotationCoordinator, photoQualityPrioritization, deferred processing, AVCaptureMovieFileOutput, session presets, capture device APIs license: MIT metadata: version: "1.0.0" --- # Camera Capture API Reference ## Quick Reference ```swift // SESSION SETUP import AVFoundation let session = AVCaptureSession() let sessionQueue = DispatchQueue(label: "camera.session") sessionQueue.async { session.beginConfiguration() session.sessionPreset = .photo guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), let input = try? AVCaptureDeviceInput(device: camera), session.canAddInput(input) else { return } session.addInput(input) let photoOutput = AVCapturePhotoOutput() if session.canAddOutput(photoOutput) { session.addOutput(photoOutput) } session.commitConfiguration() session.startRunning() } // CAPTURE PHOTO var settings = AVCapturePhotoSettings() settings.photoQualityPrioritization = .balanced photoOutput.capturePhoto(with: settings, delegate: self) // ROTATION (iOS 17+) let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: previewLayer) previewLayer.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview ``` --- ## AVCaptureSession Central coordinator for capture data flow. ### Session Presets | Preset | Resolution | Use Case | |--------|------------|----------| | `.photo` | Optimal for photos | Photo capture | | `.high` | Highest device quality | Video recording | | `.medium` | VGA quality | Preview, lower storage | | `.low` | CIF quality | Minimal storage | | `.hd1280x720` | 720p | HD video | | `.hd1920x1080` | 1080p | Full HD video | | `.hd4K3840x2160` | 4K | Ultra HD video | | `.inputPriority` | Use device format | Custom configuration | ### Session Configuration ```swift // Batch configuration (atomic) session.beginConfiguration() defer { session.commitConfiguration() } // Check preset support if session.canSetSessionPreset(.hd4K3840x2160) { session.sessionPreset = .hd4K3840x2160 } // Add input/output if session.canAddInput(input) { session.addInput(input) } if session.canAddOutput(output) { session.addOutput(output) } ``` ### Session Lifecycle ```swift // Start (ALWAYS on background queue) sessionQueue.async { session.startRunning() // Blocking call } // Stop sessionQueue.async { session.stopRunning() } // Check state session.isRunning // true/false session.isInterrupted // true during phone calls, etc. ``` ### Session Notifications ```swift // Session started NotificationCenter.default.addObserver( forName: .AVCaptureSessionDidStartRunning, object: session, queue: .main) { _ in } // Session stopped NotificationCenter.default.addObserver( forName: .AVCaptureSessionDidStopRunning, object: session, queue: .main) { _ in } // Session interrupted (phone call, etc.) NotificationCenter.default.addObserver( forName: .AVCaptureSessionWasInterrupted, object: session, queue: .main) { notification in let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int } // Interruption ended NotificationCenter.default.addObserver( forName: .AVCaptureSessionInterruptionEnded, object: session, queue: .main) { _ in } // Runtime error NotificationCenter.default.addObserver( forName: .AVCaptureSessionRuntimeError, object: session, queue: .main) { notification in let error = notification.userInfo?[AVCaptureSessionErrorKey] as? Error } ``` ### Interruption Reasons | Reason | Value | Cause | |--------|-------|-------| | `.videoDeviceNotAvailableInBackground` | 1 | App went to background | | `.audioDeviceInUseByAnotherClient` | 2 | Another app using audio | | `.videoDeviceInUseByAnotherClient` | 3 | Another app using camera | | `.videoDeviceNotAvailableWithMultipleForegroundApps` | 4 | Split View (iPad) | | `.videoDeviceNotAvailableDueToSystemPressure` | 5 | Thermal throttling | --- ## AVCaptureDevice Represents a physical capture device (camera, microphone). ### Getting Devices ```swift // Default back camera AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) // Default front camera AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) // Default microphone AVCaptureDevice.default(for: .audio) // Discovery session for all cameras let discoverySession = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera], mediaType: .video, position: .unspecified ) let cameras = discoverySession.devices ``` ### Device Types | Type | Description | |------|-------------| | `.builtInWideAngleCamera` | Standard camera (1x) | | `.builtInUltraWideCamera` | Ultra-wide camera (0.5x) | | `.builtInTelephotoCamera` | Telephoto camera (2x, 3x) | | `.builtInDualCamera` | Wide + telephoto | | `.builtInDualWideCamera` | Wide + ultra-wide | | `.builtInTripleCamera` | Wide + ultra-wide + telephoto | | `.builtInTrueDepthCamera` | Front TrueDepth (Face ID) | | `.builtInLiDARDepthCamera` | LiDAR depth | ### Device Configuration ```swift do { try device.lockForConfiguration() defer { device.unlockForConfiguration() } // Focus if device.isFocusModeSupported(.continuousAutoFocus) { device.focusMode = .continuousAutoFocus } // Exposure if device.isExposureModeSupported(.continuousAutoExposure) { device.exposureMode = .continuousAutoExposure } // Torch (flashlight) if device.hasTorch && device.isTorchModeSupported(.on) { device.torchMode = .on } // Zoom device.videoZoomFactor = 2.0 // 2x zoom } catch { print("Failed to configure device: \(error)") } ``` ### Switching Cameras ```swift // Switch between front and back during active session func switchCamera() { sessionQueue.async { [self] in session.beginConfiguration() defer { session.commitConfiguration() } // Remove current camera input if let currentInput = session.inputs.first(where: { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.video) == true }) as? AVCaptureDeviceInput { session.removeInput(currentInput) // Get opposite camera let newPosition: AVCaptureDevice.Position = currentInput.device.position == .back ? .front : .back guard let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: newPosition), let newInput = try? AVCaptureDeviceInput(device: newDevice) else { return } if session.canAddInput(newInput) { session.addInput(newInput) } } } } ``` **Important**: Always switch on the session queue, within beginConfiguration/commitConfiguration. ### Authorization ```swift // Check status let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: break case .notDetermined: await AVCaptureDevice.requestAccess(for: .video) case .denied, .restricted: // Show settings prompt @unknown default: break } ``` --- ## AVCaptureDevice.RotationCoordinator (iOS 17+) Automatically tracks device orientation and provides rotation angles. ### Setup ```swift // Create with device and preview layer let coordinator = AVCaptureDevice.RotationCoordinator( device: captureDevice, previewLayer: previewLayer ) ``` ### Properties | Property | Type | Description | |----------|------|-------------| | `videoRotationAngleForHorizonLevelPreview` | CGFloat | Rotation for preview layer | | `videoRotationAngleForHorizonLevelCapture` | CGFloat | Rotation for captured output | ### Observation ```swift // KVO observation for preview updates let observation = coordinator.observe( \.videoRotationAngleForHorizonLevelPreview, options: [.new] ) { [weak previewLayer] coordinator, _ in DispatchQueue.main.async { previewLayer?.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview } } // Set initial value previewLayer.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview ``` ### Applying to Capture ```swift func capturePhoto() { if let connection = photoOutput.connection(with: .video) { connection.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelCapture } photoOutput.capturePhoto(with: settings, delegate: self) } ``` --- ## AVCapturePhotoOutput Output for capturing still photos. ### Configuration ```swift let photoOutput = AVCapturePhotoOutput() // High resolution photoOutput.isHighResolutionCaptureEnabled = true // Max quality prioritization photoOutput.maxPhotoQualityPrioritization = .quality // Deferred processing (iOS 17+) photoOutput.isAutoDeferredPhotoDeliveryEnabled = true // Live Photo photoOutput.isLivePhotoCaptureEnabled = true // Depth photoOutput.isDepthDataDeliveryEnabled = true // Portrait Effects Matte photoOutput.isPortraitEffectsMatteDeliveryEnabled = true ``` ### Supported Features ```swift // Check support before enabling photoOutput.isHighResolutionCaptureEnabled && photoOutput.isHighResolutionCaptureSupported photoOutput.isLivePhotoCaptureSupported photoOutput.isDepthDataDeliverySupported photoOutput.isPortraitEffectsMatteDeliverySupported photoOutput.maxPhotoQualityPrioritization // .speed, .balanced, .quality ``` ### Responsive Capture APIs (iOS 17+) ```swift // Zero Shutter Lag - uses ring buffer for instant capture photoOutput.isZeroShutterLagSupported photoOutput.isZeroShutterLagEnabled // true by default for iOS 17+ apps // Responsive Capture - overlapping captures photoOutput.isResponsiveCaptureSupported photoOutput.isResponsiveCaptureEnabled // Fast Capture Prioritization - adapts quality for burst-like capture photoOutput.isFastCapturePrioritizationSupported photoOutput.isFastCapturePrioritizationEnabled // Deferred Processing - proxy + background processing photoOutput.isAutoDeferredPhotoDeliverySupported photoOutput.isAutoDeferredPhotoDeliveryEnabled ``` --- ## AVCapturePhotoOutputReadinessCoordinator (iOS 17+) Provides synchronous shutter button state updates. ### Setup ```swift let coordinator = AVCapturePhotoOutputReadinessCoordinator(photoOutput: photoOutput) coordinator.delegate = self ``` ### Tracking Captures ```swift // Call BEFORE capturePhoto() coordinator.startTrackingCaptureRequest(using: settings) photoOutput.capturePhoto(with: settings, delegate: self) ``` ### Delegate ```swift func readinessCoordinator(_ coordinator: AVCapturePhotoOutputReadinessCoordinator, captureReadinessDidChange captureReadiness: AVCapturePhotoOutput.CaptureReadiness) { switch captureReadiness { case .ready: // Can capture immediately case .notReadyMomentarily: // Brief delay, prevent double-tap case .notReadyWaitingForCapture: // Flash firing, sensor reading case .notReadyWaitingForProcessing: // Processing previous photo case .sessionNotRunning: // Session stopped @unknown default: break } } ``` --- ## AVCapturePhotoSettings Configuration for a single photo capture. ### Basic Settings ```swift // Standard JPEG var settings = AVCapturePhotoSettings() // HEIF format settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc]) // RAW settings = AVCapturePhotoSettings(rawPixelFormatType: kCVPixelFormatType_14Bayer_BGGR) // RAW + JPEG settings = AVCapturePhotoSettings( rawPixelFormatType: kCVPixelFormatType_14Bayer_BGGR, processedFormat: [AVVideoCodecKey: AVVideoCodecType.jpeg] ) ``` ### Quality Prioritization | Value | Speed | Quality | Use Case | |-------|-------|---------|----------| | `.speed` | Fastest | Lower | Social sharing, rapid capture | | `.balanced` | Medium | Good | General photography | | `.quality` | Slowest | Best | Professional, documents | ```swift settings.photoQualityPrioritization = .speed ``` ### Flash ```swift settings.flashMode = .auto // .off, .on, .auto ``` ### Apple ProRAW and HDR ```swift // Check ProRAW support if photoOutput.isAppleProRAWSupported { photoOutput.isAppleProRAWEnabled = true // Capture ProRAW let query = photoOutput.isAppleProRAWEnabled ? AVCapturePhotoOutput.AppleProRAWQuery(photoOutput) : nil if let rawType = query?.availableRawPixelFormatTypes.first { let settings = AVCapturePhotoSettings( rawPixelFormatType: rawType, processedFormat: [AVVideoCodecKey: AVVideoCodecType.hevc] ) } } // HDR configuration settings.photoQualityPrioritization = .quality // Enables computational photography/HDR // HDR is automatic with .balanced or .quality — no separate toggle needed ``` **Note**: ProRAW requires iPhone 12 Pro or later. HDR is automatic with quality prioritization — Apple's Deep Fusion and Smart HDR are controlled by the system based on the quality setting. ### Resolution ```swift // High resolution still image settings.isHighResolutionPhotoEnabled = true // Max dimensions (limit resolution) settings.maxPhotoDimensions = CMVideoDimensions(width: 4032, height: 3024) ``` ### Preview/Thumbnail ```swift // Preview for immediate display settings.previewPhotoFormat = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ] // Thumbnail settings.embeddedThumbnailPhotoFormat = [ AVVideoCodecKey: AVVideoCodecType.jpeg, AVVideoWidthKey: 160, AVVideoHeightKey: 120 ] ``` ### Important Notes ```swift // Settings cannot be reused // Each capture needs a NEW settings instance let settings1 = AVCapturePhotoSettings() // Use once let settings2 = AVCapturePhotoSettings() // Use for second capture // Copy settings for similar captures let settings2 = AVCapturePhotoSettings(from: settings1) ``` --- ## AVCapturePhotoCaptureDelegate Delegate for photo capture events. ```swift extension CameraManager: AVCapturePhotoCaptureDelegate { // Photo capture will begin func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) { // Show shutter animation } // Photo capture finished func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard error == nil else { print("Capture error: \(error!)") return } // Get JPEG data if let data = photo.fileDataRepresentation() { savePhoto(data) } // Or get raw pixel buffer if let pixelBuffer = photo.pixelBuffer { processBuffer(pixelBuffer) } } // Deferred processing proxy (iOS 17+) func photoOutput(_ output: AVCapturePhotoOutput, didFinishCapturingDeferredPhotoProxy deferredPhotoProxy: AVCaptureDeferredPhotoProxy, error: Error?) { guard error == nil, let data = deferredPhotoProxy.fileDataRepresentation() else { return } replaceThumbnailWithFinal(data) } } ``` --- ## AVCaptureMovieFileOutput Output for recording video to file. ### Setup ```swift let movieOutput = AVCaptureMovieFileOutput() if session.canAddOutput(movieOutput) { session.addOutput(movieOutput) } // Add audio input if let microphone = AVCaptureDevice.default(for: .audio), let audioInput = try? AVCaptureDeviceInput(device: microphone), session.canAddInput(audioInput) { session.addInput(audioInput) } ``` ### Recording ```swift // Start recording let outputURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("mov") // Apply rotation if let connection = movieOutput.connection(with: .video) { connection.videoRotationAngle = rotationCoordinator.videoRotationAngleForHorizonLevelCapture } movieOutput.startRecording(to: outputURL, recordingDelegate: self) // Stop recording movieOutput.stopRecording() // Check state movieOutput.isRecording movieOutput.recordedDuration movieOutput.recordedFileSize ``` ### Delegate ```swift extension CameraManager: AVCaptureFileOutputRecordingDelegate { func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { // Recording started } func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { if let error = error { print("Recording failed: \(error)") return } // Video saved to outputFileURL saveToPhotoLibrary(outputFileURL) } } ``` --- ## AVCaptureVideoPreviewLayer Layer for displaying camera preview. ### Setup ```swift let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill previewLayer.frame = view.bounds view.layer.addSublayer(previewLayer) ``` ### Video Gravity | Value | Behavior | |-------|----------| | `.resizeAspect` | Fit entire image, may letterbox | | `.resizeAspectFill` | Fill layer, may crop edges | | `.resize` | Stretch to fill (distorts) | ### SwiftUI Integration ```swift struct CameraPreview: UIViewRepresentable { let session: AVCaptureSession func makeUIView(context: Context) -> PreviewView { let view = PreviewView() view.previewLayer.session = session view.previewLayer.videoGravity = .resizeAspectFill return view } func updateUIView(_ uiView: PreviewView, context: Context) {} class PreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } } } ``` --- ## Common Code Patterns ### Complete Camera Manager ```swift import AVFoundation @MainActor class CameraManager: NSObject, ObservableObject { let session = AVCaptureSession() let photoOutput = AVCapturePhotoOutput() private let sessionQueue = DispatchQueue(label: "camera.session") private var rotationCoordinator: AVCaptureDevice.RotationCoordinator? private var rotationObservation: NSKeyValueObservation? @Published var isSessionRunning = false func setup() async -> Bool { guard await AVCaptureDevice.requestAccess(for: .video) else { return false } return await withCheckedContinuation { continuation in sessionQueue.async { [self] in session.beginConfiguration() defer { session.commitConfiguration() } session.sessionPreset = .photo guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), let input = try? AVCaptureDeviceInput(device: camera), session.canAddInput(input) else { continuation.resume(returning: false) return } session.addInput(input) guard session.canAddOutput(photoOutput) else { continuation.resume(returning: false) return } session.addOutput(photoOutput) photoOutput.maxPhotoQualityPrioritization = .quality continuation.resume(returning: true) } } } func start() { sessionQueue.async { [self] in session.startRunning() DispatchQueue.main.async { self.isSessionRunning = self.session.isRunning } } } func stop() { sessionQueue.async { [self] in session.stopRunning() DispatchQueue.main.async { self.isSessionRunning = false } } } func capturePhoto() { var settings = AVCapturePhotoSettings() settings.photoQualityPrioritization = .balanced if let connection = photoOutput.connection(with: .video), let angle = rotationCoordinator?.videoRotationAngleForHorizonLevelCapture { connection.videoRotationAngle = angle } photoOutput.capturePhoto(with: settings, delegate: self) } } extension CameraManager: AVCapturePhotoCaptureDelegate { nonisolated func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard let data = photo.fileDataRepresentation() else { return } // Handle photo data } } ``` --- ## Resources **Docs**: /avfoundation/avcapturesession, /avfoundation/avcapturedevice, /avfoundation/avcapturephotosettings, /avfoundation/avcapturedevice/rotationcoordinator **Skills**: axiom-camera-capture, axiom-camera-capture-diag