video_player_widget 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import 'dart:io';
  2. import 'dart:async';
  3. import 'package:flutter/material.dart';
  4. import 'package:media_kit/media_kit.dart';
  5. import 'package:media_kit_video/media_kit_video.dart';
  6. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  7. import 'package:path/path.dart' as path;
  8. import '../../main.dart';
  9. class VideoPlayerWidget extends StatefulWidget {
  10. final String videoUrl;
  11. const VideoPlayerWidget({super.key, required this.videoUrl});
  12. @override
  13. State<VideoPlayerWidget> createState() => _VideoPlayerWidgetState();
  14. }
  15. class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
  16. late final Player _player;
  17. late final VideoController _controller;
  18. bool _isInitialized = false;
  19. bool _hasError = false;
  20. bool _usingCache = false;
  21. double _downloadProgress = 0;
  22. double _bufferingProgress = 0;
  23. bool _isBuffering = false;
  24. File? _cachedFile;
  25. StreamSubscription? _cacheSubscription;
  26. @override
  27. void initState() {
  28. super.initState();
  29. _initializePlayer();
  30. }
  31. Future<void> _initializePlayer() async {
  32. try {
  33. _cachedFile = await videoCacheManager.getSingleFile(widget.videoUrl);
  34. _usingCache = _cachedFile != null;
  35. _player = Player();
  36. _controller = VideoController(_player);
  37. final source = _cachedFile != null
  38. ? Media(_cachedFile!.path, httpHeaders: {})
  39. : Media(widget.videoUrl);
  40. await _player.open(source);
  41. //await _player.setVolume(0);
  42. if (_cachedFile == null) {
  43. _cacheVideo();
  44. }
  45. _player.stream.error.listen(_handlePlayerError);
  46. _player.stream.buffering.listen(_handleBufferingUpdate);
  47. } catch (e) {
  48. _handleError("Player init error: $e");
  49. }
  50. }
  51. void _handleBufferingUpdate(bool isBuffering) {
  52. setState(() {
  53. _isBuffering = isBuffering;
  54. _bufferingProgress = 0;
  55. if (!isBuffering && !_isInitialized && !_hasError) {
  56. _isInitialized = true;
  57. }
  58. });
  59. }
  60. void _handlePlayerError(String error) {
  61. _handleError("Player error: ${error})");
  62. }
  63. void _handleError(String message) {
  64. print(message);
  65. if (!_hasError) setState(() => _hasError = true);
  66. }
  67. Future<void> _cacheVideo() async {
  68. final fileStream = videoCacheManager.getFileStream(
  69. widget.videoUrl,
  70. withProgress: true,
  71. );
  72. _cacheSubscription = fileStream.listen((fileResponse) {
  73. if (fileResponse is DownloadProgress) {
  74. setState(() => _downloadProgress = fileResponse.progress ?? 0);
  75. } else if (fileResponse is FileInfo) {
  76. setState(() {
  77. _cachedFile = fileResponse.file;
  78. _usingCache = true;
  79. _downloadProgress = 0;
  80. });
  81. }
  82. }, onError: (e) => print("Cache error: $e"));
  83. }
  84. void _retryPlayback() {
  85. setState(() {
  86. _hasError = false;
  87. _isInitialized = false;
  88. _downloadProgress = 0;
  89. _bufferingProgress = 0;
  90. _cacheSubscription?.cancel();
  91. });
  92. _initializePlayer();
  93. }
  94. @override
  95. Widget build(BuildContext context) {
  96. return GestureDetector(
  97. child: Stack(
  98. children: [
  99. // Video display
  100. if (_isInitialized)
  101. Video(controller: _controller),
  102. // Loading/buffering states
  103. if (!_isInitialized || _isBuffering)
  104. _buildLoadingState(),
  105. // Error state
  106. if (_hasError)
  107. _buildErrorState(),
  108. // Cache progress indicator
  109. if (_downloadProgress > 0 && _downloadProgress < 1)
  110. _buildCacheProgress(),
  111. // Buffering indicator
  112. if (_isBuffering && _isInitialized)
  113. _buildBufferingOverlay(),
  114. ],
  115. ),
  116. );
  117. }
  118. Widget _buildLoadingState() {
  119. return Container(
  120. color: Colors.black,
  121. child: Center(
  122. child: Column(
  123. mainAxisSize: MainAxisSize.min,
  124. children: [
  125. CircularProgressIndicator(
  126. value: _isBuffering ? _bufferingProgress : null,
  127. valueColor: const AlwaysStoppedAnimation(Colors.white),
  128. ),
  129. const SizedBox(height: 16),
  130. Text(
  131. _usingCache ? 'Loading cached video' : 'Streaming video',
  132. style: const TextStyle(color: Colors.white),
  133. ),
  134. ],
  135. ),
  136. ),
  137. );
  138. }
  139. Widget _buildBufferingOverlay() {
  140. return Container(
  141. color: Colors.black54,
  142. child: Center(
  143. child: Column(
  144. mainAxisSize: MainAxisSize.min,
  145. children: [
  146. CircularProgressIndicator(
  147. value: _bufferingProgress,
  148. valueColor: const AlwaysStoppedAnimation(Colors.white),
  149. ),
  150. const SizedBox(height: 8),
  151. const Text('Buffering...', style: TextStyle(color: Colors.white)),
  152. ],
  153. ),
  154. ),
  155. );
  156. }
  157. Widget _buildCacheProgress() {
  158. return Positioned(
  159. bottom: 16,
  160. right: 16,
  161. child: Container(
  162. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  163. decoration: BoxDecoration(
  164. color: Colors.black.withOpacity(0.7),
  165. borderRadius: BorderRadius.circular(20),
  166. ),
  167. child: Row(
  168. children: [
  169. const Icon(Icons.download, color: Colors.white, size: 20),
  170. const SizedBox(width: 8),
  171. Text(
  172. '${(_downloadProgress * 100).toStringAsFixed(0)}%',
  173. style: const TextStyle(color: Colors.white),
  174. ),
  175. ],
  176. ),
  177. ),
  178. );
  179. }
  180. Widget _buildErrorState() {
  181. return Container(
  182. color: Colors.black,
  183. child: Center(
  184. child: Column(
  185. mainAxisSize: MainAxisSize.min,
  186. children: [
  187. const Icon(Icons.videocam_off, size: 64, color: Colors.white54),
  188. const SizedBox(height: 16),
  189. const Text('Playback Failed',
  190. style: TextStyle(color: Colors.white, fontSize: 18)),
  191. const SizedBox(height: 8),
  192. Text(
  193. _usingCache ? 'Cached file may be corrupted' : 'Network issue',
  194. style: const TextStyle(color: Colors.white70),
  195. ),
  196. const SizedBox(height: 16),
  197. Row(
  198. mainAxisAlignment: MainAxisAlignment.center,
  199. children: [
  200. ElevatedButton.icon(
  201. icon: const Icon(Icons.refresh),
  202. label: const Text('Retry'),
  203. onPressed: _retryPlayback,
  204. ),
  205. const SizedBox(width: 16),
  206. if (!_usingCache)
  207. ElevatedButton.icon(
  208. icon: const Icon(Icons.download),
  209. label: const Text('Download'),
  210. onPressed: _cacheVideo,
  211. ),
  212. ],
  213. )
  214. ],
  215. ),
  216. ),
  217. );
  218. }
  219. @override
  220. void dispose() {
  221. _player.dispose();
  222. _cacheSubscription?.cancel();
  223. super.dispose();
  224. }
  225. }