Skip to main content

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 of android/app/src/main/AndroidManifest.xml.
  • iOS: Add NSAppTransportSecurity exceptions to ios/Runner/Info.plist if you stream from non-HTTPS endpoints. For background audio, enable the audio background mode under Signing & Capabilities in Xcode.

Platform Support​

PlatformNative EngineBridgeDRM Support
AndroidAndroidX Media3JNIWidevine (L1/L3), ClearKey
iOSAVPlayerSwift FFIFairPlay
macOSAVPlayerSwift FFIFairPlay
WebShaka PlayerJavaScript / WASMWidevine, ClearKey
WindowslibmpvC FFISoftware / 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.

Resources & Guides​