native_picture_in_picture 1.0.1 copy "native_picture_in_picture: ^1.0.1" to clipboard
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.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:native_picture_in_picture/native_picture_in_picture.dart';
import 'package:native_picture_in_picture/pip_event.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _pip = NativePictureInPicture();

  bool _supported = false;
  bool _initialized = false;
  bool _pipActive = false;
  String _lastEvent = 'none';
  String _statusMessage = '';

  StreamSubscription<PipEvent>? _eventSub;

  /// A sample video URL for testing (Apple's official HLS sample stream).
  static const _sampleVideoUrl =
      'https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8';

  @override
  void initState() {
    super.initState();
    _checkSupport();
    _listenToEvents();
  }

  Future<void> _checkSupport() async {
    try {
      final supported = await _pip.isPipSupported();
      if (!mounted) return;
      setState(() {
        _supported = supported;
        _statusMessage =
            supported ? 'PiP is supported' : 'PiP is NOT supported on this device';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Error checking support: ${e.message}';
      });
    }
  }

  void _listenToEvents() {
    _eventSub = _pip.onPipEvent.listen(
      (event) {
        if (!mounted) return;
        setState(() {
          _lastEvent = event.name;
          if (event == PipEvent.didStart) {
            _pipActive = true;
          } else if (event == PipEvent.didStop) {
            _pipActive = false;
          }
        });
      },
      onError: (error) {
        if (!mounted) return;
        setState(() {
          _statusMessage = 'Event error: $error';
        });
      },
    );
  }

  Future<void> _initialize() async {
    try {
      await _pip.initialize(_sampleVideoUrl);
      if (!mounted) return;
      setState(() {
        _initialized = true;
        _statusMessage = 'Initialized with sample video';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Initialize error: ${e.message}';
      });
    }
  }

  Future<void> _startPiP() async {
    try {
      await _pip.startPiP();
      if (!mounted) return;
      setState(() {
        _statusMessage = 'PiP started';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Start PiP error: ${e.message}';
      });
    }
  }

  Future<void> _stopPiP() async {
    try {
      await _pip.stopPiP();
      if (!mounted) return;
      setState(() {
        _statusMessage = 'PiP stopped';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Stop PiP error: ${e.message}';
      });
    }
  }

  Future<void> _disposePiP() async {
    try {
      await _pip.dispose();
      if (!mounted) return;
      setState(() {
        _initialized = false;
        _pipActive = false;
        _statusMessage = 'Disposed';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Dispose error: ${e.message}';
      });
    }
  }

  @override
  void dispose() {
    _eventSub?.cancel();
    _pip.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Native PiP Example')),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Status card
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Status',
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                      const SizedBox(height: 8),
                      Text('Supported: $_supported'),
                      Text('Initialized: $_initialized'),
                      Text('PiP Active: $_pipActive'),
                      Text('Last Event: $_lastEvent'),
                      if (_statusMessage.isNotEmpty) ...[
                        const SizedBox(height: 8),
                        Text(
                          _statusMessage,
                          style: TextStyle(
                            color: _statusMessage.contains('error') ||
                                    _statusMessage.contains('Error') ||
                                    _statusMessage.contains('NOT')
                                ? Colors.red
                                : Colors.green,
                          ),
                        ),
                      ],
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),

              // Action buttons
              ElevatedButton(
                onPressed: _supported && !_initialized ? _initialize : null,
                child: const Text('Initialize Player'),
              ),
              const SizedBox(height: 8),
              ElevatedButton(
                onPressed: _initialized && !_pipActive ? _startPiP : null,
                child: const Text('Start PiP'),
              ),
              const SizedBox(height: 8),
              ElevatedButton(
                onPressed: _pipActive ? _stopPiP : null,
                child: const Text('Stop PiP'),
              ),
              const SizedBox(height: 8),
              OutlinedButton(
                onPressed: _initialized ? _disposePiP : null,
                child: const Text('Dispose'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
9
likes
160
points
182
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on native_picture_in_picture

Packages that implement native_picture_in_picture