AVFoundation自定义相机部分iPhone设备无法工作求助
我开发的AVFoundation自定义相机在多数设备上运行正常,但在iPhone 13 Pro Max和iPhone 11设备上仅显示黑屏,点击拍照按钮时应用崩溃,报错信息为:
NSInvalidArgumentException - *** -[AVCapturePhotoOutput capturePhotoWithSettings:delegate:] No active and enabled video connection
我已确认这两台设备均授予了应用相机访问权限,且配置了拒绝、受限、未知等所有权限场景的错误提示,但未触发任何提示。
更新:我曾在if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) { }中添加else分支,发现问题设备无法找到相机设备。目前代码中已改为使用AVCaptureDevice.default(for: .video),但问题仍存在,请问这是什么原因?
相关代码
CameraService
import Foundation import AVFoundation import SwiftUI class CameraService: ObservableObject { var session = AVCaptureSession() var delegate: AVCapturePhotoCaptureDelegate? var previewLayer = AVCaptureVideoPreviewLayer() var output = AVCapturePhotoOutput() @Published var info: AlertInfo? func start(delegate: AVCapturePhotoCaptureDelegate, completion: @escaping (Error?) -> ()) { self.delegate = delegate checkPermissions(completion: completion) } private func checkPermissions(completion: @escaping (Error?) -> ()) { switch AVCaptureDevice.authorizationStatus(for: .video) { case .notDeterminedined: AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in guard granted else { return } DispatchQueue.main.async { self?.setupCamera(completion: completion) } } case .restricted: info = AlertInfo( id: .restricted, title: "Camera Access Restricted", message: "We can't open the camera.") break case .denied: info = AlertInfo( id: .denied, title: "Camera Access Denied", message: "Please enable camera access by going to Settings > Privacy & Security > Camera") break case .authorized: setupCamera(completion: completion) @unknown default: info = AlertInfo( id: .unknown, title: "Unknown Error Occurred", message: "An unknown error has occurred. Please try again later") break } } private func setupCamera(completion: @escaping (Error?) -> ()) { do { if let device = AVCaptureDevice.default(for: .video) { self.session.beginConfiguration() let input = try AVCaptureDeviceInput(device: device) if self.session.canAddInput(input) { self.session.addInput(input) } if self.session.canAddOutput(output) { self.session.addOutput(output) } self.session.commitConfiguration() } } catch { completion(error) } } func capturePhoto() { DispatchQueue.global(qos: .background).async { if let delegate = self.delegate { self.output.capturePhoto(with: AVCapturePhotoSettings(), delegate: delegate) DispatchQueue.main.async { Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { (timer) in self.session.stopRunning() } } } } } func retakePhoto() { DispatchQueue.global(qos: .background).async { self.session.startRunning() } } } struct AlertInfo: Identifiable { enum AlertType { case denied case restricted case unknown } let id: AlertType let title: String let message: String }
CameraView
import Foundation import UIKit import SwiftUI import AVFoundation struct CameraView: UIViewControllerRepresentable { typealias UIViewControllerType = UIViewController @StateObject var service = CameraService() let didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> () func makeUIViewController(context: Context) -> UIViewController { service.start(delegate: context.coordinator) { err in if let err = err { didFinishProcessingPhoto(.failure(err)) return } } let vc = UIViewController() DispatchQueue.global().async { service.session.startRunning() DispatchQueue.main.async { service.previewLayer = AVCaptureVideoPreviewLayer(session: service.session) service.previewLayer.frame = vc.view.bounds service.previewLayer.videoGravity = .resizeAspectFill vc.view.layer.addSublayer(service.previewLayer) } } return vc } func makeCoordinator() -> Coordinator { Coordinator(self, didFinishProcessingPhoto: didFinishProcessingPhoto) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } class Coordinator: NSObject, AVCapturePhotoCaptureDelegate { let parent: CameraView private var didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> () init(_ parent: CameraView, didFinishProcessingPhoto: @escaping (Result<AVCapturePhoto, Error>) -> ()) { self.parent = parent self.didFinishProcessingPhoto = didFinishProcessingPhoto } func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { if let error = error { didFinishProcessingPhoto(.failure(error)) return } didFinishProcessingPhoto(.success(photo)) } } }
CustomCameraView
import SwiftUI import PhotosUI struct CustomCameraView: View { @Environment(\.dismiss) private var dismiss @StateObject var service = CameraService() @StateObject var viewModel: UploadPostViewModel var body: some View { ZStack { CameraView(service: service) { result in switch result { case .success(let photo): if let data = photo.fileDataRepresentation() { viewModel.capturedImage = UIImage(data: data) } else { //write error message } case .failure(let err): //write error message print(err) } } VStack (alignment: .leading) { Spacer() HStack { PhotoPickerForPost { result in switch result { case .success(let image): viewModel.capturedImage = image case .failure(let error): //write error message print(error) viewModel.capturedImage = nil } } Spacer() Button { service.capturePhoto() } label: { Image(systemName: "circle") .font(.system(size: 72)) .foregroundColor(.white) } Spacer() Rectangle() .foregroundColor(.clear) .frame(width: 32, height: 28) } .padding() } } .padding(.horizontal, 4) .cornerRadius(6) .background(.black) .alert(item: $service.info, content: { info in Alert(title: Text(info.title), message: Text(info.message)) }) } }
核心问题
多实例化CameraService导致配置失效
CustomCameraView和CameraView各自创建了独立的CameraService实例,点击拍照时调用的是CustomCameraView里未完成会话配置的实例,自然找不到活跃视频连接。会话启动与预览层设置时机错误
在全局队列异步启动会话后才回到主线程设置预览层,会导致会话未完成配置就启动,同时预览层添加不及时引发黑屏。相机设备选择的鲁棒性不足
部分设备需要明确指定后置摄像头类型,仅用AVCaptureDevice.default(for: .video)可能无法适配所有机型。
具体修复步骤
1. 统一CameraService实例
修改CameraView,移除内部的@StateObject var service,改为从外部传入:
struct CameraView: UIViewControllerRepresentable { typealias UIViewControllerType = UIViewController let service: CameraService // 改为外部传入 let didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> () // 其余代码保持不变,所有service引用改为这个传入的实例 }
在CustomCameraView中传入自身的service:
CameraView(service: service) { result in // 原有逻辑不变 }
2. 修正会话启动与预览层设置时机
在CameraView的makeUIViewController方法中,确保配置完成后再启动会话并设置预览层:
func makeUIViewController(context: Context) -> UIViewController { let vc = UIViewController() service.start(delegate: context.coordinator) { err in if let err = err { self.didFinishProcessingPhoto(.failure(err)) return } // 配置完成后在主线程操作 DispatchQueue.main.async { service.previewLayer = AVCaptureVideoPreviewLayer(session: service.session) service.previewLayer.frame = vc.view.bounds service.previewLayer.videoGravity = .resizeAspectFill vc.view.layer.addSublayer(service.previewLayer) // AVCaptureSession建议在主线程启动 service.session.startRunning() } } return vc }
3. 增强相机设备选择的兼容性
在CameraService的setupCamera方法中,优先尝试获取后置广角摄像头,同时添加设备找不到的错误处理:
private func setupCamera(completion: @escaping (Error?) -> ()) { guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) ?? AVCaptureDevice.default(for: .video) else { completion(NSError(domain: "CameraService", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法找到可用的相机设备"])) return } do { session.beginConfiguration() let input = try AVCaptureDeviceInput(device: device) if session.canAddInput(input) { session.addInput(input) } if session.canAddOutput(output) { session.addOutput(output) } session.commitConfiguration() completion(nil) } catch { completion(error) } }
4. 修正拍照方法的线程问题
capturePhotoWithSettings:delegate:需在主线程调用,同时移除不必要的Timer:
func capturePhoto() { guard let delegate = self.delegate else { return } DispatchQueue.main.async { self.output.capturePhoto(with: AVCapturePhotoSettings(), delegate: delegate) self.session.stopRunning() } }
额外注意事项
- 所有
AVCaptureSession的配置操作必须放在beginConfiguration()和commitConfiguration()之间 - 应用从后台切换回来时,需要重新启动会话
- 测试时注意检查相机是否被其他应用占用
内容的提问来源于stack exchange,提问作者amelia

