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).
- Repository: github.com/wanwenfeng4798/GstPlayer
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 (example app)
Table of contents
- Screenshots
- Features
- Platform support
- When to use kinetic_player instead
- Prerequisites
- Installation
- Quick start
- Usage notes
- Network permissions
- Windows & Linux host SDK
- API reference
- Architecture
- NativeCore sync & verify
- Troubleshooting
- Repository
- License
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
DanmakuItemcues. - Frame capture (
captureCurrentFrame/onScreenshot— host saves PNG) and one-shot covers (captureThumbnail). Built-in chrome no longer shows a screenshot button. - Reactive state via
ChangeNotifierplain getters: state, position, duration, video size, aspect ratio, buffering %, volume, speed, looping, muted, and errors. Rebuild withListenableBuilder/addListener. - A drop-in
GstVideoViewwidget with a built-in, auto-hiding, themeable control bar (Material / Cupertino / adaptive viamaterial_ui; default theme accent matches Bilibili pink). - GPU-friendly video via Flutter
Texture(Android GL intoSurfaceProducer; Apple/desktop pixel-buffer textures fed from GStreamerappsink).
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"inpubspec.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-configonPATH(Cargo reads GStreamer.pcfiles from the SDK cache)- macOS:
brew install pkgconf - Linux:
sudo apt install pkg-config - Windows: e.g.
choco install pkgconfiglite
- macOS:
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.frameworkautomatically - Enable Swift Package Manager in the host
pubspec.yaml(see Installation); no CocoaPodsPodfileis 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-configas 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
- Call
GstPlayer.initialize()once early (unawaitedbeforerunAppis fine). - One
GstPlayerControllerper surface; alwaysdispose()it. - Read playback state inside
ListenableBuilder/addListener. - 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 = falseon 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
- Install
gstreamer-1.0-msvc-x86_64-<version>.exefrom gstreamer.freedesktop.org/download/ with “Runtime and development headers”. - Put
pkg-configonPATH(e.g.choco install pkgconfiglite). - 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 afile://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 toGstVideoView.danmakuGstPlayerLanguage.zh/.en—GstVideoView.languagechrome 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:
playbin3with platform video sink (appsinkorglimagesink). - Rendering: Flutter
Texture+ nativeTextureRegistry; Android usesSurfaceProducer+ VideoOverlay; Apple/desktop pull BGRA frames via C ABI (gstp_texture_*). - Control plane: Dart FFI → narrow
gstp_player_*API (seenative/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 setGSTREAMER_ROOT_ANDROID/GSTREAMER_ROOT_IOS/GSTPLAYER_GSTREAMER_ROOT. - Android
cargo: command not found/ missing Android targets: install Rust andrustup target addthe four Android triples — see Prerequisites. - Android / macOS
pkg-configerrors while building reqwest: installpkgconf/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 theEnsureGStreamerIOSSPM build plugin failed (first-time SDK download, or missingios/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.packagessymlink — current plugin code resolves that path to findios/scripts. - iOS
'gst/app/gstappsink.h' file not found: the example uses Swift Package Manager, which does not run the CocoaPods podspec.Package.swift/ theEnsureGStreamerIOSbuild plugin download the SDK into~/Library/Caches/gstplayer/gstreamer/<ver>/ios/iPhone.sdkon resolve/build. First build needs network. If headers are still missing:sh ios/scripts/ensure_gstreamer_ios.sh, thenflutter clean && flutter pub getand rebuild. - macOS
'gst/app/gstappsink.h' file not found: same SPM path as iOS — theEnsureGStreamerMacOSbuild plugin runsensure_gstreamer_macos.shbefore 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 FrameworkRun Script (see Installation) and rebuild. ConfirmYourApp.app/Contents/Frameworks/GStreamer.frameworkexists. flutter pub getrecreatesios/Podfile/macos/Podfile: enable SPM in the hostpubspec.yaml(flutter.config.enable-swift-package-manager: true). A globalflutter config --no-enable-swift-package-managerotherwise 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, thenflutter cleanand rebuild. - APK too large: narrow
abiFiltersor 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 pointPKG_CONFIG_PATHat...\lib\pkgconfig.
Repository
- Repository: github.com/wanwenfeng4798/GstPlayer
- Issues: github.com/wanwenfeng4798/GstPlayer/issues
License
See LICENSE.
Libraries
- gstplayer
- 跨平台 GStreamer 视频播放器 Flutter 插件公开 API / Public API for the cross-platform GStreamer video player plugin.