|
@@ -1,10 +1,10 @@
|
|
|
-import 'dart:io';
|
|
|
import 'dart:async';
|
|
|
+import 'dart:io';
|
|
|
import 'package:flutter/material.dart';
|
|
|
-import 'package:media_kit/media_kit.dart';
|
|
|
-import 'package:media_kit_video/media_kit_video.dart';
|
|
|
+import 'package:cached_video_player_plus/cached_video_player_plus.dart';
|
|
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
|
-import 'package:path/path.dart' as path;
|
|
|
+import 'package:visibility_detector/visibility_detector.dart';
|
|
|
+import 'package:video_player/video_player.dart';
|
|
|
import '../../main.dart';
|
|
|
|
|
|
class VideoPlayerWidget extends StatefulWidget {
|
|
@@ -17,8 +17,7 @@ class VideoPlayerWidget extends StatefulWidget {
|
|
|
}
|
|
|
|
|
|
class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|
|
- late final Player _player;
|
|
|
- late final VideoController _controller;
|
|
|
+ late final CachedVideoPlayerPlus _player;
|
|
|
bool _isInitialized = false;
|
|
|
bool _hasError = false;
|
|
|
bool _usingCache = false;
|
|
@@ -27,56 +26,177 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|
|
bool _isBuffering = false;
|
|
|
File? _cachedFile;
|
|
|
StreamSubscription? _cacheSubscription;
|
|
|
+ bool _showControls = true;
|
|
|
+ Timer? _hideTimer;
|
|
|
+ bool _isPlaying = true;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
_initializePlayer();
|
|
|
+ _startHideTimer();
|
|
|
}
|
|
|
|
|
|
Future<void> _initializePlayer() async {
|
|
|
try {
|
|
|
- _cachedFile = await videoCacheManager.getSingleFile(widget.videoUrl);
|
|
|
- _usingCache = _cachedFile != null;
|
|
|
-
|
|
|
- _player = Player();
|
|
|
- _controller = VideoController(_player);
|
|
|
-
|
|
|
- final source = _cachedFile != null
|
|
|
- ? Media(_cachedFile!.path, httpHeaders: {})
|
|
|
- : Media(widget.videoUrl);
|
|
|
-
|
|
|
- await _player.open(source);
|
|
|
- //await _player.setVolume(0);
|
|
|
-
|
|
|
- if (_cachedFile == null) {
|
|
|
- _cacheVideo();
|
|
|
- }
|
|
|
-
|
|
|
- _player.stream.error.listen(_handlePlayerError);
|
|
|
- _player.stream.buffering.listen(_handleBufferingUpdate);
|
|
|
+ _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 _handleBufferingUpdate(bool isBuffering) {
|
|
|
+ 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(() {
|
|
|
- _isBuffering = isBuffering;
|
|
|
- _bufferingProgress = 0;
|
|
|
-
|
|
|
- if (!isBuffering && !_isInitialized && !_hasError) {
|
|
|
- _isInitialized = true;
|
|
|
- }
|
|
|
+ _isPlaying ? _player.controller.pause() : _player.controller.play();
|
|
|
+ _isPlaying = !_isPlaying;
|
|
|
});
|
|
|
+ _startHideTimer();
|
|
|
}
|
|
|
|
|
|
- void _handlePlayerError(String error) {
|
|
|
- _handleError("Player error: ${error})");
|
|
|
+ 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(),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- void _handleError(String message) {
|
|
|
+ // 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);
|
|
|
}
|
|
@@ -111,33 +231,10 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|
|
_initializePlayer();
|
|
|
}
|
|
|
|
|
|
- @override
|
|
|
- Widget build(BuildContext context) {
|
|
|
- return GestureDetector(
|
|
|
- child: Stack(
|
|
|
- children: [
|
|
|
- // Video display
|
|
|
- if (_isInitialized)
|
|
|
- Video(controller: _controller),
|
|
|
-
|
|
|
- // 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(),
|
|
|
- ],
|
|
|
- ),
|
|
|
- );
|
|
|
+ Future<void> _handleVisibilityChange(bool visible) async {
|
|
|
+ if (!visible) {
|
|
|
+ await _player.controller.pause();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
Widget _buildLoadingState() {
|
|
@@ -247,8 +344,78 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
+ _hideTimer?.cancel();
|
|
|
+ _player.controller.removeListener(_videoListener);
|
|
|
_player.dispose();
|
|
|
- _cacheSubscription?.cancel();
|
|
|
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:'), '');
|
|
|
+ }
|
|
|
+}
|