native_picture_in_picture 1.0.1
native_picture_in_picture: ^1.0.1 copied to clipboard
A Flutter plugin for native Picture-in-Picture (PiP) on Android and iOS. Uses ExoPlayer and AVKit for smooth, reliable PiP with full lifecycle events and auto-PiP support.
native_picture_in_picture #
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 onComponentActivity. The defaultMainActivityextendsFlutterActivity, which does not inherit fromComponentActivity. You must change yourMainActivityto extendFlutterFragmentActivity, 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.