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

SwiftUI:相机预览无法进入PiP模式的问题求助

问题:SwiftUI相机预览无法通过Pipify进入画中画模式

我正在开发一款SwiftUI应用,实现了相机预览组件,但通过Pipify包触发画中画(PiP)模式时,普通文字内容可以正常生效,但相机预览无法进入PiP模式。期望点击「Launch PiP mode」按钮后,相机预览能进入PiP模式,求解决方法或替代实现方案。

原相机预览组件代码

import SwiftUI
import AVFoundation
import AVKit

struct CameraPreview: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        let controller = CameraPreviewViewController()
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}

class CameraPreviewViewController: UIViewController {
    var session: AVCaptureSession?
    var previewLayer: AVCaptureVideoPreviewLayer?
    var player: AVPlayer?
    var playerLayer: AVPlayerLayer?
    var pipController: AVPictureInPictureController?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSession()
        setupPreview()
        startPIP()
    }

    func setupSession() {
        session = AVCaptureSession()
        let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .front)
        guard let device = deviceDiscoverySession.devices.first else { return }
        guard let input = try? AVCaptureDeviceInput(device: device) else { return }
        session?.addInput(input)
    }

    func setupPreview() {
        previewLayer = AVCaptureVideoPreviewLayer(session: session!)
        previewLayer?.videoGravity = AVLayerVideoGravity.resize
        previewLayer?.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        view.layer.insertSublayer(previewLayer!, at: 0)
        player = AVPlayer()
        playerLayer = AVPlayerLayer(player: player)
        playerLayer?.frame = previewLayer!.frame
        view.layer.addSublayer(playerLayer!)
        session?.startRunning()
        player?.play()
    }

    func startPIP() {
        guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
        pipController = AVPictureInPictureController(playerLayer: playerLayer!)
        pipController?.startPictureInPicture()        
    }
}

原ContentView代码

import SwiftUI
import Pipify

struct ContentView: View {
    @State var isPresentedThree = false

    var body: some View {
        VStack {
            HStack{
                Image(systemName: "camera")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Image(systemName: "person")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
            }
            Text("FLOATING CAMERA")
                .padding(6.0)
            Button("Launch PiP mode") { isPresentedThree.toggle() }
                            .pipify(isPresented: $isPresentedThree) {
                                CameraPreview()
                                    .foregroundColor(.red)
                                    .padding()
                                    .onPipSkip { _ in }
                                    .onPipPlayPause { _ in }
                            }

        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

问题分析

  1. Pipify依赖AVPlayerLayer实现PiP,但原代码中创建的是空AVPlayer,没有传入相机视频流,导致PiP无内容可显示。
  2. 相机预览使用的AVCaptureVideoPreviewLayer无法直接被Pipify识别为PiP视频源。
  3. 原代码在CameraPreviewViewController内部调用startPIP(),与Pipify的触发逻辑冲突。

解决方法:适配Pipify的相机PiP实现

修改相机预览组件,将视频流传入AVPlayer

调整CameraPreview,把相机视频流输出给AVPlayer,让Pipify可基于该AVPlayer实现PiP:

import SwiftUI
import AVFoundation
import AVKit

struct CameraPreview: UIViewControllerRepresentable {
    let player: AVPlayer
    
    func makeUIViewController(context: Context) -> UIViewController {
        let controller = CameraPreviewViewController(player: player)
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}

class CameraPreviewViewController: UIViewController {
    var session: AVCaptureSession?
    var previewLayer: AVCaptureVideoPreviewLayer?
    let player: AVPlayer
    var videoOutput: AVCaptureVideoDataOutput?
    private let customAsset = AVURLAsset(url: URL(string: "camera://stream")!)
    
    init(player: AVPlayer) {
        self.player = player
        super.init(nibName: nil, bundle: nil)
        setupAssetLoader()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSession()
        setupPreview()
        setupVideoOutput()
    }

    func setupSession() {
        session = AVCaptureSession()
        session?.sessionPreset = .medium
        
        let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front)
        guard let device = deviceDiscoverySession.devices.first else { return }
        
        do {
            let input = try AVCaptureDeviceInput(device: device)
            if session!.canAddInput(input) {
                session!.addInput(input)
            }
        } catch {
            print("相机输入初始化失败: \(error)")
            return
        }
    }

    func setupPreview() {
        guard let session = session else { return }
        
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer?.videoGravity = .resizeAspectFill
        previewLayer?.frame = view.bounds
        view.layer.insertSublayer(previewLayer!, at: 0)
        
        session.startRunning()
    }
    
    func setupVideoOutput() {
        guard let session = session else { return }
        
        videoOutput = AVCaptureVideoDataOutput()
        videoOutput?.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera.video.output"))
        
        if session.canAddOutput(videoOutput!) {
            session.addOutput(videoOutput!)
        }
        
        let playerItem = AVPlayerItem(asset: customAsset)
        player.replaceCurrentItem(with: playerItem)
    }
    
    func setupAssetLoader() {
        customAsset.resourceLoader.setDelegate(self, queue: DispatchQueue(label: "camera.resource.loader"))
    }
}

extension CameraPreviewViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // 生产环境建议将CMSampleBuffer转换为AVPlayer可播放的编码格式,此处简化处理
    }
}

extension CameraPreviewViewController: AVAssetResourceLoaderDelegate {
    func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        loadingRequest.finishLoading(with: nil, redirect: nil, data: nil, response: nil, error: nil)
        return true
    }
}

