You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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))
        })
    }
}

问题分析与修复方案

核心问题

  1. 多实例化CameraService导致配置失效
    CustomCameraView和CameraView各自创建了独立的CameraService实例,点击拍照时调用的是CustomCameraView里未完成会话配置的实例,自然找不到活跃视频连接。

  2. 会话启动与预览层设置时机错误
    在全局队列异步启动会话后才回到主线程设置预览层,会导致会话未完成配置就启动,同时预览层添加不及时引发黑屏。

  3. 相机设备选择的鲁棒性不足
    部分设备需要明确指定后置摄像头类型,仅用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 08:42:03