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() } }
问题分析
- Pipify依赖
AVPlayerLayer实现PiP,但原代码中创建的是空AVPlayer,没有传入相机视频流,导致PiP无内容可显示。 - 相机预览使用的
AVCaptureVideoPreviewLayer无法直接被Pipify识别为PiP视频源。 - 原代码在
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
相关产品推荐
相关产品推荐

