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