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.
Table of Contents
- Features
- Installation
- Quick Start
- MaestroController
- VideoMaestroConfig
- OptionsVisibilityControl
- Captions
- Theming
- Localization
- Bottom Sheets
- Full Screen
- API Reference
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
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:
VideoMaestrorequires aVideoMaestroThemeextension to be present in the currentThemeData. 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 currentThemeDatais darkcurrent— the currently resolvedVideoMaestroTheme, 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 playerVideoMaestro.fullScreen(...)— used internally by the full-screen route
MaestroController
| Method / Property | Description |
|---|---|
initialize() |
Load the video. Emits Loading → Success 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