Skip to main content

Picture-in-Picture (PiP)

This guide covers how to integrate Picture-in-Picture (PiP) into your React Native application using @quickplay/rn-qp-nxg-player, including setup, programmatic control, auto-start behavior, and handling lifecycle events.

Platform: iOS only.

Prerequisites

Enable Background Modes for Picture-in-Picture

Before using any PiP APIs, enable the required iOS capability in Xcode.

  1. Open your iOS project in Xcode.
  2. Select your app target.
  3. Go to Signing & Capabilities.
  4. Add the Background Modes capability.
  5. Enable Audio, AirPlay, and Picture in Picture.
Background Mode

Overview

Picture-in-Picture allows users to continue watching video in a floating overlay while switching to other apps. The SDK exposes:

FeatureAPI
Start PiPplayer.startPictureInPicture()
Stop PiPplayer.stopPictureInPicture()
Auto-start on app backgroundplayer.allowAutoStartPiPFromInline()
Check if PiP is activeplayer.isPiPActive()
Lifecycle callbacksPlayerPictureInPictureListener
Keep PiP alive during CSAI adsshouldSupportPIP: true in PlayerConfig

Note: No configuration flag is required to enable general PiP. The APIs above work directly once the player is created. shouldSupportPIP is a separate flag only relevant when using CSAI/Google IMA ads — see Section 6.


1. Player UI Modes & PiP Button Behavior

PiP behavior differs depending on which player UI mode is active. The SDK supports two modes controlled by the canUseCustomPlayerControls flag in QPPlayerManager.


When canUseCustomPlayerControls is true — Native Controls

player.playWithNativeControls() must be called to present VideoPlayerViewController — a fully native fullscreen player UI built with AVKit.

This native screen has its own built-in PiP button that is shown or hidden automatically based on device support. When tapped, it internally calls:

// Start PiP
player.startPictureInPicture()

// Stop PiP (if already active)
player.stopPictureInPicture()

No additional wiring is needed from the React Native side — the native screen manages the entire PiP lifecycle.

Important: You must call player.playWithNativeControls() to get the native UI. Calling player.play() or player.playInline() will not present the native UI even if canUseCustomPlayerControls is true.


When canUseCustomPlayerControls is false — Custom RN Controls

QpNxgPlaybackView is rendered inline inside your React Native PlayerScreen using player.play() or player.playInline(). This screen has no built-in PiP button — you are responsible for adding one and wiring it to the SDK APIs manually.

Auto-Start PiP (App Backgrounded)

Call allowAutoStartPiPFromInline() once playback starts. This sets canStartPictureInPictureAutomaticallyFromInline = true on the underlying AVKit player, so iOS triggers PiP automatically when the user backgrounds the app — no button tap needed.

onStateChanged(playbackState: PlaybackStateValue, ...): void {
if (playbackState === 'STARTED') {
playerRef.current?.allowAutoStartPiPFromInline();
}
},

Manual PiP Button

Add an explicit PiP toggle button in your player controls UI:

{Platform.OS === 'ios' && (
<TouchableOpacity
onPress={async () => {
const player = playerRef.current;
if (player == null) return;
const isActive = await player.isPiPActive();
if (isActive) {
await player.stopPictureInPicture();
} else {
await player.startPictureInPicture();
}
}}
>
<Icon name="picture-in-picture" size={24} color="white" />
</TouchableOpacity>
)}

APIs Linked to the PiP Button

User ActionAPI CalledWhat it does
Tap PiP buttonplayer.startPictureInPicture()Starts PiP if supported and possible
Tap PiP button (PiP already active)player.stopPictureInPicture()Stops the active PiP window
App goes to backgroundAuto via allowAutoStartPiPFromInline()iOS triggers PiP automatically
Check current PiP stateplayer.isPiPActive()Returns true if PiP is currently active

UI Mode Comparison

