Native, network‑only video player for Flutter with modern MX/VLC‑style UX. Android uses ExoPlayer; iOS uses AVPlayer. Includes gestures, thumbnails on seek, DRM (Android), fullscreen, BoxFit, and more.
- ✅ Native engines: ExoPlayer (Android) and AVPlayer (iOS)
- ✅ Gestures: tap to show/hide, double‑tap seek, long‑press skip, horizontal scrub, vertical volume/brightness
- ✅ Controls: play/pause, speed, fullscreen (manual or auto), lock, BoxFit (contain/cover/fill/fitWidth/fitHeight)
- ✅ Quality, audio, and subtitle track selection with data saver toggle
- ✅ Persistent playback preferences (speed, quality, audio, subtitles, data saver)
- ✅ Configurable retry/backoff, structured errors, PiP playback controls
- ✅ Thumbnails: WebVTT sprites or image sequences during seek preview (cached in-memory)
- ✅ DRM (Android): Widevine and ClearKey
- ✅ M3U playlist parsing utility
- ✅ Overlay support (watermark, logos)
Add to pubspec.yaml:
dependencies:
tha_player: ^0.5.2Then:
flutter pub get
import 'package:tha_player/tha_player.dart';
final ctrl = ThaNativePlayerController.single(
ThaMediaSource(
'https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
// Optional VTT thumbnails
// thumbnailVttUrl: 'https://example.com/thumbs.vtt',
),
autoPlay: true,
playbackOptions: ThaPlaybackOptions(
maxRetryCount: 5,
initialRetryDelay: Duration(milliseconds: 800),
),
initialPreferences: const ThaPlayerPreferences(
playbackSpeed: 1.0,
dataSaver: false,
),
);
// In build:
AspectRatio(
aspectRatio: 16 / 9,
child: ThaModernPlayer(
controller: ctrl,
doubleTapSeek: Duration(seconds: 10),
longPressSeek: Duration(seconds: 3),
autoHideAfter: Duration(seconds: 3),
initialBoxFit: BoxFit.contain,
onErrorDetails: (err) {
if (err != null) {
debugPrint('Playback error: ${err.code} • ${err.message}');
}
},
),
)
- Preferences now live on the controller (
ctrl.preferences) and persist across fullscreen. - Use
ctrl.playbackStateinstead of listening directly to platform events. - Prefer
onErrorDetails/ctrl.errorDetailsfor structured failures (stringonErrorstill works).
Tap the fullscreen icon in the control bar. Playback state, BoxFit, and preferences are preserved when entering/exiting fullscreen.
Choose between contain, cover, fill, fitWidth, and fitHeight from the menu. BoxFit is shared via the controller by default.
Use the control bar to switch quality, audio, or subtitle tracks at runtime. You can also fetch tracks directly:
final qualities = await ctrl.getVideoTracks();
final audios = await ctrl.getAudioTracks();
final subtitles = await ctrl.getSubtitleTracks();
await ctrl.selectAudioTrack(audios.first.id);
await ctrl.selectSubtitleTrack(null); // disable captions
Listen to the controller’s playback state without reaching into the event channel:
ValueListenableBuilder<ThaPlaybackState>(
valueListenable: ctrl.playbackState,
builder: (_, state, __) => Text(
'${state.position.inSeconds}s / ${state.duration.inSeconds}s',
),
)
Preferences live on the controller so fullscreen swaps preserve your choices:
await ctrl.setSpeed(1.5); // persists
await ctrl.setDataSaver(true);
await ctrl.selectAudioTrack(null); // reset to default
You can also inspect the current preference snapshot via ctrl.preferences.value, or reset to the initial defaults:
ctrl.resetPreferences();
Use the lock icon to prevent controls/gestures; unlock with the floating button.
final ctrl = ThaNativePlayerController.single(
ThaMediaSource(
'https://my.cdn.com/drm/manifest.mpd',
drm: ThaDrmConfig(
type: ThaDrmType.widevine, // or ThaDrmType.clearKey
licenseUrl: 'https://license.server/wv',
headers: {'Authorization': 'Bearer <token>'},
// clearKey: '{"keys":[{"kty":"oct","k":"...","kid":"..."}]}'
),
),
);
Provide a .vtt with sprites or images and optional #xywh regions:
ThaMediaSource(
'https://example.com/video.m3u8',
thumbnailVttUrl: 'https://example.com/thumbs.vtt',
)
- Android: ExoPlayer backend with Media3; Widevine/ClearKey supported; per‑item HTTP headers.
- iOS: AVPlayer backend;
fitWidth/fitHeightapproximate viaresizeAspect. - Keep‑screen‑on is enabled during playback (Android/iOS).
- Playability depends on device codecs, stream, and network.
Thumbnails are cached in-memory. Call clearThumbnailCache() if you need to purge the cache.
ThaPlaybackOptions lets you tweak retry/backoff behaviour and rebuffer handling. Failures are surfaced via ThaNativeEvents.error for legacy use, plus ThaPlayerError through ctrl.errorDetails or onErrorDetails on ThaModernPlayer.
Provide a bespoke OkHttpClient to inject interceptors or caching:
Register the factory inside your Android Application:
class App : FlutterApplication() {
override fun onCreate() {
super.onCreate()
ThaPlayerPlugin.setHttpClientFactory {
OkHttpClient.Builder()
.addInterceptor(MyHeaderInterceptor())
.cache(Cache(cacheDir.resolve("video"), 100L * 1024L * 1024L))
.build()
}
}
}Set the factory before creating any Flutter controllers so every instance shares the same client.
This plugin does not ship custom native decoder binaries. If you add native libraries, link them with a max page size compatible with 16 KB systems (e.g., -Wl,-z,max-page-size=16384 on Android NDK).
See example/ for a runnable app that demonstrates the modern controls, gestures, fullscreen, and thumbnails.
If this package helps you, consider a tip:
- Tron (TRC20):
TLbwVrZyaZujcTCXAb94t6k7BrvChVfxzi
Issues and PRs are welcome! Please file bugs or ideas at the issue tracker.
MIT — see LICENSE.