gstplayer

gstplayer

English | 简体中文

A polished, all-in-one Flutter video player with a beautiful built-in UI and broad format support — most local and network codecs via GStreamer (native C core + Dart FFI), rendered into Flutter external Texture widgets (GStreamer appsink on Apple/desktop; glimagesink + SurfaceProducer on Android).

Supported platforms: Android, iOS, macOS, Windows, Linux.

Android / iOS / macOS: GStreamer SDK is downloaded automatically on the first build. Android also needs a local Rust toolchain (for HTTPS / reqwest). Windows / Linux: install GStreamer once on the machine (see below).

Scope: video playback — open / play / pause / stop / seek / volume / mute / speed / looping, plus state / position / duration / resolution / buffering / EOS / error reporting, scrub preview, poster / last-frame, external subtitles (SRT/VTT overlay), danmaku overlay, and screenshots. It does not do recording, streaming (as a server), or system picture-in-picture.

Screenshots

Android   iOS   macOS

Android · iOS · macOS (example app)

Table of contents

Features

  • Local files, Flutter assets, and network URLs (http(s)://, rtsp://, ...).
  • Play / pause / stop / seek / looping.
  • Volume (popup vertical slider with live 0–100 readout), mute, and playback speed.
  • Bilibili-inspired two-row chrome: pink progress (#FB7299), remaining time, speed / settings / volume / fullscreen, then danmaku input + CC; auto-hiding.
  • Side lock on the same row as the center play/pause (far right). Locking hides the bottom chrome and ignores surface gestures / keyboard shortcuts; tap only shows or hides the unlock button.
  • Two-level settings: mirror, loop, autoplay, play-next, 16:9 / 4:3, hide black bars, lights-off, audio tracks. Chrome copy is zh / en via language.
  • Mobile surface gestures (inline and fullscreen): horizontal seek, left brightness / right volume (HUD shows percent). Disabled while controls are locked.
  • Progress-bar scrub thumbnail preview (external WebVTT + sprite / frame list).
  • Poster image and keep-last-frame after EOS.
  • External subtitle overlay (SRT/WebVTT via SubtitleParser) plus embedded subtitle track selection API/UI.
  • Danmaku (bullet comment) overlay driven by app-supplied DanmakuItem cues.
  • Frame capture (captureCurrentFrame / onScreenshot — host saves PNG) and one-shot covers (captureThumbnail). Built-in chrome no longer shows a screenshot button.
  • Reactive state via ChangeNotifier plain getters: state, position, duration, video size, aspect ratio, buffering %, volume, speed, looping, muted, and errors. Rebuild with ListenableBuilder / addListener.
  • A drop-in GstVideoView widget with a built-in, auto-hiding, themeable control bar (Material / Cupertino / adaptive via material_ui; default theme accent matches Bilibili pink).
  • GPU-friendly video via Flutter Texture (Android GL into SurfaceProducer; Apple/desktop pixel-buffer textures fed from GStreamer appsink).

Platform support

Platform Min version Architectures GStreamer
Android API 24 (7.0) arm64-v8a, armeabi-v7a, x86, x86_64 Auto on first build
iOS 13.0 Physical arm64 device (no Simulator) Auto on first SPM resolve / build
macOS 10.13 x86_64 / arm64 Auto on first SPM resolve / build
Windows 10+ x86_64 Install once on the machine (below)
Linux x86_64 Install once on the machine (below)

Apple Silicon iOS Simulator is not supported (no arm64 simulator slice in the official iOS SDK).

When to use kinetic_player instead

If your app targets Android / iOS / macOS / Web and you want a smaller binary that leans on each platform’s native player, prefer kinetic_player (GitHub).

Use gstplayer when you need a GStreamer pipeline (especially Windows / Linux, or codecs / protocols that benefit from GStreamer).

Prerequisites

Install these once on the machine that builds the app. The GStreamer SDK itself is still auto-downloaded for Android / iOS / macOS on first build.

All platforms

  • Flutter matching this plugin (sdk: ^3.12.2, flutter: ">=3.44.0" in pubspec.yaml)
  • Network access on the first Android / iOS / macOS build (GStreamer cache)

Android

HTTPS (reqwesthttpsrc) is built from Rust during the umbrella native build:

# Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default stable

# Android targets used by this plugin
rustup target add \
  aarch64-linux-android \
  armv7-linux-androideabi \
  i686-linux-android \
  x86_64-linux-android

Also required:

  • Android SDK + NDK (normal Flutter Android setup; NDK is used by Gradle)

  • pkg-config on PATH (Cargo reads GStreamer .pc files from the SDK cache)

    • macOS: brew install pkgconf
    • Linux: sudo apt install pkg-config
    • Windows: e.g. choco install pkgconfiglite

iOS

  • macOS with Xcode (Command Line Tools)
  • A physical iPhone / iPad (Simulator is not supported)
  • First build downloads the GStreamer iOS SDK automatically (~500MB)

macOS

  • Xcode
  • First build downloads the official universal GStreamer.framework automatically
  • Enable Swift Package Manager in the host pubspec.yaml (see Installation); no CocoaPods Podfile is required

Windows / Linux

Host GStreamer + pkg-config — see Windows & Linux host SDK.

Installation

1. Add the dependency

dependencies:
  gstplayer: ^0.0.3
flutter pub get

2. Run your app

flutter run

That is enough for Android / iOS / macOS (after Prerequisites):

  • The first build downloads the official GStreamer SDK into ~/Library/Caches/gstplayer/... (needs network once; no sudo).
  • Later builds reuse the cache.
  • No manual GStreamer installer for those platforms; Android still needs Rust / pkg-config as listed above.

iOS: use a physical device (flutter run -d <device>). Simulator is not supported.

Enable Swift Package Manager in the host pubspec.yaml (the plugin already sets this for its own package):

flutter:
  config:
    enable-swift-package-manager: true

The example app is SPM-only (no ios/Podfile or macos/Podfile).

macOS (SPM, one-time): Package.swift links GStreamer.framework, but nothing copies it into the .app. Add an Xcode Run Script build phase on Runner named [gstplayer] Embed GStreamer Framework (after Embed Frameworks):

set -euo pipefail
PLUGINS_JSON="${SRCROOT}/../.flutter-plugins-dependencies"
PLUGIN_MACOS="$(python3 -c "
import json, sys
plugins = json.load(open(sys.argv[1]))['plugins']['macos']
print(next(p['path'] for p in plugins if p['name'] == 'gstplayer') + '/macos')
" "$PLUGINS_JSON")"
# shellcheck source=/dev/null
source "${PLUGIN_MACOS}/scripts/gstreamer_paths.sh"
bash "${PLUGIN_MACOS}/scripts/embed_gstreamer_framework.sh"

Set the phase input to the two scripts under macos/scripts/ and the output to ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GStreamer.framework. See example/macos/Runner.xcodeproj.

CocoaPods-only hosts still get the framework via vendored_frameworks. If a host keeps a macos/Podfile under SPM hybrid mode, install_gstreamer_embed_script! in macos/gstreamer_podfile_helper.rb injects the same Run Script.

Windows / Linux: install the host GStreamer SDK once — see Windows & Linux host SDK.

Quick start

import 'dart:async';

import 'package:material_ui/material_ui.dart';
import 'package:gstplayer/gstplayer.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Kickoff only (under 50ms); gst_init continues in the background.
  unawaited(GstPlayer.initialize());
  runApp(const MyApp());
}

class PlayerPage extends StatefulWidget {
  const PlayerPage({super.key});
  @override
  State<PlayerPage> createState() => _PlayerPageState();
}

class _PlayerPageState extends State<PlayerPage> {
  final controller = GstPlayerController();

  @override
  void initState() {
    super.initState();
    controller.initialize().then((_) {
      controller.open(
        VideoSource.network('https://example.com/video.mp4'),
        autoPlay: true,
      );
    });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GstVideoView(
        controller: controller,
        language: GstPlayerLanguage.zh, // or GstPlayerLanguage.en
      ),
    );
  }
}

