Camera Capture Diagnostics
Systematic troubleshooting for AVFoundation camera issues: frozen preview, wrong rotation, slow capture, session interruptions, and permission problems.
Overview
Core Principle: When camera doesn't work, the problem is usually:
- Threading (session work on main thread) - 35%
- Session lifecycle (not started, interrupted, not configured) - 25%
- Rotation (deprecated APIs, missing coordinator) - 20%
- Permissions (denied, not requested) - 15%
- Configuration (wrong preset, missing input/output) - 5%
Always check threading and session state BEFORE debugging capture logic.
Red Flags
Symptoms that indicate camera-specific issues:
| Symptom |
Likely Cause |
| Preview shows black screen |
Session not started, permission denied, no camera input |
| UI freezes when opening camera |
startRunning() called on main thread |
| Camera freezes on phone call |
No interruption handling |
| Preview rotated 90Β° wrong |
Not using RotationCoordinator (iOS 17+) |
| Captured photo rotated wrong |
Rotation angle not applied to output connection |
| Front camera photo not mirrored |
This is correct! (preview mirrors, photo does not) |
| "Camera in use by another app" |
Another app has exclusive access |
| Capture takes 2+ seconds |
photoQualityPrioritization set to .quality |
| Session won't start on iPad |
Split View - camera unavailable |
| Crash on older iOS |
Using iOS 17+ APIs without availability check |
Mandatory First Steps
Before investigating code, run these diagnostics:
Step 1: Check Session State
print("π· Session state:")
print(" isRunning: \(session.isRunning)")
print(" inputs: \(session.inputs.count)")
print(" outputs: \(session.outputs.count)")
for input in session.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
print(" Input: \(deviceInput.device.localizedName)")
}
}
for output in session.outputs {
print(" Output: \(type(of: output))")
}
Expected output:
- β
isRunning: true, inputs β₯ 1, outputs β₯ 1 β Session working
- β οΈ isRunning: false β Session not started or interrupted
- β inputs: 0 β Camera not added (permission? configuration?)
Step 2: Check Threading
print("π§΅ Thread check:")
sessionQueue.async {
print(" Setup thread: \(Thread.isMainThread ? "β MAIN" : "β
Background")")
}
sessionQueue.async {
print(" Start thread: \(Thread.isMainThread ? "β MAIN" : "β
Background")")
}
Expected output:
- β
All background β Correct
- β Any main thread β UI will freeze
Step 3: Check Permissions
let status = AVCaptureDevice.authorizationStatus(for: .video)
print("π Camera permission: \(status.rawValue)")
switch status {
case .authorized: print(" β
Authorized")
case .notDetermined: print(" β οΈ Not yet requested")
case .denied: print(" β Denied by user")
case .restricted: print(" β Restricted (parental controls?)")
@unknown default: print(" β Unknown")
}
Step 4: Check for Interruptions
NotificationCenter.default.addObserver(
forName: .AVCaptureSessionWasInterrupted,
object: session,
queue: .main
) { notification in
if let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int {
print("π¨ Interrupted: reason \(reason)")
}
}
Decision Tree
Camera not working as expected?
β
ββ Black/frozen preview?
β ββ Check Step 1 (session state)
β β ββ isRunning = false β See Pattern 1 (session not started)
β β ββ inputs = 0 β See Pattern 2 (no camera input)
β β ββ isRunning = true, inputs > 0 β See Pattern 3 (preview layer)
β
ββ UI freezes when opening camera?
β ββ Check Step 2 (threading)
β ββ Main thread β See Pattern 4 (move to session queue)
β
ββ Camera freezes during use?
β ββ After phone call β See Pattern 5 (interruption handling)
β ββ In Split View (iPad) β See Pattern 6 (multitasking)
β ββ Random freezes β See Pattern 7 (thermal pressure)
β
ββ Preview/photo rotated wrong?
β ββ Preview rotated β See Pattern 8 (RotationCoordinator preview)
β ββ Captured photo rotated β See Pattern 9 (capture rotation)
β ββ Front camera "wrong" β See Pattern 10 (mirroring expected)
β
ββ Capture too slow?
β ββ 2+ seconds delay β See Pattern 11 (quality prioritization)
β ββ Slight delay β See Pattern 12 (deferred processing)
β
ββ Permission issues?
β ββ Status: notDetermined β See Pattern 13 (request permission)
β ββ Status: denied β See Pattern 14 (settings prompt)
β
ββ Crash on some devices?
ββ See Pattern 15 (API availability)
Diagnostic Patterns
Pattern 1: Session Not Started
Symptom: Black preview, isRunning = false
Common causes:
startRunning() never called
startRunning() called but session has no inputs
- Session stopped and never restarted
Diagnostic:
print("isRunning before start: \(session.isRunning)")
session.startRunning()
print("isRunning after start: \(session.isRunning)")
Fix:
func startSession() {
sessionQueue.async { [self] in
guard !session.isRunning else { return }
guard !session.inputs.isEmpty else {
print("β Cannot start - no inputs configured")
return
}
session.startRunning()
}
}
Time to fix: 10 min
Pattern 2: No Camera Input
Symptom: session.inputs.count = 0
Common causes:
- Camera permission denied
AVCaptureDeviceInput creation failed
canAddInput() returned false
- Configuration not committed
Diagnostic:
guard let camera = AVCaptureDevice.default(for: .video) else {
print("β No camera device found")
return
}
print("β
Camera: \(camera.localizedName)")
do {
let input = try AV