video_maestro 0.0.2 copy "video_maestro: ^0.0.2" to clipboard
video_maestro: ^0.0.2 copied to clipboard

Video Maestro

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