video_player_widget.dart 12 KB

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