wechat UI
This commit is contained in:
290
lib/app/modules/selfMedia/video/services/videoPreloader.dart
Normal file
290
lib/app/modules/selfMedia/video/services/videoPreloader.dart
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class VideoPreloader {
|
||||
static final VideoPreloader _instance = VideoPreloader._internal();
|
||||
factory VideoPreloader() => _instance;
|
||||
VideoPreloader._internal();
|
||||
|
||||
final Map<String, String> cachedVideoPaths = {};
|
||||
final Map<String, Future<String?>> _downloadingVideos = {};
|
||||
final Set<String> _preloadQueue = {};
|
||||
|
||||
// Configuration
|
||||
int maxPreloadCount = 3; // Maximum number of videos to preload ahead
|
||||
int maxCacheSize = 500 * 1024 * 1024; // 500MB max cache size
|
||||
|
||||
/// Get cache directory for videos
|
||||
Future<Directory> get _cacheDirectory async {
|
||||
final Directory tempDir = await getTemporaryDirectory();
|
||||
final Directory videoCache = Directory('${tempDir.path}/video_cache');
|
||||
if (!await videoCache.exists()) {
|
||||
await videoCache.create(recursive: true);
|
||||
}
|
||||
return videoCache;
|
||||
}
|
||||
|
||||
/// Generate a unique filename for the video URL
|
||||
String _generateFileName(String url) {
|
||||
final bytes = utf8.encode(url);
|
||||
final digest = sha256.convert(bytes);
|
||||
final extension = url.split('.').last.split('?').first;
|
||||
return '${digest.toString()}.${extension.isNotEmpty ? extension : 'mp4'}';
|
||||
}
|
||||
|
||||
/// Check if video is already cached
|
||||
Future<bool> isVideoCached(String url) async {
|
||||
String normalizedUrl = url;
|
||||
if (normalizedUrl.startsWith('http://')) {
|
||||
normalizedUrl = normalizedUrl.replaceFirst('http://', 'https://');
|
||||
}
|
||||
|
||||
if (cachedVideoPaths.containsKey(normalizedUrl)) {
|
||||
final file = File(cachedVideoPaths[normalizedUrl]!);
|
||||
if (await file.exists()) {
|
||||
return true;
|
||||
} else {
|
||||
cachedVideoPaths.remove(normalizedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
final cacheDir = await _cacheDirectory;
|
||||
final fileName = _generateFileName(normalizedUrl);
|
||||
final file = File('${cacheDir.path}/$fileName');
|
||||
|
||||
if (await file.exists()) {
|
||||
cachedVideoPaths[normalizedUrl] = file.path;
|
||||
debugPrint('Found cached video: ${file.path}');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get cached video path if available
|
||||
Future<String?> getCachedVideoPath(String url) async {
|
||||
String normalizedUrl = url;
|
||||
if (normalizedUrl.startsWith('http://')) {
|
||||
normalizedUrl = normalizedUrl.replaceFirst('http://', 'https://');
|
||||
}
|
||||
|
||||
if (await isVideoCached(normalizedUrl)) {
|
||||
final path = cachedVideoPaths[normalizedUrl];
|
||||
debugPrint('Retrieved cached video path: $path');
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Download video to cache
|
||||
Future<String?> downloadVideo(String url, {Function(double)? onProgress}) async {
|
||||
// ✅ Skip RTMP streams
|
||||
if (url.startsWith('rtmp://')) {
|
||||
debugPrint('⏭️ Skipping download for RTMP stream: $url');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String normalizedUrl = url;
|
||||
if (normalizedUrl.startsWith('http://')) {
|
||||
normalizedUrl = normalizedUrl.replaceFirst('http://', 'https://');
|
||||
}
|
||||
|
||||
if (_downloadingVideos.containsKey(normalizedUrl)) {
|
||||
debugPrint('Video already downloading: $normalizedUrl');
|
||||
return await _downloadingVideos[normalizedUrl];
|
||||
}
|
||||
|
||||
if (await isVideoCached(normalizedUrl)) {
|
||||
debugPrint('Video already cached: $normalizedUrl');
|
||||
return cachedVideoPaths[normalizedUrl];
|
||||
}
|
||||
|
||||
debugPrint('⬇️ Starting download: $normalizedUrl');
|
||||
|
||||
final downloadFuture = _performDownload(normalizedUrl, onProgress: onProgress);
|
||||
_downloadingVideos[normalizedUrl] = downloadFuture;
|
||||
|
||||
final result = await downloadFuture;
|
||||
_downloadingVideos.remove(normalizedUrl);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
_downloadingVideos.remove(url);
|
||||
debugPrint('Error downloading video: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _performDownload(String url, {Function(double)? onProgress}) async {
|
||||
try {
|
||||
debugPrint('Downloading video from: $url');
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to download video: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final cacheDir = await _cacheDirectory;
|
||||
final fileName = _generateFileName(url);
|
||||
final file = File('${cacheDir.path}/$fileName');
|
||||
|
||||
await _manageCacheSize();
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
cachedVideoPaths[url] = file.path;
|
||||
|
||||
debugPrint('✅ Video cached at: ${file.path} (${response.bodyBytes.length} bytes)');
|
||||
return file.path;
|
||||
} catch (e) {
|
||||
debugPrint('Error in _performDownload: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Preload videos based on current index
|
||||
Future<void> preloadVideos(List<String> videoUrls, int currentIndex) async {
|
||||
_preloadQueue.clear();
|
||||
final List<String> videosToPreload = [];
|
||||
|
||||
for (int i = 1; i <= maxPreloadCount; i++) {
|
||||
final nextIndex = currentIndex + i;
|
||||
if (nextIndex < videoUrls.length) {
|
||||
videosToPreload.add(videoUrls[nextIndex]);
|
||||
}
|
||||
if (i == 1) {
|
||||
final prevIndex = currentIndex - 1;
|
||||
if (prevIndex >= 0) {
|
||||
videosToPreload.add(videoUrls[prevIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final url in videosToPreload) {
|
||||
// ✅ Skip RTMP streams
|
||||
if (url.startsWith('rtmp://')) {
|
||||
debugPrint('⏭️ Skipping preload for RTMP stream: $url');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!await isVideoCached(url) && !_preloadQueue.contains(url)) {
|
||||
_preloadQueue.add(url);
|
||||
_preloadInBackground(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _preloadInBackground(String url) {
|
||||
downloadVideo(url).then((path) {
|
||||
_preloadQueue.remove(url);
|
||||
if (path != null) {
|
||||
debugPrint('✅ Preloaded video: $url');
|
||||
}
|
||||
}).catchError((error) {
|
||||
_preloadQueue.remove(url);
|
||||
debugPrint('❌ Failed to preload video: $url, Error: $error');
|
||||
});
|
||||
}
|
||||
|
||||
/// Manage cache size by removing old files
|
||||
Future<void> _manageCacheSize() async {
|
||||
try {
|
||||
final cacheDir = await _cacheDirectory;
|
||||
final files = await cacheDir.list().toList();
|
||||
int totalSize = 0;
|
||||
final List<MapEntry<File, int>> fileStats = [];
|
||||
|
||||
for (final entity in files) {
|
||||
if (entity is File) {
|
||||
final stat = await entity.stat();
|
||||
totalSize += stat.size;
|
||||
fileStats.add(MapEntry(entity, stat.modified.millisecondsSinceEpoch));
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSize > maxCacheSize) {
|
||||
fileStats.sort((a, b) => a.value.compareTo(b.value));
|
||||
for (final entry in fileStats) {
|
||||
if (totalSize <= maxCacheSize * 0.8) break;
|
||||
final file = entry.key;
|
||||
final stat = await file.stat();
|
||||
await file.delete();
|
||||
totalSize -= stat.size;
|
||||
cachedVideoPaths.removeWhere((key, value) => value == file.path);
|
||||
debugPrint('🗑 Removed cached video: ${file.path}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error managing cache size: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cached videos
|
||||
Future<void> clearCache() async {
|
||||
try {
|
||||
final cacheDir = await _cacheDirectory;
|
||||
if (await cacheDir.exists()) {
|
||||
await cacheDir.delete(recursive: true);
|
||||
}
|
||||
cachedVideoPaths.clear();
|
||||
_preloadQueue.clear();
|
||||
debugPrint('🧹 Video cache cleared');
|
||||
} catch (e) {
|
||||
debugPrint('Error clearing cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load existing cached videos on app startup
|
||||
Future<void> loadExistingCache() async {
|
||||
try {
|
||||
final cacheDir = await _cacheDirectory;
|
||||
if (!await cacheDir.exists()) return;
|
||||
final files = await cacheDir.list().toList();
|
||||
debugPrint('🔍 Loading existing cache entries...');
|
||||
for (final entity in files) {
|
||||
if (entity is File) {
|
||||
final fileName = entity.path.split('/').last;
|
||||
debugPrint('Found cached file: $fileName');
|
||||
}
|
||||
}
|
||||
debugPrint('✅ Cache load complete. Found ${files.length} files.');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading existing cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache size
|
||||
Future<int> getCacheSize() async {
|
||||
try {
|
||||
final cacheDir = await _cacheDirectory;
|
||||
if (!await cacheDir.exists()) return 0;
|
||||
final files = await cacheDir.list().toList();
|
||||
int totalSize = 0;
|
||||
for (final entity in files) {
|
||||
if (entity is File) {
|
||||
final stat = await entity.stat();
|
||||
totalSize += stat.size;
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
} catch (e) {
|
||||
debugPrint('Error getting cache size: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get formatted cache size
|
||||
Future<String> getFormattedCacheSize() async {
|
||||
final size = await getCacheSize();
|
||||
if (size < 1024) return '${size}B';
|
||||
if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)}KB';
|
||||
return '${(size / (1024 * 1024)).toStringAsFixed(1)}MB';
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user