Skip to main content

Picture-in-Picture Mode

Picture-in-Picture (PiP) allows users to watch videos in a small floating window while using other apps. This guide shows you how to implement PiP in React Native Video.

Platform Support

  • iOS: Supported on iOS 14+ and iPadOS
  • Android: Supported on Android 8.0 (API 26) and above
  • visionOS: Supported

Basic Setup

Enable PiP in VideoView

import { VideoView, useVideoPlayer } from 'react-native-video';
import { View } from 'react-native';

export function PiPPlayer() {
  const player = useVideoPlayer('https://example.com/video.mp4');

  return (
    <View style={{ flex: 1 }}>
      <VideoView 
        player={player}
        pictureInPicture={true}
        controls={true}
        style={{ width: '100%', height: 300 }}
      />
    </View>
  );
}
When pictureInPicture={true}, the native controls will show a PiP button (platform support permitting).

Programmatic Control

Control PiP mode programmatically using a ref:
import { VideoView, useVideoPlayer, VideoViewRef } from 'react-native-video';
import { useRef } from 'react';
import { View, Button } from 'react-native';

export function PiPWithControls() {
  const player = useVideoPlayer('https://example.com/video.mp4');
  const videoRef = useRef<VideoViewRef>(null);

  const enterPiP = () => {
    videoRef.current?.enterPictureInPicture();
  };

  const exitPiP = () => {
    videoRef.current?.exitPictureInPicture();
  };

  const checkPiPSupport = () => {
    const canEnter = videoRef.current?.canEnterPictureInPicture();
    console.log('PiP supported:', canEnter);
  };

  return (
    <View style={{ flex: 1 }}>
      <VideoView 
        ref={videoRef}
        player={player}
        pictureInPicture={true}
        style={{ width: '100%', height: 300 }}
      />
      
      <View style={{ padding: 20, gap: 10 }}>
        <Button title="Enter PiP" onPress={enterPiP} />
        <Button title="Exit PiP" onPress={exitPiP} />
        <Button title="Check PiP Support" onPress={checkPiPSupport} />
      </View>
    </View>
  );
}

VideoView PiP Methods

MethodDescription
enterPictureInPicture()Enter picture-in-picture mode
exitPictureInPicture()Exit picture-in-picture mode
canEnterPictureInPicture()Check if PiP is supported and available

Auto-Enter PiP

Automatically enter PiP when the app goes to background:
<VideoView
  player={player}
  pictureInPicture={true}
  autoEnterPictureInPicture={true}
  style={{ width: '100%', height: 300 }}
/>
autoEnterPictureInPicture will attempt to enter PiP mode when the video is playing and the app is backgrounded. Behavior may vary by platform.

PiP Events

Listen to PiP state changes using event callbacks:
import { VideoView, useVideoPlayer } from 'react-native-video';
import { useState } from 'react';
import { View, Text } from 'react-native';

export function PiPWithEvents() {
  const player = useVideoPlayer('https://example.com/video.mp4');
  const [isPiPActive, setIsPiPActive] = useState(false);

  return (
    <View style={{ flex: 1 }}>
      <VideoView
        player={player}
        pictureInPicture={true}
        controls={true}
        onPictureInPictureChange={(isActive) => {
          setIsPiPActive(isActive);
          console.log('PiP mode:', isActive ? 'Active' : 'Inactive');
        }}
        willEnterPictureInPicture={() => {
          console.log('About to enter PiP');
        }}
        willExitPictureInPicture={() => {
          console.log('About to exit PiP');
        }}
        style={{ width: '100%', height: 300 }}
      />
      
      <Text style={{ padding: 20 }}>
        PiP Status: {isPiPActive ? '🎬 Active' : '❌ Inactive'}
      </Text>
    </View>
  );
}

PiP Event Callbacks

EventTypeDescription
onPictureInPictureChange(isActive: boolean) => voidFired when PiP mode starts or stops
willEnterPictureInPicture() => voidFired just before entering PiP mode
willExitPictureInPicture() => voidFired just before exiting PiP mode

Using addEventListener

You can also add PiP event listeners directly via the ref:
import { VideoView, useVideoPlayer, VideoViewRef } from 'react-native-video';
import { useRef, useEffect } from 'react';

export function PiPWithListeners() {
  const player = useVideoPlayer('https://example.com/video.mp4');
  const videoRef = useRef<VideoViewRef>(null);

  useEffect(() => {
    const subscription = videoRef.current?.addEventListener(
      'onPictureInPictureChange',
      (isActive) => {
        console.log('PiP state changed:', isActive);
      }
    );

    return () => {
      subscription?.remove();
    };
  }, []);

  return (
    <VideoView
      ref={videoRef}
      player={player}
      pictureInPicture={true}
      style={{ width: '100%', height: 300 }}
    />
  );
}

Platform Configuration

iOS Configuration

1
Enable Background Modes
2
Add the Audio background mode to your Info.plist:
3
<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>
4
Enable PiP capability
5
In Xcode, go to your target’s “Signing & Capabilities” and add the “Background Modes” capability with “Audio, AirPlay, and Picture in Picture” enabled.

Android Configuration

1
Update AndroidManifest.xml
2
Add PiP support to your activity:
3
<activity
  android:name=".MainActivity"
  android:supportsPictureInPicture="true"
  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
4
Handle PiP in Activity (Optional)
5
For custom PiP behavior, you can override in MainActivity.java:
6
import android.app.PictureInPictureParams;
import android.util.Rational;
import android.os.Build;

