Video Maestro 🎬

A powerful, customizable, and feature-rich Flutter video player package built on top of video_player. Video Maestro removes the boilerplate of building a video player UI from scratch — giving you smart auto-detected sources, gesture controls, captions, full-screen, theming, and localization out of the box.

License: MIT Platform


Table of Contents


Features

Feature Description
🔗 Auto Source Detection Pass any URL/path string — network, asset, or local file is detected automatically
▶️ Playback Controls Play, pause, seek with a polished progress bar and floating time tooltip
👆 Double-Tap to Seek Tap the left/right edges to seek backward/forward by a configurable number of seconds
Hold for 2× Speed Long-press anywhere on the video to jump to 2× speed; release to restore
📺 Full Screen Native push-based full-screen transition with automatic orientation handling
📝 Captions .vtt and .srt from assets, local files, or network — with caching and deduplication
🎨 Full Theming Every color, icon size, gradient, text style, and slider is customizable via ThemeExtension
🌍 Localization Ships with English and Arabic built in; reads the Locale automatically
🧩 Modular Visibility Show or hide any UI element individually with OptionsVisibilityControl
Custom Widgets Inject extra bottom-bar buttons or overlay widgets onto the video

Screenshots

Light Mode     Dark Mode     Full Screen

Light Mode                Dark Mode                Full Screen


Installation

Add the package to your pubspec.yaml:

dependencies:
  video_maestro: ^0.0.2

Then run:

flutter pub get

Quick Start

import 'package:flutter/material.dart';
import 'package:video_maestro/video_maestro.dart';

class MyVideoScreen extends StatefulWidget {
  const MyVideoScreen({super.key});

  @override
  State<MyVideoScreen> createState() => _MyVideoScreenState();
}

class _MyVideoScreenState extends State<MyVideoScreen> {
  late final MaestroController _controller;

  @override
  void initState() {
    super.initState();
    _controller = MaestroController(
      url: 'https://www.example.com/sample_video.mp4',
    );
    _controller.initialize();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: VideoMaestro(
          controller: _controller,
        ),
      ),
    );
  }
}

Important: VideoMaestro requires a VideoMaestroTheme extension to be present in the current ThemeData. See Theming for setup.


MaestroController

MaestroController is the single entry point for all video operations. Create one instance per player and pass it to VideoMaestro.

Creating a controller

// Network URL
final controller = MaestroController(url: 'https://example.com/video.mp4');

// Asset
final controller = MaestroController(url: 'assets/videos/intro.mp4');

// Local file path
final controller = MaestroController(url: '/storage/emulated/0/video.mp4');

// Provide your own VideoPlayerController
final controller = MaestroController(
  url: 'https://example.com/video.mp4',
  videoPlayerController: myExistingController,
);

Source type (network / asset / file) is auto-detected from the string. You never need to specify it manually.

Lifecycle

// Initialize and start loading
await controller.initialize();

// Playback
controller.play();
controller.pause();

// Seek to a position
await controller.seekTo(const Duration(seconds: 30));

// Always dispose when done
controller.dispose();

State stream

MaestroController exposes a BehaviorSubject you can listen to:

StreamBuilder<VideoMaestroState>(
  stream: controller.playerStateNotifier,
  builder: (context, snapshot) {
    return switch (snapshot.data) {
      VideoMaestroInitialState()  => const SizedBox(),
      VideoMaestroLoadingState()  => const CircularProgressIndicator(),
      VideoMaestroSuccessState()  => const Text('Playing'),
      VideoMaestroFailureState(:final message) => Text('Error: $message'),
      null => const SizedBox(),
    };
  },
);
State Meaning
VideoMaestroInitialState Controller created, not yet initialized
VideoMaestroLoadingState initialize() called, loading in progress
VideoMaestroSuccessState Video ready to play
VideoMaestroFailureState Initialization failed (optional error message)

Useful getters

Getter Type Description
isPlaying bool Whether the video is currently playing
playerController VideoPlayerController Direct access to the underlying controller
captionLoaderHelper CaptionLoaderHelper Manages caption loading and switching
playerStateNotifier BehaviorSubject<VideoMaestroState> Observable state stream
isInFullScreenNotifier BehaviorSubject<bool> Tracks full-screen state

VideoMaestroConfig

VideoMaestroConfig is passed to VideoMaestro to configure behavior and visible elements.

VideoMaestro(
  controller: _controller,
  config: VideoMaestroConfig(
    doubleTapSeekTime: 10, // Seek ±10 s on double-tap (default: 5)
    optionsVisibility: OptionsVisibilityControl(
      showSpeedButton: true,
      showFullScreenButton: true,
      doubleTapToSeek: true,
      holdToDoubleSpeed: true,
      showCaptions: true,
    ),
    captions: [
      CaptionData(name: 'English', path: 'assets/en.vtt'),
    ],
    bufferLoader: const CircularProgressIndicator(strokeWidth: 3),
    extraBottomButtons: [
      IconButton(icon: const Icon(Icons.share), onPressed: _shareVideo),
    ],
    onVideoWidgets: [
      const Positioned(top: 12, right: 12, child: MyBadgeWidget()),
    ],
  ),
)