Other sources:

await controller.open(const VideoSource.file('/path/to/video.mp4'));
await controller.open(const VideoSource.asset('assets/sample.mp4'));

Usage notes

  1. Call GstPlayer.initialize() once early (unawaited before runApp is fine).
  2. One GstPlayerController per surface; always dispose() it.
  3. Read playback state inside ListenableBuilder / addListener.
  4. First Android / iOS / macOS build needs network for the SDK cache.

Network permissions

Local files and Flutter assets need no extra setup.

Platform For http(s):// / rtsp://
Android Ensure INTERNET in AndroidManifest.xml (Flutter templates usually already have it). Optional: android:usesCleartextTraffic="true" for plain http://.
iOS Nothing. GStreamer does not use ATS / NSURLSession. Device only.
macOS With App Sandbox, keep com.apple.security.network.client in Runner entitlements (Flutter templates usually already have it).
Windows / Linux No app permission; host GStreamer must be installed (below).

HTTPS note: for maximum compatibility the pipeline sets ssl-strict = false on the HTTP source (skips server cert verification). Open an issue if you need strict TLS made configurable.

Windows & Linux host SDK

Only these platforms need a one-time machine install. Android / iOS / macOS do not.

Windows

  1. Install gstreamer-1.0-msvc-x86_64-<version>.exe from gstreamer.freedesktop.org/download/ with “Runtime and development headers”.
  2. Put pkg-config on PATH (e.g. choco install pkgconfiglite).
  3. GUI install sets GSTREAMER_1_0_ROOT_MSVC_X86_64; set it yourself for silent installs.

