Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
779
.claude/skills/axiom-camera-capture-ref/SKILL.md
Normal file
779
.claude/skills/axiom-camera-capture-ref/SKILL.md
Normal file
@@ -0,0 +1,779 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user