diff --git a/example/lib/pages/advance_cache_management_page.dart b/example/lib/pages/advance_cache_management_page.dart index b1afc17..20fa547 100644 --- a/example/lib/pages/advance_cache_management_page.dart +++ b/example/lib/pages/advance_cache_management_page.dart @@ -57,11 +57,14 @@ class _AdvanceCacheManagementPageState maxNrOfCacheObjects: 20, ), ); + final _customMetadataStorage = _MemoryVideoPlayerMetadataStorage(); final _asyncPrefs = SharedPreferencesAsync(); int _selectedIndex = 0; String _customKey = ''; bool _forceFetch = false; + bool _overrideCacheManager = false; + bool _overrideMetadataStorage = false; bool _isLoading = false; bool _isCaching = false; bool _isClearing = false; @@ -205,13 +208,52 @@ class _AdvanceCacheManagementPageState ], ), const SizedBox(height: 12), - Row( + Wrap( + spacing: 12.0, + runSpacing: 12.0, children: [ - const Text('Force fetch latest:'), - const SizedBox(width: 12), - Switch.adaptive( - value: _forceFetch, - onChanged: (value) => setState(() => _forceFetch = value), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Force fetch latest:'), + const SizedBox(width: 12), + Switch.adaptive( + value: _forceFetch, + onChanged: (value) => setState(() => _forceFetch = value), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Override default cache manager:'), + const SizedBox(width: 12), + Switch.adaptive( + value: _overrideCacheManager, + onChanged: (value) { + CachedVideoPlayerPlus.cacheManager = value + ? _customCacheManager + : CachedVideoPlayerPlus.defaultCacheManager; + setState(() => _overrideCacheManager = value); + }, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Override default metadata storage:'), + const SizedBox(width: 12), + Switch.adaptive( + value: _overrideMetadataStorage, + onChanged: (value) { + CachedVideoPlayerPlus.metadataStorage = value + ? _customMetadataStorage + : CachedVideoPlayerPlus.defaultMetadataStorage; + setState(() => _overrideMetadataStorage = value); + }, + ), + ], ), ], ), @@ -328,3 +370,28 @@ class _SmallLoader extends StatelessWidget { ); } } + +/// Stores video metadata in memory. +class _MemoryVideoPlayerMetadataStorage implements IVideoPlayerMetadataStorage { + final _data = {}; + + @override + Future read(String key) { + return Future.value(_data[key]); + } + + @override + Future write(String key, int value) { + return Future.sync(() => _data[key] = value); + } + + @override + Future remove(String key) { + return Future.sync(() => _data.remove(key)); + } + + @override + Future erase() { + return Future.sync(() => _data.clear()); + } +} diff --git a/lib/cached_video_player_plus.dart b/lib/cached_video_player_plus.dart index 8342f44..28d557e 100644 --- a/lib/cached_video_player_plus.dart +++ b/lib/cached_video_player_plus.dart @@ -17,3 +17,6 @@ library; export 'src/cached_video_player_plus.dart'; +export 'src/i_video_player_metadata_storage.dart'; +export 'src/video_cache_manager.dart'; +export 'src/video_player_metadata_storage.dart'; diff --git a/lib/src/cached_video_player_plus.dart b/lib/src/cached_video_player_plus.dart index 23589cd..6b42565 100644 --- a/lib/src/cached_video_player_plus.dart +++ b/lib/src/cached_video_player_plus.dart @@ -5,8 +5,9 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:video_player/video_player.dart'; import 'cache_key_helpers.dart'; +import 'i_video_player_metadata_storage.dart'; import 'video_cache_manager.dart'; -import 'video_player_storage.dart'; +import 'video_player_metadata_storage.dart'; /// A video player that wraps [VideoPlayerController] with intelligent /// caching capabilities using [flutter_cache_manager]. @@ -53,7 +54,8 @@ class CachedVideoPlayerPlus { invalidateCacheIfOlderThan = Duration.zero, skipCache = true, _cacheKey = '', - _cacheManager = _defaultCacheManager; + _cacheManager = CachedVideoPlayerPlus.cacheManager, + _metadataStorage = CachedVideoPlayerPlus.metadataStorage; /// Constructs a [CachedVideoPlayerPlus] playing a video from a network URL. /// @@ -96,6 +98,7 @@ class CachedVideoPlayerPlus { this.skipCache = false, String? cacheKey, CacheManager? cacheManager, + IVideoPlayerMetadataStorage? metadataStorage, }) : dataSource = url.toString(), dataSourceType = DataSourceType.network, package = null, @@ -103,7 +106,9 @@ class CachedVideoPlayerPlus { _cacheKey = cacheKey != null ? getCustomCacheKey(cacheKey) : getCacheKey(url.toString()), - _cacheManager = cacheManager ?? _defaultCacheManager; + _cacheManager = cacheManager ?? CachedVideoPlayerPlus.cacheManager, + _metadataStorage = + metadataStorage ?? CachedVideoPlayerPlus.metadataStorage; /// Constructs a [CachedVideoPlayerPlus] playing a video from a file. /// @@ -126,7 +131,8 @@ class CachedVideoPlayerPlus { invalidateCacheIfOlderThan = Duration.zero, skipCache = true, _cacheKey = '', - _cacheManager = _defaultCacheManager; + _cacheManager = CachedVideoPlayerPlus.cacheManager, + _metadataStorage = CachedVideoPlayerPlus.metadataStorage; /// Constructs a [CachedVideoPlayerPlus] playing a video from a contentUri. /// @@ -152,7 +158,8 @@ class CachedVideoPlayerPlus { invalidateCacheIfOlderThan = Duration.zero, skipCache = true, _cacheKey = '', - _cacheManager = _defaultCacheManager; + _cacheManager = CachedVideoPlayerPlus.cacheManager, + _metadataStorage = CachedVideoPlayerPlus.metadataStorage; /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. @@ -218,6 +225,11 @@ class CachedVideoPlayerPlus { /// Defaults to [VideoCacheManager] if not provided. final CacheManager _cacheManager; + /// The [IVideoPlayerMetadataStorage] instance used for caching video metadata. + /// + /// Defaults to [VideoPlayerMetadataStorage] if not provided. + final IVideoPlayerMetadataStorage _metadataStorage; + /// The underlying video player controller that handles actual video playback. late VideoPlayerController _videoPlayerController; @@ -255,12 +267,6 @@ class CachedVideoPlayerPlus { return dataSourceType == DataSourceType.network && !kIsWeb && !skipCache; } - /// The default cache manager for video file caching operations. - static final _defaultCacheManager = VideoCacheManager(); - - /// Default storage for cache metadata and expiration timestamps. - static final _storage = VideoPlayerStorage(); - /// Initializes the video player and sets up caching if applicable. /// /// This method must be called before accessing the [controller] or playing @@ -287,7 +293,7 @@ class CachedVideoPlayerPlus { _debugPrint('Cached video of [$dataSource] is: ${cachedFile?.file.path}'); if (cachedFile != null) { - final cachedElapsedMillis = await _storage.read(_cacheKey); + final cachedElapsedMillis = await _metadataStorage.read(_cacheKey); bool isCacheExpired = true; if (cachedElapsedMillis != null) { @@ -316,7 +322,7 @@ class CachedVideoPlayerPlus { _cacheManager .downloadFile(dataSource, authHeaders: _authHeaders, key: _cacheKey) .then((_) { - _storage.write( + _metadataStorage.write( _cacheKey, DateTime.timestamp().millisecondsSinceEpoch, ); @@ -388,10 +394,28 @@ class CachedVideoPlayerPlus { Future removeFromCache() async { await Future.wait([ _cacheManager.removeFile(_cacheKey), - _storage.remove(_cacheKey), + _metadataStorage.remove(_cacheKey), ]); } + /// The default cache manager for video file caching operations. + static final defaultCacheManager = VideoCacheManager(); + + /// The globally used cache manager for video file caching operations. + /// + /// Changing this will affect all [CachedVideoPlayerPlus] instances that use + /// the default cache manager. + static CacheManager cacheManager = defaultCacheManager; + + /// Default storage for cache metadata and expiration timestamps. + static final defaultMetadataStorage = VideoPlayerMetadataStorage(); + + /// The globally used storage for video file metadata. + /// + /// Changing this will affect all [CachedVideoPlayerPlus] instances that use + /// the default metadata storage. + static IVideoPlayerMetadataStorage metadataStorage = defaultMetadataStorage; + /// Removes the cached file for the specified [url] from the cache. /// /// Use this static method to remove specific cached videos by their URL. @@ -402,19 +426,25 @@ class CachedVideoPlayerPlus { /// The [cacheManager] parameter allows specifying a custom [CacheManager] /// instance. If not provided, the default [VideoCacheManager] will be used. /// + /// The [metadataStorage] parameter allows specifying a custom + /// [IVideoPlayerMetadataStorage] instance. If not provided, the default + /// [VideoPlayerMetadataStorage] will be used. + /// /// Both the cached video file and its expiration metadata are deleted. static Future removeFileFromCache( Uri url, { CacheManager? cacheManager, + IVideoPlayerMetadataStorage? metadataStorage, }) async { final urlString = url.toString(); final cacheKey = getCacheKey(urlString); - cacheManager ??= _defaultCacheManager; + cacheManager ??= CachedVideoPlayerPlus.cacheManager; + metadataStorage ??= CachedVideoPlayerPlus.metadataStorage; await Future.wait([ cacheManager.removeFile(cacheKey), - _storage.remove(cacheKey), + metadataStorage.remove(cacheKey), ]); } @@ -428,18 +458,24 @@ class CachedVideoPlayerPlus { /// The [cacheManager] parameter allows specifying a custom [CacheManager] /// instance. If not provided, the default [VideoCacheManager] will be used. /// + /// The [metadataStorage] parameter allows specifying a custom + /// [IVideoPlayerMetadataStorage] instance. If not provided, the default + /// [VideoPlayerMetadataStorage] will be used. + /// /// Both the cached video file and its expiration metadata are deleted. static Future removeFileFromCacheByKey( String cacheKey, { CacheManager? cacheManager, + IVideoPlayerMetadataStorage? metadataStorage, }) async { cacheKey = getCustomCacheKey(cacheKey); - cacheManager ??= _defaultCacheManager; + cacheManager ??= CachedVideoPlayerPlus.cacheManager; + metadataStorage ??= CachedVideoPlayerPlus.metadataStorage; await Future.wait([ cacheManager.removeFile(cacheKey), - _storage.remove(cacheKey), + metadataStorage.remove(cacheKey), ]); } @@ -452,12 +488,23 @@ class CachedVideoPlayerPlus { /// The [cacheManager] parameter allows specifying a custom [CacheManager] /// instance. If not provided, the default [VideoCacheManager] will be used. /// + /// The [metadataStorage] parameter allows specifying a custom + /// [IVideoPlayerMetadataStorage] instance. If not provided, the default + /// [VideoPlayerMetadataStorage] will be used. + /// /// This operation cannot be undone. All cached videos will need to be /// re-downloaded from their original sources. - static Future clearAllCache({CacheManager? cacheManager}) async { - cacheManager ??= _defaultCacheManager; + static Future clearAllCache({ + CacheManager? cacheManager, + IVideoPlayerMetadataStorage? metadataStorage, + }) async { + cacheManager ??= CachedVideoPlayerPlus.cacheManager; + metadataStorage ??= CachedVideoPlayerPlus.metadataStorage; - await Future.wait([cacheManager.emptyCache(), _storage.erase()]); + await Future.wait([ + cacheManager.emptyCache(), + metadataStorage.erase(), + ]); } /// Pre-caches a video file from the specified [url]. @@ -482,14 +529,20 @@ class CachedVideoPlayerPlus { /// The [cacheManager] parameter allows providing a custom [CacheManager] /// instance for caching operations. If not provided, the default /// [VideoCacheManager] will be used. + /// + /// The [metadataStorage] parameter allows specifying a custom + /// [IVideoPlayerMetadataStorage] for storing cache metadata. If not provided, + /// the default [VideoPlayerMetadataStorage] will be used. static Future preCacheVideo( Uri url, { Duration invalidateCacheIfOlderThan = const Duration(days: 69), Map downloadHeaders = const {}, String? cacheKey, CacheManager? cacheManager, + IVideoPlayerMetadataStorage? metadataStorage, }) async { - cacheManager ??= _defaultCacheManager; + cacheManager ??= CachedVideoPlayerPlus.cacheManager; + metadataStorage ??= CachedVideoPlayerPlus.metadataStorage; final effectiveCacheKey = cacheKey != null ? getCustomCacheKey(cacheKey) @@ -501,7 +554,7 @@ class CachedVideoPlayerPlus { ); if (cachedFile != null) { - final cachedElapsedMillis = await _storage.read(effectiveCacheKey); + final cachedElapsedMillis = await metadataStorage.read(effectiveCacheKey); bool isCacheExpired = true; if (cachedElapsedMillis != null) { @@ -529,7 +582,7 @@ class CachedVideoPlayerPlus { authHeaders: downloadHeaders, ); - await _storage.write( + await metadataStorage.write( effectiveCacheKey, DateTime.timestamp().millisecondsSinceEpoch, ); diff --git a/lib/src/i_video_player_metadata_storage.dart b/lib/src/i_video_player_metadata_storage.dart new file mode 100644 index 0000000..6263dc9 --- /dev/null +++ b/lib/src/i_video_player_metadata_storage.dart @@ -0,0 +1,20 @@ +/// An interface class for storing video metadata. +abstract interface class IVideoPlayerMetadataStorage { + /// Reads the cached video duration in milliseconds from storage. + /// + /// Returns the stored value for the given [key], or null if not found. + Future read(String key); + + /// Writes the video duration in milliseconds to storage with the given [key]. + Future write(String key, int value); + + /// Removes a value from storage. + /// + /// Deletes the value associated with the given [key]. + Future remove(String key); + + /// Clears all cached video player metadata from storage. + /// + /// This removes all keys that start with the video player prefix. + Future erase(); +} diff --git a/lib/src/video_player_storage.dart b/lib/src/video_player_metadata_storage.dart similarity index 61% rename from lib/src/video_player_storage.dart rename to lib/src/video_player_metadata_storage.dart index 8289651..c7c21aa 100644 --- a/lib/src/video_player_storage.dart +++ b/lib/src/video_player_metadata_storage.dart @@ -1,48 +1,39 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'cache_key_helpers.dart' show cacheKeyPrefix; +import 'i_video_player_metadata_storage.dart'; -/// Storage abstraction for cached video metadata. -/// /// This class handles the storage of cache expiration timestamps and provides /// migration functionality from get_storage to shared_preferences. -class VideoPlayerStorage { +class VideoPlayerMetadataStorage implements IVideoPlayerMetadataStorage { /// SharedPreferences instance for storing cache metadata. final _asyncPrefs = SharedPreferencesAsync(); /// Singleton instance of VideoPlayerStorage. - static final _instance = VideoPlayerStorage._internal(); + static final _instance = VideoPlayerMetadataStorage._internal(); /// Private constructor for singleton pattern implementation. - VideoPlayerStorage._internal(); + VideoPlayerMetadataStorage._internal(); /// Factory constructor that returns the singleton instance. - factory VideoPlayerStorage() => _instance; + factory VideoPlayerMetadataStorage() => _instance; - /// Reads a value from storage. - /// - /// Returns the stored value for the given [key], or null if not found. + @override Future read(String key) { return _asyncPrefs.getInt(key); } - /// Writes a value to storage. - /// - /// Stores the [value] with the given [key]. + @override Future write(String key, int value) async { return _asyncPrefs.setInt(key, value); } - /// Removes a value from storage. - /// - /// Deletes the value associated with the given [key]. + @override Future remove(String key) async { return _asyncPrefs.remove(key); } - /// Clears all cached video player data from storage. - /// - /// This removes all keys that start with the video player prefix. + @override Future erase() async { final keys = await _asyncPrefs.getKeys(); final videoPlayerKeys = keys.where((key) => key.startsWith(cacheKeyPrefix));