Picture-in-Picture (PiP) keeps a video stream visible in a small floating window when users switch to other apps — letting them stay engaged in a call, class, or live stream without interruption. This topic shows how to implement PiP in an iOS app using Alibaba Real-Time Communication (ARTC).
Feature overview
PiP displays a video in a small, floating window that stays visible in a corner of the screen while the user switches to other applications.
Common use cases:
|
Use case |
Description |
|
Video calling |
Keeps the call visible when the user switches apps or returns to the home screen. Useful for customer service, remote meetings, and social calls. |
|
Online education |
Lets students watch a teacher's video while reviewing notes or using other apps. |
|
Interactive live streaming |
Lets viewers browse, chat, or use other apps while the stream continues in the background. |
Sample code
ARTC provides open-source sample code for reference: Implement PiP on iOS.
Prerequisites
Before you begin, make sure that:
iOS 15 or later is required.
To keep the camera active after the app 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 ARTC video stream inside the PiP window, enable custom video rendering first. For details, see Custom video rendering.
2. Create a PiP controller
Call setupPipController(with:) to initialize the PiP controller. The function saves a reference to the source view (without moving it), creates a content source bound to that view, and instantiates AVPictureInPictureController.
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 pipController.startPictureInPicture to enter PiP mode. If the controller has not been initialized, call setupPipController(with:) first using the local preview view.
// 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
Implement AVPictureInPictureControllerDelegate to manage view migration when PiP starts and stops. When PiP starts, move the seat view into the PiP window. When it stops, return the seat view to its original superview.
// 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)
}
}
}