记录一下解决问题的过程,希望自己以后可以参考看看,解决更多的问题。
需求:flutter 缓存网络视频文件,可离线观看。
解决:
1,flutter APP视频播放组件调整;
2,找到视频播放组件,传入url解析的地方;
Dart
_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,添加基于这个新依赖的功能代码:
Dart
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, // 最大缓存 2GB
stalePeriod: Duration(days: 7), // 缓存保留时间
repo: JsonCacheInfoRepository(databaseName: key),
fileSystem: IOFileSystem(key),
fileService: HttpFileService(),
),
);
}
暴露出来新加的dart类

3.1.3,在video_widget组件中使用缓存工具缓存视频

3.2,在没有网的时候使用该缓存视频,改造步骤2中的播放方法:
Dart
_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"),
// ),
//所以根据条件判断用上边的任一个dataSource
dataSource,
autoplay: !background && widget.autoplay,
);
3.2.1,这个步骤中的插曲,就是使用本地文件一直报空,打印了_meeduPlayerController,和cacheFile都不为空,但是还是报空。
可能得问题有:
一,以为是异步写法awiat获得值,会产生后边的代码先于值计算出来,就运行了导致空
二,错误写法只是照搬了网络视频的写法,更换了一下type的参数,并没有多想,以为也是根据source来写;
解决问题一:
1看下await是怎么产生的。
1.1,拿本地的缓存文件就有异步

1.2,判断文件是否完成,是否可播也有await
2如何避免;如果避免不了await,如何等值完全算完,不为空了再进行下一步的调用。
判断内容长度,是否下载完成,获取sp,判断是否可播都需要异步,就是不能直接拿到值。
Dart
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()方法也必定是异步的
Dart
// 单独封装异步判断逻辑
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一样写回调了。)
Dart
// 封装异步获取 DataSource 的逻辑
dataSource = await _getDataSource(cachedFile, lastUrl);
_meeduPlayerController.setDataSource(
//所以根据条件判断用上边的任一个dataSource
dataSource,
autoplay: !background && widget.autoplay,
);
不用特意写then来完成这个异步操作,以下代码不推荐:
Dart
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的完整代码,仅供参考
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});
@override
State<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; // 是否处于后台
@override
void 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();
}
@override
void dispose() {
_playerEventSubs?.cancel();
_meeduPlayerController.dispose();
WidgetsBinding.instance.removeObserver(this);
AppRouteObserver().routeObserver.unsubscribe(this);
super.dispose();
}
@override
Future<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();
}
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void 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;
}
}
@override
Widget build(BuildContext context) {
setSource();
return AspectRatio(
aspectRatio: 16 / 9,
child: MeeduVideoPlayer(
key: UniqueKey(),
controller: _meeduPlayerController,
),
);
}
}