video_player_widget 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. import 'package:cached_video_player_plus/cached_video_player_plus.dart';
  5. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  6. import 'package:visibility_detector/visibility_detector.dart';
  7. import 'package:video_player/video_player.dart';
  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 CachedVideoPlayerPlus _player;
  17. bool _isInitialized = false;
  18. bool _hasError = false;
  19. bool _usingCache = false;
  20. double _downloadProgress = 0;
  21. double _bufferingProgress = 0;
  22. bool _isBuffering = false;
  23. File? _cachedFile;
  24. StreamSubscription? _cacheSubscription;
  25. bool _showControls = true;
  26. Timer? _hideTimer;
  27. bool _isPlaying = true;
  28. @override
  29. void initState() {
  30. super.initState();
  31. _initializePlayer();
  32. _startHideTimer();
  33. }
  34. Future<void> _initializePlayer() async {
  35. try {
  36. _player = CachedVideoPlayerPlus.networkUrl(
  37. Uri.parse(widget.videoUrl),
  38. invalidateCacheIfOlderThan: const Duration(days: 60),
  39. );
  40. await _player.initialize();
  41. _player.controller.addListener(_videoListener);
  42. setState(() => _isInitialized = true);
  43. _player.controller.play();
  44. } catch (e) {
  45. _handleError("Player init error: $e");
  46. }
  47. }
  48. void _videoListener() {
  49. if (!mounted) return;
  50. // Update playing state
  51. if (_isPlaying != _player.controller.value.isPlaying) {
  52. setState(() => _isPlaying = _player.controller.value.isPlaying);
  53. }
  54. // Handle buffering updates
  55. final isBuffering = _player.controller.value.isBuffering;
  56. if (isBuffering != _isBuffering) {
  57. setState(() => _isBuffering = isBuffering);
  58. }
  59. }
  60. void _toggleControls() {
  61. setState(() => _showControls = !_showControls);
  62. if (_showControls) {
  63. _startHideTimer();
  64. } else {
  65. _hideTimer?.cancel();
  66. }
  67. }
  68. void _startHideTimer() {
  69. _hideTimer?.cancel();
  70. _hideTimer = Timer(const Duration(seconds: 3), () {
  71. if (mounted) setState(() => _showControls = false);
  72. });
  73. }
  74. void _togglePlayPause() {
  75. setState(() {
  76. _isPlaying ? _player.controller.pause() : _player.controller.play();
  77. _isPlaying = !_isPlaying;
  78. });
  79. _startHideTimer();
  80. }
  81. Widget _buildControlsOverlay() {
  82. return AnimatedOpacity(
  83. opacity: _showControls ? 1.0 : 0.0,
  84. duration: const Duration(milliseconds: 300),
  85. child: Stack(
  86. children: [
  87. // Play/Pause Center Button
  88. Positioned.fill(
  89. child: Center(
  90. child: IconButton(
  91. icon: Icon(
  92. _isPlaying ? Icons.pause : Icons.play_arrow,
  93. size: 48,
  94. color: Colors.white.withOpacity(0.8),
  95. ),
  96. onPressed: _togglePlayPause,
  97. ),
  98. ),
  99. ),
  100. // Bottom Controls Bar
  101. Positioned(
  102. bottom: 0,
  103. left: 0,
  104. right: 0,
  105. child: Container(
  106. height: 60,
  107. decoration: BoxDecoration(
  108. gradient: LinearGradient(
  109. begin: Alignment.bottomCenter,
  110. end: Alignment.topCenter,
  111. colors: [
  112. Colors.black.withOpacity(0.7),
  113. Colors.transparent,
  114. ],
  115. ),
  116. ),
  117. child: Row(
  118. children: [
  119. Expanded(
  120. child: VideoProgressBar(
  121. controller: _player.controller,
  122. onSeek: (duration) {
  123. _player.controller.seekTo(duration);
  124. _startHideTimer();
  125. },
  126. ),
  127. ),
  128. ],
  129. ),
  130. ),
  131. ),
  132. ],
  133. ),
  134. );
  135. }
  136. @override
  137. Widget build(BuildContext context) {
  138. return VisibilityDetector(
  139. key: Key(widget.videoUrl),
  140. onVisibilityChanged: (info) => _handleVisibilityChange(info.visibleFraction > 0.5),
  141. child: GestureDetector(
  142. onTap: _toggleControls,
  143. child: Stack(
  144. alignment: Alignment.center,
  145. children: [
  146. // Video Display
  147. if (_isInitialized)
  148. Center(
  149. child: AspectRatio(
  150. aspectRatio: _player.controller.value.aspectRatio,
  151. child: VideoPlayer(_player.controller),
  152. ),
  153. ),
  154. // Controls Overlay
  155. if (_isInitialized && !_hasError) _buildControlsOverlay(),
  156. // Loading/buffering states
  157. if (!_isInitialized || _isBuffering)
  158. _buildLoadingState(),
  159. // Error state
  160. if (_hasError)
  161. _buildErrorState(),
  162. // Cache progress indicator
  163. if (_downloadProgress > 0 && _downloadProgress < 1)
  164. _buildCacheProgress(),
  165. // Buffering indicator
  166. if (_isBuffering && _isInitialized)
  167. _buildBufferingOverlay(),
  168. ],
  169. ),
  170. ),
  171. );
  172. }
  173. // Keep existing helper methods (_handleError, _cacheVideo, _retryPlayback,
  174. // _handleVisibilityChange, _buildLoadingState, _buildBufferingOverlay,
  175. // _buildCacheProgress, _buildErrorState) as provided in original code
  176. void _handleError(String message) {
  177. print(message);
  178. if (!_hasError) setState(() => _hasError = true);
  179. }
  180. Future<void> _cacheVideo() async {
  181. final fileStream = videoCacheManager.getFileStream(
  182. widget.videoUrl,
  183. withProgress: true,
  184. );
  185. _cacheSubscription = fileStream.listen((fileResponse) {
  186. if (fileResponse is DownloadProgress) {
  187. setState(() => _downloadProgress = fileResponse.progress ?? 0);
  188. } else if (fileResponse is FileInfo) {
  189. setState(() {
  190. _cachedFile = fileResponse.file;
  191. _usingCache = true;
  192. _downloadProgress = 0;
  193. });
  194. }
  195. }, onError: (e) => print("Cache error: $e"));
  196. }
  197. void _retryPlayback() {
  198. setState(() {
  199. _hasError = false;
  200. _isInitialized = false;
  201. _downloadProgress = 0;
  202. _bufferingProgress = 0;
  203. _cacheSubscription?.cancel();
  204. });
  205. _initializePlayer();
  206. }
  207. Future<void> _handleVisibilityChange(bool visible) async {
  208. if (!visible) {
  209. await _player.controller.pause();
  210. }
  211. }
  212. Widget _buildLoadingState() {
  213. return Container(
  214. color: Colors.black,
  215. child: Center(
  216. child: Column(
  217. mainAxisSize: MainAxisSize.min,
  218. children: [
  219. CircularProgressIndicator(
  220. value: _isBuffering ? _bufferingProgress : null,
  221. valueColor: const AlwaysStoppedAnimation(Colors.white),
  222. ),
  223. const SizedBox(height: 16),
  224. Text(
  225. _usingCache ? 'Loading cached video' : 'Streaming video',
  226. style: const TextStyle(color: Colors.white),
  227. ),
  228. ],
  229. ),
  230. ),
  231. );
  232. }
  233. Widget _buildBufferingOverlay() {
  234. return Container(
  235. color: Colors.black54,
  236. child: Center(
  237. child: Column(
  238. mainAxisSize: MainAxisSize.min,
  239. children: [
  240. CircularProgressIndicator(
  241. value: _bufferingProgress,
  242. valueColor: const AlwaysStoppedAnimation(Colors.white),
  243. ),
  244. const SizedBox(height: 8),
  245. const Text('Buffering...', style: TextStyle(color: Colors.white)),
  246. ],
  247. ),
  248. ),
  249. );
  250. }
  251. Widget _buildCacheProgress() {
  252. return Positioned(
  253. bottom: 16,
  254. right: 16,
  255. child: Container(
  256. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  257. decoration: BoxDecoration(
  258. color: Colors.black.withOpacity(0.7),
  259. borderRadius: BorderRadius.circular(20),
  260. ),
  261. child: Row(
  262. children: [
  263. const Icon(Icons.download, color: Colors.white, size: 20),
  264. const SizedBox(width: 8),
  265. Text(
  266. '${(_downloadProgress * 100).toStringAsFixed(0)}%',
  267. style: const TextStyle(color: Colors.white),
  268. ),
  269. ],
  270. ),
  271. ),
  272. );
  273. }
  274. Widget _buildErrorState() {
  275. return Container(
  276. color: Colors.black,
  277. child: Center(
  278. child: Column(
  279. mainAxisSize: MainAxisSize.min,
  280. children: [
  281. const Icon(Icons.videocam_off, size: 64, color: Colors.white54),
  282. const SizedBox(height: 16),
  283. const Text('Playback Failed',
  284. style: TextStyle(color: Colors.white, fontSize: 18)),
  285. const SizedBox(height: 8),
  286. Text(
  287. _usingCache ? 'Cached file may be corrupted' : 'Network issue',
  288. style: const TextStyle(color: Colors.white70),
  289. ),
  290. const SizedBox(height: 16),
  291. Row(
  292. mainAxisAlignment: MainAxisAlignment.center,
  293. children: [
  294. ElevatedButton.icon(
  295. icon: const Icon(Icons.refresh),
  296. label: const Text('Retry'),
  297. onPressed: _retryPlayback,
  298. ),
  299. const SizedBox(width: 16),
  300. if (!_usingCache)
  301. ElevatedButton.icon(
  302. icon: const Icon(Icons.download),
  303. label: const Text('Download'),
  304. onPressed: _cacheVideo,
  305. ),
  306. ],
  307. )
  308. ],
  309. ),
  310. ),
  311. );
  312. }
  313. @override
  314. void dispose() {
  315. _hideTimer?.cancel();
  316. _player.controller.removeListener(_videoListener);
  317. _player.dispose();
  318. super.dispose();
  319. }
  320. }
  321. class VideoProgressBar extends StatelessWidget {
  322. final VideoPlayerController controller;
  323. final Function(Duration) onSeek;
  324. const VideoProgressBar({
  325. super.key,
  326. required this.controller,
  327. required this.onSeek,
  328. });
  329. @override
  330. Widget build(BuildContext context) {
  331. return ValueListenableBuilder(
  332. valueListenable: controller,
  333. builder: (context, value, child) {
  334. final position = value.position;
  335. final duration = value.duration;
  336. return Padding(
  337. padding: const EdgeInsets.symmetric(horizontal: 16),
  338. child: Row(
  339. children: [
  340. Text(
  341. _formatDuration(position),
  342. style: const TextStyle(color: Colors.white),
  343. ),
  344. Expanded(
  345. child: SliderTheme(
  346. data: SliderTheme.of(context).copyWith(
  347. trackHeight: 2,
  348. thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
  349. ),
  350. child: Slider(
  351. min: 0,
  352. max: duration.inSeconds.toDouble(),
  353. value: position.inSeconds.toDouble().clamp(0, duration.inSeconds.toDouble()),
  354. onChanged: (value) => onSeek(Duration(seconds: value.toInt())),
  355. onChangeStart: (_) => controller.pause(),
  356. onChangeEnd: (_) => controller.play(),
  357. activeColor: Colors.red,
  358. inactiveColor: Colors.white.withOpacity(0.3),
  359. ),
  360. ),
  361. ),
  362. Text(
  363. _formatDuration(duration),
  364. style: const TextStyle(color: Colors.white),
  365. ),
  366. ],
  367. ),
  368. );
  369. },
  370. );
  371. }
  372. String _formatDuration(Duration duration) {
  373. String twoDigits(int n) => n.toString().padLeft(2, '0');
  374. final hours = duration.inHours;
  375. final minutes = duration.inMinutes.remainder(60);
  376. final seconds = duration.inSeconds.remainder(60);
  377. return [
  378. if (hours > 0) twoDigits(hours),
  379. twoDigits(minutes),
  380. twoDigits(seconds),
  381. ].join(':').replaceFirst(RegExp(r'^0:'), '');
  382. }
  383. }