Native Controls (canUseCustomPlayerControls: true)Custom RN Controls (canUseCustomPlayerControls: false)
Play API to callplayer.playWithNativeControls()player.play() or player.playInline()
Player UIVideoPlayerViewController (AVKit)QpNxgPlaybackView (React Native)
PiP ButtonBuilt-in, auto shown/hiddenMust be added manually
PiP APIs wired byNative layerYour RN code

2. Listening for PiP Lifecycle Events

Implement the PlayerPictureInPictureListener interface and register it on your player to receive callbacks for all PiP state transitions. These callbacks fire in both UI modes.

Interface

export interface PlayerPictureInPictureListener {
onPictureInPictureStarted(): void;
onPictureInPictureStopped(): void;
onPictureInPictureFailed(error: PlatformError): void;
onPictureInPictureEnded(): void;
}
CallbackDescription
onPictureInPictureStartedPiP window has become visible
onPictureInPictureStoppedPiP has stopped but playback may continue inline
onPictureInPictureFailedPiP failed to start; the error param contains details
onPictureInPictureEndedPiP session has fully ended and the player is dismissed

Register the Listener

import { Player, PlayerPictureInPictureListener, PlatformError } from '@quickplay/rn-qp-nxg-player';

const pipListener: PlayerPictureInPictureListener = {
onPictureInPictureStarted(): void {
console.log('PiP started');
},
onPictureInPictureStopped(): void {
console.log('PiP stopped');
},
onPictureInPictureFailed(error: PlatformError): void {
console.warn('PiP failed to start', error);
},
onPictureInPictureEnded(): void {
console.log('PiP ended — player dismissed');
},
};

// Add after the player is created
player.addListener(pipListener);

// Remove when the player is disposed
player.removeListener(pipListener);

3. Starting and Stopping PiP

Use player.startPictureInPicture() and player.stopPictureInPicture() to control PiP programmatically. The SDK only starts PiP if the device supports it and PiP is possible at that moment.

Note: These APIs are only relevant when using custom RN controls (canUseCustomPlayerControls: false). In native controls mode, the built-in PiP button handles this automatically.

// Start PiP
await player.startPictureInPicture();

// Stop PiP
await player.stopPictureInPicture();

4. Auto-Start PiP When Backgrounded

Call player.allowAutoStartPiPFromInline() after playback starts to enable automatic PiP when the user moves the app to the background. This sets canStartPictureInPictureAutomaticallyFromInline = true on the underlying AVKit player.

import { PlaybackStateValue } from '@quickplay/rn-qp-nxg-player';

// Inside your PlayerStateListener
onStateChanged(playbackState: PlaybackStateValue, ...): void {
if (playbackState === 'STARTED') {
player.allowAutoStartPiPFromInline();
}
},

5. Checking PiP Status

Use player.isPiPActive() to query whether PiP is currently active for a specific player instance.

const isActive: boolean = await player.isPiPActive();
if (isActive) {
// Player is currently in PiP mode
}

6. PiP with CSAI Ads

Restricting Seek During Ads in PiP

During ad playback, call player.setRequiresLinearPlayback(true) to prevent the user from seeking via the PiP controls. Reset it to false when the ad ends.

// Ad started (onAdBreakStart callback)
await player.setRequiresLinearPlayback(true);

// Ad ended (onAdBreakEnd callback)
await player.setRequiresLinearPlayback(false);

Keeping PiP Alive Across CSAI Preroll → Content Transition

When using Google IMA CSAI ads, set shouldSupportPIP: true in your PlayerConfig to keep the PiP window open after a preroll ad finishes and seamlessly transition back to main content playback.

const playerConfig: PlayerConfig = {
// ... other config
shouldSupportPIP: true, // Only relevant when using CSAI/Google IMA ads
};

Important: shouldSupportPIP has no effect on non-ad players. It is only passed internally to the Google IMA ad player to control PiP behaviour during the preroll→content transition.