Linux

sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
  gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
  libgtk-3-dev

API reference

GstPlayer.initialize()

Kickoff-only: opens the native library and starts background gst_init / FFI worker spawn. Target under 50ms; does not wait for runtime readiness. Call early (e.g. before runApp). Idempotent; concurrent calls share one Future. After return, isInitialized may still be false.

GstPlayer.ensureReady()

Awaits full runtime readiness (gst_init success + worker started). Starts initialize() if needed. Controller create and captureThumbnail call this automatically.

GstPlayer.captureThumbnail(VideoSource, {Duration? at, int maxWidth})

One-shot cover extraction via a headless GStreamer pipeline in C (gstp_thumbnail_capture). Returns PNG Uint8List. Does not require an open controller. When at is null, native picks ~5% of duration (or 1s). Progress-bar scrub preview uses scrubPreview (external VTT/sprite), not live capture.

GstPlayerController

Method Description
initialize() Creates the native player and subscribes to events.
open(VideoSource, {bool autoPlay}) Loads a source; optionally starts playback.
play() / pause() / stop() Playback transport.
togglePlayPause() Play if paused, pause if playing.
seek(Duration) Seek to a position.
setVolume(double) Volume in 0.0..1.0.
setMuted(bool) / toggleMuted() Mute control.
setSpeed(double) Playback speed multiplier.
setLooping(bool) Loop at end-of-stream.
tracks / refreshTracks() / selectTrack(MediaTrack, {enable}) Audio / video / subtitle tracks.
captureCurrentFrame() Latest decoded frame as PNG (gstp_player_capture_frame).
queryPosition() / queryDuration() Query the pipeline directly.
dispose() Tear down the player and release all resources.

Reactive state (plain getters on ChangeNotifier; read inside ListenableBuilder or after addListener): state, position, duration, videoSize, aspectRatio, bufferingPercent, volume, speed, looping, muted, isPlaying, isCompleted, error, playerId, initialized, mediaSource, tracks, supportsTracks.

PlayerState: idle, ready, buffering, playing, paused, stopped, completed, error.

