native_picture_in_picture

pub package Platform License: MIT

A Flutter plugin for native Picture-in-Picture (PiP) on Android (API 26+) and iOS (14+).

  • 🤖 Android uses ExoPlayer with a hardware-accelerated full-screen SurfaceView overlay.
  • 🍎 iOS uses AVKit (AVPictureInPictureController).
  • 🔄 Full sync API: seek, play, pause the native player to match your Flutter video player.
  • 📡 Lifecycle events stream: willStart, didStart, willStop, didStop, restoreUI.
  • 🏠 Auto-PiP on iOS when the user swipes home.
  • 🎵 No double audio — native player is carefully managed to never clash with your Flutter player.

Platform Requirements

Platform Minimum Version
Android API 26 (Android 8.0)
iOS 14.0

Setup

iOS

Add the audio background mode to your Info.plist. This is required by iOS for the PiP session to stay active when the app is in the background:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

Android

1. Use FlutterFragmentActivity

Important: This plugin uses addOnPictureInPictureModeChangedListener, which is only available on ComponentActivity. The default MainActivity extends FlutterActivity, which does not inherit from ComponentActivity. You must change your MainActivity to extend FlutterFragmentActivity, otherwise no PiP events will be emitted.

In android/app/src/main/kotlin/.../MainActivity.kt:

-import io.flutter.embedding.android.FlutterActivity
-class MainActivity : FlutterActivity()
+import io.flutter.embedding.android.FlutterFragmentActivity
+class MainActivity : FlutterFragmentActivity()

2. Declare PiP support in AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    ...>

Usage

Basic Example

import 'dart:io';
import 'package:native_picture_in_picture/native_picture_in_picture.dart';
import 'package:native_picture_in_picture/pip_event.dart';

final pip = NativePictureInPicture();

// 1. Check support
final supported = await pip.isPipSupported();
if (!supported) return;

// 2. Initialize with your video URL
await pip.initialize('https://example.com/video.mp4');

// 3. Disable auto-PiP on Android (recommended — see notes below)
if (Platform.isAndroid) {
  await pip.setAutoPipEnabled(false);
}

// 4. Listen to lifecycle events
pip.onPipEvent.listen((event) async {
  if (event == PipEvent.willStart) {
    // Pause your Flutter video player here
    _videoController.pause();
  } else if (event == PipEvent.restoreUI || event == PipEvent.didStop) {
    // Get the position the native player stopped at
    final position = await pip.getPosition();
    await pip.pause(); // silence native player
    await _videoController.seekTo(position);
    _videoController.play();
  }
});

// 5. Sync position and start PiP
await pip.seekTo(_videoController.value.position);
await pip.startPiP();

// 6. Dispose when done
await pip.dispose();

Keeping the Native Player in Sync

To ensure Auto-PiP on iOS always starts from the right position, add a listener to your Flutter video controller:

_videoController.addListener(() {
  if (_isPipActive) return; // don't interfere while PiP is running
  if (!_videoController.value.isPlaying) {
    pip.pause();
    pip.seekTo(_videoController.value.position);
  }
});

API Reference

NativePictureInPicture

Method Returns Description
isPipSupported() Future<bool> Returns true if the device supports PiP
initialize(url) Future<void> Loads the video URL into the native player
startPiP() Future<void> Enters Picture-in-Picture mode
stopPiP() Future<void> Exits Picture-in-Picture mode
isPipActive() Future<bool> Returns true if PiP is currently active
seekTo(Duration) Future<void> Seeks the native player to a position
getPosition() Future<Duration> Gets the current native player position
play() Future<void> Plays the native player
pause() Future<void> Pauses the native player
setAutoPipEnabled(bool) Future<void> Enables/disables auto-PiP on home swipe
dispose() Future<void> Releases all native resources
onPipEvent Stream<PipEvent> Stream of PiP lifecycle events

PipEvent

Event iOS Android Description
willStart PiP window is about to appear
didStart PiP window is now active
willStop PiP window is about to close
didStop PiP window has fully closed (app is finishing)
restoreUI User tapped the expand button to return to the app

Notes

Android Auto-PiP

Android Auto-PiP (via setAutoEnterEnabled) shows whatever the Activity looks like at the moment of backgrounding — which would be the Flutter UI, not your video. For this reason, setAutoPipEnabled(false) is recommended on Android. Use the manual startPiP() button instead.

iOS Auto-PiP

On iOS, setAutoPipEnabled(true) (the default) enables seamless Auto-PiP when the user swipes home. The plugin handles this automatically via the willResignActive notification.

Why a separate native player?

Flutter's video_player uses a platform texture that cannot be displayed inside a system PiP window. This plugin runs a second, native player (ExoPlayer / AVPlayer) that provides the video surface for the PiP window. Use the sync API (seekTo, getPosition, play, pause) to keep both players in lockstep.