better_native_video_player 0.1.0
better_native_video_player: ^0.1.0 copied to clipboard
A Flutter plugin for native video playback using AVPlayerViewController on iOS and ExoPlayer on Android, with HLS support, Picture-in-Picture, and fullscreen playback.
better_native_video_player #
A Flutter plugin for native video playback on iOS and Android with advanced features.
Features #
- ✅ Native video players: AVPlayerViewController on iOS and ExoPlayer (Media3) on Android
- ✅ HLS streaming support with adaptive quality selection
- ✅ Picture-in-Picture (PiP) mode on both platforms with automatic state management
- ✅ Native fullscreen playback
- ✅ Now Playing integration (Control Center on iOS, lock screen notifications on Android)
- ✅ Background playback with media notifications
- ✅ Playback controls: play, pause, seek, volume, speed
- ✅ Quality selection for HLS streams
- ✅ Separated event streams: Activity events (play/pause/buffering) and Control events (quality/speed/PiP/fullscreen)
- ✅ Real-time playback position tracking with buffered position
- ✅ Custom HTTP headers support for video requests
- ✅ Multiple controller instances support
Platform Support #
| Platform | Minimum Version |
|---|---|
| iOS | 12.0+ |
| Android | API 24+ (Android 7.0) |
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
better_native_video_player: ^0.0.1
Then run:
flutter pub get
iOS Setup #
Add the following to your Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
For Picture-in-Picture support, enable Background Modes in Xcode:
- Target → Signing & Capabilities → Background Modes
- Check "Audio, AirPlay, and Picture in Picture"
Android Setup #
The plugin automatically configures the required permissions and services in its manifest.
Usage #
Basic Example #
import 'package:flutter/material.dart';
import 'package:better_native_video_player/better_native_video_player.dart';
class VideoPlayerPage extends StatefulWidget {
const VideoPlayerPage({super.key});
@override
State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
late NativeVideoPlayerController _controller;
@override
void initState() {
super.initState();
_initializePlayer();
}
Future<void> _initializePlayer() async {
// Create controller
_controller = NativeVideoPlayerController(
id: 1,
autoPlay: true,
showNativeControls: true,
);
// Listen to events
_controller.addListener(_handlePlayerEvent);
// Initialize
await _controller.initialize();
// Load video
await _controller.load(
url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
);
}
void _handlePlayerEvent(NativeVideoPlayerEvent event) {
print('Player event: ${event.type}');
}
@override
void dispose() {
_controller.removeListener(_handlePlayerEvent);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NativeVideoPlayer(controller: _controller),
);
}
}
Advanced Usage #
Custom Media Info (Now Playing)
_controller = NativeVideoPlayerController(
id: 1,
mediaInfo: const NativeVideoPlayerMediaInfo(
title: 'My Video Title',
subtitle: 'Artist or Channel Name',
album: 'Album Name',
artworkUrl: 'https://example.com/artwork.jpg',
),
);
Picture-in-Picture Configuration
_controller = NativeVideoPlayerController(
id: 1,
allowsPictureInPicture: true,
canStartPictureInPictureAutomatically: true, // iOS 14.2+
);
Playback Controls
// Play/Pause
await _controller.play();
await _controller.pause();
// Seek
await _controller.seekTo(const Duration(seconds: 30));
// Volume (0.0 to 1.0)
await _controller.setVolume(0.8);
// Speed
await _controller.setSpeed(1.5); // 0.5x, 1.0x, 1.5x, 2.0x, etc.
// Fullscreen
await _controller.enterFullScreen();
await _controller.exitFullScreen();
await _controller.toggleFullScreen();
Quality Selection (HLS)
// Get available qualities
final qualities = _controller.qualities;
// Set quality
if (qualities.isNotEmpty) {
await _controller.setQuality(qualities.first);
}
Separated Event Handling
The plugin separates events into two categories for better control:
Activity Events - Playback state changes:
@override
void initState() {
super.initState();
_controller.addActivityListener(_handleActivityEvent);
_controller.addControlListener(_handleControlEvent);
}
void _handleActivityEvent(PlayerActivityEvent event) {
switch (event.state) {
case PlayerActivityState.playing:
print('Playing');
break;
case PlayerActivityState.paused:
print('Paused');
break;
case PlayerActivityState.buffering:
final buffered = event.data?['buffered'] as int?;
print('Buffering... buffered position: $buffered ms');
break;
case PlayerActivityState.completed:
print('Playback completed');
break;
case PlayerActivityState.error:
print('Error: ${event.data?['message']}');
break;
default:
break;
}
}
Control Events - User interactions and settings:
void _handleControlEvent(PlayerControlEvent event) {
switch (event.state) {
case PlayerControlState.timeUpdated:
final position = event.data?['position'] as int?;
final duration = event.data?['duration'] as int?;
final bufferedPosition = event.data?['bufferedPosition'] as int?;
print('Position: $position ms / $duration ms (buffered: $bufferedPosition ms)');
break;
case PlayerControlState.qualityChanged:
final quality = event.data?['quality'];
print('Quality changed: $quality');
break;
case PlayerControlState.pipStarted:
print('PiP mode started');
break;
case PlayerControlState.pipStopped:
print('PiP mode stopped');
break;
case PlayerControlState.fullscreenEntered:
print('Entered fullscreen');
break;
case PlayerControlState.fullscreenExited:
print('Exited fullscreen');
break;
default:
break;
}
}
@override
void dispose() {
_controller.removeActivityListener(_handleActivityEvent);
_controller.removeControlListener(_handleControlEvent);
_controller.dispose();
super.dispose();
}
Custom HTTP Headers
await _controller.load(
url: 'https://example.com/video.m3u8',
headers: {
'Referer': 'https://example.com',
'Authorization': 'Bearer token',
},
);
Picture-in-Picture Mode
// Check if PiP is available on the device
final isPipAvailable = await _controller.isPictureInPictureAvailable();
if (isPipAvailable) {
// Enter PiP mode
await _controller.enterPictureInPicture();
// Exit PiP mode
await _controller.exitPictureInPicture();
}
// Listen for PiP state changes
_controller.addControlListener((event) {
if (event.state == PlayerControlState.pipStarted) {
print('Entered PiP mode');
} else if (event.state == PlayerControlState.pipStopped) {
print('Exited PiP mode');
}
});
Multiple Video Players
class MultiPlayerScreen extends StatefulWidget {
@override
State<MultiPlayerScreen> createState() => _MultiPlayerScreenState();
}
class _MultiPlayerScreenState extends State<MultiPlayerScreen> {
late NativeVideoPlayerController _controller1;
late NativeVideoPlayerController _controller2;
@override
void initState() {
super.initState();
// Create multiple controllers with unique IDs
_controller1 = NativeVideoPlayerController(id: 1, autoPlay: false);
_controller2 = NativeVideoPlayerController(id: 2, autoPlay: false);
_initializePlayers();
}
Future<void> _initializePlayers() async {
await _controller1.initialize();
await _controller2.initialize();
await _controller1.load(url: 'https://example.com/video1.m3u8');
await _controller2.load(url: 'https://example.com/video2.m3u8');
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: NativeVideoPlayer(controller: _controller1)),
Expanded(child: NativeVideoPlayer(controller: _controller2)),
],
);
}
@override
void dispose() {
_controller1.dispose();
_controller2.dispose();
super.dispose();
}
}
API Reference #
NativeVideoPlayerController #
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id |
int |
required | Unique identifier for the player instance |
autoPlay |
bool |
false |
Start playing automatically after loading |
mediaInfo |
NativeVideoPlayerMediaInfo? |
null |
Media metadata for Now Playing |
allowsPictureInPicture |
bool |
true |
Enable Picture-in-Picture |
canStartPictureInPictureAutomatically |
bool |
true |
Auto-start PiP on app background (iOS 14.2+) |
showNativeControls |
bool |
true |
Show native player controls |
Methods
Future<void> initialize()- Initialize the controllerFuture<void> load({required String url, Map<String, String>? headers})- Load video URL with optional HTTP headersFuture<void> play()- Start playbackFuture<void> pause()- Pause playbackFuture<void> seekTo(Duration position)- Seek to positionFuture<void> setVolume(double volume)- Set volume (0.0-1.0)Future<void> setSpeed(double speed)- Set playback speedFuture<void> setQuality(NativeVideoPlayerQuality quality)- Set video qualityFuture<bool> isPictureInPictureAvailable()- Check if PiP is available on deviceFuture<bool> enterPictureInPicture()- Enter Picture-in-Picture modeFuture<bool> exitPictureInPicture()- Exit Picture-in-Picture modeFuture<void> enterFullScreen()- Enter fullscreenFuture<void> exitFullScreen()- Exit fullscreenFuture<void> toggleFullScreen()- Toggle fullscreenvoid addActivityListener(void Function(PlayerActivityEvent) listener)- Add activity event listenervoid removeActivityListener(void Function(PlayerActivityEvent) listener)- Remove activity event listenervoid addControlListener(void Function(PlayerControlEvent) listener)- Add control event listenervoid removeControlListener(void Function(PlayerControlEvent) listener)- Remove control event listenerFuture<void> dispose()- Clean up resources
Properties
List<NativeVideoPlayerQuality> qualities- Available HLS quality variantsbool isFullScreen- Current fullscreen stateDuration currentPosition- Current playback positionDuration duration- Total video durationDuration bufferedPosition- How far the video has been buffereddouble volume- Current volume (0.0-1.0)PlayerActivityState activityState- Current activity statePlayerControlState controlState- Current control stateString? url- Current video URL
Activity Event States #
| State | Description |
|---|---|
PlayerActivityState.idle |
Player is idle |
PlayerActivityState.initializing |
Player is initializing |
PlayerActivityState.initialized |
Player initialized |
PlayerActivityState.loading |
Video is loading |
PlayerActivityState.loaded |
Video loaded successfully |
PlayerActivityState.playing |
Playback is active |
PlayerActivityState.paused |
Playback is paused |
PlayerActivityState.buffering |
Video is buffering |
PlayerActivityState.completed |
Playback completed |
PlayerActivityState.stopped |
Playback stopped |
PlayerActivityState.error |
Error occurred |
Control Event States #
| State | Description |
|---|---|
PlayerControlState.none |
No control event |
PlayerControlState.qualityChanged |
Video quality changed |
PlayerControlState.speedChanged |
Playback speed changed |
PlayerControlState.seeked |
Seek operation completed |
PlayerControlState.pipStarted |
PiP mode started |
PlayerControlState.pipStopped |
PiP mode stopped |
PlayerControlState.fullscreenEntered |
Fullscreen entered |
PlayerControlState.fullscreenExited |
Fullscreen exited |
PlayerControlState.timeUpdated |
Playback time updated |
Architecture #
iOS #
- Uses
AVPlayerViewControllerfor video playback - Implements
FlutterPlatformViewfor embedding native views - Supports HLS streaming with native
AVPlayer - Picture-in-Picture via
AVPictureInPictureController - Now Playing info via
MPNowPlayingInfoCenter
Android #
- Uses ExoPlayer (Media3) for video playback
- Implements
PlatformViewwithAndroidView - HLS support via Media3 HLS extension
- Picture-in-Picture via native Android PiP APIs
- Media notifications via
MediaSessionService
Troubleshooting #
Common Issues #
Controller not initializing:
// Always call initialize() before load()
await _controller.initialize();
await _controller.load(url: 'https://example.com/video.m3u8');
Events not firing:
// Make sure to add listeners BEFORE calling initialize()
_controller.addActivityListener(_handleActivityEvent);
_controller.addControlListener(_handleControlEvent);
await _controller.initialize();
Multiple controllers interfering:
// Ensure each controller has a unique ID
final controller1 = NativeVideoPlayerController(id: 1);
final controller2 = NativeVideoPlayerController(id: 2);
Memory leaks:
// Always remove listeners and dispose controllers
@override
void dispose() {
_controller.removeActivityListener(_handleActivityEvent);
_controller.removeControlListener(_handleControlEvent);
_controller.dispose();
super.dispose();
}
iOS #
Video doesn't play:
- Ensure
Info.plisthasNSAppTransportSecurityconfigured for HTTP videos - For HTTPS with self-signed certificates, add exception domains
- For local files, ensure proper file access permissions
- Check that the video format is supported by AVPlayer (HLS, MP4, MOV)
PiP not working:
- Enable Background Modes in Xcode: Target → Signing & Capabilities → Background Modes
- Check "Audio, AirPlay, and Picture in Picture"
- Ensure iOS version is 14.0+ (check with
await controller.isPictureInPictureAvailable()) - PiP requires video to be playing before entering PiP mode
- Some simulators don't support PiP; test on a physical device
Now Playing not showing:
// Provide mediaInfo when creating the controller
_controller = NativeVideoPlayerController(
id: 1,
mediaInfo: const NativeVideoPlayerMediaInfo(
title: 'Video Title',
subtitle: 'Artist Name',
),
);
Background audio stops:
- Verify Background Modes are enabled in Xcode capabilities
- Ensure "Audio, AirPlay, and Picture in Picture" is checked
Android #
Video doesn't play:
- Check internet permissions in your app's
AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
- Ensure minimum SDK version is 24+ in
build.gradle:
minSdkVersion 24
- For HTTPS issues, check your network security configuration
- Verify ExoPlayer supports the video format (HLS, MP4, WebM)
PiP not working:
- PiP requires Android 8.0+ (API 26+)
- Check device support:
await controller.isPictureInPictureAvailable() - Ensure your
AndroidManifest.xmlhas the activity configured:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
- PiP events are automatically handled by the MainActivity
- Listen for PiP state changes using
PlayerControlState.pipStartedandPlayerControlState.pipStopped
Fullscreen issues:
- The plugin handles fullscreen natively using a Dialog on Android
- Fullscreen works automatically; no additional configuration needed
- Ensure proper activity lifecycle management
- If orientation is locked, fullscreen may not rotate automatically
Media notifications not showing:
- The plugin automatically configures
MediaSessionService - Ensure foreground service permissions are granted (handled automatically)
- Media info must be provided via
mediaInfoparameter - Notifications appear when video is playing in background
ExoPlayer errors:
- Check logcat for detailed error messages
- Common issues:
- Network timeouts: Check internet connectivity
- Unsupported format: Verify video codec compatibility
- DRM content: This plugin doesn't support DRM (yet)
General Debugging #
Enable verbose logging:
// Check player state
print('Activity State: ${_controller.activityState}');
print('Control State: ${_controller.controlState}');
print('Is Fullscreen: ${_controller.isFullScreen}');
print('Current Position: ${_controller.currentPosition}');
print('Duration: ${_controller.duration}');
Test with known working URLs:
// Apple's test HLS stream
const testUrl = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
// Big Buck Bunny
const testUrl = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
Platform-specific issues:
import 'dart:io';
if (Platform.isIOS) {
// iOS-specific code
} else if (Platform.isAndroid) {
// Android-specific code
}
Example App #
See the example folder for a complete working example demonstrating:
Features Demonstrated #
- Video List with Inline Players: Multiple video players in a scrollable list
- Full-Screen Video Detail Page: Dedicated page with comprehensive controls
- Playback Controls: Play, pause, seek (±10 seconds), volume control
- Speed Adjustment: 0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x playback speeds
- Quality Selection: Automatic quality detection and manual selection for HLS streams
- Picture-in-Picture: Enter/exit PiP mode with state tracking
- Fullscreen Toggle: Native fullscreen support on both platforms
- Real-time Statistics: Current position, duration, buffered position tracking
- Separated Event Handling: Activity and control events with detailed logging
- Custom Media Info: Now Playing integration with metadata
Running the Example #
cd example
flutter run
The example includes:
video_list_screen_with_players.dart- Multiple inline video playersvideo_detail_screen_full.dart- Full-featured video player with controlsvideo_player_card.dart- Reusable video player widgetvideo_item.dart- Video model with sample HLS streams
License #
MIT License - see LICENSE file for details
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
Credits #
Developed for the Flutter community. Based on native video player implementations using industry-standard libraries:
- iOS: AVFoundation
- Android: ExoPlayer (Media3)