flutter缓存网络视频到本地,可离线观看
记录一下解决问题的过程,希望自己以后可以参考看看,解决更多的问题。
需求:flutter 缓存网络视频文件,可离线观看。
解决:
1,flutter APP视频播放组件调整;
2,找到视频播放组件,传入url解析的地方;
_meeduPlayerController.setDataSource(DataSource(//指定是网络类型的数据type: DataSourceType.network,//设置url参数source: widget.videoUrl != ""? widget.videoUrl: "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",httpHeaders: {"Range": "bytes=0-1023"},),autoplay: !background && widget.autoplay,);
3,那也就是无网路的时候播放本地已经缓存了的对应url的对应视频,先在没有缓存的时候,缓存该文件
3.1,添加缓存(保存视频)功能依赖
3.1.1,在视频播放依赖包中添加缓存依赖:flutter_cache_manager: ^3.4.1
3.1.2,添加基于这个新依赖的功能代码:
import 'package:flutter_cache_manager/flutter_cache_manager.dart';class CustomVideoCacheManager {static const key = 'customCacheKey';static final CacheManager instance = CacheManager(Config(key,maxNrOfCacheObjects: 50, // 最多缓存 50 个视频// maxTotalSize: 1024 * 1024 * 1024 * 2, // 最大缓存 2GBstalePeriod: Duration(days: 7), // 缓存保留时间repo: JsonCacheInfoRepository(databaseName: key),fileSystem: IOFileSystem(key),fileService: HttpFileService(),),);
}
暴露出来新加的dart类
3.1.3,在video_widget组件中使用缓存工具缓存视频
3.2,在没有网的时候使用该缓存视频,改造步骤2中的播放方法:
_meeduPlayerController.setDataSource(//1网络时候的DataSource// DataSource(// type: DataSourceType.network,// source: widget.videoUrl != ""// ? widget.videoUrl// : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",// httpHeaders: {"Range": "bytes=0-1023"},// ),//2本地文件//错误写法:// DataSource(// type: DataSourceType.file,// // file: cacheFile,// source:// "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4",// ),//正确写法// DataSource(// type: DataSourceType.file,// // file: cacheFile,// file: File(// "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4"),// ),//所以根据条件判断用上边的任一个dataSourcedataSource,autoplay: !background && widget.autoplay,);
3.2.1,这个步骤中的插曲,就是使用本地文件一直报空,打印了_meeduPlayerController,和cacheFile都不为空,但是还是报空。
可能得问题有:
一,以为是异步写法awiat获得值,会产生后边的代码先于值计算出来,就运行了导致空
二,错误写法只是照搬了网络视频的写法,更换了一下type的参数,并没有多想,以为也是根据source来写;
解决问题一:
1看下await是怎么产生的。
1.1,拿本地的缓存文件就有异步
1.2,判断文件是否完成,是否可播也有await
2如何避免;如果避免不了await,如何等值完全算完,不为空了再进行下一步的调用。
判断内容长度,是否下载完成,获取sp,判断是否可播都需要异步,就是不能直接拿到值。
Future<int?> getCachedContentLength(String url) async {final prefs = await SharedPreferences.getInstance();return prefs.getInt('video_content_length_$url');}Future<void> cacheContentLength(String url, int length) async {final prefs = await SharedPreferences.getInstance();prefs.setInt('video_content_length_$url', length);}Future<bool> isVideoFileComplete(String url) async {// 获取之前缓存的原始大小final expectedLength = await getCachedContentLength(url);if (expectedLength == null) return false;// 获取本地缓存文件FileInfo? fileInfo = await DefaultCacheManager().getFileFromCache(url);final file = fileInfo?.file;if (file == null || !file.existsSync()) return false;final localLength = file.lengthSync();bool isSame = (localLength == expectedLength);print("video_widget 是否下载完成:$isSame");return isSame;}Future<bool> isVideoPlayable(String filePath) async {final controller = VideoPlayerController.file(File(filePath));try {await controller.initialize();await controller.dispose();print("video_widget 可以 正常播放");return true;} catch (e) {print("video_widget 不能 正常播放");return false;}}
所以用到这几个方法的设置setDataSource()方法也必定是异步的
// 单独封装异步判断逻辑Future<DataSource> _getDataSource(File? cachedFile, String url) async {if (cachedFile != null) {final exists = cachedFile.existsSync();final playable = await isVideoPlayable(cachedFile.path);final complete = await isVideoFileComplete(cachedFile.path);print("video_widget: cachedFile != null: ${cachedFile != null}");print("video_widget: existsSync: $exists");print("video_widget: isVideoPlayable: $playable");print("video_widget: isVideoFileComplete: $complete");if (exists && playable && complete) {print("video_widget:即将使用缓存视频");return DataSource(type: DataSourceType.file,source: cachedFile.path,httpHeaders: {"Range": "bytes=0-1023"},);}}// 如果没有命中缓存或缓存不完整,则走网络加载File? cacheFile;try {cacheFile = await CustomVideoCacheManager.instance.getSingleFile(url);} catch (e) {print("video_widget:网络文件获取失败: $e");}final networkSource = DataSource(type: DataSourceType.network,source: widget.videoUrl.isNotEmpty? widget.videoUrl: "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",httpHeaders: {"Range": "bytes=0-1023"},);return cacheFile != null? DataSource(type: DataSourceType.file,file: cacheFile,): networkSource;}
使用这种方法,就不用在_meeduPlayerController设置参数的时候使用一个异步返回的dataSource了,代码如下,这样就可以使用一个看似同步的代码,完成了一个异步的操作。(并不会因为看起来像是同步的写法,就会发生dataSource的值还没回来的时候就执行了后边的代码,导致null产生。这个就是典型的支持异步操作的代码,不然就得像Java一样写回调了。)
// 封装异步获取 DataSource 的逻辑
dataSource = await _getDataSource(cachedFile, lastUrl);_meeduPlayerController.setDataSource(//所以根据条件判断用上边的任一个dataSourcedataSource,autoplay: !background && widget.autoplay,);
不用特意写then来完成这个异步操作,以下代码不推荐:
await _getDataSource(cachedFile, lastUrl).then((dataSource) {if (dataSource != null) {_meeduPlayerController.setDataSource(dataSource,autoplay: !background && widget.autoplay,);} else {print("video_widget:dataSource为空");}});
解决问题二:
1更换本地缓存文件的地址来写死dataSource参数,还是不行
2想到看下这个依赖包的说明文件是否支持本地文件播放,flutter_meedu_videoplayer example | Flutter package 看到是支持的,
3看依赖包的例子是怎么写的,没有具体写怎么播放本地视频
4看依赖包的源码是怎么使用
4.1,dataSource源码怎么使用的(只是设置参数,没有使用这个参数的逻辑)
4.2,那就找使用这个参数的源码:_meeduPlayerController,有怎么设置本地文件的DataSource方法,最终调整成正确的参数设置方式。
最后,以下是video_widget.dart的完整代码,仅供参考
import 'dart:async';
import 'dart:io';import 'package:audio_session/audio_session.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_meedu_videoplayer/meedu_player.dart';
import 'package:game_lib/common/common_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wakelock_plus/wakelock_plus.dart';class VideoWidget extends StatefulWidget {String videoUrl;Function? onVideoEnd;bool autoplay;Function(MeeduPlayerController)? onInit;Function(PlayerStatus)? onVideoStatusChanged;Function(Duration)? onVideoPositionChanged;bool closeFullscreenOnEnd;BoxFit fit;bool fullscreen;Function(bool)? onBackground;VideoWidget({super.key,required this.videoUrl,this.onVideoEnd,this.onInit,this.autoplay = true,this.fullscreen = true,this.fit = BoxFit.contain,this.onVideoStatusChanged,this.closeFullscreenOnEnd = true,this.onVideoPositionChanged,this.onBackground});@overrideState<VideoWidget> createState() => _VideoWidgetState();
}class _VideoWidgetState extends State<VideoWidget>with WidgetsBindingObserver, RouteAware {late final _meeduPlayerController = MeeduPlayerController(controlsStyle: ControlsStyle.primary,screenManager: const ScreenManager(orientations: [DeviceOrientation.landscapeLeft,]),enabledButtons: EnabledButtons(videoFit: false, muteAndSound: false, fullscreen: widget.fullscreen),fits: [BoxFit.contain],initialFit: widget.fit);StreamSubscription? _playerEventSubs;int lastPosition = 0;String lastUrl = "";bool background = false; // 是否处于后台@overridevoid initState() {WidgetsBinding.instance.addObserver(this);WidgetsBinding.instance.addPostFrameCallback((_) {Future.delayed(const Duration(milliseconds: 500), () {_init();});});widget.onInit?.call(_meeduPlayerController);_meeduPlayerController.onPositionChanged.listen((event) {if (event.inSeconds != lastPosition) {lastPosition = event.inMilliseconds;widget.onVideoPositionChanged?.call(event);//print("onPositionChanged: $event ${event.inSeconds}");}});_playerEventSubs = _meeduPlayerController.onPlayerStatusChanged.listen((PlayerStatus status) async {widget.onVideoStatusChanged?.call(status);print("onPlayerStatusChanged: $status");if (status == PlayerStatus.playing) {WakelockPlus.enable();Future.delayed(const Duration(milliseconds: 100), () {if (widget.fit == BoxFit.contain) {_meeduPlayerController.toggleVideoFit();}});} else {WakelockPlus.disable();final session = await AudioSession.instance;if (await session.setActive(false)) {print("AudioSession setActive abandon");}}if (status == PlayerStatus.completed) {if (widget.closeFullscreenOnEnd &&_meeduPlayerController.fullscreen.value &&Navigator.canPop(context)) {
// Navigator.pop(context);
// 注释上面代码,播放完后不退出全屏}if (widget.onVideoEnd != null) {widget.onVideoEnd!();}}},);Timer? timer;_meeduPlayerController.onDataStatusChanged.listen((DataStatus status) {if (status == DataStatus.error) {setState(() {_meeduPlayerController.errorText = "";});print("============= video widget onDataStatusChanged: $status videoUrl: ${widget.videoUrl}");if (widget.videoUrl.isNotEmpty) {timer?.cancel();timer = Timer(const Duration(milliseconds: 1), () {setSource();});}}});super.initState();}@overridevoid dispose() {_playerEventSubs?.cancel();_meeduPlayerController.dispose();WidgetsBinding.instance.removeObserver(this);AppRouteObserver().routeObserver.unsubscribe(this);super.dispose();}@overrideFuture<void> didChangeAppLifecycleState(AppLifecycleState state) async {print("video widget didChangeAppLifecycleState: $state");final session = await AudioSession.instance;if (state == AppLifecycleState.resumed) {background = false;widget.onBackground?.call(background);_meeduPlayerController.play();} else if (state == AppLifecycleState.paused) {background = true;widget.onBackground?.call(background);_meeduPlayerController.pause();}}@overridevoid didChangeDependencies() {// TODO: implement didChangeDependenciessuper.didChangeDependencies();AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);}@overridevoid didPushNext() {Future.delayed(const Duration(milliseconds: 500), () {if (!_meeduPlayerController.fullscreen.value) {_meeduPlayerController.pause();}});}_init() {print("autoplay: ${widget.autoplay}");setSource();}Future<void> setSource() async {if (widget.videoUrl == lastUrl) {return;}lastUrl = widget.videoUrl;File? cachedFile;DataSource? dataSource;try {print("video_widget:设置视频资源,lastUrl:$lastUrl");FileInfo? fileInfo =await CustomVideoCacheManager.instance.getFileFromCache(lastUrl);cachedFile = fileInfo?.file;print("video_widget:缓存文件地址${cachedFile?.path}");} catch (e) {print("video_widget:未找到缓存视频");}// 封装异步获取 DataSource 的逻辑dataSource = await _getDataSource(cachedFile, lastUrl);// await _getDataSource(cachedFile, lastUrl).then((dataSource) {// print(// "=====video_widget:_meeduPlayerController是否为空:${_meeduPlayerController == null}");// print("=====video_widget:dataSource是否为空:${dataSource == null}");// if (dataSource != null) {// _meeduPlayerController.setDataSource(// // DataSource(// // type: DataSourceType.network,// // source: widget.videoUrl != ""// // ? widget.videoUrl// // : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",// // httpHeaders: {"Range": "bytes=0-1023"},// // ),//// dataSource,//// autoplay: !background && widget.autoplay,// );// } else {// print("video_widget:dataSource为空");// }// });//清除缓存//await CustomVideoCacheManager.instance.emptyCache();_meeduPlayerController.setDataSource(// DataSource(// type: DataSourceType.network,// source: widget.videoUrl != ""// ? widget.videoUrl// : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",// httpHeaders: {"Range": "bytes=0-1023"},// ),// DataSource(// type: DataSourceType.file,// // file: cacheFile,// file: File(// "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4"),// ),dataSource,autoplay: !background && widget.autoplay,);}// 单独封装异步判断逻辑Future<DataSource> _getDataSource(File? cachedFile, String url) async {if (cachedFile != null) {final exists = cachedFile.existsSync();final playable = await isVideoPlayable(cachedFile.path);final complete = await isVideoFileComplete(cachedFile.path);print("video_widget: cachedFile != null: ${cachedFile != null}");print("video_widget: existsSync: $exists");print("video_widget: isVideoPlayable: $playable");print("video_widget: isVideoFileComplete: $complete");if (exists && playable && complete) {print("video_widget:即将使用缓存视频");return DataSource(type: DataSourceType.file,source: cachedFile.path,httpHeaders: {"Range": "bytes=0-1023"},);}}// 如果没有命中缓存或缓存不完整,则走网络加载File? cacheFile;try {cacheFile = await CustomVideoCacheManager.instance.getSingleFile(url);} catch (e) {print("video_widget:网络文件获取失败: $e");}final networkSource = DataSource(type: DataSourceType.network,source: widget.videoUrl.isNotEmpty? widget.videoUrl: "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",httpHeaders: {"Range": "bytes=0-1023"},);return cacheFile != null? DataSource(type: DataSourceType.file,file: cacheFile,): networkSource;}Future<int?> getCachedContentLength(String url) async {final prefs = await SharedPreferences.getInstance();return prefs.getInt('video_content_length_$url');}Future<void> cacheContentLength(String url, int length) async {final prefs = await SharedPreferences.getInstance();prefs.setInt('video_content_length_$url', length);}Future<bool> isVideoFileComplete(String url) async {// 获取之前缓存的原始大小final expectedLength = await getCachedContentLength(url);if (expectedLength == null) return false;// 获取本地缓存文件FileInfo? fileInfo = await DefaultCacheManager().getFileFromCache(url);final file = fileInfo?.file;if (file == null || !file.existsSync()) return false;final localLength = file.lengthSync();bool isSame = (localLength == expectedLength);print("video_widget 是否下载完成:$isSame");return isSame;}Future<bool> isVideoPlayable(String filePath) async {final controller = VideoPlayerController.file(File(filePath));try {await controller.initialize();await controller.dispose();print("video_widget 可以 正常播放");return true;} catch (e) {print("video_widget 不能 正常播放");return false;}}@overrideWidget build(BuildContext context) {setSource();return AspectRatio(aspectRatio: 16 / 9,child: MeeduVideoPlayer(key: UniqueKey(),controller: _meeduPlayerController,),);}
}