VideoSource

  • VideoSource.network(String url)
  • VideoSource.file(String path) (accepts a plain path or a file:// URI)
  • VideoSource.asset(String assetKey)

GstVideoView

Embeds a Flutter Texture for the controller's video and, by default, an auto-hiding two-row Bilibili-style control bar.

Parameter Default Description
controller required The GstPlayerController to render.
aspectRatioMode AspectRatioMode.fit Layout scaling (fit / fill / stretch). Also via controller.setAspectRatioMode.
backgroundColor black Letterbox / background color.
showControls true Overlay the built-in control bar.
controlsStyle adaptive adaptive / material / cupertino.
fullscreen VideoControlsFullscreenConfig() Immersive / fullscreen chrome options.
language GstPlayerLanguage.zh Chrome copy: zh or en.
onPlayNext null Called at EOS when looping is off and play-next is enabled in settings.
onLightsOffChanged null Lights-off / theater mode changed.
poster null ImageProvider shown before frames / while idle.
keepLastFrame true Capture and keep the last frame after EOS.
scrubPreview null External WebVTT / sprite / frame list; time-only if null.
danmaku [] App-supplied DanmakuItem list.
danmakuEnabled false Toggle danmaku overlay.
onDanmakuSend null Bottom-bar send; shows the input row when non-null.
onDanmakuEnabledChanged null Danmaku toggle from chrome.
onSubtitlesEnabledChanged null CC toggle from chrome.
subtitles [] External SubtitleCue list (e.g. from SubtitleParser).
subtitlesEnabled true Toggle external subtitle overlay.
showCaptureButton true Kept for API compatibility; two-row chrome does not render a screenshot button.
onScreenshot null Host receives PNG bytes and owns saving. Plugin does not write disk or show a preview dialog.

Example with poster, subtitles, and danmaku:

GstVideoView(
  controller: controller,
  poster: MemoryImage(posterPng),
  keepLastFrame: true,
  subtitles: await SubtitleParser.loadAsset('assets/sample.srt'),
  subtitlesEnabled: true,
  danmaku: [
    DanmakuItem(at: Duration(seconds: 1), text: 'Hello'),
  ],
  danmakuEnabled: true,
)

Controls, gestures, and theming

Bottom chrome (Bilibili-inspired):

  • Lock: padlock on the far right of the center play/pause row. When locked, the bottom chrome is hidden and not hittable; tap the video to show/hide unlock only. Gestures and keyboard shortcuts do not bypass the lock.
  • Row 1: play / pause, current time, progress, remaining time, speed, settings gear, volume popup, fullscreen.
  • Row 2: danmaku toggle, capsule input + send, CC. Input/send disable when danmaku is off.
  • Settings (two pages): mirror, single-episode loop, autoplay; then play-next vs pause-at-end, 16:9 / 4:3, hide black bars, lights-off, audio tracks.

The plugin does not draw a screenshot button on this chrome. Use controller.captureCurrentFrame() or onScreenshot from the host if needed.

Volume popup: tap the speaker icon for a vertical pink slider; the live 0–100 value is shown above the track. Long-press toggles mute.

Mobile gestures (Android / iOS, both inline and fullscreen; disabled while locked):

Zone Gesture Effect
Left ~40% Vertical drag Screen brightness + HUD %
Right ~40% Vertical drag Pipeline volume + HUD %
Horizontal Drag Seek preview / seek

Scrub preview: while dragging or hovering the progress bar, a thumbnail bubble appears when scrubPreview is set (GSY-style WebVTT + sprite / frames). Without a track, only time is shown. Live sources may show time only.

Register a VideoControlsTheme in ThemeData.extensions, or use presets material() / cupertino() / bilibili() (pink #FB7299):

MaterialApp(
  theme: ThemeData(
    extensions: [VideoControlsTheme.bilibili()],
  ),
);

Overlays helpers

  • SubtitleParser.parse(String) / SubtitleParser.loadAsset(String)List<SubtitleCue>
  • DanmakuItem({at, text, color, duration}) — supply to GstVideoView.danmaku
  • GstPlayerLanguage.zh / .enGstVideoView.language chrome copy

Architecture

Dart:  GstPlayerController ──FFI──► FfiPlayerCommandPort (gstp_player_*)
       GstVideoView (Texture) ◄──native texture── GStreamer sink
C:     native/ playbin3 ─► appsink (Apple/desktop) or glimagesink (Android)
                     │ bus ─► GstpEventCallback ─► Dart Stream
  • Decoding: playbin3 with platform video sink (appsink or glimagesink).
  • Rendering: Flutter Texture + native TextureRegistry; Android uses SurfaceProducer + VideoOverlay; Apple/desktop pull BGRA frames via C ABI (gstp_texture_*).
  • Control plane: Dart FFI → narrow gstp_player_* API (see native/include/gstp_player.h).

Regenerating FFI bindings

After changing native/include/gstp_player.h:

dart run ffigen --config ffigen.yaml

NativeCore sync & verify

native/ is the canonical C source tree. iOS/macOS SPM targets compile from ios/gstplayer/NativeCore and macos/gstplayer/NativeCore, so keep them synced before build/publish:

# after editing native/{include,src}
./tool/native_core.sh sync

# before publish / release checks
./tool/native_core.sh verify

Troubleshooting

  • First build is slow / needs network (Android / iOS / macOS): normal — the official GStreamer SDK is downloaded once into ~/Library/Caches/gstplayer/. Offline CI: pre-seed that cache or set GSTREAMER_ROOT_ANDROID / GSTREAMER_ROOT_IOS / GSTPLAYER_GSTREAMER_ROOT.
  • Android cargo: command not found / missing Android targets: install Rust and rustup target add the four Android triples — see Prerequisites.
  • Android / macOS pkg-config errors while building reqwest: install pkgconf / pkg-config (e.g. brew install pkgconf).
  • iOS Simulator: not supported. Use a physical device.
  • iOS Plug-in ended with non-zero exit code: 1: usually the EnsureGStreamerIOS SPM build plugin failed (first-time SDK download, or missing ios/scripts). Open the Xcode Report navigator for the real stderr. Manual fix: sh ios/scripts/ensure_gstreamer_ios.sh (needs network, ~480MB), then rebuild. First Flutter SPM builds load the package via a .packages symlink — current plugin code resolves that path to find ios/scripts.
  • iOS 'gst/app/gstappsink.h' file not found: the example uses Swift Package Manager, which does not run the CocoaPods podspec. Package.swift / the EnsureGStreamerIOS build plugin download the SDK into ~/Library/Caches/gstplayer/gstreamer/<ver>/ios/iPhone.sdk on resolve/build. First build needs network. If headers are still missing: sh ios/scripts/ensure_gstreamer_ios.sh, then flutter clean && flutter pub get and rebuild.
  • macOS 'gst/app/gstappsink.h' file not found: same SPM path as iOS — the EnsureGStreamerMacOS build plugin runs ensure_gstreamer_macos.sh before compile (first build downloads ~870MB into ~/Library/Caches/gstplayer/gstreamer/<ver>/). Manual fix: sh macos/scripts/ensure_gstreamer_macos.sh, then rebuild.
  • macOS Library not loaded: ...GStreamer.framework: SPM hosts must add the [gstplayer] Embed GStreamer Framework Run Script (see Installation) and rebuild. Confirm YourApp.app/Contents/Frameworks/GStreamer.framework exists.
  • flutter pub get recreates ios/Podfile / macos/Podfile: enable SPM in the host pubspec.yaml (flutter.config.enable-swift-package-manager: true). A global flutter config --no-enable-swift-package-manager otherwise wins for the plugin package itself unless that key is set.
  • Release crash: Failed to lookup symbol 'gstp_init' (iOS/macOS): set Runner Strip Style = Non-Global Symbols for Release/Profile (the example already does). See Flutter C interop — Stripping symbols.
  • Link error: undefined symbol: _gstp_* (path / SPM): run ./tool/native_core.sh sync, then flutter clean and rebuild.
  • APK too large: narrow abiFilters or ship an App Bundle (each ABI carries a large GStreamer runtime).
  • Android minify / R8: keep org.freedesktop.gstreamer.** (plugin AAR already ships consumer ProGuard rules).
  • Windows pkg-config / missing glib: install the development GStreamer MSVC package and point PKG_CONFIG_PATH at ...\lib\pkgconfig.

Repository

License

See LICENSE.

Libraries

gstplayer
跨平台 GStreamer 视频播放器 Flutter 插件公开 API / Public API for the cross-platform GStreamer video player plugin.