playora 0.5.0 copy "playora: ^0.5.0" to clipboard
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

CI Demo License: MIT

🕹 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. qualityValidate hides unwanted renditions.
  • Scrub previews — WebVTT thumbnail tracks (individual images or #xywh sprite 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 providersvodType exchanges 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 with resolveResume, and report a watch heartbeat to your current tracker with onWatchInterval.
  • 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.

2
likes
160
points
108
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Production-grade Flutter video player with a custom RTL-aware skin — HLS/MP4, quality/speed/subtitle/audio menus, ads, gestures, playlists and optional analytics.

Homepage
Repository (GitHub)
View/report issues

Topics

#video #player #hls #media-kit #rtl

License

MIT (license)

Dependencies

flutter, http, media_kit, media_kit_libs_video, media_kit_video, path_provider, shared_preferences, url_launcher

More

Packages that depend on playora