All Products
Search
Document Center

ApsaraVideo Live:Implement Picture-in-Picture on iOS

Last Updated:Sep 24, 2025

This topic describes how to implement the Picture-in-Picture (PiP) feature on iOS.

Feature overview

Picture-in-Picture (PiP) is a feature that displays a video in a small, floating window. This window remains in a corner of the screen, allowing you to watch the video without interruption while you use other applications, such as replying to messages or browsing the web.

Sample code

Alibaba Real-Time Communication (ARTC) provides open-source sample code for your reference: Implement PiP on iOS.

Prerequisites

To implement PiP, the following requirements must be met:

  • iOS 15 or later is required.

  • To keep the camera active after the application enters the background, add the multitasking camera access entitlement to your developer account. For more information, see the Apple Entitlements documentation.

Implementation

1. Implement custom video rendering

To display the RTC video in the PiP window on iOS, implement the custom rendering feature. For more information, see Custom video rendering.

2. Create a PiP controller

func setupPictureInPicture() {
    guard #available(iOS 15.0, *) else {
        print("iOS < 15.0, system PiP is not supported")
        return
    }

    let callVC = AVPictureInPictureVideoCallViewController()
    callVC.preferredContentSize = CGSize(width: 720, height: 1280)
    callVC.view.backgroundColor = .clear

    self.pipCallViewController = callVC
}

@available(iOS 15.0, *)
func setupPipController(with seatView: CustomVideoRenderSeatView) {
    
    // Save only the reference and superview information. Do not shift the view.
    seatView.originalSuperview = seatView.superview
    self.pipSourceView = seatView
    
    // PiP container (initially empty)
    let callVC = AVPictureInPictureVideoCallViewController()
    callVC.preferredContentSize = CGSize(width: 720, height: 1280)
    callVC.view.backgroundColor = .clear
    self.pipCallViewController = callVC
    
    // Create ContentSource (activeVideoCallSourceView is still seatView here)
    // Note: Use seatView as the source, not callVC.view
    let contentSource = AVPictureInPictureController.ContentSource(
        activeVideoCallSourceView: seatView,
        contentViewController: callVC
    )
    
    let pipController = AVPictureInPictureController(contentSource: contentSource)
    pipController.delegate = self
    pipController.canStartPictureInPictureAutomaticallyFromInline = false
    
    self.pipController = pipController
}

3. Enter PiP mode

Call the system API pipController.startPictureInPicture to enter PiP mode.

// MARK: - Enter PIP Mode
@IBAction func onEnterPIPModeBtnClicked(_ sender: UIButton) {
    enterPictureInPictureMode()
}

func enterPictureInPictureMode() {
    guard AVPictureInPictureController.isPictureInPictureSupported() else {
        UIAlertController.showAlertWithMainThread(msg: "The current device does not support PiP", vc: self)
        return
    }

    guard #available(iOS 15.0, *) else {
        UIAlertController.showAlertWithMainThread(msg: "PiP requires iOS 15 or later", vc: self)
        return
    }

    // If the controller has not been created, try to initialize it.
    if pipController == nil {
        guard let seatView = self.seatViewList.first(where: { $0.uid == self.userId }) else {
            print("Failed to get the local preview view")
            return
        }
        setupPipController(with: seatView)
    }

    guard let pipController = self.pipController else { return }

    if pipController.isPictureInPictureActive {
        pipController.stopPictureInPicture()
    } else {
        pipController.startPictureInPicture()
    }
}

4. Enable PiP and load the view into the PiP window

// MARK: - AVPictureInPictureControllerDelegate
@available(iOS 15.0, *)
extension PictureInPictureMainVC: AVPictureInPictureControllerDelegate {

    func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("Will start PiP mode")
        
        // Migrate seatView.
        if let seatView = self.pipSourceView as? CustomVideoRenderSeatView,
           let callVC = self.pipCallViewController as? AVPictureInPictureVideoCallViewController {
            
            seatView.removeFromSuperview()
            callVC.view.addSubview(seatView)
            seatView.frame = callVC.view.bounds
            seatView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        }
    }


    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("Did start PiP mode")
    }

    func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("Will stop PiP mode")
    }

    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("Did stop PiP mode")
        
        if let seatView = self.pipSourceView as? CustomVideoRenderSeatView,
           let originalSuperview = seatView.originalSuperview {
            
            seatView.removeFromSuperview()
            originalSuperview.addSubview(seatView)
            self.updateSeatViewsLayout()
        }
    }



    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
                   restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        print("Restore user interface")
        completionHandler(true)
    }

    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
                   failedToStartPictureInPictureWithError error: Error) {
        print("Failed to start PiP: \(error.localizedDescription)")
        DispatchQueue.main.async {
            UIAlertController.showAlertWithMainThread(msg: "Failed to start PiP: \(error.localizedDescription)", vc: self)
        }
    }
}