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

iOS(Swift)中类似WhatsApp的画中画模式摄像头访问异常问题

问题解决:iOS后台PIP模式下摄像头画面冻结且触发videoDeviceNotAvailableWithMultipleForegroundApps错误

核心问题分析

你的代码存在几个关键问题,导致后台PIP时摄像头被中断:

  1. 音频会话配置错误:使用了.playback类别和.moviePlayback模式,这两类配置不支持视频通话所需的后台音视频同时工作,系统会在后台限制摄像头访问。
  2. 视图层级冲突:将同一个cameraPreview同时添加到主控制器视图和PIP内容控制器视图,导致实时画面无法正确传递到PIP窗口。
  3. 错误处理缺失:多处使用try?忽略异常,无法排查权限或设备初始化失败的问题。
  4. 多任务摄像头权限的启用逻辑:虽然开启了isMultitaskingCameraAccessEnabled,但音频会话的配置顺序和类型不对,无法触发多任务摄像头权限生效。

修复后的完整代码

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {
    
    private var pictureInPictureController: AVPictureInPictureController!
    let captureSession = AVCaptureSession()
    let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
    var pipContentSource: AVPictureInPictureController.ContentSource!
    // 单独为PIP创建预览层,避免视图复用冲突
    private var pipPreviewLayer: AVCaptureVideoPreviewLayer!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 添加捕获会话通知监听
        NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted), name: .AVCaptureSessionWasInterrupted, object: captureSession)
        NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: .AVCaptureSessionInterruptionEnded, object: captureSession)
        NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError), name: .AVCaptureSessionRuntimeError, object: captureSession)
        
        // 优先配置音频会话(视频通话必须用playAndRecord类别)
        do {
            let audioSession = AVAudioSession.sharedInstance()
            try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.mixWithOthers, .allowBluetooth])
            try audioSession.setActive(true)
        } catch {
            print("音频会话配置失败:\(error.localizedDescription)")
            return
        }
        
        // 检查摄像头权限
        AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
            guard granted, let self = self else {
                print("摄像头权限被拒绝")
                return
            }
            
            DispatchQueue.main.async {
                self.setupCaptureSession()
                self.setupPictureInPicture()
            }
        }
    }
    
    private func setupCaptureSession() {
        // 获取后置摄像头
        guard let captureDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first else {
            print("无法获取后置摄像头")
            return
        }
        
        do {
            let deviceInput = try AVCaptureDeviceInput(device: captureDevice)
            if captureSession.canAddInput(deviceInput) {
                captureSession.addInput(deviceInput)
            }
            
            // 配置多任务摄像头访问
            captureSession.beginConfiguration()
            if captureSession.isMultitaskingCameraAccessSupported {
                captureSession.isMultitaskingCameraAccessEnabled = true
            }
            // 视频通话用合适的分辨率,不要用photo(太高且不适合实时传输)
            captureSession.sessionPreset = .hd1280x720
            captureSession.commitConfiguration()
            
            // 启动捕获会话
            DispatchQueue.global(qos: .userInitiated).async {
                self.captureSession.startRunning()
            }
            
            // 为主视图创建预览层
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = view.bounds
            previewLayer.videoGravity = .resizeAspectFill
            view.layer.addSublayer(previewLayer)
            
            // 为PIP创建独立的预览层
            pipPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            pipPreviewLayer.frame = pipVideoCallViewController.view.bounds
            pipPreviewLayer.videoGravity = .resizeAspectFill
            pipVideoCallViewController.view.layer.addSublayer(pipPreviewLayer)
            
        } catch {
            print("摄像头输入配置失败:\(error.localizedDescription)")
        }
    }
    
    private func setupPictureInPicture() {
        guard AVPictureInPictureController.isPictureInPictureSupported() else {
            print("设备不支持PIP模式")
            return
        }
        
        pipVideoCallViewController.preferredContentSize = CGSize(width: 640, height: 480)
        
        pipContentSource = AVPictureInPictureController.ContentSource(
            activeVideoCallSourceView: view,
            contentViewController: pipVideoCallViewController)
        
        pictureInPictureController = AVPictureInPictureController(contentSource: pipContentSource)
        pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
        // 监听PIP状态变化,确保后台时会话正常运行
        pictureInPictureController.delegate = self
    }
    
    @objc func sessionWasInterrupted(notification: NSNotification) {
        guard let userInfo = notification.userInfo,
              let reasonValue = userInfo[AVCaptureSessionInterruptionReasonKey] as? Int,
              let reason = AVCaptureSession.InterruptionReason(rawValue: reasonValue) else {
            return
        }
        
        switch reason {
        case .videoDeviceNotAvailableInBackground:
            print("后台无法访问摄像头")
        case .audioDeviceInUseByAnotherClient, .videoDeviceInUseByAnotherClient:
            print("音视频设备被其他应用占用")
        case .videoDeviceNotAvailableWithMultipleForegroundApps:
            print("多前台应用导致摄像头不可用")
        case .videoDeviceNotAvailableDueToSystemPressure:
            print("系统压力导致摄像头不可用")
        @unknown default:
            print("未知中断原因")
        }
    }
    
    @objc func sessionInterruptionEnded(_ notification: NSNotification) {
        print("捕获会话中断结束")
        // 重新启动会话(如果需要)
        if !captureSession.isRunning {
            DispatchQueue.global(qos: .userInitiated).async {
                self.captureSession.startRunning()
            }
        }
    }
    
    @objc func sessionRuntimeError(_ notification: NSNotification) {
        if let error = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError {
            print("捕获会话运行错误:\(error.localizedDescription)")
            // 处理会话错误,比如重新启动
            if error.code == AVError.deviceWasDisconnected.rawValue {
                DispatchQueue.global(qos: .userInitiated).async {
                    self.captureSession.startRunning()
                }
            }
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        captureSession.stopRunning()
    }
}

// 实现PIP代理,处理状态变化
extension ViewController: AVPictureInPictureControllerDelegate {
    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PIP模式已启动")
    }
    
    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PIP模式已停止")
    }
}

关键修复点说明

  • 音频会话配置:改用.playAndRecord类别和.videoChat模式,这是视频通话场景的标准配置,支持后台音视频同时工作,确保系统不会限制摄像头访问。
  • 独立预览层:为主视图和PIP窗口分别创建AVCaptureVideoPreviewLayer,避免同一个视图被添加到两个层级导致的画面冻结问题。
  • 权限检查:显式请求摄像头权限,确保用户授权后再初始化捕获会话。
  • 错误处理:替换try?为do-catch,输出具体错误信息,便于排查问题。
  • PIP代理:实现AVPictureInPictureControllerDelegate,监听PIP状态变化,必要时恢复捕获会话。
  • 会话预设调整:改用.hd1280x720适合视频通话的分辨率,避免photo预设带来的性能和兼容性问题。

额外注意事项

  1. 确保在Info.plist中添加以下权限描述:
    • NSCameraUsageDescription:摄像头权限说明
    • NSMicrophoneUsageDescription:麦克风权限说明
    • UIBackgroundModes:添加audio和voip(如果是VOIP通话)
  2. 测试时确保没有其他前台应用占用摄像头,否则仍会触发videoDeviceNotAvailableWithMultipleForegroundApps错误。
  3. 对于iOS 15+,可以使用AVCaptureDevice.isMultitaskingAccessSupported进一步验证多任务摄像头访问支持情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 20:42:32