@Override
public void onUserLeaveHint() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        PictureInPictureParams params = new PictureInPictureParams.Builder()
            .setAspectRatio(new Rational(16, 9))
            .build();
        enterPictureInPictureMode(params);
    }
}

Complete Example with State Management

import { VideoView, useVideoPlayer, VideoViewRef } from 'react-native-video';
import { useRef, useState, useEffect } from 'react';
import { View, Button, Text, StyleSheet, AppState } from 'react-native';

export function AdvancedPiPPlayer() {
  const player = useVideoPlayer('https://www.w3schools.com/html/mov_bbb.mp4');
  const videoRef = useRef<VideoViewRef>(null);
  const [isPiPActive, setIsPiPActive] = useState(false);
  const [isPiPSupported, setIsPiPSupported] = useState(false);

  useEffect(() => {
    // Check PiP support on mount
    const canEnter = videoRef.current?.canEnterPictureInPicture();
    setIsPiPSupported(canEnter ?? false);
  }, []);

  useEffect(() => {
    // Handle app state changes
    const subscription = AppState.addEventListener('change', (nextAppState) => {
      if (nextAppState === 'background' && player.isPlaying && isPiPSupported) {
        // Automatically enter PiP when app goes to background
        videoRef.current?.enterPictureInPicture();
      }
    });

    return () => {
      subscription.remove();
    };
  }, [player.isPlaying, isPiPSupported]);

  const togglePiP = () => {
    if (isPiPActive) {
      videoRef.current?.exitPictureInPicture();
    } else {
      videoRef.current?.enterPictureInPicture();
    }
  };

  return (
    <View style={styles.container}>
      <VideoView
        ref={videoRef}
        player={player}
        pictureInPicture={true}
        controls={true}
        onPictureInPictureChange={(isActive) => {
          setIsPiPActive(isActive);
        }}
        willEnterPictureInPicture={() => {
          console.log('Entering PiP mode');
        }}
        willExitPictureInPicture={() => {
          console.log('Exiting PiP mode');
        }}
        style={styles.video}
      />

      <View style={styles.info}>
        <Text style={styles.status}>
          PiP Support: {isPiPSupported ? '✅ Available' : '❌ Not Available'}
        </Text>
        <Text style={styles.status}>
          PiP Status: {isPiPActive ? '🎬 Active' : '⏸️ Inactive'}
        </Text>
      </View>

      <View style={styles.controls}>
        <Button
          title={isPiPActive ? 'Exit PiP' : 'Enter PiP'}
          onPress={togglePiP}
          disabled={!isPiPSupported}
        />
        <Button
          title={player.isPlaying ? 'Pause' : 'Play'}
          onPress={() => (player.isPlaying ? player.pause() : player.play())}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
  video: {
    width: '100%',
    height: 300,
  },
  info: {
    padding: 20,
    backgroundColor: '#fff',
  },
  status: {
    fontSize: 16,
    marginBottom: 8,
  },
  controls: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    padding: 20,
    backgroundColor: '#fff',
  },
});

Best Practices

1. Check PiP Support

Always verify PiP is available before attempting to use it:
const isPiPAvailable = videoRef.current?.canEnterPictureInPicture();
if (isPiPAvailable) {
  videoRef.current?.enterPictureInPicture();
}

2. Handle State Changes

Respond to PiP events to update your UI:
<VideoView
  onPictureInPictureChange={(isActive) => {
    if (isActive) {
      // Update UI for PiP mode (hide controls, etc.)
    } else {
      // Restore normal UI
    }
  }}
/>

3. Enable Background Playback

For smooth PiP experience, enable background playback:
const player = useVideoPlayer('https://example.com/video.mp4', (player) => {
  player.playInBackground = true;
});

4. Maintain Playback State

Ensure video continues playing in PiP:
useEffect(() => {
  const subscription = videoRef.current?.addEventListener(
    'onPictureInPictureChange',
    (isActive) => {
      if (isActive && !player.isPlaying) {
        player.play();
      }
    }
  );

  return () => subscription?.remove();
}, []);

Platform-Specific Notes

iOS

  • PiP requires iOS 14+ or iPadOS
  • Background audio mode must be enabled in capabilities
  • The video must have audio (or be playing) to enter PiP
  • System controls in PiP window are provided automatically

Android

  • PiP requires Android 8.0 (API 26)+
  • The activity must declare android:supportsPictureInPicture="true"
  • PiP window aspect ratio can be customized (typically 16:9 for video)
  • Some devices may have PiP disabled by the manufacturer

Troubleshooting

PiP Button Not Showing

  • Ensure pictureInPicture={true} is set
  • Verify controls={true} is enabled
  • Check platform support (iOS 14+, Android 8+)
  • Confirm background modes are enabled (iOS)

PiP Not Working on Android

  • Check AndroidManifest.xml has android:supportsPictureInPicture="true"
  • Verify Android version is 8.0 or higher
  • Some Android devices disable PiP in settings - check device PiP settings

Video Stops When Entering PiP

  • Enable background playback: player.playInBackground = true
  • On iOS, ensure audio background mode is configured

Can’t Enter PiP Programmatically

  • Call canEnterPictureInPicture() first to verify support
  • Ensure video is loaded and playing
  • Check that the ref is properly attached to VideoView

Next Steps

Build docs developers (and LLMs) love