关于 axiom-camera-capture-diag
面向开发者的 AVFoundation 摄像头问题诊断技能,涵盖预览冻结、权限和旋转等故障。它先检查会话状态、线程、权限和中断,再排查拍摄行为。
主要功能
- 01摄像头会话状态检查
- 02线程与权限诊断
- 03中断处理指引
- 04预览与照片旋转检查
- 05拍摄速度排查
适用场景
- 01摄像头预览黑屏或冻结
- 02打开摄像头时界面卡顿
- 03照片方向异常
- 04拍摄缓慢或权限被拒
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-camera-capture-diag安装后在 Claude Code 中输入/axiom-camera-capture-diag确认已加载。
详细介绍
摄像头采集诊断
针对 AVFoundation 摄像头问题的系统化故障排查:预览冻结、旋转方向错误、采集缓慢、会话中断和权限问题。
概述
核心原则:摄像头无法正常工作时,问题通常出在:
- 线程处理(在主线程上执行会话操作)- 35%
- 会话生命周期(未启动、已中断或未配置)- 25%
- 旋转方向(使用已弃用的 API、缺少协调器)- 20%
- 权限(已拒绝或未请求)- 15%
- 配置(预设错误、缺少输入/输出)- 5%
调试采集逻辑之前,务必先检查线程处理和会话状态。
危险信号
表明存在摄像头特有问题的症状:
| 症状 | 可能原因 |
|---|---|
| 预览显示黑屏 | 会话未启动、权限被拒绝、没有摄像头输入 |
| 打开摄像头时 UI 冻结 | 在主线程上调用 startRunning() |
| 电话来电时摄像头冻结 | 未处理中断 |
| 预览旋转方向错误 90° | 未使用 RotationCoordinator(iOS 17+) |
| 拍摄的照片旋转方向错误 | 未将旋转角度应用于输出连接 |
| 前置摄像头拍摄的照片未镜像 | 这是正常的!(预览会镜像,照片不会) |
| “摄像头正被其他应用使用” | 其他应用独占了摄像头 |
| 采集耗时超过 2 秒 | photoQualityPrioritization 设为 .quality |
| 会话无法在 iPad 上启动 | 分屏模式下无法使用摄像头 |
| 在较旧的 iOS 版本上崩溃 | 使用 iOS 17+ API 时未检查可用性 |
必须先做的步骤
调查代码之前,先运行以下诊断:
步骤 1:检查会话状态
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))")
}
预期输出:
- ✅ isRunning: true,inputs ≥ 1,outputs ≥ 1 → 会话正常工作
- ⚠️ isRunning: false → 会话未启动或已中断
- ❌ inputs: 0 → 未添加摄像头(权限问题?配置问题?)
步骤 2:检查线程处理
print("🧵 Thread check:")
// When setting up session
sessionQueue.async {
print(" Setup thread: \(Thread.isMainThread ? "❌ MAIN" : "✅ Background")")
}
// When starting session
sessionQueue.async {
print(" Start thread: \(Thread.isMainThread ? "❌ MAIN" : "✅ Background")")
}
预期输出:
- ✅ 全部在后台线程 → 正确
- ❌ 有任何操作在主线程上 → UI 将会冻结
步骤 3:检查权限
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")
}
步骤 4:检查是否发生中断
// Add temporary observer to see interruptions
NotificationCenter.default.addObserver(
forName: .AVCaptureSessionWasInterrupted,
object: session,
queue: .main
) { notification in
if let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int {
print("🚨 Interrupted: reason \(reason)")
}
}
决策树
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)
诊断模式
模式 1:会话未启动
症状:预览为黑屏,isRunning = false
常见原因:
- 从未调用
startRunning() - 调用了
startRunning(),但会话没有输入 - 会话已停止,且从未重新启动
诊断:
// Check if startRunning was called
print("isRunning before start: \(session.isRunning)")
session.startRunning()
print("isRunning after start: \(session.isRunning)")
修复:
// Ensure session is started on session queue
func startSession() {
sessionQueue.async { [self] in
guard !session.isRunning else { return }
// Verify we have inputs before starting
guard !session.inputs.isEmpty else {
print("❌ Cannot start - no inputs configured")
return
}
session.startRunning()
}
}
修复耗时:10 分钟
模式 2:没有摄像头输入
症状:session.inputs.count = 0
常见原因:
- 摄像头权限被拒绝
- 创建
AVCaptureDeviceInput失败 canAddInput()返回 false- 未提交配置
诊断:
// Step through input setup
guard let camera = AVCaptureDevice.default(for: .video) else {
print("❌ No camera device found")
return
}
print("✅ Camera: \(camera.localizedName)")
do {
let input = try AVCaptureDeviceInput(device: camera)
print("✅ Input created")
if session.canAddInput(input) {
print("✅ Can add input")
} else {
print("❌ Cannot add input - check session preset compatibility")
}
} catch {
print("❌ Input creation failed: \(error)")
}
修复:确保在创建输入之前已获得权限,并将操作包装在配置块中:
session.beginConfiguration()
// Add input here
session.commitConfiguration()
修复耗时:15 分钟
模式 3:预览图层未连接
症状:isRunning = true,已配置输入,但预览为黑屏
常见原因:
- 未设置预览图层的 session
- 预览图层不在视图层级中
- 预览图层的 frame 为零
诊断:
print("Preview layer session: \(previewLayer.session != nil)")
print("Preview layer superlayer: \(previewLayer.superlayer != nil)")
print("Preview layer frame: \(previewLayer.frame)")
print("Preview layer connection: \(previewLayer.connection != nil)")
修复:
// Ensure preview layer is properly configured
previewLayer.session = session
previewLayer.videoGravity = .resizeAspectFill
// Ensure frame is set (common in SwiftUI)
previewLayer.frame = view.bounds
修复耗时:10 分钟
模式 4:主线程阻塞
症状:打开摄像头时 UI 冻结 1-3 秒
根本原因:在主线程上执行了阻塞调用 startRunning()
诊断:
// If this prints on main thread, that's the problem
print("startRunning on thread: \(Thread.current)")
session.startRunning()
修复:
// Create dedicated serial queue
private let sessionQueue = DispatchQueue(label: "camera.session")
func startSession() {
sessionQueue.async { [self] in
session.startRunning()
}
}
修复耗时:15 分钟
模式 5:电话来电中断
症状:摄像头正常工作,但有电话来电时冻结
根本原因:会话已中断,但未处理,也未向 UI 提供反馈
诊断:
// Check if session is still running after returning from call
print("Session running: \(session.isRunning)")
// Will be false during active call, true after call ends
修复:添加中断观察器(参见 camera-capture 技能模式 5)
要点:中断结束后,会话会自动恢复。你无需再次调用 startRunning(),只需更新 UI。
修复耗时:30 分钟
模式 6:分屏视图中相机不可用
症状:iPad 进入分屏视图时,相机停止工作
根本原因:多个应用处于前台时,相机不可用
诊断:
// Check interruption reason
// InterruptionReason.videoDeviceNotAvailableWithMultipleForegroundApps
修复:显示适当的 UI 消息,并在用户退出分屏视图后恢复:
case .videoDeviceNotAvailableWithMultipleForegroundApps:
showMessage("Camera unavailable in Split View. Use full screen.")
修复时间:15 分钟
模式 7:热压力
症状:相机随机停止,尤其是在长时间使用后
根本原因:设备过热,系统正在减少资源
诊断:
// Check thermal state
print("Thermal state: \(ProcessInfo.processInfo.thermalState.rawValue)")
// 0 = nominal, 1 = fair, 2 = serious, 3 = critical
修复:降低画质或显示设备冷却提示:
case .videoDeviceNotAvailableDueToSystemPressure:
// Reduce quality
session.sessionPreset = .medium
showMessage("Camera quality reduced due to device temperature")
修复时间:20 分钟
模式 8:预览方向错误
症状:预览方向与预期相差 90°
根本原因:未使用 RotationCoordinator(iOS 17+),或未观察更新
诊断:
print("Preview connection rotation: \(previewLayer.connection?.videoRotationAngle ?? -1)")
修复:
// Create and observe RotationCoordinator
let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: previewLayer)
// Set initial rotation
previewLayer.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview
// Observe changes
observation = coordinator.observe(\.videoRotationAngleForHorizonLevelPreview) { [weak previewLayer] coord, _ in
DispatchQueue.main.async {
previewLayer?.connection?.videoRotationAngle = coord.videoRotationAngleForHorizonLevelPreview
}
}
修复时间:30 分钟
模式 9:拍摄的照片方向错误
症状:预览正常,但拍摄的照片方向错误
根本原因:未将旋转角度应用到照片输出连接
诊断:
if let connection = photoOutput.connection(with: .video) {
print("Photo connection rotation: \(connection.videoRotationAngle)")
}
修复:
func capturePhoto() {
// Apply current rotation to capture
if let connection = photoOutput.connection(with: .video) {
connection.videoRotationAngle = rotationCoordinator.videoRotationAngleForHorizonLevelCapture
}
photoOutput.capturePhoto(with: settings, delegate: self)
}
修复时间:15 分钟
模式 10:前置相机镜像
症状:设计师表示“前置相机拍的照片和预览不一致”
实际情况:这是正确行为,并非 Bug。
说明:
- 预览会镜像显示(如同照镜子,符合用户预期)
- 拍摄的照片不会镜像(分享时文字可正常阅读)
- 这与系统相机 App 的行为一致
如果业务要求照片镜像(自拍 App):
func mirrorImage(_ image: UIImage) -> UIImage? {
guard let cgImage = image.cgImage else { return nil }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: .upMirrored)
}
修复时间:5 分钟(说明)或 15 分钟(需要镜像时)
模式 11:拍摄缓慢(画质优先)
症状:拍照需要 2 秒以上
根本原因:photoQualityPrioritization = .quality(某些设备的默认设置)
诊断:
print("Max quality prioritization: \(photoOutput.maxPhotoQualityPrioritization.rawValue)")
// Check what you're requesting in AVCapturePhotoSettings
修复:
var settings = AVCapturePhotoSettings()
// For fast capture (social/sharing)
settings.photoQualityPrioritization = .speed
// For balanced (general use)
settings.photoQualityPrioritization = .balanced
// Only use .quality when image quality is critical
修复时间:5 分钟
模式 12:延迟处理
症状:希望实现最高响应速度(零快门延迟)
解决方案:启用延迟处理(iOS 17+)
photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
// Then handle proxy in delegate:
// - didFinishProcessingPhoto gives proxy for immediate display
// - didFinishCapturingDeferredPhotoProxy gives final image later
修复时间:30 分钟
模式 13:未请求权限
症状:authorizationStatus = .notDetermined
修复:
// Must request before setting up session
Task {
let granted = await AVCaptureDevice.requestAccess(for: .video)
if granted {
setupSession()
}
}
修复时间:10 分钟
模式 14:权限被拒绝
症状:authorizationStatus = .denied
修复:显示设置提示
func showSettingsPrompt() {
let alert = UIAlertController(
title: "Camera Access Required",
message: "Please enable camera access in Settings to use this feature.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
修复时间:15 分钟
模式 15:API 可用性导致崩溃
症状:在 iOS 16 或更早版本上崩溃
根本原因:使用 iOS 17+ API 时未检查可用性
修复:
if #available(iOS 17.0, *) {
// Use RotationCoordinator
let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: preview)
} else {
// Fallback to deprecated videoOrientation
if let connection = previewLayer.connection {
connection.videoOrientation = .portrait
}
}
修复时间:20 分钟
速查表
| 症状 | 首先检查 | 可能的模式 |
|---|---|---|
| 预览画面黑屏 | 步骤 1(会话状态) | 1、2 或 3 |
| UI 卡住 | 步骤 2(线程处理) | 4 |
| 通话时卡住 | 步骤 4(中断) | 5 |
| 方向错误 | 打印旋转角度 | 8 或 9 |
| 拍摄缓慢 | 打印画质设置 | 11 |
| 访问被拒绝 | 步骤 3(权限) | 14 |
| 在旧版 iOS 上崩溃 | 检查 @available | 15 |
检查清单
升级处理相机问题前:
基础项:
- ☑ 会话至少有一个输入
- ☑ 会话至少有一个输出
- ☑ Session 的 isRunning = true
- ☑ 预览图层已连接到会话
- ☑ 预览图层的 frame 非零
线程处理:
- ☑ 所有会话操作都在 sessionQueue 上执行
- ☑ startRunning() 在后台线程上执行
- ☑ UI 更新在主线程上执行
权限:
- ☑ 已检查授权状态
- ☑ 状态为 notDetermined 时已请求权限
- ☑ 为被拒绝状态提供了恰当的 UI
方向:
- ☑ 使用 device 和 previewLayer 创建了 RotationCoordinator
- ☑ 已设置对预览角度变化的观察
- ☑ 拍照时已应用拍摄角度
中断:
- ☑ 已注册中断观察者
- ☑ 为中断状态提供了 UI 反馈
- ☑ 已使用来电进行测试
资源
WWDC:2021-10247、2023-10105
文档:/avfoundation/avcapturesession、/avfoundation/avcapturesessionwasinterruptednotification
技能:axiom-camera-capture、axiom-camera-capture-ref