import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:visibility_detector/visibility_detector.dart'; import 'package:video_player/video_player.dart'; import '../../main.dart'; class VideoPlayerWidget extends StatefulWidget { final String videoUrl; const VideoPlayerWidget({super.key, required this.videoUrl}); @override State createState() => _VideoPlayerWidgetState(); } class _VideoPlayerWidgetState extends State { late final CachedVideoPlayerPlus _player; bool _isInitialized = false; bool _hasError = false; bool _usingCache = false; double _downloadProgress = 0; double _bufferingProgress = 0; bool _isBuffering = false; File? _cachedFile; StreamSubscription? _cacheSubscription; bool _showControls = true; Timer? _hideTimer; bool _isPlaying = true; @override void initState() { super.initState(); _initializePlayer(); _startHideTimer(); } Future _initializePlayer() async { try { _player = CachedVideoPlayerPlus.networkUrl( Uri.parse(widget.videoUrl), invalidateCacheIfOlderThan: const Duration(days: 60), ); await _player.initialize(); _player.controller.addListener(_videoListener); setState(() => _isInitialized = true); _player.controller.play(); } catch (e) { _handleError("Player init error: $e"); } } void _videoListener() { if (!mounted) return; // Update playing state if (_isPlaying != _player.controller.value.isPlaying) { setState(() => _isPlaying = _player.controller.value.isPlaying); } // Handle buffering updates final isBuffering = _player.controller.value.isBuffering; if (isBuffering != _isBuffering) { setState(() => _isBuffering = isBuffering); } } void _toggleControls() { setState(() => _showControls = !_showControls); if (_showControls) { _startHideTimer(); } else { _hideTimer?.cancel(); } } void _startHideTimer() { _hideTimer?.cancel(); _hideTimer = Timer(const Duration(seconds: 3), () { if (mounted) setState(() => _showControls = false); }); } void _togglePlayPause() { setState(() { _isPlaying ? _player.controller.pause() : _player.controller.play(); _isPlaying = !_isPlaying; }); _startHideTimer(); } Widget _buildControlsOverlay() { return AnimatedOpacity( opacity: _showControls ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Stack( children: [ // Play/Pause Center Button Positioned.fill( child: Center( child: IconButton( icon: Icon( _isPlaying ? Icons.pause : Icons.play_arrow, size: 48, color: Colors.white.withOpacity(0.8), ), onPressed: _togglePlayPause, ), ), ), // Bottom Controls Bar Positioned( bottom: 0, left: 0, right: 0, child: Container( height: 60, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: Row( children: [ Expanded( child: VideoProgressBar( controller: _player.controller, onSeek: (duration) { _player.controller.seekTo(duration); _startHideTimer(); }, ), ), ], ), ), ), ], ), ); } @override Widget build(BuildContext context) { return VisibilityDetector( key: Key(widget.videoUrl), onVisibilityChanged: (info) => _handleVisibilityChange(info.visibleFraction > 0.5), child: GestureDetector( onTap: _toggleControls, child: Stack( alignment: Alignment.center, children: [ // Video Display if (_isInitialized) Center( child: AspectRatio( aspectRatio: _player.controller.value.aspectRatio, child: VideoPlayer(_player.controller), ), ), // Controls Overlay if (_isInitialized && !_hasError) _buildControlsOverlay(), // Loading/buffering states if (!_isInitialized || _isBuffering) _buildLoadingState(), // Error state if (_hasError) _buildErrorState(), // Cache progress indicator if (_downloadProgress > 0 && _downloadProgress < 1) _buildCacheProgress(), // Buffering indicator if (_isBuffering && _isInitialized) _buildBufferingOverlay(), ], ), ), ); } // Keep existing helper methods (_handleError, _cacheVideo, _retryPlayback, // _handleVisibilityChange, _buildLoadingState, _buildBufferingOverlay, // _buildCacheProgress, _buildErrorState) as provided in original code void _handleError(String message) { print(message); if (!_hasError) setState(() => _hasError = true); } Future _cacheVideo() async { final fileStream = videoCacheManager.getFileStream( widget.videoUrl, withProgress: true, ); _cacheSubscription = fileStream.listen((fileResponse) { if (fileResponse is DownloadProgress) { setState(() => _downloadProgress = fileResponse.progress ?? 0); } else if (fileResponse is FileInfo) { setState(() { _cachedFile = fileResponse.file; _usingCache = true; _downloadProgress = 0; }); } }, onError: (e) => print("Cache error: $e")); } void _retryPlayback() { setState(() { _hasError = false; _isInitialized = false; _downloadProgress = 0; _bufferingProgress = 0; _cacheSubscription?.cancel(); }); _initializePlayer(); } Future _handleVisibilityChange(bool visible) async { if (!visible) { await _player.controller.pause(); } } Widget _buildLoadingState() { return Container( color: Colors.black, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( value: _isBuffering ? _bufferingProgress : null, valueColor: const AlwaysStoppedAnimation(Colors.white), ), const SizedBox(height: 16), Text( _usingCache ? 'Loading cached video' : 'Streaming video', style: const TextStyle(color: Colors.white), ), ], ), ), ); } Widget _buildBufferingOverlay() { return Container( color: Colors.black54, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( value: _bufferingProgress, valueColor: const AlwaysStoppedAnimation(Colors.white), ), const SizedBox(height: 8), const Text('Buffering...', style: TextStyle(color: Colors.white)), ], ), ), ); } Widget _buildCacheProgress() { return Positioned( bottom: 16, right: 16, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(20), ), child: Row( children: [ const Icon(Icons.download, color: Colors.white, size: 20), const SizedBox(width: 8), Text( '${(_downloadProgress * 100).toStringAsFixed(0)}%', style: const TextStyle(color: Colors.white), ), ], ), ), ); } Widget _buildErrorState() { return Container( color: Colors.black, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.videocam_off, size: 64, color: Colors.white54), const SizedBox(height: 16), const Text('Playback Failed', style: TextStyle(color: Colors.white, fontSize: 18)), const SizedBox(height: 8), Text( _usingCache ? 'Cached file may be corrupted' : 'Network issue', style: const TextStyle(color: Colors.white70), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Retry'), onPressed: _retryPlayback, ), const SizedBox(width: 16), if (!_usingCache) ElevatedButton.icon( icon: const Icon(Icons.download), label: const Text('Download'), onPressed: _cacheVideo, ), ], ) ], ), ), ); } @override void dispose() { _hideTimer?.cancel(); _player.controller.removeListener(_videoListener); _player.dispose(); super.dispose(); } } class VideoProgressBar extends StatelessWidget { final VideoPlayerController controller; final Function(Duration) onSeek; const VideoProgressBar({ super.key, required this.controller, required this.onSeek, }); @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller, builder: (context, value, child) { final position = value.position; final duration = value.duration; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Text( _formatDuration(position), style: const TextStyle(color: Colors.white), ), Expanded( child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 2, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6), ), child: Slider( min: 0, max: duration.inSeconds.toDouble(), value: position.inSeconds.toDouble().clamp(0, duration.inSeconds.toDouble()), onChanged: (value) => onSeek(Duration(seconds: value.toInt())), onChangeStart: (_) => controller.pause(), onChangeEnd: (_) => controller.play(), activeColor: Colors.red, inactiveColor: Colors.white.withOpacity(0.3), ), ), ), Text( _formatDuration(duration), style: const TextStyle(color: Colors.white), ), ], ), ); }, ); } String _formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, '0'); final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); final seconds = duration.inSeconds.remainder(60); return [ if (hours > 0) twoDigits(hours), twoDigits(minutes), twoDigits(seconds), ].join(':').replaceFirst(RegExp(r'^0:'), ''); } }