AVCaptureVideoPreviewLayer in both pip controller and view controller doesn't work

210 views Asked by At

I'm attempting to integrate a PiP (Picture-in-Picture) controller alongside the camera preview within a view controller. However, the camera preview is only visible in the view to which it's first assigned(The other one just goes black). Is it possible to use the same AVCaptureVideoPreviewLayer instance for both views? Or what needs to change here?

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)

        let previewLayer = previewView.previewLayer
        previewLayer.frame = view.layer.bounds
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.connection?.videoOrientation = .landscapeLeft
        view.layer.addSublayer(previewLayer)

        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)
    }
}

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)
        ])
    }

}
1

There are 1 answers

1
Hezy Ziv On

AVCaptureVideoPreviewLayer can only be added to a single view, create a single AVCaptureVideoPreviewLayer add it as a sublayer to both views

var previewLayer: AVCaptureVideoPreviewLayer?

viewDidLoad add the AV to both

override func viewDidLoad() {
    super.viewDidLoad()

    let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    previewLayer.frame = view.layer.bounds
    previewLayer.videoGravity = .resizeAspectFill
    previewLayer.connection?.videoOrientation = .landscapeLeft
    view.layer.addSublayer(previewLayer)
    self.previewLayer = previewLayer

    pipVideoCallViewController = .init(previewLayer: previewLayer,
                                       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()
}

Update the Controller

convenience init(previewLayer: AVCaptureVideoPreviewLayer, preferredContentSize: CGSize) {
    self.init()
    self.preferredContentSize = preferredContentSize
    previewLayer.frame = view.frame
    view.layer.addSublayer(previewLayer)
}