AVPictureInPictureVideoCallController with AVCaptureVideoPreviewLayer

The documentation for AVPictureInPictureVideoCallController suggests that it works with an AVCaptureVideoPreviewLayer in addition to the AVSampleBufferDisplayLayer, however I'm having a hard time getting it to actually work. Here's my sample code:

class PIPPreviewView: UIView {
    override class var layerClass: AnyClass {
        get { return AVCaptureVideoPreviewLayer.self }
    }
    
    var previewLayer: AVCaptureVideoPreviewLayer {
        return layer as! AVCaptureVideoPreviewLayer
    }
    
    var session: AVCaptureSession? {
        get { previewLayer.session }
        set { previewLayer.session = newValue }
    }
}

final class PreviewViewController: UIViewController, AVPictureInPictureControllerDelegate {
    private let captureSession = AVCaptureSession()
    var previewLayer: AVCaptureVideoPreviewLayer!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
        
        do {
            let videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            captureSession.addInput(videoInput)
        } catch {
            return
        }
        
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = view.layer.bounds
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.connection?.videoOrientation = .landscapeLeft
        view.layer.addSublayer(previewLayer)

        let pipview = PIPPreviewView()
        pipview.session = self.captureSession
        
        let pipvc = AVPictureInPictureVideoCallViewController()
        pipvc.preferredContentSize = CGSize(width: 1080, height: 1920)
        pipvc.view.addSubview(pipview)
        
        let pipcs = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: pipview, contentViewController: pipvc)
        
        let pipc = AVPictureInPictureController(contentSource: pipcs)
        pipc.canStartPictureInPictureAutomaticallyFromInline = true
        pipc.delegate = self
        
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
            print("starting pip")
            pipc.startPictureInPicture()
        }
        
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (!captureSession.isRunning) {
            DispatchQueue.global(qos: .background).async {
                self.captureSession.startRunning()
            }
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        if (captureSession.isRunning) {
            captureSession.stopRunning()
        }
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
}

When I run this, the preview layer in app is black and the application doesn't trigger PIP. Has anyone gotten this to work properly?

Answered by DTS Engineer in 742593022

Hello,

It does work for AVCaptureVideoPreviewLayer, your example has two different AVCaptureVideoPreviewLayers, so perhaps that is causing some issues. In any case, I will just post a minimal working example here, I recommend that you compare against this example to see what may be different in your implementation:

PreviewView.swift

import UIKit
import AVKit

class PreviewView: UIView {
    override class var layerClass: AnyClass {
        AVCaptureVideoPreviewLayer.self
    }
    
    var previewLayer: AVCaptureVideoPreviewLayer {
        layer as! AVCaptureVideoPreviewLayer
    }
    
    init(_ session: AVCaptureSession) {
        super.init(frame: .zero)
        
        previewLayer.session = session
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension AVPictureInPictureVideoCallViewController {
    
    convenience init(_ previewView: PreviewView, preferredContentSize: CGSize) {
        
        // Initialize.
        self.init()
        
        // Set the preferredContentSize.
        self.preferredContentSize = preferredContentSize
        
        // Configure the PreviewView.
        previewView.translatesAutoresizingMaskIntoConstraints = false
        previewView.frame = self.view.frame
        
        self.view.addSubview(previewView)
        
        NSLayoutConstraint.activate([
            previewView.topAnchor.constraint(equalTo: self.view.topAnchor),
            previewView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            previewView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            previewView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
        ])
    }
    
}

ViewController.swift

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {
    
    let captureSession = AVCaptureSession()
    let captureSessionQueue = DispatchQueue(label: "Capture Session Queue")
    
    var pipVideoCallViewController: AVPictureInPictureVideoCallViewController!
    var pipController: AVPictureInPictureController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let previewView = PreviewView(captureSession)
        
        pipVideoCallViewController = .init(previewView,
                                           preferredContentSize: CGSize(width: 1080, height: 1920))
        
        let pipContentSource = AVPictureInPictureController.ContentSource(
                                    activeVideoCallSourceView: view,
                                    contentViewController: pipVideoCallViewController)
        
        pipController = AVPictureInPictureController(contentSource: pipContentSource)
        pipController.delegate = self
        pipController.canStartPictureInPictureAutomaticallyFromInline = true
        
        startSession()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if !pipController.isPictureInPictureActive {
            pipController.startPictureInPicture()
        }
    }

    private func startSession() {
        captureSessionQueue.async { [unowned self] in
            
            let device = AVCaptureDevice.default(for: .video)!
            
            captureSession.addInput(try! AVCaptureDeviceInput(device: device))
            
            captureSession.sessionPreset = .hd1920x1080
            
            captureSession.isMultitaskingCameraAccessEnabled = captureSession.isMultitaskingCameraAccessSupported
                        
            captureSession.startRunning()
        }
    }

}

extension ViewController: AVPictureInPictureControllerDelegate {
    
    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
        print(error.localizedDescription)
    }
}
Accepted Answer

