Building Picture-in-Picture (PiP) Mode in React Native with Expo and TypeScript
StackQuill
Building Picture-in-Picture (PiP) Mode in React Native with Expo and TypeScript
Picture-in-Picture (PiP) mode is one of the most useful features for media-based mobile applications. It allows users to continue watching video content while navigating outside the app.
In this article, we will build a modern React Native PiP Mode Proof of Concept using:
- React Native
- Expo
- TypeScript
- react-native-video
- Native Android PiP APIs
What is Picture-in-Picture (PiP)?
Picture-in-Picture mode allows a video player to continue playing inside a small floating window while users interact with other applications.
Popular apps using PiP:
- YouTube
- Google Meet
- Netflix
- WhatsApp Video Calls
Final Result
Features implemented:
- Floating mini-player
- Background playback
- Auto-enter PiP mode
- Manual PiP trigger
- Modern UI
- Reusable custom hook
- Native Android integration
Source Code
The complete source code for this project is available on GitHub:
🔗 GitHub Repository: https://github.com/dainyjose/pip-video-player-mobile
Create Project
Expo Project Setup
bashnpx create-expo-app react-native-pip-poc
Move into the project:
bashcd react-native-pip-poc
Install Dependencies
Install video package:
bashnpm install react-native-video
Project Structure
Instead of putting everything inside App.tsx, use a scalable architecture.
txtsrc/ ├── components/ │ ├── MiniPlayer.tsx │ ├── PipButton.tsx │ ├── PipStatus.tsx │ └── VideoPlayer.tsx │ ├── hooks/ │ └── usePictureInPicture.ts │ ├── constants/ │ └── video.ts │ ├── screens/ │ └── PlayerScreen.tsx │ └── types/ └── video.ts
This keeps the code reusable and production-ready.
Android PiP Configuration
AndroidManifest.xml
Open:
txtandroid/app/src/main/AndroidManifest.xml
Add:
xml<activity android:name=".MainActivity" android:supportsPictureInPicture="true" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:exported="true">
This enables native Android PiP support.
Configure MainActivity.kt
Open:
txtandroid/app/src/main/java/.../MainActivity.kt
Add:
kotlinoverride fun onUserLeaveHint() { super.onUserLeaveHint() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val params = PictureInPictureParams.Builder() .setAspectRatio(Rational(16, 9)) .build() enterPictureInPictureMode(params) } }
This automatically enters PiP mode when the app moves to the background.
Create Reusable PiP Hook
usePictureInPicture.ts
Instead of writing all logic directly inside the screen, create a reusable hook.
tsximport { useCallback, useRef, useState } from "react"; import { Platform } from "react-native"; import { VideoRef } from "react-native-video"; export const usePictureInPicture = ( videoRef: React.RefObject<VideoRef>, ) => { const loadedRef = useRef(false); const pendingPipRef = useRef(false); const [pipActive, setPipActive] = useState(false); const [paused, setPaused] = useState(true); const enterPip = useCallback(() => { const delays = Platform.OS === "ios" ? [250, 700, 1200] : [300]; delays.forEach((delay) => { setTimeout(() => { videoRef.current?.enterPictureInPicture(); }, delay); }); }, []); const activateFloatingPlayer = () => { pendingPipRef.current = true; setPaused(false); if (loadedRef.current) { enterPip(); } }; return { paused, setPaused, pipActive, setPipActive, loadedRef, activateFloatingPlayer, }; };
Benefits:
- reusable logic
- cleaner screens
- easier maintenance
- scalable architecture
Build Video Player Component
VideoPlayer.tsx
tsximport Video from "react-native-video"; <Video ref={videoRef} source={VIDEO_SOURCE} style={styles.video} resizeMode="contain" paused={paused} controls playInBackground playWhenInactive enterPictureInPictureOnLeave ignoreSilentSwitch="ignore" />
Important PiP props:
| Prop | Purpose |
|---|---|
| playInBackground | Continue playback in background |
| playWhenInactive | Allow inactive playback |
| enterPictureInPictureOnLeave | Auto-enter PiP |
Auto PiP on Background
Inside the screen:
tsxuseEffect(() => { const sub = AppState.addEventListener( "change", (state) => { if (state === "background") { activateFloatingPlayer(); } }, ); return () => sub.remove(); }, []);
This creates a YouTube-like experience.
Modern UI Layout
Instead of basic buttons, create:
- Player card
- Floating mini-player
- Status cards
- Dark streaming UI
Example:
tsx<View style={styles.playerCard}> <VideoPlayer /> <PipButton /> <PipStatus /> </View>
Running the Project
Android
bashnpx expo run:android
Important Notes
Expo Go Limitation
Expo Go may not fully support native PiP functionality.
Use native builds instead.
Common Issues
PiP Not Starting
Check:
- Android version >= 8
- Manifest configuration
- Native build usage
- Video loaded state
App Crashes
Verify:
- correct MainActivity.kt imports
- react-native-video installation
- Android SDK compatibility
Future Improvements
This PoC can be extended with:
- Custom PiP controls
- Live streaming
- Video call PiP
- Draggable mini-player
- Media session controls
- Netflix-style player UI
Conclusion
Picture-in-Picture mode significantly improves user experience for media applications.
With React Native, Expo, and native Android APIs, we can build scalable and modern PiP experiences similar to YouTube and Netflix.
This project demonstrates:
- native Android PiP integration
- modular React Native architecture
- reusable hooks
- background playback handling
- scalable UI structure
You can now extend this implementation into:
- OTT applications
- Video conferencing apps
- Live class platforms
- Social media video apps
- Streaming platforms
Related Articles
Android System Back Button Closes the App After Upgrading to Target SDK 36 — React Native Fix
If you're maintaining a React Native Android application and recently upgraded your project to target...
Google Play Requires Android 16 (API Level 36) by August 31, 2026 — React Native Migration Guide
Google Play has announced that starting August 31, 2026, all new apps and app updates must target...
Seamless Map Integration in React Native: A Complete Guide
Introduction Integrating maps into a React Native app sounds simple — until you face...