Browse Source

added video caching

Veloe 3 months ago
parent
commit
77badf84f9

+ 1 - 0
lib/pages/media_carousel_screen

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:photo_view/photo_view.dart';
+import 'package:veloe_kemono_party_flutter/pages/widgets/video_player_widget';
 import '../models/post.dart';
 
 class MediaCarouselItem {

+ 222 - 20
lib/pages/widgets/video_player_widget

@@ -1,6 +1,11 @@
+import 'dart:io';
+import 'dart:async';
+import 'package:flutter/material.dart';
 import 'package:media_kit/media_kit.dart';
 import 'package:media_kit_video/media_kit_video.dart';
-import '../models/post.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:path/path.dart' as path;
+import '../../main.dart';
 
 class VideoPlayerWidget extends StatefulWidget {
   final String videoUrl;
@@ -12,41 +17,238 @@ class VideoPlayerWidget extends StatefulWidget {
 }
 
 class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
-  late final Player player;
-  late final VideoController controller;
+  late final Player _player;
+  late final VideoController _controller;
   bool _isInitialized = false;
+  bool _hasError = false;
+  bool _usingCache = false;
+  double _downloadProgress = 0;
+  double _bufferingProgress = 0;
+  bool _isBuffering = false;
+  File? _cachedFile;
+  StreamSubscription? _cacheSubscription;
 
   @override
   void initState() {
     super.initState();
-    player = Player();
-    controller = VideoController(player);
-    
-    player.open(Media(widget.videoUrl));
-    player.stream.error.listen((error) => print("Player error: $error"));
-    
-    player.stream.buffering.listen((buffering) {
-      if (!buffering && !_isInitialized) {
-        setState(() => _isInitialized = true);
+    _initializePlayer();
+  }
+
+  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);
+      
+    } catch (e) {
+      _handleError("Player init error: $e");
+    }
+  }
+
+  void _handleBufferingUpdate(bool isBuffering) {
+    setState(() {
+      _isBuffering = isBuffering;
+      _bufferingProgress = 0;
+      
+      if (!isBuffering && !_isInitialized && !_hasError) {
+        _isInitialized = true;
+      }
+    });
+  }
+
+  void _handlePlayerError(String error) {
+    _handleError("Player error: ${error})");
+  }
+
+  void _handleError(String message) {
+    print(message);
+    if (!_hasError) setState(() => _hasError = true);
+  }
+
+  Future<void> _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();
   }
 
   @override
   Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        Video(controller: controller),
-        
-        if (!_isInitialized)
-          const Center(child: CircularProgressIndicator()),
-      ],
+    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(),
+        ],
+      ),
+    );
+  }
+
+  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() {
-    player.dispose();
+    _player.dispose();
+    _cacheSubscription?.cancel();
     super.dispose();
   }
 }

+ 24 - 1
lib/pages/widgets/video_preview.dart

@@ -1,6 +1,7 @@
 import 'dart:typed_data';
 import 'package:flutter/material.dart';
 import 'package:veloe_kemono_party_flutter/video_thumbnail_generator';
+import '../../main.dart';
 
 class VideoPreview extends StatefulWidget {
   final String videoUrl;
@@ -19,10 +20,12 @@ class VideoPreview extends StatefulWidget {
 class _VideoPreviewState extends State<VideoPreview> {
   Future<Uint8List?>? _thumbnailFuture;
   bool _isGenerating = false;
+  bool _isCached = false;
 
   @override
   void initState() {
     super.initState();
+    _checkCacheStatus();
     _loadThumbnail();
   }
 
@@ -32,6 +35,13 @@ class _VideoPreviewState extends State<VideoPreview> {
       .whenComplete(() => setState(() => _isGenerating = false));
   }
 
+  Future<void> _checkCacheStatus() async {
+  final cachedFile = await videoCacheManager.getFileFromCache(widget.videoUrl);
+  if (cachedFile != null && mounted) {
+    setState(() => _isCached = true);
+  }
+}
+
   @override
   Widget build(BuildContext context) {
     return GestureDetector(
@@ -70,7 +80,20 @@ class _VideoPreviewState extends State<VideoPreview> {
                     child: const Text('VIDEO',
                       style: TextStyle(color: Colors.white)),
                   ),
-                )
+                ),
+                if (_isCached)
+                  Positioned(
+                    top: 8,
+                    right: 8,
+                    child: Container(
+                      padding: const EdgeInsets.all(4),
+                      decoration: const BoxDecoration(
+                        color: Colors.green,
+                        shape: BoxShape.circle,
+                      ),
+                      child: const Icon(Icons.download_done, size: 16, color: Colors.white),
+                    ),
+                  ),
               ],
             ),
           );

+ 1 - 1
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.0.4
+version: 1.0.5
 
 environment:
   sdk: ^3.7.2