playora 0.5.0
playora: ^0.5.0 copied to clipboard
Production-grade Flutter video player with a custom RTL-aware skin — HLS/MP4, quality/speed/subtitle/audio menus, ads, gestures, playlists and optional analytics.
🎬 Playora #
A production-grade Flutter video player with a custom RTL-aware, gold-on-dark skin.
HLS/MP4 · quality/speed/subtitle/audio menus · pre/mid/post-roll ads · touch gestures · playlists & seasons · optional analytics + resume · Persian (RTL) & English out of the box
🕹 Live demo — the same example app, built for the web.
پلیر ویدیوی اختصاصی فلاتر با اسکین سفارشی راستچین: کیفیت/سرعت/زیرنویس/صدا، تبلیغات، ژستهای لمسی، لیست پخش و فصلها، «ادامه پخش» و آنالیتیکس اختیاری.
Features #
- HLS + MP4 — adaptive HLS or progressive MP4. Pass a list of MP4 renditions for a manual quality menu.
- Real adaptive quality — mpv itself never switches HLS variants (it pins the highest one and buffers), so Playora runs its own governor: it measures how fast media actually arrives and steps the quality down when the buffer drains, back up when the link proves headroom.
- Buffers deep — reads ahead for as long as the byte cap allows (often the rest of the episode) with a disk-backed cache, long network timeouts and transparent HTTP reconnects, so a weak connection doesn't stall playback.
- Custom skin — dark, gold-accented, light mode, RTL/LTR, responsive. Layout stays physical; RTL only right-aligns text (like YouTube).
- Quality / speed / subtitles / audio menus — embedded HLS subtitle and multi-language audio tracks are detected automatically; add external WebVTT/SRT subtitles on top.
qualityValidatehides unwanted renditions. - Scrub previews — WebVTT thumbnail tracks (individual images or
#xywhsprite sheets). - Ads — pre-roll, mid-rolls (at content positions) and post-roll, with skip countdown and click-through. Ad playback is never counted in content analytics.
- Playlist & seasons — episode panel with sticky season headers (
Episode.group), prev/next, auto-advance, and an up-next card near the end of an episode. - Gestures — double-tap edges ±10s, long-press 2×, brightness/volume vertical swipes; desktop gets click-to-pause, double-click fullscreen and hover-reveal.
- Fullscreen — route-based fullscreen with automatic landscape orientation + immersive mode on mobile.
- Overlays — like button (controllable), transient info badge, operator notice banner, blocking IP/network restriction with retry/exit.
- VOD providers —
vodTypeexchanges an opaque play token for the real stream URL via a provider API (ABR Hamrahi, Poyan), so existing back-ends keep working. - Optional analytics + resume — canonical events to your ingest endpoint (batched, retried, heartbeats) and a "continue watching" banner, or auto-resume straight at the saved position (
resumeMode). Drive it from your own back-end withresolveResume, and report a watch heartbeat to your current tracker withonWatchInterval. - Persistence — remember volume/mute/speed/brightness across sessions (
persistSettings). - fa / en built in — Persian digits, RTL text, overridable strings.
Runs on Android, iOS, Windows, macOS, Linux and Web (web uses hls.js under the hood, via media_kit).
Install #
# pubspec.yaml
dependencies:
playora:
git:
url: https://github.com/Ali-Roodi/playora.git
Initialize media_kit once in main():
void main() {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
runApp(const MyApp());
}
Quick start #
import 'package:playora/playora.dart';
PlayoraPlayer(
src: 'https://cdn.example.com/movie/master.m3u8',
poster: 'https://cdn.example.com/poster.jpg',
title: 'Sample Movie',
locale: PlayerLocale.fa, // fa (RTL) | en (LTR)
)
That's it — a self-contained player with zero network calls beyond the stream itself.
Playlist, seasons & up-next #
final episodes = [
Episode(id: 'e1', src: '.../e1.m3u8', title: 'سریال', subtitle: 'قسمت اول', group: 'فصل اول', poster: '...'),
Episode(id: 'e2', src: '.../e2.m3u8', title: 'سریال', subtitle: 'قسمت دوم', group: 'فصل اول', poster: '...'),
];
PlayoraPlayer(
episodes: episodes,
currentEpisodeId: current,
onEpisodeChange: (id) => setState(() => current = id),
)
Episode.group renders season headers in the playlist panel. Near the end of an episode
that has a next one, an up-next card (cover + filling progress bar) appears; ignoring it
auto-advances on end, tapping it jumps straight to the next episode.
Ads #
PlayoraPlayer(
src: src,
// Pre-roll shorthand:
ad: const AdConfig(src: 'https://ads.example.com/creative.m3u8', clickThrough: 'https://sponsor.example.com'),
// …or multiple breaks:
ads: [
const AdBreak(src: '...', offset: AdOffset.pre),
AdBreak(src: '...', offset: AdOffset.at(const Duration(minutes: 10))),
const AdBreak(src: '...', offset: AdOffset.post),
],
)
Optional analytics + resume #
PlayoraPlayer(
src: src,
analytics: const LogplexAnalyticsConfig(
baseUrl: 'https://ingest.example.com',
apiKey: '…',
userId: 'viewer-1',
contentId: 'movie-42',
contentType: 'movie',
),
resume: true, // continue-watching banner
)
Omit analytics entirely to run the player standalone — no requests, nothing backend-specific.
Your own back-end (no analytics needed) #
PlayoraPlayer(
src: playToken, // opaque token for the provider
vodType: VodProvider.abrHamrahi, // standard | abrHamrahi | poyan
vodCustomUrl: {VodProvider.abrHamrahi: 'https://api.example.com/vod/{token}'},
qualityValidate: (height) => height > 400, // hide tiny renditions
// periodic "user watch" report to your current tracker
onWatchInterval: (info) async {
// info.quality is "W*H" (e.g. "1920*1080"), info.playDuration in seconds
final id = await reportWatch(info);
return id; // chained into the next call
},
// "continue watching" banner sourced from your back-end
resolveResume: () async {
final w = await getWatch(contentId);
return w == null ? null : ResumePoint(position: Duration(seconds: w.seconds));
},
)
Weak connections #
Two independent knobs, both on by default.
PlayoraPlayer(
src: src,
// How far ahead to pre-load. aggressive() (the default) keeps downloading
// for as long as the byte cap allows — usually the rest of the episode.
buffering: const PlayoraBufferConfig.aggressive(), // or .balanced() / .minimal()
// Adaptive quality. mpv has no ABR of its own, this is what makes "Auto"
// adapt: it steps down when the buffer drains or playback stalls, and back
// up once the connection has proven headroom.
abr: const PlayoraAbrConfig(
startupHeightCeiling: 720, // where a fresh session opens; null = the top
minBufferAhead: Duration(seconds: 8),
upshiftBufferAhead: Duration(seconds: 30),
),
)
The cache is backed by a file in the app's temp directory when one is writable — that is what allows the cap to be large without costing RAM. Without one it falls back to a smaller, RAM-safe cap automatically.
With persistSettings: true the height the governor settled on is remembered,
so the next session opens there instead of probing up again.
Set abr: const PlayoraAbrConfig(enabled: false) to hand quality selection
back to mpv, and buffering: const PlayoraBufferConfig.minimal() to return to
media_kit's stock cache.
Continue watching #
PlayoraPlayer(
src: src,
resume: true,
resumeMode: PlayerResumeMode.auto, // open at the saved position
resolveResume: () async => ResumePoint(position: savedPosition),
)
PlayerResumeMode.prompt (the default) starts at zero and shows a banner the
viewer has to tap. Note that if they ignore it, the watch heartbeat reports
the new near-zero position and the saved one is lost — which is why a
half-watched title can come back playing from the start. auto resolves the
point before the first open() and hands it to mpv as the start position, and
turns the card into a "start over" offer.
Theming & i18n #
PlayoraPlayer(
appearance: PlayerAppearance.dark, // dark | light
theme: const PlayoraTheme(accent: Color(0xFFE8B84B)), // full token overrides
locale: PlayerLocale.fa, // RTL text; layout stays physical
strings: myCustomStrings, // override any label
)
API overview #
| Parameter | Type | Description |
|---|---|---|
src / sources |
String / List<VideoSource> |
HLS/MP4 URL, or MP4 renditions for a manual quality menu. For a non-standard vodType, an opaque play token. |
vodType / vodCustomUrl |
VodProvider / Map |
Provider token exchange (ABR Hamrahi, Poyan). {token} is substituted. |
qualityValidate |
bool Function(int height) |
Hide embedded renditions whose height fails the predicate. Auto stays. |
poster / title / episodeLabel |
String |
Cover image and titles above the scrubber. |
thumbnails |
String |
WebVTT thumbnails track for scrub previews. |
subtitles |
List<ExternalSubtitle> |
External subtitle files (embedded tracks are auto-detected). |
locale / textDirection |
PlayerLocale / TextDirection? |
UI language and direction. fa → RTL. |
theme / appearance |
PlayoraTheme / PlayerAppearance |
Design token overrides and color scheme. |
episodes / currentEpisodeId / onEpisodeChange |
— | Controlled playlist. |
ad / ads |
AdConfig / List<AdBreak> |
Pre-roll shorthand, or breaks at pre/post/AdOffset.at(...). |
notice / restriction / badge |
— | Operator notice, blocking overlay, transient info pill. |
analytics / resume / resolveResume |
— | Built-in analytics, resume banner, host-driven resume. |
resumeMode |
PlayerResumeMode |
prompt (banner) or auto (open at the saved position). |
buffering |
PlayoraBufferConfig |
Read-ahead, cache caps, network timeouts, reconnects. |
abr |
PlayoraAbrConfig |
Adaptive quality governor: thresholds, cooldowns, on/off. |
debugLogging |
bool |
Print [playora] diagnostics for resume/load/ABR. |
onWatchInterval / watchInterval |
— | External watch heartbeat + cadence (default 5s). |
onPlayerReady |
ValueChanged<PlayoraController?> |
Imperative control (seek, pause, tracks, …). |
persistSettings / settingsKey |
bool / String |
Remember volume/mute/speed/brightness. |
fullscreenOnPlay / onBack / onLike / liked / loading |
— | Behavior toggles & host hooks. |
overlayBuilder |
List<Widget> Function(BuildContext) |
Extra overlays inside the player surface. |
Development #
flutter pub get
flutter analyze
flutter test
cd example && flutter run # demo app (any platform)
flutter build web --base-href /playora/ # the GitHub Pages demo
License #
MIT © Ali Roodi
Built on media_kit. Inspired by logplex-player-react; the demo uses the Sprite Fight sample assets from Vidstack.