From 3dfe9189cecc6a816d091fc2c979ab211dc0c7ee Mon Sep 17 00:00:00 2001 From: C Kumara <> Date: Thu, 21 Mar 2024 19:34:24 +0300 Subject: [PATCH] - Added removeFileFromCache function to clear cache and get storage entry - Exposed getter for isDisposed to check if player already disposed - Added clearCache function to clear all cache and get storage - Added addListener override method to prevent exception to be thrown when attempting to add a listener after the controller has already been disposed - Code refactor --- lib/cached_video_player_plus.dart | 242 +++++++++++++++++------------- 1 file changed, 141 insertions(+), 101 deletions(-) diff --git a/lib/cached_video_player_plus.dart b/lib/cached_video_player_plus.dart index a8cabcc..04efbe4 100644 --- a/lib/cached_video_player_plus.dart +++ b/lib/cached_video_player_plus.dart @@ -70,10 +70,10 @@ class CachedVideoPlayerPlusValue { /// Returns an instance with the given [errorDescription]. const CachedVideoPlayerPlusValue.erroneous(String errorDescription) : this( - duration: Duration.zero, - isInitialized: false, - errorDescription: errorDescription, - ); + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription, + ); /// This constant is just to indicate that parameter is not passed to [copyWith] /// workaround for this issue https://github.com/dart-lang/language/issues/2009 @@ -219,42 +219,42 @@ class CachedVideoPlayerPlusValue { @override bool operator ==(Object other) => identical(this, other) || - other is CachedVideoPlayerPlusValue && - runtimeType == other.runtimeType && - duration == other.duration && - position == other.position && - caption == other.caption && - captionOffset == other.captionOffset && - listEquals(buffered, other.buffered) && - isPlaying == other.isPlaying && - isLooping == other.isLooping && - isBuffering == other.isBuffering && - volume == other.volume && - playbackSpeed == other.playbackSpeed && - errorDescription == other.errorDescription && - size == other.size && - rotationCorrection == other.rotationCorrection && - isInitialized == other.isInitialized && - isCompleted == other.isCompleted; + other is CachedVideoPlayerPlusValue && + runtimeType == other.runtimeType && + duration == other.duration && + position == other.position && + caption == other.caption && + captionOffset == other.captionOffset && + listEquals(buffered, other.buffered) && + isPlaying == other.isPlaying && + isLooping == other.isLooping && + isBuffering == other.isBuffering && + volume == other.volume && + playbackSpeed == other.playbackSpeed && + errorDescription == other.errorDescription && + size == other.size && + rotationCorrection == other.rotationCorrection && + isInitialized == other.isInitialized && + isCompleted == other.isCompleted; @override int get hashCode => Object.hash( - duration, - position, - caption, - captionOffset, - buffered, - isPlaying, - isLooping, - isBuffering, - volume, - playbackSpeed, - errorDescription, - size, - rotationCorrection, - isInitialized, - isCompleted, - ); + duration, + position, + caption, + captionOffset, + buffered, + isPlaying, + isLooping, + isBuffering, + volume, + playbackSpeed, + errorDescription, + size, + rotationCorrection, + isInitialized, + isCompleted, + ); } /// Controls a platform video player, and provides updates when the state is @@ -275,15 +275,16 @@ class CachedVideoPlayerPlusController /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. CachedVideoPlayerPlusController.asset( - this.dataSource, { - this.package, - Future? closedCaptionFile, - this.videoPlayerOptions, - }) : _closedCaptionFileFuture = closedCaptionFile, + this.dataSource, { + this.package, + Future? closedCaptionFile, + this.videoPlayerOptions, + }) : _closedCaptionFileFuture = closedCaptionFile, dataSourceType = DataSourceType.asset, formatHint = null, httpHeaders = const {}, invalidateCacheIfOlderThan = const Duration(days: 30), + skipCache = false, super(const CachedVideoPlayerPlusValue(duration: Duration.zero)); /// Constructs a [CachedVideoPlayerPlusController] playing a network video. @@ -297,13 +298,14 @@ class CachedVideoPlayerPlusController /// for the request to the [dataSource]. @Deprecated('Use CachedVideoPlayerPlusController.networkUrl instead') CachedVideoPlayerPlusController.network( - this.dataSource, { - this.formatHint, - Future? closedCaptionFile, - this.videoPlayerOptions, - this.httpHeaders = const {}, - this.invalidateCacheIfOlderThan = const Duration(days: 30), - }) : _closedCaptionFileFuture = closedCaptionFile, + this.dataSource, { + this.formatHint, + Future? closedCaptionFile, + this.videoPlayerOptions, + this.httpHeaders = const {}, + this.invalidateCacheIfOlderThan = const Duration(days: 30), + this.skipCache = false, + }) : _closedCaptionFileFuture = closedCaptionFile, dataSourceType = DataSourceType.network, package = null, super(const CachedVideoPlayerPlusValue(duration: Duration.zero)); @@ -318,13 +320,14 @@ class CachedVideoPlayerPlusController /// [httpHeaders] option allows to specify HTTP headers /// for the request to the [dataSource]. CachedVideoPlayerPlusController.networkUrl( - Uri url, { - this.formatHint, - Future? closedCaptionFile, - this.videoPlayerOptions, - this.httpHeaders = const {}, - this.invalidateCacheIfOlderThan = const Duration(days: 30), - }) : _closedCaptionFileFuture = closedCaptionFile, + Uri url, { + this.formatHint, + Future? closedCaptionFile, + this.videoPlayerOptions, + this.httpHeaders = const {}, + this.invalidateCacheIfOlderThan = const Duration(days: 30), + this.skipCache = false, + }) : _closedCaptionFileFuture = closedCaptionFile, dataSource = url.toString(), dataSourceType = DataSourceType.network, package = null, @@ -335,16 +338,17 @@ class CachedVideoPlayerPlusController /// This will load the file from a file:// URI constructed from [file]'s path. /// [httpHeaders] option allows to specify HTTP headers, mainly used for hls files like (m3u8). CachedVideoPlayerPlusController.file( - File file, { - Future? closedCaptionFile, - this.videoPlayerOptions, - this.httpHeaders = const {}, - }) : _closedCaptionFileFuture = closedCaptionFile, + File file, { + Future? closedCaptionFile, + this.videoPlayerOptions, + this.httpHeaders = const {}, + }) : _closedCaptionFileFuture = closedCaptionFile, dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, formatHint = null, invalidateCacheIfOlderThan = const Duration(days: 30), + skipCache = false, super(const CachedVideoPlayerPlusValue(duration: Duration.zero)); /// Constructs a [CachedVideoPlayerPlusController] playing a video from a contentUri. @@ -352,13 +356,13 @@ class CachedVideoPlayerPlusController /// This will load the video from the input content-URI. /// This is supported on Android only. CachedVideoPlayerPlusController.contentUri( - Uri contentUri, { - Future? closedCaptionFile, - this.videoPlayerOptions, - }) : assert( - defaultTargetPlatform == TargetPlatform.android, - 'CachedVideoPlayerPlusController.contentUri is only supported on Android.', - ), + Uri contentUri, { + Future? closedCaptionFile, + this.videoPlayerOptions, + }) : assert( + defaultTargetPlatform == TargetPlatform.android, + 'CachedVideoPlayerPlusController.contentUri is only supported on Android.', + ), _closedCaptionFileFuture = closedCaptionFile, dataSource = contentUri.toString(), dataSourceType = DataSourceType.contentUri, @@ -366,6 +370,7 @@ class CachedVideoPlayerPlusController formatHint = null, httpHeaders = const {}, invalidateCacheIfOlderThan = const Duration(days: 30), + skipCache = false, super(const CachedVideoPlayerPlusValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on @@ -395,6 +400,9 @@ class CachedVideoPlayerPlusController /// older than the provided [Duration] and re-fetches data. final Duration invalidateCacheIfOlderThan; + /// To provide a way to skip the cache + final bool skipCache; + Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; Timer? _timer; @@ -402,6 +410,8 @@ class CachedVideoPlayerPlusController Completer? _creatingCompleter; StreamSubscription? _eventSubscription; _VideoAppLifeCycleObserver? _lifeCycleObserver; + final CacheManager _cacheManager = DefaultCacheManager(); + final GetStorage _storage = GetStorage(); /// The id of a texture that hasn't been initialized. @visibleForTesting @@ -415,6 +425,7 @@ class CachedVideoPlayerPlusController /// Return true if caching is supported based on the platform. bool get _isCachingSupported { + if (skipCache) return false; return !kIsWeb && [ TargetPlatform.android, @@ -423,24 +434,44 @@ class CachedVideoPlayerPlusController ].contains(defaultTargetPlatform); } + /// This will return if video player has disposed or not + bool get isDisposed => _isDisposed; + + /// This will remove cached file from cache, if not key then it will remove current data source + Future removeFileFromCache({String? key}) async { + FileInfo? cachedFile = + await _cacheManager.getFileFromCache(key ?? dataSource); + if (cachedFile != null) { + await _cacheManager.removeFile(key ?? dataSource); + await _storage + .remove('cached_video_player_plus_video_expiration_of_${Uri.parse( + (key ?? dataSource), + )}'); + } + } + + /// This will clear all cache + Future clearCache() async { + await _cacheManager.emptyCache(); + await _storage.erase(); + } + /// Attempts to open the given [dataSource] and load metadata about the video. Future initialize() async { await GetStorage.init( 'cached_video_player_plus', ); - final storage = GetStorage(); late String realDataSource; bool isCacheAvailable = false; if (dataSourceType == DataSourceType.network && _isCachingSupported) { - final cacheManager = DefaultCacheManager(); - FileInfo? cachedFile = await cacheManager.getFileFromCache(dataSource); + FileInfo? cachedFile = await _cacheManager.getFileFromCache(dataSource); debugPrint('Cached video of [$dataSource] is: ${cachedFile?.file.path}'); if (cachedFile != null) { - final cachedElapsedMillis = storage.read( + final cachedElapsedMillis = _storage.read( 'cached_video_player_plus_video_expiration_of_${Uri.parse(dataSource)}', ); @@ -453,26 +484,26 @@ class CachedVideoPlayerPlusController debugPrint( 'Cache for [$dataSource] valid till: ' - '${cachedDate.add(invalidateCacheIfOlderThan)}', + '${cachedDate.add(invalidateCacheIfOlderThan)}', ); if (difference > invalidateCacheIfOlderThan) { debugPrint('Cache of [$dataSource] expired. Removing...'); - await cacheManager.removeFile(dataSource); + await _cacheManager.removeFile(dataSource); cachedFile = null; } } else { debugPrint('Cache of [$dataSource] expired. Removing...'); - await cacheManager.removeFile(dataSource); + await _cacheManager.removeFile(dataSource); cachedFile = null; } } if (cachedFile == null) { - cacheManager + _cacheManager .downloadFile(dataSource, authHeaders: httpHeaders) .then((value) { - storage.write( + _storage.write( 'cached_video_player_plus_video_expiration_of_${Uri.parse( dataSource, )}', @@ -565,10 +596,10 @@ class CachedVideoPlayerPlusController _applyPlayPause(); break; case VideoEventType.completed: - // In this case we need to stop _timer, set isPlaying=false, and - // position=value.duration. Instead of setting the values directly, - // we use pause() and seekTo() to ensure the platform stops playing - // and seeks to the last frame of the video. + // In this case we need to stop _timer, set isPlaying=false, and + // position=value.duration. Instead of setting the values directly, + // we use pause() and seekTo() to ensure the platform stops playing + // and seeks to the last frame of the video. pause().then((void pauseResult) => seekTo(value.duration)); value = value.copyWith(isCompleted: true); break; @@ -681,7 +712,7 @@ class CachedVideoPlayerPlusController _timer?.cancel(); _timer = Timer.periodic( const Duration(milliseconds: 500), - (Timer timer) async { + (Timer timer) async { if (_isDisposed) { return; } @@ -845,15 +876,15 @@ class CachedVideoPlayerPlusController /// /// If [closedCaptionFile] is null, closed captions will be removed. Future setClosedCaptionFile( - Future? closedCaptionFile, - ) async { + Future? closedCaptionFile, + ) async { await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; } Future _updateClosedCaptionWithFuture( - Future? closedCaptionFile, - ) async { + Future? closedCaptionFile, + ) async { _closedCaptionFile = await closedCaptionFile; value = value.copyWith(caption: _getCaptionAt(value.position)); } @@ -875,6 +906,15 @@ class CachedVideoPlayerPlusController } } + @override + void addListener(VoidCallback listener) { + // Prevent CachedVideoPlayerPlus from causing an exception to be thrown when attempting to + // add a listener after the controller has already been disposed. + if (!_isDisposed) { + super.addListener(listener); + } + } + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; } @@ -962,9 +1002,9 @@ class _CachedVideoPlayerPlusState extends State { return _textureId == CachedVideoPlayerPlusController.kUninitializedTextureId ? Container() : _CachedVideoPlayerPlusWithRotation( - rotation: widget.controller.value.rotationCorrection, - child: _videoPlayerPlatform.buildView(_textureId), - ); + rotation: widget.controller.value.rotationCorrection, + child: _videoPlayerPlatform.buildView(_textureId), + ); } } @@ -980,9 +1020,9 @@ class _CachedVideoPlayerPlusWithRotation extends StatelessWidget { Widget build(BuildContext context) => rotation == 0 ? child : Transform.rotate( - angle: rotation * math.pi / 180, - child: child, - ); + angle: rotation * math.pi / 180, + child: child, + ); } /// Used to configure the [VideoProgressIndicator] widget's colors for how it @@ -1111,12 +1151,12 @@ class VideoProgressIndicator extends StatefulWidget { /// provided. [allowScrubbing] defaults to false, and [padding] will default /// to `top: 5.0`. const VideoProgressIndicator( - this.controller, { - super.key, - this.colors = const VideoProgressColors(), - required this.allowScrubbing, - this.padding = const EdgeInsets.only(top: 5.0), - }); + this.controller, { + super.key, + this.colors = const VideoProgressColors(), + required this.allowScrubbing, + this.padding = const EdgeInsets.only(top: 5.0), + }); /// The [CachedVideoPlayerPlusController] that actually associates a video with this /// widget. @@ -1267,9 +1307,9 @@ class ClosedCaption extends StatelessWidget { final TextStyle effectiveTextStyle = textStyle ?? DefaultTextStyle.of(context).style.copyWith( - fontSize: 36.0, - color: Colors.white, - ); + fontSize: 36.0, + color: Colors.white, + ); return Align( alignment: Alignment.bottomCenter,