Picture-in-Picture
Updated
Keep video visible in a floating overlay when your app moves to the background or the user switches to another app.
This feature is supported only on React Native.
When your application moves to the background or the user switches to another app, the Picture-in-Picture (PiP) feature in Video SDK keeps the current video visible as a floating overlay window on screen. This lets users multitask without interrupting an ongoing call or viewing session, improving the multitasking experience and user retention.
PiP is useful in the following scenarios:
| Use case | Description |
|---|---|
| Video calling | Keeps the call visible when the user switches apps or returns to the home screen. Useful for social calls, remote meetings, and online customer service. |
| Online education | Lets students watch a teacher's video explanation while reviewing study materials, taking notes, or using other apps. |
| Interactive live streaming | Lets viewers browse products, chat, or use other apps while the stream continues in the background. Useful for e-commerce and content live streaming. |
| Screen sharing | Keeps shared content visible when the user switches apps during remote collaboration or a presentation. Useful for remote work, technical support, and product demos. |
Prerequisites
Before implementing Picture-in-Picture, make sure you have the following:
-
Video SDK for React Native v4.6.2 or later integrated into your project. See SDK quickstart.
-
To use PiP, the target device must run one of the following:
- Android 8.0 or later
- iOS 15.0 or later
Set up your project
Before using PiP, complete the required configuration for each target platform.
Declare that the current Activity supports PiP mode in your Android project:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />Then, make MainActivity extend AgoraPIPActivity:
import io.agora.rtc.ng.react.AgoraPIPActivity
class MainActivity : AgoraPIPActivity() {
}If you need audio or video capture and playback to continue after the app moves to the background or enters PiP, also complete the foreground service configuration. For the required permissions, manifest declarations, and integration steps, see Android background restrictions.
The way PiP is triggered differs by Android version:
- Android 12 and later: The system can automatically enter PiP when the user leaves the current screen in appropriate scenarios. You can combine this with
autoEnterEnabled. - Earlier than Android 12: The system does not support automatic PiP entry via
autoEnterEnabled. You typically need to trigger PiP entry explicitly. WhenMainActivityextendsAgoraPIPActivity, the SDK handles the relevant system callback adaptation.
Add the background playback capability to your app in Xcode:
-
Open your target's Signing & Capabilities tab.
-
Click + Capability.
-
Add Background Modes.
-
Select Audio, AirPlay, and Picture in Picture.
If your use case requires capturing local camera video in multi-tasking mode, contact support@agora.io for guidance on enabling iOS multi-tasking capture. The React Native SDK does not directly expose the
multitaskingCameraAccessEnabledconfiguration. If your use case requires local camera capture during PiP, additional native-side adaptation is needed.
Implement Picture-in-Picture
Starting from Video SDK v4.6.2, Agora provides the AgoraPip controller and related configuration options for enabling and managing PiP on Android and iOS. This section describes how to implement Picture-in-Picture in your React Native project.
Get the PiP controller and register a listener
After initializing the RTC engine, call getAgoraPip to get the PiP controller and register a state change listener:
this.engine = createAgoraRtcEngine();
this.engine.initialize({
appId,
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
});
const pip = this.engine.getAgoraPip();
pip.registerPipStateChangedObserver(this);Check device support
Check whether the current device and system support the required capabilities:
const isPipSupported = pip.pipIsSupported();
const isPipAutoEnterSupported = pip.pipIsAutoEnterSupported();pipIsSupportedchecks whether the current device supports PiP.pipIsAutoEnterSupportedchecks whether the current device and system support automatic PiP entry.
Configure PiP options
Call pipSetup to configure PiP options. Use AgoraPipOptions to set platform-specific options for Android and iOS.
Pass the measured bounds of the video view to sourceRectHint to improve the transition animation when entering PiP.
On Android, you typically need to configure the aspect ratio, the initial source region, and seamless resize:
const videoViewBounds = {
left: measuredLeft,
top: measuredTop,
right: measuredRight,
bottom: measuredBottom,
};
const options: AgoraPipOptions = {
autoEnterEnabled: pip.pipIsAutoEnterSupported(),
aspectRatioX: 960,
aspectRatioY: 540,
sourceRectHintLeft: videoViewBounds.left,
sourceRectHintTop: videoViewBounds.top,
sourceRectHintRight: videoViewBounds.right,
sourceRectHintBottom: videoViewBounds.bottom,
seamlessResizeEnabled: true,
useExternalStateMonitor: true,
externalStateMonitorInterval: 100,
};
pip.pipSetup(options);useExternalStateMonitor defaults to true. The example above keeps this value explicit for clarity. Only set it to false if you have already extended AgoraPIPActivity and explicitly do not want polling-based state synchronization.
On iOS, you can configure the PiP window size, video stream layout, and system control style:
const options: AgoraPipOptions = {
autoEnterEnabled: pip.pipIsAutoEnterSupported(),
preferredContentWidth: 960,
preferredContentHeight: 540,
contentView: 0,
sourceContentView: 0,
contentViewLayout: {
padding: 0,
spacing: 2,
row: 1,
column: 2,
},
videoStreams: [
{
connection: {
channelId,
localUid: uid,
},
canvas: {
sourceType: VideoSourceType.VideoSourceCamera,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
renderMode: RenderModeType.RenderModeHidden,
mirrorMode: VideoMirrorModeType.VideoMirrorModeEnabled,
},
},
...remoteUsers.map((userUid) => ({
connection: {
channelId,
localUid: userUid,
},
canvas: {
uid: userUid,
sourceType: VideoSourceType.VideoSourceRemote,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
renderMode: RenderModeType.RenderModeHidden,
},
})),
],
controlStyle: 2,
};
pip.pipSetup(options);In this example:
- When
contentViewis set to0, the SDK manages the native views inside the PiP window. If you pass a non-zero value, you are responsible for managing the native view layout. videoStreamsdefines the local and remote streams to display.contentViewLayoutconfigures the grid layout for video streams inside the PiP window. Streams are arranged from left to right, then top to bottom.controlStyle: 2hides the play/pause button and progress bar, keeping only the essential window control buttons. This is recommended for real-time interactive scenarios.
For standard React Native integrations, keep both contentView and sourceContentView set to 0. This lets the SDK manage the native views, layout, and restore target inside the PiP window. Pass non-zero values only if you have already completed the corresponding native view integration.
Listen for PiP state changes
Implement AgoraPipStateChangedObserver to listen for PiP state changes. Use the AgoraPipState value to update the UI or handle errors:
onPipStateChanged(state: AgoraPipState, error: string | null): void {
if (state === AgoraPipState.pipStateFailed) {
this.engine?.getAgoraPip().pipDispose();
}
this.setState({ pipState: state });
}AgoraPipState defines the following states:
pipStateStarted: PiP has started successfully.pipStateStopped: PiP has stopped.pipStateFailed: PiP failed to start or encountered an error while running.
Start, stop, and destroy PiP
PiP-related calls operate at two lifecycle levels:
- Session level:
pipStart,pipStop, andpipDisposecontrol the start, stop, and resource release of a single PiP session. - Controller or page level:
releaseperforms the final cleanup when the page or controller is no longer needed.
After completing configuration, call pipStart to start PiP. Call release only when destroying the page or when the controller is no longer needed, to avoid redundant session-level calls during teardown.
// Session level: start PiP
pip.pipStart();
// Session level: end the current PiP session
pip.pipDispose();
// Controller/page destroy level: final release
pip.release();On Android, pipStop is not the standard path for exiting PiP and restoring the foreground page. If you no longer need the current PiP session, call pipDispose to release resources. Restoring full-screen view is typically handled by the user through the system PiP window controls.
Call pipStop to stop PiP and restore the app interface, then call pipDispose to release the session resources. Call release only when destroying the page or when the controller is no longer needed.
// Session level: start PiP
pip.pipStart();
// Session level: stop PiP and restore the app interface
pip.pipStop();
// Session level: release the current PiP session resources
pip.pipDispose();
// Controller/page destroy level: final release
pip.release();Development considerations
This section covers platform-specific behavior and best practices to keep in mind when integrating PiP.
- After entering PiP on Android, you typically need to update the app UI to match the PiP state.
- Android 12 and later support automatic PiP entry when the app moves to the background. On earlier versions, you must trigger PiP entry explicitly, for example by calling
pipStartin response to a user action. ExtendingAgoraPIPActivitysimplifies this adaptation. - PiP does not bypass Android background restrictions. If you need to continue capturing or playing audio and video in the background, combine PiP with a foreground service. For details, see Android background restrictions.
- On Android,
pipStopmoves the task to the background rather than exiting PiP and restoring the foreground page. Full-screen restoration is typically handled by the user through the system PiP window controls. - Distinguish between session-level and controller-level release: call
pipDisposeto end the current PiP session, and callreleaseonly when the page is destroyed or the controller is no longer needed.
- PiP must be triggered by explicit user action only, such as tapping a button or swiping up to the home screen. Triggering PiP programmatically without a user action may result in App Store review rejection.
- The PiP window is managed by the system and the SDK. Your app does not need to render a custom floating window.
- To display multiple video streams in PiP mode, configure the layout explicitly using
videoStreamsandcontentViewLayout. When remote users join or leave, callpipSetupagain with the updatedvideoStreams. You do not need to callpipDisposefirst. AgoraPipOptions.controlStyleaccepts values0to3. Use2for video call scenarios, which hides the play/pause button and progress bar. Style3hides all controls, including the close and restore buttons.- Distinguish between session-level and controller-level release: call
pipStopandpipDisposeto end the current PiP session, and callreleaseonly when the page is destroyed or the controller is no longer needed.
Reference
This section includes links to related API reference and additional resources.
API reference
AgoraPipAgoraPipOptionsAgoraPipContentViewLayoutAgoraPipVideoStreampipIsSupportedpipIsAutoEnterSupportedpipSetuppipStartpipStoppipDisposeregisterPipStateChangedObserverAgoraPipStateChangedObserverAgoraPipStateonPipStateChanged
Sample project
Agora provides a sample project demonstrating Picture-in-Picture. Download or view the source code.