Hello,

It does work for AVCaptureVideoPreviewLayer, your example has two different AVCaptureVideoPreviewLayers, so perhaps that is causing some issues. In any case, I will just post a minimal working example here, I recommend that you compare against this example to see what may be different in your implementation:

PreviewView.swift

import UIKit
import AVKit

class PreviewView: UIView {
    override class var layerClass: AnyClass {
        AVCaptureVideoPreviewLayer.self
    }
    
    var previewLayer: AVCaptureVideoPreviewLayer {
        layer as! AVCaptureVideoPreviewLayer
    }
    
    init(_ session: AVCaptureSession) {
        super.init(frame: .zero)
        
        previewLayer.session = session
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension AVPictureInPictureVideoCallViewController {
    
    convenience init(_ previewView: PreviewView, preferredContentSize: CGSize) {
        
        // Initialize.
        self.init()
        
        // Set the preferredContentSize.
        self.preferredContentSize = preferredContentSize
        
        // Configure the PreviewView.
        previewView.translatesAutoresizingMaskIntoConstraints = false
        previewView.frame = self.view.frame
        
        self.view.addSubview(previewView)
        
        NSLayoutConstraint.activate([
            previewView.topAnchor.constraint(equalTo: self.view.topAnchor),
            previewView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            previewView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            previewView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
        ])
    }
    
}

ViewController.swift

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {
    
    let captureSession = AVCaptureSession()
    let captureSessionQueue = DispatchQueue(label: "Capture Session Queue")
    
    var pipVideoCallViewController: AVPictureInPictureVideoCallViewController!
    var pipController: AVPictureInPictureController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let previewView = PreviewView(captureSession)
        
        pipVideoCallViewController = .init(previewView,
                                           preferredContentSize: CGSize(width: 1080, height: 1920))
        
        let pipContentSource = AVPictureInPictureController.ContentSource(
                                    activeVideoCallSourceView: view,
                                    contentViewController: pipVideoCallViewController)
        
        pipController = AVPictureInPictureController(contentSource: pipContentSource)
        pipController.delegate = self
        pipController.canStartPictureInPictureAutomaticallyFromInline = true
        
        startSession()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if !pipController.isPictureInPictureActive {
            pipController.startPictureInPicture()
        }
    }

    private func startSession() {
        captureSessionQueue.async { [unowned self] in
            
            let device = AVCaptureDevice.default(for: .video)!
            
            captureSession.addInput(try! AVCaptureDeviceInput(device: device))
            
            captureSession.sessionPreset = .hd1920x1080
            
            captureSession.isMultitaskingCameraAccessEnabled = captureSession.isMultitaskingCameraAccessSupported
                        
            captureSession.startRunning()
        }
    }

}

extension ViewController: AVPictureInPictureControllerDelegate {
    
    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
        print(error.localizedDescription)
    }
}

@gchiste Thanks for your awesome answer! I managed to open my app to PiP mode with your code. However, the camera view freezes and the AVCaptureVideoDataOutput stops outputting after a moment when PiP started. How can I make the camera continue to work? By the way, I'm working on iOS16, which should be valid for multitasking camera without a entitlement according to this document, but captureSession.isMultitaskingCameraAccessSupported always returns false nonetheless. What should I do?

AVPictureInPictureVideoCallController with AVCaptureVideoPreviewLayer
 
 
Q