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

  • 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 VideoPlayer and bare CorePlayer
  • ThemingVideoPlayerTheme via ThemeData.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 VideoPlayerController instances 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 CorePlayer only, 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: AVAssetImageGenerator at 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 on cross_file yourself)

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 in main
  • Controller initialize(): once per controller lifetime; do not reuse after dispose()

License

Licensed under the Apache License 2.0.