Config properties

Property Type Default Description
doubleTapSeekTime int? 5 Seconds to seek on double-tap
optionsVisibility OptionsVisibilityControl all true UI toggle flags
captions List<CaptionData>? null Subtitle tracks to offer
bufferLoader Widget CircularProgressIndicator Widget shown while buffering
extraBottomButtons List<Widget>? null Extra icons appended to the bottom bar
onVideoWidgets List<Widget>? null Widgets stacked on top of the video (use Positioned)

Separate full-screen config

You can provide a different config when the player enters full screen:

VideoMaestro(
  controller: _controller,
  config: VideoMaestroConfig(doubleTapSeekTime: 5),
  fullScreenConfig: VideoMaestroConfig(
    doubleTapSeekTime: 15,
    optionsVisibility: OptionsVisibilityControl(showCaptions: false),
  ),
)

OptionsVisibilityControl

Fine-tune which UI elements are visible.

// Show everything (default)
const OptionsVisibilityControl()

// Custom combination
const OptionsVisibilityControl(
  showSpeedButton: false,
  showFullScreenButton: true,
  doubleTapToSeek: true,
  holdToDoubleSpeed: false,
  showCaptions: true,
)
Flag Default Description
showSpeedButton true Speed icon in the bottom bar
showFullScreenButton true Full-screen toggle icon
doubleTapToSeek true Double-tap left/right to seek
holdToDoubleSpeed true Long-press to play at 2×
showCaptions true Caption text overlay + captions icon

Captions

Video Maestro has a built-in CaptionLoaderHelper that handles loading, caching, and switching between multiple subtitle tracks.

Supported formats

  • WebVTT (.vtt)
  • SubRip (.srt)

Supported sources

  • App assets
  • Local file paths
  • Network URLs (downloaded, cached to disk, deduplicated)

Setting up captions

VideoMaestroConfig(
  captions: [
    // From asset (type auto-detected from extension)
    CaptionData(name: 'English', path: 'assets/captions/en.vtt'),

    // From network
    CaptionData(name: 'Arabic', path: 'https://example.com/ar.srt'),

    // Explicit type
    CaptionData(
      name: 'French',
      path: 'assets/fr_captions',
      type: CaptionTypeEnum.vtt,
    ),
  ],
)
  • If one caption track is provided, it is loaded and applied automatically.
  • If multiple tracks are provided, a bottom sheet is shown when the user taps the caption icon, allowing them to pick a track or remove captions.

CaptionData fields

Field Type Description
name String Display name shown in the selection sheet
path String Asset path, file path, or network URL
type CaptionTypeEnum? vtt or srt — auto-detected from extension if omitted

Using CaptionLoaderHelper directly

// Load and apply a track programmatically
await controller.captionLoaderHelper.loadAndApply(
  const CaptionData(name: 'English', path: 'assets/en.vtt'),
);

// Remove the active caption
await controller.captionLoaderHelper.clearCaption();

// Check if a path is the currently active caption
final bool active = controller.captionLoaderHelper.isActive('assets/en.vtt');

// Observe the active caption
ValueListenableBuilder<CaptionData?>(
  valueListenable: controller.captionLoaderHelper.activeCaptionNotifier,
  builder: (context, caption, _) => Text(caption?.name ?? 'No caption'),
);

Theming

Video Maestro integrates with Flutter's ThemeExtension system. Add VideoMaestroTheme to your app's ThemeData (this is required.)

Basic setup

MaterialApp(
  theme: ThemeData(
    extensions: const [
      VideoMaestroTheme(), // All defaults — dark player on black background
    ],
  ),
  home: MyVideoScreen(),
)

Full customization

MaterialApp(
  theme: ThemeData(
    extensions: [
      VideoMaestroTheme(
        // Background
        backgroundColor: Colors.black,

        // Icons
        iconColor: Colors.white,
        iconSize: 26,

        // Seek overlay (double-tap animation)
        seekOverlayGradient: [Colors.black12, Colors.black38],
        seekOverlayIconColor: Colors.white70,
        seekOverlayIconSize: 34,

        // Bottom controls gradient
        controlsBarGradient: [Colors.transparent, Colors.black54, Colors.black87],

        // Progress / seek bar
        seekBarTrackColor: Colors.black45,
        seekBarBufferColor: Color(0x64757575),
        sliderThemeData: SliderThemeData(
          trackHeight: 4,
          thumbShape: FlatThumbShape(thumbRadius: 8),
          overlayColor: Colors.transparent,
          inactiveTrackColor: Colors.white24,
        ),

        // Time labels
        timePositionStyle: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
        timeDurationStyle: TextStyle(color: Color(0xDCFFFFFF), fontSize: 14),

        // Floating time tooltip
        floatingTimeBackgroundColor: Colors.black26,
        floatingTimeTextStyle: TextStyle(fontSize: 12, color: Colors.white),

        // Captions
        captionTextStyle: TextStyle(fontSize: 15, color: Colors.white),

        // 2× speed badge shown during long-press
        doubleSpeedBadgeView: MaestroDoubleSpeedView(),

        // Device orientations to restore after exiting full screen
        appPreferredOrientations: [DeviceOrientation.portraitUp],
      ),
    ],
  ),
)

