DEV Community

Dainy Jose
Dainy Jose

Posted on

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

npx create-expo-app react-native-pip-poc
Enter fullscreen mode Exit fullscreen mode

Move into the project:

cd react-native-pip-poc
Enter fullscreen mode Exit fullscreen mode

Install Dependencies

Install video package:

npm install react-native-video
Enter fullscreen mode Exit fullscreen mode

Project Structure

Instead of putting everything inside App.tsx, use a scalable architecture.

src/
 โ”œโ”€โ”€ components/
 โ”‚    โ”œโ”€โ”€ MiniPlayer.tsx
 โ”‚    โ”œโ”€โ”€ PipButton.tsx
 โ”‚    โ”œโ”€โ”€ PipStatus.tsx
 โ”‚    โ””โ”€โ”€ VideoPlayer.tsx
 โ”‚
 โ”œโ”€โ”€ hooks/
 โ”‚    โ””โ”€โ”€ usePictureInPicture.ts
 โ”‚
 โ”œโ”€โ”€ constants/
 โ”‚    โ””โ”€โ”€ video.ts
 โ”‚
 โ”œโ”€โ”€ screens/
 โ”‚    โ””โ”€โ”€ PlayerScreen.tsx
 โ”‚
 โ””โ”€โ”€ types/
      โ””โ”€โ”€ video.ts
Enter fullscreen mode Exit fullscreen mode

This keeps the code reusable and production-ready.


Android PiP Configuration

AndroidManifest.xml

Open:

android/app/src/main/AndroidManifest.xml
Enter fullscreen mode Exit fullscreen mode

Add:

<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">
Enter fullscreen mode Exit fullscreen mode

This enables native Android PiP support.


Configure MainActivity.kt

Open:

android/app/src/main/java/.../MainActivity.kt
Enter fullscreen mode Exit fullscreen mode

Add:

override fun onUserLeaveHint() {
    super.onUserLeaveHint()

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

        val params =
            PictureInPictureParams.Builder()
                .setAspectRatio(Rational(16, 9))
                .build()

        enterPictureInPictureMode(params)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

import { 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,
  };
};
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • reusable logic
  • cleaner screens
  • easier maintenance
  • scalable architecture

Build Video Player Component

VideoPlayer.tsx

import Video from "react-native-video";

<Video
  ref={videoRef}
  source={VIDEO_SOURCE}
  style={styles.video}
  resizeMode="contain"
  paused={paused}
  controls
  playInBackground
  playWhenInactive
  enterPictureInPictureOnLeave
  ignoreSilentSwitch="ignore"
/>
Enter fullscreen mode Exit fullscreen mode

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:

useEffect(() => {
  const sub = AppState.addEventListener(
    "change",
    (state) => {
      if (state === "background") {
        activateFloatingPlayer();
      }
    },
  );

  return () => sub.remove();
}, []);
Enter fullscreen mode Exit fullscreen mode

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:

<View style={styles.playerCard}>
  <VideoPlayer />
  <PipButton />
  <PipStatus />
</View>
Enter fullscreen mode Exit fullscreen mode

Running the Project

Android

npx expo run:android
Enter fullscreen mode Exit fullscreen mode

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

โœ๏ธ Written by Dainy Jose โ€” React Native Mobile Application Developer with 3+ years of experience building cross-platform mobile apps using React Native (Expo, TypeScript, Redux).
Currently expanding backend knowledge through the MERN Stack (MongoDB, Express.js, React.js, Node.js) to create more efficient, full-stack mobile experiences.

๐Ÿ’ผ Tech Stack: React Native ยท TypeScript ยท Redux ยท Expo ยท Firebase ยท Node.js ยท Express.js ยท MongoDB ยท REST API ยท JWT ยท Jest ยท Google Maps ยท Razorpay ยท PayU ยท Agile ยท SDLC ยท Git ยท Bitbucket ยท Jira

๐Ÿ“ฌ Connect with me:
๐ŸŒ Portfolio
๐Ÿ”— LinkedIn
๐Ÿ’ป GitHub

Top comments (0)