修改ContentView,关联AVPlayer到Pipify

import SwiftUI
import Pipify
import AVKit

struct ContentView: View {
    @State var isPresentedThree = false
    private let player = AVPlayer()

    var body: some View {
        VStack {
            HStack{
                Image(systemName: "camera")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Image(systemName: "person")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
            }
            Text("FLOATING CAMERA")
                .padding(6.0)
            
            Button("Launch PiP mode") { 
                isPresentedThree.toggle() 
            }
            .pipify(isPresented: $isPresentedThree) {
                CameraPreview(player: player)
                    .frame(width: 200, height: 200)
                    .onPipSkip { _ in }
                    .onPipPlayPause { isPlaying in
                        isPlaying ? player.play() : player.pause()
                    }
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

替代方案:不依赖Pipify,直接使用系统API实现PiP

如果觉得流转换逻辑复杂,可直接基于系统AVPictureInPictureController实现,无需第三方库:

import SwiftUI
import AVFoundation
import AVKit

struct CameraPiPView: UIViewControllerRepresentable {
    @Binding var isPiPActive: Bool
    
    func makeUIViewController(context: Context) -> CameraPiPViewController {
        let controller = CameraPiPViewController()
        controller.pipStateChanged = { isActive in
            isPiPActive = isActive
        }
        return controller
    }
    
    func updateUIViewController(_ uiViewController: CameraPiPViewController, context: Context) {
        if isPiPActive && !uiViewController.isPiPActive {
            uiViewController.startPiP()
        } else if !isPiPActive && uiViewController.isPiPActive {
            uiViewController.stopPiP()
        }
    }
}

class CameraPiPViewController: UIViewController {
    var session: AVCaptureSession?
    var previewLayer: AVCaptureVideoPreviewLayer?
    var pipController: AVPictureInPictureController?
    var pipStateChanged: ((Bool) -> Void)?
    var isPiPActive: Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCameraSession()
        setupPreviewLayer()
        setupPiPController()
    }
    
    func setupCameraSession() {
        session = AVCaptureSession()
        session?.sessionPreset = .medium
        
        guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front).devices.first else { return }
        
        do {
            let input = try AVCaptureDeviceInput(device: device)
            if session!.canAddInput(input) {
                session!.addInput(input)
            }
        } catch {
            print("相机初始化失败: \(error)")
            return
        }
        
        session?.startRunning()
    }
    
    func setupPreviewLayer() {
        guard let session = session else { return }
        
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer?.videoGravity = .resizeAspectFill
        previewLayer?.frame = view.bounds
        view.layer.addSublayer(previewLayer!)
    }
    
    func setupPiPController() {
        guard AVPictureInPictureController.isPictureInPictureSupported(), 
              let previewLayer = previewLayer as? AVPlayerLayer else { return }
        
        pipController = AVPictureInPictureController(playerLayer: previewLayer)
        pipController?.delegate = self
    }
    
    func startPiP() {
        pipController?.startPictureInPicture()
    }
    
    func stopPiP() {
        pipController?.stopPictureInPicture()
    }
}

extension CameraPiPViewController: AVPictureInPictureControllerDelegate {
    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        isPiPActive = true
        pipStateChanged?(true)
    }
    
    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        isPiPActive = false
        pipStateChanged?(false)
    }
}

struct ContentView: View {
    @State var isPiPActive = false
    
    var body: some View {
        VStack {
            HStack{
                Image(systemName: "camera")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Image(systemName: "person")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
            }
            Text("FLOATING CAMERA")
                .padding(6.0)
            
            Button(isPiPActive ? "Stop PiP" : "Launch PiP mode") {
                isPiPActive.toggle()
            }
            
            CameraPiPView(isPiPActive: $isPiPActive)
                .frame(width: 200, height: 200)
                .cornerRadius(10)
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

内容的提问来源于stack exchange,提问作者Baudouin Arbarétier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 19:40:26