Per-widget theme override

Use VideoMaestroThemeOverrider to override the theme for a specific VideoMaestro widget without touching the global theme:

VideoMaestroThemeOverrider(
  data: (isDark, current) => current.copyWith(
    backgroundColor: isDark ? Colors.grey[900]! : Colors.white,
    iconColor: isDark ? Colors.white : Colors.black,
  ),
  child: VideoMaestro(controller: _controller),
)

The data callback receives:

  • isDark — whether the current ThemeData is dark
  • current — the currently resolved VideoMaestroTheme, so you can use .copyWith() to change only what you need

VideoMaestroTheme properties reference

Property Type Default
backgroundColor Color Colors.black
iconColor Color Colors.white
iconSize double 26
seekOverlayGradient List<Color> [black12, black26]
seekOverlayIconColor Color Colors.white38
seekOverlayIconSize double 34
controlsBarGradient List<Color> [black12, black54, black87]
seekBarTrackColor Color Colors.black45
seekBarBufferColor Color Color(0x64757575)
sliderThemeData SliderThemeData See above
timePositionStyle TextStyle white, 14sp, w500
timeDurationStyle TextStyle white 87%, 14sp, w500
floatingTimeBackgroundColor Color Colors.black26
floatingTimeTextStyle TextStyle white, 12sp, w500
captionTextStyle TextStyle? white, 14sp
doubleSpeedBadgeView Widget MaestroDoubleSpeedView()
appPreferredOrientations List<DeviceOrientation> All orientations

FlatThumbShape

A custom SliderComponentShape included in the package that renders a flat circular thumb for the seek bar. Configurable via thumbRadius.

sliderThemeData: SliderThemeData(
  thumbShape: FlatThumbShape(thumbRadius: 6),
)

Localization

Video Maestro reads the Locale from Flutter's Localizations widget. English and Arabic are built in.

Setup

MaterialApp(
supportedLocales: const [
Locale('en'),
Locale('ar'),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// ...
)

No additional delegate registration is required for Video Maestro itself — it hooks into the existing Localizations.localeOf(context).


Bottom Sheets

Video Maestro provides two ready-made modal bottom sheets accessible via MaestroBottomSheets. They are already wired into the default bottom controls bar, but you can invoke them manually too.

Speed sheet

await MaestroBottomSheets.openSpeedBottomSheet(
context: context,
controller: _controller,
// Optional route settings for navigator-based dismissal
routeSettings: const RouteSettings(name: 'maestro-speed-bottom-sheet'),
);
  • Displays an interactive slider from 0.25× to 2.0× (step: 0.05×).
  • Speed is applied live as the thumb is dragged.
  • Video is automatically paused before opening and resumed after closing.

Captions sheet

await MaestroBottomSheets.openCaptionsBottomSheet(
context: context,
controller: _controller,
captions: myCaptionList,
routeSettings: const RouteSettings(name: 'maestro-captions-bottom-sheet'),
);
  • Lists all available caption tracks.
  • The active track is highlighted with a check mark.
  • Includes a Remove Caption option when a caption is active.
  • Video is paused/resumed automatically.

Full Screen

Full-screen mode is handled internally via VideoMaestroFullScreen. Tapping the full-screen icon in the bottom bar pushes a new route that forces landscape orientation.

// Entering full screen is done automatically via the button.
// To control orientations restored after exiting, configure:
VideoMaestroTheme(
appPreferredOrientations: [DeviceOrientation.portraitUp],
)

You can also pass a distinct fullScreenConfig to VideoMaestro so the full-screen player has different settings (e.g., larger seek increments, hidden captions):

VideoMaestro(
controller: _controller,
config: VideoMaestroConfig(doubleTapSeekTime: 5),
fullScreenConfig: VideoMaestroConfig(doubleTapSeekTime: 30),
)

API Reference

VideoMaestro widget

Parameter Type Description
controller MaestroController The playback controller
config VideoMaestroConfig Behavior and UI config (defaults apply)
fullScreenConfig VideoMaestroConfig? Overrides config when in full-screen

Named constructors:

  • VideoMaestro(...) — standard embedded player
  • VideoMaestro.fullScreen(...) — used internally by the full-screen route

MaestroController

Method / Property Description
initialize() Load the video. Emits LoadingSuccess or Failure
play() Start playback
pause() Pause playback
seekTo(Duration) Seek; tracks whether target is buffered
dispose() Release all resources
isPlaying bool — current playback state
playerController Underlying VideoPlayerController
playerStateNotifier BehaviorSubject<VideoMaestroState>
isInFullScreenNotifier BehaviorSubject<bool>
captionLoaderHelper CaptionLoaderHelper instance

CaptionLoaderHelper

Method / Property Description
loadAndApply(CaptionData) Load (with cache) and activate a caption track
clearCaption() Remove the active caption
isActive(String path) Returns true if the given path is the active track
activeCaptionNotifier BehaviorSubject<CaptionData?> — observe active caption

License

MIT License

Libraries

video_maestro