🎬 Playora

A production Flutter video player — HLS/MP4, a fully custom RTL/LTR gold-on-dark skin, quality / subtitle / audio menus, ads, gestures, playlists & seasons, and optional built-in analytics + resume.

#Live demo

The player itself, live — the same build that ships in the package: HLS with quality levels, playlist & seasons, subtitles and the full skin. Every feature toggle (ads, restriction, badge, …) lives in the full playground.

playora — live player Open the full playground ↗

Gestures are mobile-first — open the demo full-page on a phone for the complete experience.

#Quick start

Add the git dependency, initialize media_kit once, then point the player at an HLS or MP4 source.

# pubspec.yaml
dependencies:
  playora:
    git:
      url: https://github.com/Ali-Roodi/playora.git
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MediaKit.ensureInitialized();
  runApp(const MyApp());
}
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. Analytics, resume and providers are all opt-in.

#Features

#Episodes, seasons & up-next

Provide a list of episodes and the playlist panel with prev/next nav appears automatically. Episode.group renders sticky season headers, and near the end of an episode an up-next card (cover + filling progress) offers the next one — or auto-advances.

final episodes = [
  Episode(id: 'e1', src: '.../e1.m3u8', title: 'سریال', subtitle: 'قسمت اول', group: 'فصل اول'),
  Episode(id: 'e2', src: '.../e2.m3u8', title: 'سریال', subtitle: 'قسمت دوم', group: 'فصل اول'),
];

PlayoraPlayer(
  episodes: episodes,
  currentEpisodeId: current,
  onEpisodeChange: (id) => setState(() => current = id),
)

#Ads

Pre-roll, mid-rolls (at content positions) and post-roll, with skip countdown and click-through. The player handles playback and resumes the content afterwards; ad playback is never counted in content analytics.

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),
  ],
)

#Analytics & resume (optional)

Analytics is entirely optional — the player works fully without it. Pass a config and it emits canonical playback events to your ingest endpoint (batched, retried, heartbeats) and offers a “continue watching” banner from the saved position. No extra wiring.

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
)

#Your own back-end

No Logplex back-end needed: exchange an opaque play token via a VOD provider, report a periodic watch heartbeat to your own tracker, and feed the resume banner from your own API.

PlayoraPlayer(
  src: playToken,                             // opaque token for the provider
  vodType: VodProvider.abrHamrahi,            // standard | abrHamrahi | poyan
  qualityValidate: (height) => height > 400,  // hide tiny renditions

  onWatchInterval: (info) async {
    final id = await reportWatch(info);       // your tracker
    return id;                                // chained into the next call
  },

  resolveResume: () async {
    final w = await getWatch(contentId);
    return w == null ? null : ResumePoint(position: Duration(seconds: w.seconds));
  },
)

#Theming & localization

Theme via design tokens, switch between dark and light, and flip the language with locale. RTL only right-aligns text — controls, seek direction and gestures stay physical, like YouTube.

PlayoraPlayer(
  appearance: PlayerAppearance.dark,                 // dark | light
  theme: const PlayoraTheme(accent: Color(0xFFE8B84B)),
  locale: PlayerLocale.fa,                           // RTL text; layout stays physical
  strings: myCustomStrings,                          // override any label
)

#API overview

The most important PlayoraPlayer parameters. Everything is optional except a source (src / sources / episodes).

ParameterTypeDescription

#Platforms

Runs everywhere Flutter does — web uses hls.js under the hood, via media_kit.

🤖 Android🍎 iOS🪟 Windows🖥 macOS🐧 Linux🌐 Web