flutter_video_cache 0.1.8
flutter_video_cache: ^0.1.8 copied to clipboard
A Flutter plugin that enables video caching and offline playback alongside the video_player package. Supports downloads, playback from cache, and progress tracking.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_video_cache/flutter_video_cache.dart';
import 'dart:async';
import 'package:video_player/video_player.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Cache Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Video Cache Demo'),
bottom: const TabBar(
tabs: [
Tab(text: 'Video List'),
Tab(text: 'Custom Buttons'),
Tab(text: 'Manual API'),
],
),
),
body: const TabBarView(
children: [VideoListTab(), CustomButtonsTab(), ManualApiTab()],
),
),
);
}
}
// Example 1: Video list with default cache buttons
class VideoListTab extends StatelessWidget {
const VideoListTab({super.key});
@override
Widget build(BuildContext context) {
final List<VideoExample> examples = [
VideoExample(
title: 'Big Buck Bunny',
description: 'A short animated film by the Blender Foundation',
url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8#uid=1234567',
thumbnail:
'https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
),
VideoExample(
title: 'Лучшее лето',
description:
'В центре сюжета находится непростой подросток Дэниел, слушающий трэш-метал. Летом он планирует провести каникулы с отцом и его новой супругой в Америке, но планы срываются. Теперь герою предстоит проводить время с матерью, которая далека от интересов сына и вообще движения молодого поколения.',
url:
'https://meta.vcdn.biz/7a8fdb5c1a40f1a7ff1637e3ce44c77e_mgg/vod/hls/b/450_900_1350_1500_2000_5000/u_sid/0/o/100586581/rsid/6883e917-71a8-4c91-8122-6c25454bebc4/u_uid/1765872116/u_vod/4/u_device/24seven_uz/u_devicekey/_24seven_uz_web/u_did/MToxNzY1ODcyMTE2OjE3NDcxMzg4NjU6OjA1ZGEzNmE1Njc1OTg3MzVhZTc4MTVlZmMzNDA3NmVl/a/0/type.amlst/playlist.m3u8#uid=U5695762',
thumbnail:
'https://service.24seven.uz/uploads/video/jpg/1745587645876.jpg',
),
VideoExample(
title: 'Elephant Dream',
description: 'Another animated film from the Blender Foundation',
url:
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
thumbnail:
'https://storage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg',
),
VideoExample(
title: 'For Bigger Blazes',
description: 'A Google Chrome advertisement',
url:
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
thumbnail:
'https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg',
),
];
return ListView.builder(
itemCount: examples.length,
itemBuilder: (context, index) {
return VideoListItem(example: examples[index]);
},
);
}
}
// Example 2: Custom button styles
class CustomButtonsTab extends StatelessWidget {
const CustomButtonsTab({super.key});
@override
Widget build(BuildContext context) {
const String videoUrl =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Button Customization Examples',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 20),
// Default style
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Default Style',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
const Center(
child: CacheControlButton(
videoUrl: videoUrl,
showPlayButton: true,
),
),
],
),
),
),
const SizedBox(height: 20),
// Custom icons
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Custom Icons',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
const Center(
child: CacheControlButton(
videoUrl: videoUrl,
showPlayButton: true,
downloadWidget: Icon(
Icons.cloud_download,
color: Colors.indigo,
),
cancelWidget: Icon(
Icons.cancel_rounded,
color: Colors.red,
),
deleteWidget: Icon(
Icons.delete_forever,
color: Colors.orange,
),
playWidget: Icon(Icons.play_circle, color: Colors.green),
),
),
],
),
),
),
const SizedBox(height: 20),
// With labels below
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'With Labels Below',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
const Center(
child: CacheControlButton(
videoUrl: videoUrl,
showPlayButton: true,
downloadLabel: "Save",
cancelLabel: "Stop",
deleteLabel: "Remove",
playLabel: "Play",
labelPosition: LabelPosition.below,
),
),
],
),
),
),
const SizedBox(height: 20),
// Labels on right
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Labels on Right',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
const Center(
child: CacheControlButton(
videoUrl: videoUrl,
showPlayButton: true,
downloadLabel: "Save Offline",
cancelLabel: "Cancel Download",
deleteLabel: "Delete from Device",
playLabel: "Play Video",
labelPosition: LabelPosition.right,
labelSpacing: 8,
),
),
],
),
),
),
const SizedBox(height: 20),
// Fully customized
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Fully Customized',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Center(
child: CacheControlButton(
videoUrl: videoUrl,
showPlayButton: true,
size: 48,
color: Colors.white,
backgroundColor: Colors.deepPurple,
downloadWidget: const Icon(
Icons.cloud_download,
color: Colors.white,
),
cancelWidget: const Icon(
Icons.cancel_rounded,
color: Colors.white,
),
deleteWidget: const Icon(
Icons.delete_forever,
color: Colors.white,
),
playWidget: const Icon(
Icons.play_circle,
color: Colors.white,
),
downloadLabel: "Download",
cancelLabel: "Cancel",
deleteLabel: "Delete",
playLabel: "Play",
labelPosition: LabelPosition.below,
labelStyle: const TextStyle(
color: Colors.deepPurple,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
],
),
);
}
}
// Example 3: Manual API usage
class ManualApiTab extends StatefulWidget {
const ManualApiTab({super.key});
@override
State<ManualApiTab> createState() => _ManualApiTabState();
}
class _ManualApiTabState extends State<ManualApiTab> {
final VideoCache _videoCache = VideoCache();
final String _videoUrl =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4';
double _progress = 0.0;
bool _isDownloading = false;
bool _isCached = false;
String? _cachedPath;
VideoPlayerController? _controller;
@override
void initState() {
super.initState();
_checkCacheStatus();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> _checkCacheStatus() async {
final isCached = await _videoCache.isVideoCached(_videoUrl);
final path =
isCached ? await _videoCache.getCachedVideoPath(_videoUrl) : null;
setState(() {
_isCached = isCached;
_cachedPath = path;
});
}
Future<void> _startDownload() async {
setState(() {
_isDownloading = true;
_progress = 0.0;
});
await _videoCache.startDownload(_videoUrl);
// Listen to progress updates
_videoCache
.getDownloadProgressStream(_videoUrl)
.listen(
(progress) {
setState(() {
_progress = progress;
if (progress >= 0.99) {
_isDownloading = false;
_checkCacheStatus();
}
});
},
onError: (error) {
setState(() {
_isDownloading = false;
});
},
);
}
Future<void> _cancelDownload() async {
await _videoCache.cancelDownload(_videoUrl);
setState(() {
_isDownloading = false;
_progress = 0.0;
});
}
Future<void> _removeDownload() async {
await _videoCache.removeDownload(_videoUrl);
setState(() {
_isCached = false;
_cachedPath = null;
});
}
Future<void> _playVideo() async {
if (_controller != null) {
await _controller!.dispose();
}
_controller = await VideoCacheController.networkWithCaching(_videoUrl);
await _controller!.initialize();
await _controller!.play();
setState(() {});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Manual API Usage',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
// Video info
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Video URL:',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
_videoUrl,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'Status:',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
_isCached
? 'Cached'
: _isDownloading
? 'Downloading'
: 'Not cached',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color:
_isCached
? Colors.green
: _isDownloading
? Colors.orange
: Colors.red,
),
),
if (_cachedPath != null) ...[
const SizedBox(height: 8),
Text(
'Cached Path:',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
_cachedPath!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
],
),
),
),
const SizedBox(height: 16),
// Progress indicator
if (_isDownloading)
Column(
children: [
LinearProgressIndicator(
value: _progress,
backgroundColor: Colors.grey[300],
),
const SizedBox(height: 8),
Text('${(_progress * 100).toStringAsFixed(1)}%'),
const SizedBox(height: 16),
],
),
// Actions
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (!_isDownloading && !_isCached)
ElevatedButton.icon(
onPressed: _startDownload,
icon: const Icon(Icons.cloud_download),
label: const Text('Start Download'),
),
if (_isDownloading)
ElevatedButton.icon(
onPressed: _cancelDownload,
icon: const Icon(Icons.cancel),
label: const Text('Cancel Download'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
if (_isCached)
ElevatedButton.icon(
onPressed: _removeDownload,
icon: const Icon(Icons.delete),
label: const Text('Remove Download'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
if (_isCached)
ElevatedButton.icon(
onPressed: _playVideo,
icon: const Icon(Icons.play_arrow),
label: const Text('Play Video'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
),
if (!_isCached && !_isDownloading)
ElevatedButton.icon(
onPressed: _checkCacheStatus,
icon: const Icon(Icons.refresh),
label: const Text('Check Cache Status'),
),
],
),
// Video player
if (_controller != null && _controller!.value.isInitialized)
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
VideoPlayer(_controller!),
VideoProgressIndicator(
_controller!,
allowScrubbing: true,
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
),
Positioned(
top: 8,
right: 8,
child: FloatingActionButton.small(
onPressed: () {
setState(() {
_controller!.value.isPlaying
? _controller!.pause()
: _controller!.play();
});
},
child: Icon(
_controller!.value.isPlaying
? Icons.pause
: Icons.play_arrow,
),
),
),
],
),
),
),
),
],
),
);
}
}
// Video list item component
class VideoListItem extends StatefulWidget {
final VideoExample example;
const VideoListItem({super.key, required this.example});
@override
State<VideoListItem> createState() => _VideoListItemState();
}
class _VideoListItemState extends State<VideoListItem> {
VideoPlayerController? _controller;
bool _isPlaying = false;
bool _mounted = true;
@override
void initState() {
super.initState();
_mounted = true;
}
@override
void dispose() {
_mounted = false;
_disposeController();
super.dispose();
}
void _disposeController() {
_controller?.pause();
_controller?.dispose();
_controller = null;
}
Future<void> _playVideo() async {
if (!_mounted) return;
setState(() {
_isPlaying = true;
});
try {
_controller = await VideoCacheController.networkWithCaching(
widget.example.url,
);
if (!_mounted) {
_controller?.dispose();
return;
}
await _controller!.initialize();
await _controller!.play();
if (_mounted) {
setState(() {});
}
} catch (e) {
if (_mounted) {
setState(() {
_isPlaying = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error playing video: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
void _stopVideo() {
if (_controller != null) {
_controller!.pause().then((_) {
if (_mounted) {
setState(() {
_isPlaying = false;
});
}
});
}
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Video or thumbnail
if (_isPlaying &&
_controller != null &&
_controller!.value.isInitialized)
AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
VideoPlayer(_controller!),
VideoProgressIndicator(
_controller!,
allowScrubbing: true,
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
),
],
),
)
else
Stack(
alignment: Alignment.center,
children: [
// Thumbnail image
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
widget.example.thumbnail,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported, size: 50),
);
},
),
),
// Play icon overlay
IconButton(
onPressed: _playVideo,
icon: const Icon(Icons.play_circle_fill, size: 50),
color: Colors.white.withOpacity(0.8),
),
],
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
widget.example.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
// Description
Text(
widget.example.description,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
// Controls row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Cache control button
CacheControlButton(
videoUrl: widget.example.url,
showPlayButton: true,
onPlayPressed: _playVideo,
size: 42,
onDownloadStarted: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Started downloading: ${widget.example.title}',
),
duration: const Duration(seconds: 2),
),
);
},
onDownloadCompleted: () async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Download complete: ${widget.example.title}',
),
duration: const Duration(seconds: 2),
),
);
},
),
// Stop button (only when playing)
if (_isPlaying)
ElevatedButton.icon(
onPressed: _stopVideo,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor:
Theme.of(context).colorScheme.onError,
),
),
],
),
],
),
),
],
),
);
}
}
// Data class for video examples
class VideoExample {
final String title;
final String description;
final String url;
final String thumbnail;
const VideoExample({
required this.title,
required this.description,
required this.url,
required this.thumbnail,
});
}