Better Player
Better Player runs native media engines under the hood so you do not have to stitch video_player, custom UI packages, caching plugins, and platform channels together. Powered by AndroidX Media3 on Android, AVPlayer via Swift FFI on iOS and macOS, Shaka Player on Web, and libmpv on Windows.
Installation
Add Better Player to your pubspec.yaml:
flutter pub add better_player
Platform Permissions
- Android: If you stream over plain HTTP (local test servers), set
android:usesCleartextTraffic="true"inside the<application>tag ofandroid/app/src/main/AndroidManifest.xml. - iOS: Add
NSAppTransportSecurityexceptions toios/Runner/Info.plistif you stream from non-HTTPS endpoints. For background audio, enable theaudiobackground mode under Signing & Capabilities in Xcode.
Platform Support
| Platform | Native Engine | Bridge | DRM Support |
|---|---|---|---|
| Android | AndroidX Media3 | JNI | Widevine (L1/L3), ClearKey |
| iOS | AVPlayer | Swift FFI | FairPlay |
| macOS | AVPlayer | Swift FFI | FairPlay |
| Web | Shaka Player | JavaScript / WASM | Widevine, ClearKey |
| Windows | libmpv | C FFI | Software / Token |
Quick Start
For basic video without custom controls or listeners, use the convenience constructor:
import 'package:better_player/better_player.dart';
import 'package:flutter/material.dart';
class SimpleVideoPlayer extends StatelessWidget {
const SimpleVideoPlayer({super.key});
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 16 / 9,
child: BetterPlayer.network(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
betterPlayerConfiguration: const PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
looping: true,
),
),
);
}
}
Controller Architecture
Production setups use BetterPlayerController. Configuration is cleanly split between player behavior (PlayerConfiguration) and media data (PlayerDataSource):
import 'package:better_player/better_player.dart';
import 'package:flutter/material.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({super.key});
State<VideoScreen> createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
late final BetterPlayerController _controller;
void initState() {
super.initState();
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
);
_controller = BetterPlayerController(
const PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
fit: BoxFit.contain,
),
betterPlayerDataSource: dataSource,
);
}
void dispose() {
// Only dispose if you set autoDispose: false in PlayerConfiguration
// By default, BetterPlayer disposes the controller when unmounted
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BetterPlayer(controller: _controller),
),
);
}
}
Data Source Types
Load videos from three origins:
// Network URL
PlayerDataSource(DataSourceType.network, "https://example.com/video.mp4");
// Local asset bundled with your app
PlayerDataSource(DataSourceType.asset, "assets/videos/intro.mp4");
// Device filesystem file
PlayerDataSource(DataSourceType.file, "/storage/emulated/0/Download/video.mp4");
Adaptive Streaming (HLS & DASH)
Better Player parses HLS (.m3u8) and DASH (.mpd) manifests natively. Enable adaptive track and audio selection in PlayerDataSource:
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/stream/master.m3u8",
useAsmsTracks: true, // Parse video resolutions and bitrates
useAsmsSubtitles: true, // Parse in-manifest WebVTT / CEA tracks
useAsmsAudioTracks: true, // Parse alternate audio languages
);
Users can select resolutions and audio tracks through the built-in overflow menu, or you can switch them programmatically:
// Switch track programmatically
final tracks = await _controller.betterPlayerSubtitlesSourceList;
_controller.setTrack(tracks.first);
Offline Disk Caching
Avoid re-downloading video chunks when users replay clips. Caching writes encrypted chunks to app-isolated disk storage:
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/clip.mp4",
cacheConfiguration: const CacheConfiguration(
useCache: true,
maxCacheSize: 150 * 1024 * 1024, // 150 MB total pool limit
maxCacheFileSize: 20 * 1024 * 1024, // 20 MB ceiling per individual video
),
);
To pre-cache media before the user taps play:
await BetterPlayerCacheManager.preCacheLargeFile(
"https://example.com/clip.mp4",
maxCacheSize: 20 * 1024 * 1024,
);
Hardware DRM
Protect media with hardware-backed encryption keys:
Widevine (Android & Web)
final widevineSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/drm-stream.mpd",
drmConfiguration: DrmConfiguration(
drmType: DrmType.widevine,
licenseUrl: "https://license.example.com/widevine/v1",
headers: {
"Authorization": "Bearer YOUR_DRM_TOKEN",
},
),
);
FairPlay (iOS & macOS)
final fairPlaySource = PlayerDataSource(
DataSourceType.network,
"https://example.com/stream.m3u8",
drmConfiguration: DrmConfiguration(
drmType: DrmType.fairplay,
licenseUrl: "https://license.example.com/fairplay",
certificateUrl: "https://license.example.com/fairplay.cer",
headers: {
"Authorization": "Bearer YOUR_DRM_TOKEN",
},
),
);
ClearKey & Token Auth
For token-based authorization or open ClearKey streaming:
final tokenSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/stream.m3u8",
drmConfiguration: DrmConfiguration(
drmType: DrmType.token,
token: "Bearer YOUR_ACCESS_TOKEN",
),
);
Subtitles & Closed Captions
Attach external subtitles or parse in-stream broadcast captions. Supported formats include SRT, WebVTT, TTML/DFXP, CEA-608, and CEA-708:
final dataSource = PlayerDataSource(
DataSourceType.network,
"https://example.com/video.mp4",
subtitles: [
PlayerSubtitlesSource(
type: PlayerSubtitlesSourceType.network,
name: "English",
urls: ["https://example.com/subtitles_en.vtt"],
selectedByDefault: true,
),
PlayerSubtitlesSource(
type: PlayerSubtitlesSourceType.network,
name: "Spanish",
urls: ["https://example.com/subtitles_es.vtt"],
),
],
);
const config = PlayerConfiguration(
subtitlesConfiguration: BetterPlayerSubtitlesConfiguration(
fontSize: 16,
fontColor: Colors.white,
outlineColor: Colors.black,
outlineSize: 2.0,
backgroundColor: Color(0x66000000),
),
);
Controls & Customization
Better Player includes full Material and Cupertino control overlays:
const config = PlayerConfiguration(
controlsConfiguration: PlayerControlsConfiguration(
// Theme selection
theme: PlayerTheme.material, // or PlayerTheme.cupertino
// Display toggles
enablePlayPause: true,
enableMute: true,
enableFullscreen: true,
enablePip: true,
enableOverflowMenu: true,
// Timeouts and colors
controlsHideTime: Duration(seconds: 3),
progressBarPlayedColor: Colors.redAccent,
progressBarHandleColor: Colors.red,
progressBarBufferedColor: Colors.white24,
progressBarBackgroundColor: Colors.grey,
),
);
Custom Overflow Menu Items
Add your own actions (like Quality selector, Share button, or Report video) directly into the settings sheet:
PlayerControlsConfiguration(
customControlsElements: [
BetterPlayerCustomControlsElement(
icon: Icons.share,
title: "Share Video",
onTap: () {
// Handle share action
},
),
],
);
Localization
Translate control strings using PlayerTranslations:
PlayerControlsConfiguration(
playerTranslations: PlayerTranslations.spanish(),
// Or provide custom strings:
// playerTranslations: PlayerTranslations(
// overflowMenuQuality: "Calidad",
// overflowMenuSubtitles: "Subtítulos",
// ),
);
Pooled Video Feeds (BetterPlayerFeed)
Building a vertical scrolling video list (Reels, TikTok, Shorts) by putting unpooled players inside a PageView.builder quickly exhausts hardware decoders.
BetterPlayerFeed recycles a bounded controller pool:
final feedController = BetterPlayerFeedController(
dataSources: videoUrls.map((url) {
return PlayerDataSource(DataSourceType.network, url);
}).toList(),
poolConfiguration: const PlayerPoolConfiguration(
poolSize: 3,
preloadForward: 1,
preloadBackward: 1,
),
);
BetterPlayerFeed(
controller: feedController,
configuration: PlayerFeedConfiguration(
scrollDirection: Axis.vertical,
placeholderBuilder: (context, index, dataSource) =>
const Center(child: CircularProgressIndicator()),
),
);
For an architectural breakdown of decoder limits and lifecycle pooling, read the Vertical Video Feed Deep Dive.
Picture-in-Picture (PiP)
PiP runs natively on Android and iOS:
// Check if current device supports PiP
if (await _controller.isPictureInPictureSupported()) {
_controller.enablePictureInPicture(
left: 0,
top: 0,
width: 16,
height: 9,
);
}
Event Listeners
Track playback progress, buffering stalls, and errors:
_controller.addEventsListener((PlayerEvent event) {
switch (event.betterPlayerEventType) {
case PlayerEventType.play:
print("Playback started");
break;
case PlayerEventType.pause:
print("Playback paused");
break;
case PlayerEventType.bufferingStart:
print("Stall / buffering started");
break;
case PlayerEventType.bufferingEnd:
print("Buffering finished");
break;
case PlayerEventType.finished:
print("Video finished");
break;
case PlayerEventType.exception:
print("Playback error: ${event.parameters}");
break;
default:
break;
}
});
Enterprise Extensions
For monetization, video health telemetry, or white-labeling, attach better_player_enterprise:
- Pure-Dart VAST & VMAP Ads: Pre-roll, mid-roll, and post-roll video ad breaks without native IMA SDKs.
- Instant Resume: Main video buffer stays in memory during ad breaks, eliminating re-buffering spinners.
- Real-Time Video QoE: Collect startup latency (TTFF), stall ratios, and rendition shifts with built-in Bitmovin, Mux, or HTTP exporters.
- White-Labeling: Hide the About menu item and brand controls for your app.