xue_hua_navite_video_player
A cross-platform Flutter audio/video player plugin. Dart exposes a unified controller and optional UI; each platform decodes and renders with a native engine (ExoPlayer / AVPlayer / libmpv / HTML5).
| Version | 1.3.0 |
| Flutter | >= 3.44.0 |
| Dart SDK | ^3.12.0 |
| Repository | GitHub |
| Homepage | jsontodart.cn |
| License | Apache 2.0 |
Table of contents
- Features
- Platform engines & rendering
- Architecture
- Installation
- Platform setup
- Quick start
- Important: single active session
- VideoSource
- VideoPlayerController
- PlayState
- Widgets
- VideoPlayerTheme
- Fullscreen contract
- Gestures & keyboard
- Snapshots & media probe
- Custom UI with Signals
- Example
- FAQ
- License
Features
- 6 platforms — Android, iOS, macOS, Linux, Windows, Web
- Native rendering — PlatformView on Android / iOS / macOS; Texture (libmpv) on Linux / Windows; HTML5
<video>on Web - Sources — network URL, local file, Flutter asset (assets are extracted to a temp directory)
- Controls — play / pause / seek / volume / mute / speed / brightness / aspect mode / snapshot
- Built-in UI — drop-in
VideoPlayerand bareCorePlayer - Theming —
VideoPlayerThemeviaThemeData.extensions - Fullscreen — mobile orientation / immersive UI, desktop window fullscreen, and the browser Fullscreen API; visual Overlay chrome requires a mounted
VideoPlayer - Utilities — duration probing and cover-frame extraction without starting playback
- Reactive state — playback state exposed with
signals_flutter
Platform engines & rendering
| Platform | Native engine | Surface |
|---|---|---|
| Android | ExoPlayer (Media3) | AndroidView PlatformView (TextureView) |
| iOS | AVPlayer | UiKitView PlatformView |
| macOS | AVPlayer | AppKitView PlatformView |
| Linux | libmpv (software) | Flutter Texture |
| Windows | libmpv (software) | Flutter Texture |
| Web | HTML5 <video> |
HtmlElementView |
AspectRatioMode (fit / fill / stretch) maps to each platform’s native fit property (videoGravity, resizeMode, object-fit, mpv keepaspect/panscan, etc.).
Architecture
┌─────────────────────────────────────────────────────────┐
│ App UI │
│ VideoPlayer / CorePlayer / custom widgets │
└───────────────────────────┬─────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────┐
│ VideoPlayerController (public facade) │
│ └── PlaybackSession (state machine + Signals) │
│ └── PlayerBackend / ChannelPlayerBackend │
└───────────────────────────┬─────────────────────────────┘
│ MethodChannel / EventChannel
┌───────────────────────────▼─────────────────────────────┐
│ Native: ExoPlayer / AVPlayer / libmpv / HTML5 │
└─────────────────────────────────────────────────────────┘
XueHuaNaviteVideoPlayer.instance
└── MediaProbe (duration / covers; does not own the play session)
| Term | Meaning |
|---|---|
| VideoSource | Sealed source model (network / file / asset) resolved to a native URL |
| PlayState | High-level playback state owned by PlaybackSession |
| PlayerBackend | Session transport: create / open / play / pause / seek / volume / speed / snapshot |
| PlaybackSession | Owns open → ready → playing / paused / stopped / completed / error |
| VideoPlayerController | Stable public API for apps and UI |
| MediaProbe | Probe APIs that do not create a live play session |
See CONTEXT.md for domain notes.
Installation
dependencies:
xue_hua_navite_video_player: ^1.1.0
flutter pub get
import 'package:xue_hua_navite_video_player/xue_hua_navite_video_player.dart';
Public exports include: VideoSource, VideoPlayerController, VideoPlayer, CorePlayer, VideoPlayerTheme, PlayState, AspectRatioMode, SkipSecondType, VideoCoverFrame, XueHuaNaviteVideoPlayer, and XFile (re-exported from cross_file).
Platform setup
Android
Ensure network permission for remote media:
<uses-permission android:name="android.permission.INTERNET"/>
iOS
HTTPS playback usually needs no extra setup. For cleartext HTTP, configure App Transport Security in Info.plist as needed (avoid wide-open ATS in production). Local files must be readable inside the sandbox.
macOS
Add to macos/Runner/DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
File access outside the sandbox may require additional entitlements.
Linux
Install libmpv development packages:
# Debian / Ubuntu
sudo apt install libmpv-dev
# Fedora
sudo dnf install mpv-libs-devel
# Arch
sudo pacman -S mpv
Windows
Playback uses libmpv software rendering. The first Windows build downloads a prebuilt libmpv SDK from shinchiro/mpv-winbuild-cmake (mpv-dev-x86_64 or mpv-dev-aarch64, matching FLUTTER_TARGET_PLATFORM) into windows/mpv-dev-<arch>/.
If that download fails, get the matching mpv-dev-*.7z manually, extract it, and point CMake at the folder that contains include/mpv/: set the MPV_DIR environment variable or pass -DMPV_DIR=<path>. You can also override the archive with -DMPV_DOWNLOAD_URL= and -DMPV_DOWNLOAD_SHA256=. If playback fails after a successful build, check missing mpv native dependencies first.
Web
Uses HTML5 <video>:
- Cross-origin media needs correct CORS headers (playback and/or snapshots may fail otherwise)
- Browsers may block unmuted autoplay until a user gesture
Quick start
import 'package:flutter/material.dart';
import 'package:xue_hua_navite_video_player/xue_hua_navite_video_player.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await XueHuaNaviteVideoPlayer.instance.initialize();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final VideoPlayerController _controller = VideoPlayerController();
@override
void initState() {
super.initState();
_boot();
}
Future<void> _boot() async {
await _controller.initialize();
await _controller.playNetwork('https://example.com/video.mp4');
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
extensions: const [
VideoPlayerTheme(
foregroundColor: Colors.white,
backgroundColor: Colors.black,
),
],
),
home: Scaffold(
backgroundColor: Colors.black,
body: VideoPlayer(
controller: _controller,
fill: true,
onClose: () {},
),
),
);
}
}
XueHuaNaviteVideoPlayer.instance.initialize() is an idempotent binding helper. The native player session is created by VideoPlayerController.initialize().
Important: single active session
The plugin hosts one native player behind process-wide channels.
- Do not treat multiple
VideoPlayerControllerinstances as independent parallel players - A later
open/play*takes over the same native session - Dispose the controller when leaving a page, then create +
initialize()on the next page
Multi-instance concurrent players (e.g. PiP + list preview) are not supported.
VideoSource
Sealed source types:
| Type | Factory | Notes |
|---|---|---|
NetworkVideoSource |
VideoSource.network(url) |
HTTP(S); opened directly by the native player |
FileVideoSource |
VideoSource.file(path) |
Absolute path or file:// URI |
AssetVideoSource |
VideoSource.asset(path) |
Flutter asset; extracted to temp on first use |
final network = VideoSource.network('https://example.com/a.mp4');
final file = VideoSource.file('/absolute/path/to/movie.mp4');
final asset = VideoSource.asset('assets/videos/intro.mp4');
final nativeUrl = await network.resolveToNativeUrl();
Declare assets in your app pubspec.yaml:
flutter:
assets:
- assets/videos/intro.mp4
Network media is opened directly by the native player via the original URL.
VideoPlayerController
Lifecycle
final controller = VideoPlayerController();
await controller.initialize(); // create native session + subscribe events
// ... control playback ...
await controller.dispose(); // release native resources; instance unusable after
controller.reset(); // reset Dart-side state without disposing the session
Optional constructor deps (mainly for tests):
VideoPlayerController({
PlayerBackend? backend,
FullscreenCoordinator? fullscreen,
BrightnessController? brightness,
});
Open & play
| API | Behavior |
|---|---|
playNetwork(url) |
Open network source and start |
openNetwork(url) |
Open only; call play() later |
playFile / openFile |
Local file |
playAsset / openAsset |
Flutter asset |
playSource / openSource |
Generic entry |
play / pause / playOrPause |
Transport |
stop |
Explicit stop (vs natural completion) |
seek |
Absolute seek |
seekForward / seekBackward |
Step by skipSecondType |
await controller.openNetwork('https://example.com/video.mp4');
await controller.play();
await controller.playSource(VideoSource.file('/tmp/a.mp4'));
Volume, mute, speed, brightness
await controller.setVolume(0.8); // 0.0 – 1.0
await controller.setMuted(true);
await controller.toggleMuted();
await controller.setSpeed(1.5); // built-in menu: 0.5 / 1.0 / 1.25 / 1.5 / 2.0
await controller.setBrightness(0.6); // 0.0 – 1.0 (screen brightness)
Skip step & aspect mode
controller.setSkipSecondType(SkipSecondType.second15);
// second5 / second10 / second15 / second30 / second45 / second60
await controller.setAspectRatioMode(AspectRatioMode.fill);
// fit | fill | stretch
Fullscreen
await controller.enterFullscreen();
await controller.exitFullscreen();
await controller.toggleFullscreen();
See Fullscreen contract.
Snapshot
final XFile png = await controller.takeSnapshot();
final XFile saved = await controller.takeSnapshot(savePath: '/tmp/frame.png');
Reactive signals
| Signal / computed | Type | Meaning |
|---|---|---|
playState |
PlayState |
Playback state |
position / duration |
Duration |
Progress / total |
volume / speed |
double |
Volume / rate |
isBuffering |
bool |
Buffering |
errorMessage |
String? |
Error text |
currentUrl |
String? |
Current native URL |
mimeType |
String? |
MIME type |
videoSize |
Size |
Video size |
rotationDegrees |
int |
Rotation |
videoAspectRatio |
double |
Ratio after rotation |
isVideo / isAudio |
bool |
From mime |
isPlaying |
bool |
Playing |
progressPercent |
double |
0.0 – 1.0 |
muted |
bool |
Muted |
skipSecondType |
SkipSecondType |
Skip step |
aspectRatioMode |
AspectRatioMode |
Fit mode |
isFullscreen |
bool |
Fullscreen |
brightness |
double |
Screen brightness |
textureId |
int? |
Texture id (desktop) |
SignalBuilder(
builder: (context) {
final state = controller.playState.value;
final pos = controller.position.value;
return Text('${state.name} ${pos.inSeconds}s');
},
);
PlayState
enum PlayState { idle, loading, playing, paused, stopped, completed, error }
| State | Meaning |
|---|---|
idle |
Initial / after reset |
loading |
Opening / preparing |
playing |
Playing |
paused |
Paused |
stopped |
Explicit stop() |
completed |
Natural end-of-stream (UI may show replay) |
error |
Failure; see errorMessage |
isBuffering can overlap other states (e.g. playing + buffering).
Widgets
CorePlayer — surface only
Renders the native frame plus loading / buffering / error. Use for fully custom chrome.
CorePlayer(
controller: controller,
aspectRatio: 16 / 9,
backgroundColor: Colors.black,
loadingBuilder: (context) => const CircularProgressIndicator(),
errorBuilder: (context, message) => Text(message ?? 'Error'),
);
With
CorePlayeronly,enterFullscreen()still changes orientation / window / browser fullscreen, but does not host a visual edge-to-edge Overlay.
VideoPlayer — full chrome
Top bar, center transport, bottom scrubber. Tap toggles chrome; auto-hides after ~3s while playing.
VideoPlayer(
controller: controller,
fill: true,
aspectRatio: null,
autoHideDelay: const Duration(seconds: 3),
fadeDuration: const Duration(milliseconds: 250),
initiallyVisible: true,
onClose: () => Navigator.pop(context),
leading: null,
title: const Text('Title'),
actions: const [],
showAspectRatioMenu: true,
enableFullscreen: true,
skipSecondType: SkipSecondType.second10,
// topBarBuilder / centerControlsBuilder /
// bottomScrubberBuilder / extraOverlayBuilder
// errorBuilder / loadingBuilder
);
Slot builders receive VideoPlayerSlotContext:
class VideoPlayerSlotContext {
final VideoPlayerController controller;
final VideoPlayerTheme theme;
final VoidCallback showControls;
final VoidCallback hideControls;
}
Visual fullscreen: VideoPlayer reparents the surface into a root Overlay via OverlayPortal. Prefer controller.toggleFullscreen() while this widget is mounted under an Overlay ancestor (MaterialApp provides one).
VideoPlayerTheme
Register on ThemeData.extensions. Defaults apply when omitted.
MaterialApp(
theme: ThemeData(
extensions: const [
VideoPlayerTheme(
foregroundColor: Colors.white,
backgroundColor: Colors.black,
chromeIconSize: 22,
scrubberActiveColor: Colors.white,
menuBackgroundColor: Color(0xF01A1A1A),
),
],
),
);
| Group | Examples |
|---|---|
| Base | foregroundColor, backgroundColor |
| Center | centerControlsSpacing, centerPlayButtonIconSize, centerSkipButtonIconSize |
| Bars | chromeIconSize, topBarPadding, bottomBarPadding, timeTextStyle |
| Scrubber | active / buffered / inactive / thumb colors and sizes |
| Menu | menuBackgroundColor, menuBorderRadius, menuItemTextStyle |
| Gesture HUD | hudBackgroundColor, hudPadding, hudTextStyle |
final theme = VideoPlayerTheme.of(context);
Fullscreen contract
await controller.enterFullscreen(); // isFullscreen + platform fullscreen host
await controller.exitFullscreen();
| Call site | Effect |
|---|---|
Mounted VideoPlayer under an Overlay |
Platform fullscreen and edge-to-edge Overlay chrome |
Controller / CorePlayer only |
Platform fullscreen only — no visual Overlay host |
Platform host:
- Mobile — preferred orientation from
videoAspectRatio, plus immersive system UI - Desktop — OS window fullscreen (Windows borderless, macOS native fullscreen, Linux
gtk_window_fullscreen) - Web — browser Fullscreen API on the Flutter document (not the
<video>element)
Gestures (mobile) are active only while fullscreen and a VideoPlayer is mounted. Keyboard shortcuts (desktop / web) work in fullscreen or inline when the player is focused. Escape leaves fullscreen.
Gestures & keyboard
Mobile (fullscreen)
| Gesture | Zone | Action |
|---|---|---|
| Horizontal drag | Anywhere (past threshold) | Seek (scaled by skipSecondType) |
| Vertical drag | Left ~40% | Brightness |
| Vertical drag | Right ~40% | Volume |
| Tap | — | Toggle chrome |
Desktop / Web (focused — fullscreen or inline)
| Key | Action |
|---|---|
Space |
Play / pause |
← / → |
Seek backward / forward |
↑ / ↓ |
Volume ±0.05 |
Esc |
Exit fullscreen |
Snapshots & media probe
Current-frame snapshot
final XFile png = await controller.takeSnapshot();
- iOS / macOS:
AVAssetImageGeneratorat the current time (PlatformView display; no Texture frame buffer required) - Other platforms: native snapshot paths
- Returns a PNG
XFile(re-exported; no need to depend oncross_fileyourself)
Cover candidates (no playback session)
final frames = await XueHuaNaviteVideoPlayer.instance.extractCoverCandidates(
VideoSource.network('https://example.com/video.mp4'),
count: 5,
minBrightness: 0.08,
);
for (final frame in frames) {
// frame.image, frame.position, frame.brightness
}
On native platforms XFile.path is a real file path; on web it may be a blob / data URL.
Duration probe (no playback session)
final duration = await XueHuaNaviteVideoPlayer.instance.getDuration(
VideoSource.network('https://example.com/video.mp4'),
timeout: const Duration(seconds: 15),
);
// null on failure, timeout, or non-finite duration (e.g. live)
Custom UI with Signals
class MyPlayer extends StatelessWidget {
const MyPlayer({super.key, required this.controller});
final VideoPlayerController controller;
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: CorePlayer(controller: controller)),
SignalBuilder(
builder: (context) {
final playing = controller.isPlaying.value;
return IconButton(
icon: Icon(playing ? Icons.pause : Icons.play_arrow),
onPressed: controller.playOrPause,
);
},
),
],
);
}
}
Exported scrubber:
PlayerScrubberSlider(controller: controller);
Example
See example/ for a full demo: playlist, themed VideoPlayer, fullscreen, snapshots, cover extraction, and state chips.
cd example
flutter run
FAQ
Why does a second controller interrupt the first?
Process-wide single native session. Reuse serially and dispose() on leave.
Fullscreen does not go edge-to-edge?
Use mounted VideoPlayer under an Overlay, then toggleFullscreen() / enterFullscreen(). On desktop / web this also makes the window or browser document fullscreen.
Asset playback fails?
Declare the asset in pubspec.yaml with a matching path. First play extracts to temp storage.
Linux build cannot find mpv?
Install libmpv-dev (or distro equivalent).
Web snapshot / black frame?
Check CORS and whether the browser allows reading cross-origin media pixels.
How many initialize() calls?
- Plugin
initialize(): optional, once inmain - Controller
initialize(): once per controller lifetime; do not reuse afterdispose()
License
Licensed under the Apache License 2.0.