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

iOS应用AVCaptureSession间歇性失效问题排查咨询

相机预览黑屏、会话停止运行的随机复现问题

问题现象

  • 部分用户相机预览黑屏,captureSession.isRunning 返回 false
  • 拍照时抛出AVFoundation相关错误,错误码包括-11800(底层-12780/-16800)、-11819(底层-16405)
  • 问题随机出现,持续数天后自动恢复,卸载重装无法解决
  • 已确认相机权限正常授予

捕获的错误日志

Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-12780), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x30012adc0 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}}

Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-16800), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x301666b50 {Error Domain=NSOSStatusErrorDomain Code=-16800 "(null)"}}

Error Domain=AVFoundationErrorDomain Code=-11819 "Cannot Complete Action" UserInfo={AVErrorRecordingSuccessfullyFinishedKey=false, NSLocalizedDescription=Cannot Complete Action, NSLocalizedRecoverySuggestion=Try again later., AVErrorRecordingFailureDomainKey=2, NSUnderlyingError=0x10cb61a70 {Error Domain=NSOSStatusErrorDomain Code=-16405 "(null)"}}

当前错误监听代码

NotificationCenter.default.addObserver(self, 
     selector: #selector(sessionRuntimeError),
     name: .AVCaptureSessionRuntimeError,
     object: captureSession)

会话初始化核心代码

func initSession(finished: @escaping ((CameraSessionPermissionState) -> Void)) {
        checkPermissions { permissionState in
            self.configure { config in
                config.position = SharedPrefs.Camera.cameraPosition
                config.photo = .enabled(config: CameraConfiguration.PhotoConfig())
                config.video = .enabled(config: CameraConfiguration.VideoConfig())
                config.audio = .enabled(config: CameraConfiguration.AudioConfig())
                config.orientation = .portrait
                config.isActive = true
            } finished: {
                finished(permissionState)
            }
        }
    }
    
    func configure(update: @escaping (_ configuration: CameraConfiguration) throws -> Void,
                   finished: (() -> Void)? = nil) {
        isConfiguring = true
        
        CameraQueues.cameraQueue.async { [weak self] in
            guard let self = self else { return }
            self.executeConfiguration(update: update, finished: finished)
        }
    }
    
    private func executeConfiguration(update: @escaping (_ configuration: CameraConfiguration) throws -> Void,
                                      finished: (() -> Void)? = nil) {
        // Let caller configure a new configuration for the Camera.
        let config = CameraConfiguration(copyOf: self.configuration)
        
        do {
            try update(config)
        } catch CameraConfiguration.AbortThrow.abort {
            // call has been aborted and changes shall be discarded
            return
        } catch {
            // another error occured, possibly while trying to parse enums
            self.onConfigureError(error)
            return
        }
        
        if config.cameraId == nil {
            switch config.position {
            case .back:
                config.cameraId = self.backCameraId
            case .front:
                config.cameraId = self.frontCameraId
            default:
                config.cameraId = self.backCameraId
            }
        }
        
        let difference = CameraConfiguration.Difference(between: self.configuration, and: config)
        
        do {
            // If needed, configure the AVCaptureSession (inputs, outputs)
            if difference.isSessionConfigurationDirty {
                self.captureSession.beginConfiguration()
                self.captureSession.sessionPreset = .hd1920x1080
                
                // 1. Update input device
                if difference.inputChanged {
                    try self.configureDevice(configuration: config)
                }
                // 2. Update outputs
                if difference.outputsChanged {
                    try self.configureOutputs(configuration: config)
                }
                // 3. Update Video Stabilization
                if difference.videoStabilizationChanged {
                    self.configureVideoStabilization(configuration: config)
                }
                // 4. Update output orientation
                if difference.orientationChanged {
                    self.configureOrientation(configuration: config)
                }
            }
            
            guard let device = self.videoDeviceInput?.device else {
                self.captureSession.commitConfiguration()
                throw CameraError.device(.noDevice)
            }
            
            // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..)
            if difference.isDeviceConfigurationDirty {
                try device.lockForConfiguration()
                defer {
                    device.unlockForConfiguration()
                }
                
                // 4. Configure format
                if difference.formatChanged {
                    try self.configureFormat(configuration: config, device: device)
                }
                // 5. After step 2. and 4., we also need to configure some output properties that depend on format.
                //    This needs to be done AFTER we updated the `format`, as this controls the supported properties.
                if difference.outputsChanged || difference.formatChanged {
                    self.configureVideoOutputFormat(configuration: config)
                    self.configurePhotoOutputFormat(configuration: config)
                }
                // 6. Configure side-props (fps, lowLightBoost)
                if difference.sidePropsChanged {
                    try self.configureSideProps(configuration: config, device: device)
                }
                // 7. Configure zoom
                if difference.zoomChanged {
                    self.configureZoom(configuration: config, device: device)
                }
                // 8. Configure exposure bias
                if difference.exposureChanged {
                    self.configureExposure(configuration: config, device: device)
                }
            }
            
            if difference.isSessionConfigurationDirty {
                // We commit the session config updates AFTER the device config,
                // that way we can also batch those changes into one update instead of doing two updates.
                self.captureSession.commitConfiguration()
            }
            
            // 9. Start or stop the session if needed
            self.checkIsActive(configuration: config)
            
            // 10. Enable or disable the Torch if needed (requires session to be running)
            if difference.torchChanged {
                try device.lockForConfiguration()
                defer {
                    device.unlockForConfiguration()
                }
                try self.configureTorch(configuration: config, device: device)
            }
            
            // Notify about Camera initialization
            if difference.inputChanged {
                self.delegate?.onSessionInitialized()
            }
            
            // After configuring, set this to the new configuration.
            self.configuration = config
        } catch {
            self.onConfigureError(error)
        }
        
        // Set up Audio Capture Session (on audio queue)
        if difference.audioSessionChanged {
            CameraQueues.audioQueue.async {
                do {
                    // Lock Capture Session for configuration
                    self.audioCaptureSession.beginConfiguration()
                    
                    try self.configureAudioSession(configuration: config)
                    
                    // Unlock Capture Session again and submit configuration to Hardware
                    self.audioCaptureSession.commitConfiguration()
                } catch {
                    self.onConfigureError(error)
                }
            }
        }
        
        // Check if Hardware Cost is okay
        if #available(iOS 16.0, *) {
            if self.captureSession.hardwareCost > 1 {
                // Throw this error to the user, but don't abort the configuration. Maybe it still works.
                self.onConfigureError(CameraError.session(.hardwareCostTooHigh(cost: self.captureSession.hardwareCost)))
            }
        }
        
        DispatchQueue.main.async {
            finished?()
            self.isConfiguring = false
        }
    }