Warning: To support this behaviour, the SDK internally sets allowRepeatPlayback(allow: true) on the content player when PiP mode is active. Do not override this preference via player.set(preferences: [.allowRepeatPlayback(allow: false)]) while PiP is enabled, as it will cause the PiP window to close when the preroll ad ends.

If you need to reset or modify playback preferences, do so only when PiP is not active, or ensure allowRepeatPlayback(allow: true) is re-applied afterwards.


7. Complete Example

Custom RN Controls Mode (canUseCustomPlayerControls: false)

Call player.play() or player.playInline(). You must add the PiP button manually and wire it to the SDK APIs.

import {
createPlayer,
Player,
PlayerPictureInPictureListener,
PlayerConfig,
PlaybackStateValue,
PlatformError,
} from '@quickplay/rn-qp-nxg-player';
import { Platform, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

useEffect((): any => {
const playerListener = {
onStateChanged(playbackState: PlaybackStateValue): void {
if (playbackState === 'STARTED') {
// Enable auto-start PiP when app is backgrounded
playerRef.current?.allowAutoStartPiPFromInline();
}
},
onPictureInPictureStarted() {
console.log('PiP started');
// Optionally hide your inline player UI
},
onPictureInPictureStopped() {
console.log('PiP stopped');
// Restore inline player UI
},
onPictureInPictureFailed(error: PlatformError) {
console.warn('PiP failed', error);
},
onPictureInPictureEnded() {
console.log('PiP ended');
},
onAdBreakStart() {
playerRef.current?.setRequiresLinearPlayback(true);
},
onAdBreakEnd() {
playerRef.current?.setRequiresLinearPlayback(false);
},
};

const player = await createPlayer(playerConfig);
playerRef.current = player;
player.addListener(playerListener);

return () => {
await player.stop();
player.removeListener(playerListener);
await player.dispose();
};
}, [playerConfig]);

// PiP toggle button — add this inside your player controls UI
const PiPButton = () => (
Platform.OS === 'ios' ? (
<TouchableOpacity
onPress={async () => {
const player = playerRef.current;
if (player == null) return;
const isActive = await player.isPiPActive();
if (isActive) {
await player.stopPictureInPicture();
} else {
await player.startPictureInPicture();
}
}}
>
<Icon name="picture-in-picture" size={24} color="white" />
</TouchableOpacity>
) : null
);

Native Controls Mode (canUseCustomPlayerControls: true)

Call player.playWithNativeControls() to present the native fullscreen UI. No PiP button wiring needed — the native screen handles it.

import {
createPlayer,
Player,
PlayerPictureInPictureListener,
PlayerConfig,
PlatformError,
} from '@quickplay/rn-qp-nxg-player';

useEffect((): any => {
const playerListener = {
onPictureInPictureStarted() {
console.log('PiP started');
},
onPictureInPictureStopped() {
console.log('PiP stopped');
},
onPictureInPictureFailed(error: PlatformError) {
console.warn('PiP failed', error);
},
onPictureInPictureEnded() {
console.log('PiP ended');
},
};

const player = await createPlayer(playerConfig);
player.addListener(playerListener);
// Must call playWithNativeControls() to get the native UI with built-in PiP button
await player.playWithNativeControls();

return () => {
await player.stop();
player.removeListener(playerListener);
await player.dispose();
};
}, [playerConfig]);

Limitations

  • SMPTE-TT subtitles (SMPTE_ID3 and SMPTE_SIDECAR) are not supported during PiP playback.
  • PiP requires a device that supports AVPictureInPictureController. The native PiP button in the player controls is shown or hidden automatically based on device support.
  • startPictureInPicture() is a no-op if PiP is not supported or not currently possible (e.g., app is not in the foreground).
  • In custom RN controls mode (canUseCustomPlayerControls: false), you are responsible for showing and hiding the PiP button based on context (e.g., hide it during ad playback if needed).
  • shouldSupportPIP in PlayerConfig only affects CSAI/Google IMA ad players. It has no effect on non-ad playback.