flutter_tv_media3 0.0.8
flutter_tv_media3: ^0.0.8 copied to clipboard
Flutter TV Media3 plugin for playing video on Android TV using the native Media3 player, which runs in its own `Activity`.
import "dart:async";
import "dart:math";
import "package:flutter/material.dart";
import "package:flutter_tv_media3/flutter_tv_media3.dart";
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Media3 Plugin Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
/// Throws an exception (failure).
Future<List<MediaItemSubtitle>?> _mockSearchSubtitles({
required String id,
}) async {
debugPrint('Searching subtitles for media ID: $id');
await Future.delayed(const Duration(seconds: 2));
final random = Random();
final outcome = random.nextInt(3); // Generates 0, 1, or 2
switch (outcome) {
case 0:
debugPrint('Mock search: Success - found 2 subtitles.');
return [
MediaItemSubtitle(
url:
'https://raw.githubusercontent.com/mtoczko/hls-test-streams/refs/heads/master/test-vtt/text/1.vtt',
language: 'en',
label: 'English (Found)',
),
MediaItemSubtitle(
url:
'https://raw.githubusercontent.com/mtoczko/hls-test-streams/refs/heads/master/test-vtt/text/1.vtt',
language: 'uk',
label: 'Ukrainian (Found)',
),
];
case 1:
debugPrint('Mock search: Success - no subtitles found.');
return []; // Represents a successful search that found nothing
case 2:
default:
debugPrint('Mock search: Failure - throwing an exception.');
throw Exception('Failed to connect to the subtitle server.');
}
}
class _MyHomePageState extends State<MyHomePage> {
final controller = FtvMedia3PlayerController();
int lastPlayedIndex = 0;
late StreamSubscription<PlayerState> _playerStateSubscription;
Timer? _infoTimer;
final List<PlaylistMediaItem> mediaItems = [
PlaylistMediaItem(
id: 'bbb_hls_res',
label: 'Sintel HLS (Sintel with Subtitles)',
title: 'Sintel',
subTitle: 'Sintel with Subtitles',
description:
'The film follows a girl named Sintel who is searching for a baby dragon she calls Scales.',
url: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
startPosition: 60,
duration: 888,
headers: {'Referer': 'https://example.com/player'},
placeholderImg:
'https://media.themoviedb.org/t/p/w1066_and_h600_bestv2/msqeiEyIRpPAtrCeRGFNZQ9tkJL.jpg',
coverImg:
'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Sintel_poster.jpg/636px-Sintel_poster.jpg',
saveWatchTime: ({
required String id,
required int duration,
required int position,
required int playIndex,
}) async {
debugPrint(
'SAVE WATCH TIME: id=$id, duration=$duration, position=$position, playIndex=$playIndex',
);
},
subtitles: [
MediaItemSubtitle(
url:
'https://raw.githubusercontent.com/mtoczko/hls-test-streams/refs/heads/master/test-vtt/text/1.vtt',
language: 'en',
label: 'English (external)',
),
],
audioTracks: [
MediaItemAudioTrack(
url: 'https://download.samplelib.com/mp3/sample-15s.mp3',
language: 'en',
label: 'US (external)',
mimeType: 'audio/mpeg',
),
],
),
PlaylistMediaItem(
id: 'bbb_mp4_res',
label: 'getDirectLink (success)',
url: 'myapp://needs_resolving/video1',
startPosition: 0,
saveWatchTime: ({
required String id,
required int duration,
required int position,
required int playIndex,
}) async {
debugPrint(
'SAVE WATCH TIME: id=$id, duration=$duration, position=$position, playIndex=$playIndex',
);
},
getDirectLink: ({
required PlaylistMediaItem item,
Function({
required String state,
double? progress,
required int requestId,
})?
onProgress,
required int requestId,
}) async {
onProgress?.call(
requestId: requestId,
state: 'downloading 1',
progress: 0.1,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 2',
progress: 0.2,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 3',
progress: 0.3,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 4',
progress: 0.4,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 5',
progress: 0.5,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 6',
progress: 0.6,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 7',
progress: 0.7,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 8',
progress: 0.8,
);
await Future.delayed(const Duration(seconds: 1));
onProgress?.call(
requestId: requestId,
state: 'downloading 9',
progress: 0.9,
);
await Future.delayed(const Duration(seconds: 1));
final resolved =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
return item.copyWith(url: resolved);
},
),
PlaylistMediaItem(
id: 'bbb_mp4_res_error',
label: 'getDirectLink (error)',
url: 'myapp://resolving_error/video2',
saveWatchTime: ({
required String id,
required int duration,
required int position,
required int playIndex,
}) async {
debugPrint(
'SAVE WATCH TIME: id=$id, duration=$duration, position=$position, playIndex=$playIndex',
);
},
getDirectLink: ({
required PlaylistMediaItem item,
Function({
required String state,
double? progress,
required int requestId,
})?
onProgress,
required int requestId,
}) async {
await Future.delayed(const Duration(milliseconds: 300));
throw Exception("Failed to get direct link from API");
},
),
PlaylistMediaItem(
id: 'bbb_mp4_res',
label: 'MP4 (BBB with Resolutions) MP4 (BBB with Resolutions)',
url:
'https://www.sample-videos.com/video321/mp4/360/big_buck_bunny_360p_30mb.mp4',
saveWatchTime: ({
required String id,
required int duration,
required int position,
required int playIndex,
}) async {
debugPrint(
'SAVE WATCH TIME: id=$id, duration=$duration, position=$position, playIndex=$playIndex',
);
},
resolutions: {
'480p':
'https://www.sample-videos.com/video321/mp4/480/big_buck_bunny_480p_30mb.mp4',
'720p':
'https://www.sample-videos.com/video321/mp4/720/big_buck_bunny_720p_30mb.mp4',
'360p':
'https://www.sample-videos.com/video321/mp4/360/big_buck_bunny_360p_30mb.mp4',
'240':
'https://www.sample-videos.com/video321/mp4/240/big_buck_bunny_240p_30mb.mp4',
},
headers: {'User-Agent': 'MyApp/1.0'},
),
];
Future<void> saveSubtitleStyle({required SubtitleStyle subtitleStyle}) async {
debugPrint(subtitleStyle.toString());
}
Future<void> saveClockSettings({required ClockSettings clockSettings}) async {
debugPrint(clockSettings.toString());
}
Future<void> savePlayerSettings({
required PlayerSettings playerSettings,
}) async {
debugPrint(playerSettings.toString());
}
void sleepTimerExec() {
debugPrint('SLEEP TIMER EXEC!!!!!!!!!!!!!!!!!!!');
}
@override
void initState() {
super.initState();
Locale? deviceLocale = WidgetsBinding.instance.platformDispatcher.locale;
final localeStrings = {'loading': 'Wird geladen…'};
final subtitleStyle = SubtitleStyle(foregroundColor: BasicColors.yellow);
final playerSettings = PlayerSettings(
videoQuality: VideoQuality.high,
preferredAudioLanguages: [deviceLocale.languageCode],
preferredTextLanguages: [deviceLocale.languageCode],
forcedAutoEnable: true,
deviceLocale: deviceLocale,
isAfrEnabled: true,
);
final clockSettings = ClockSettings(clockPosition: ClockPosition.random);
controller.setConfig(
localeStrings: localeStrings,
subtitleStyle: subtitleStyle,
saveSubtitleStyle: saveSubtitleStyle,
playerSettings: playerSettings,
clockSettings: clockSettings,
saveClockSettings: saveClockSettings,
savePlayerSettings: savePlayerSettings,
sleepTimerExec: sleepTimerExec,
searchExternalSubtitle: _mockSearchSubtitles,
findSubtitlesLabel: 'Find on MockSubtitles.com',
findSubtitlesStateInfoLabel: '10/10',
labelSearchExternalSubtitle: labelSearchExternalSubtitle,
);
//This listener is required to update the playlist screen.
_playerStateSubscription = controller.playerStateStream.listen((
PlayerState state,
) {
setState(() {
lastPlayedIndex = state.playIndex;
});
});
_infoTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
final now = DateTime.now();
final timeString =
"${now.hour}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}";
controller.sendCustomInfoToOverlay('Last update: $timeString');
});
}
Future<String> labelSearchExternalSubtitle() async {
return '9/10';
}
@override
void dispose() {
_infoTimer?.cancel();
_playerStateSubscription.cancel();
controller.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Media3 Plugin Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView.builder(
key: Key(
lastPlayedIndex.toString(),
), //**need for rebuild after change lastPlayedIndex**
itemCount: mediaItems.length,
itemBuilder: (BuildContext context, int index) {
final mediaItem = mediaItems[index];
return ListTile(
autofocus: index == lastPlayedIndex,
title: Text(mediaItem.label ?? 'No Label'),
subtitle: Text(
mediaItem.url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
child: Text('${index + 1}'),
),
onTap:
() => controller.openPlayer(
context: context,
playlist: mediaItems,
initialIndex: index,
),
/*
or Using Flutter's Navigator directly
onTap:
() => Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => Media3PlayerScreen(
playlist: mediaItems,
initialIndex: index,
playerLabel: null,
),
),
),
or This method does not use Flutter's `Navigator`. It's a direct call to the native side.
onTap: () => controller.openNativePlayer(
playlist: mediaItems,
initialIndex: 0,
);
*/
);
},
),
);
}
}