Implementing Picture-in-Picture Mode for Android Apps
PiP on Android came in API 26 (Android 8.0) and evolved noticeably over versions. Android 12+ added automatic PiP on home swipe and seamless resize. Basic errors on implementation remain: Activity doesn't restore correctly, control buttons don't work, video freezes after PiP exit.
Enabling PiP
In AndroidManifest.xml for Activity:
<activity
android:name=".VideoActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
configChanges—critical. Without it, entering PiP recreates Activity (onDestroy → onCreate), video interrupts.
Enter PiP via enterPictureInPictureMode:
override fun onUserLeaveHint() {
// automatic on Home press — API 26+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
enterPictureInPictureMode(params)
}
}
Android 12+ (API 31): setAutoEnterEnabled(true) in PictureInPictureParams—PiP on home swipe without overriding onUserLeaveHint. Plus setSeamlessResizeEnabled(true) for smooth window resize.
Lifecycle and Playback Management
When Activity enters PiP, onPictureInPictureModeChanged called. In PiP, UI controls hidden, player continues playback.
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
videoControlsView.visibility = View.GONE
// ExoPlayer continues — nothing to do
} else {
videoControlsView.visibility = View.VISIBLE
// user returned — restore UI
}
}
Problem with onResume / onPause: entering PiP calls onPause. If onPause has player.pause()—video stops. Check isInPictureInPictureMode() in onPause:
override fun onPause() {
super.onPause()
if (!isInPictureInPictureMode) {
player.pause()
}
}
Custom Remote Actions
In PiP window, add up to 3 buttons via RemoteAction. Example: Previous, Pause, Next:
fun buildPipParams(isPlaying: Boolean): PictureInPictureParams {
val playPauseIntent = PendingIntent.getBroadcast(
this, REQUEST_PLAY_PAUSE,
Intent(ACTION_PLAY_PAUSE),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val playPauseAction = RemoteAction(
Icon.createWithResource(this, if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play),
if (isPlaying) "Pause" else "Play",
if (isPlaying) "Pause" else "Play",
playPauseIntent
)
return PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.setActions(listOf(playPauseAction))
.build()
}
BroadcastReceiver handles ACTION_PLAY_PAUSE and after processing updates PiP params via setPictureInPictureParams to refresh button icon.
Video Calls and Custom Content
For PiP with arbitrary View (not just video)—same approach: Activity in PiP mode with custom layout. Difference from iOS: Android has no "video call only" restriction—any content can be PiP. WebRTC, camera, game screen—all work.
For WebRTC video call: SurfaceViewRenderer from org.webrtc in Activity; entering PiP hides call controls, shows video. EglRenderer continues rendering without changes.
Common Problems
Activity recreates on PiP. Forgotten configChanges in manifest.
Black screen in PiP with ExoPlayer. SurfaceView sometimes doesn't scale correctly in small PiP. Replace with TextureView via playerView.useController = false and SimpleExoPlayer.setVideoTextureView.
PiP doesn't work on MIUI / ColorOS. Manufacturers may restrict PiP. MIUI needs separate permission in app settings. Test on real devices, not just emulator.
Timelines
2–3 working days for standard integration with ExoPlayer. Custom Remote Actions and complex lifecycle with video calls—up to 4–5 days. Cost calculated individually.