可能的原因分析

1. 系统级相机资源冲突

  • 其他应用(如系统相机、第三方相机类APP)长时间占用相机硬件资源,导致系统拒绝当前APP的相机请求。这类冲突通常会在系统资源回收后自动解除,对应问题数天后自行恢复的现象。
  • 部分iOS版本存在相机资源调度的偶发Bug,系统重启或资源自动释放后问题消失。

2. AVFoundation会话配置的隐性问题

  • 会话配置过程中,beginConfiguration()和commitConfiguration()的调用时机或线程处理存在潜在问题,导致会话处于异常状态。虽然代码中使用了专用队列,但极端情况下可能因系统调度问题导致配置未正确提交。
  • 相机设备锁定(lockForConfiguration())未正确释放,导致后续会话启动失败。不过代码中使用了defer解锁,这种概率较低,但仍需考虑极端场景下的系统异常。

3. 底层硬件临时故障

  • 设备相机模块出现临时硬件异常(如过热、供电波动),系统进入保护状态禁止相机访问。待硬件恢复正常后,相机功能自动恢复。此类问题与APP无关,卸载重装无法解决。

4. 错误监听的遗漏

  • .AVCaptureSessionRuntimeError 仅捕获会话运行时的严重错误,部分配置阶段的错误可能未触发该通知,导致Crashlytics未收到日志。建议补充监听会话中断相关通知,排查是否存在会话被中断的情况。

建议的排查/修复措施

  • 增加会话中断监听,记录中断原因和恢复事件:
    NotificationCenter.default.addObserver(self, selector: #selector(sessionInterrupted), name: .AVCaptureSessionWasInterrupted, object: captureSession)
    NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: .AVCaptureSessionInterruptionEnded, object: captureSession)
    
  • 在会话启动失败时,增加重试逻辑(如间隔1-2秒后尝试重新启动会话),避免一次性失败导致永久黑屏。
  • 检查checkIsActive方法的实现,确保会话启动逻辑正确处理了异常状态(如启动失败时的错误捕获和重试)。
  • 针对iOS 16+的hardwareCost检查,考虑在成本过高时降级会话预设(如从.hd1920x1080改为.hd1280x720),减少硬件资源占用。

内容的提问来源于stack exchange,提问作者BenCLT

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:57:32