Skip to content

Picture-in-picture

Picture-in-picture keeps content playing in a small system window while the viewer uses other apps. It is available on Android, iOS, the web, React Native and Flutter, and it is off by default — there is deliberately no PiP button in the player chrome. You decide whether your app offers PiP, and enabling it is a single switch; entering is automatic — leaving the app while content plays puts it in the little window. An imperative API exists for hosts with their own flows, but auto-enter is the intended experience.

Playback continues in the system picture-in-picture window.
The demo app on an iPhone 17 Pro Max, running the published SDK.
// Passing a handler IS the enable switch.
val pipHandler = remember { ActivityPipHandler(activity, player) }
OGPlayerView(
player = player,
pipHandler = pipHandler,
autoEnterPipOnBackground = true, // default: Home while playing enters PiP
)
// Imperative enter, if your app's own flow ever needs it:
pipHandler.enterPip()
// Observe transitions:
pipHandler.addPipListener { event ->
// ENTERED, EXPANDED (window grew back into the app),
// DISMISSED (the ✕ — the SDK pauses playback)
}
// When the player screen closes while the activity lives on
// (navigation, single-activity apps), release the handler:
DisposableEffect(Unit) {
onDispose { pipHandler.release() }
}

Host requirements — the player’s activity entry in AndroidManifest.xml needs:

<activity
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />

A missing flag doesn’t crash: the SDK logs the exact fix once and disables PiP for that player.

  • Chrome is stripped — the little window shows video and subtitles only; system PiP affordances (and on Android the SDK’s play/pause remote action) drive playback.
  • Dismissing the window pauses playback (Android’s ✕, iOS swipe-away) — otherwise audio would keep running with no window left to control it. On the web the browser decides what closing its window does.
  • Auto-enter is suppressed while an ad plays, while casting, and in error states; an ad break that starts while in PiP exits PiP — the small window has no skip/clickthrough UI, so ads never play in it. On the web the browser’s auto-enter is disarmed for the length of a break.
  • The analytics stream emits PictureInPictureChanged(isActive=…) on every transition — see Analytics & events.
  • Android — PiP needs API 26+. On API 31+ auto-enter uses the platform’s seamless transition; on 26–30 it is approximated via the user-leave hint. ActivityPipHandler is the ready-made handler; implement the PipHandler interface directly only for custom presentations, and release whichever you use (release()) when its player screen closes — an armed activity would otherwise keep auto-entering PiP from screens without a player.
  • Web — the window stays open across a new load(), retry() or a playlist advance (same element, new media); unload() and release() close it and report the exit. The browser draws the window’s own controls.
  • iOS — entering is driven by AVKit; isPipActive only reflects true once the system actually starts the window. If the binding snaps back to false right after you set it, the start was declined (no media on the layer yet, or another app’s PiP session is active) — try again once playback is up.
  • React Native — exitPip() on Android re-launches the activity to expand the window (the platform’s own expand pattern); on iOS it stops the PiP session directly.
  • Flutter — the same enterPip() / exitPip() on the controller, with the same platform behaviour as React Native. Keep the player widget’s type and position in the tree stable across the PiP transition: a rebuilt platform view is a new player, and the PiP window closes with the old one.