Flutter列表如何预缓存后续4-5条视频前4-6秒,实现类似Instagram Reels效果
Flutter 全屏视频流预缓存(类Instagram Reels)实现方案
针对你需要在PageView.builder列表中预缓存前后5条视频前5秒左右内容的需求,可按以下方案实现:
1. 依赖选型
放弃仅支持全量缓存的cached_video_player,改用原生支持部分预缓存、队列预加载的播放器库,无需自行实现复杂的下载分片逻辑。
2. 核心实现逻辑
2.1 预缓存触发逻辑
监听PageController的滚动事件,页面索引变化时自动触发前后指定范围视频的预缓存任务:
final PageController _pageController = PageController(); final int _preloadRange = 5; // 前后各预缓存5条视频 final int _preloadSeconds = 5; // 单条预缓存前5秒内容 @override void initState() { super.initState(); _pageController.addListener(_handlePageChange); } void _handlePageChange() { final currentIndex = _pageController.page?.round() ?? 0; // 计算合法的预缓存索引范围 final startIndex = max(0, currentIndex - _preloadRange); final endIndex = min(videoDataSource.length - 1, currentIndex + _preloadRange); for (int i = startIndex; i <= endIndex; i++) { if (i == currentIndex) continue; // 跳过当前播放中视频避免重复处理 _triggerPreload(videoDataSource[i].videoUrl, _preloadSeconds); } }
2.2 部分片段预缓存实现
通过播放器自带的缓存管理能力,指定单条视频的预缓存大小/时长,同时判断是否已缓存避免重复请求:
Future<void> _triggerPreload(String videoUrl, int duration) async { if (await videoCacheManager.isFileInCache(videoUrl)) return; // 预缓存指定时长的视频片段,对应大小可根据码率换算 await videoCacheManager.preCache( videoUrl, maxVideoDuration: Duration(seconds: duration), ); }
2.3 VideoWidget改造
优先读取本地缓存内容,缓存不足时自动拉取剩余部分,原有业务逻辑保持兼容:
typedef OnVideoCompleted = void Function(); class VideoWidget extends StatefulWidget { final bool isDuetVideo; final String url; final OnVideoCompleted? onVideoCompleted; final HomeWidgetBloc? homeWidgetBloc; const VideoWidget({ Key? key, this.onVideoCompleted, required this.url, required this.homeWidgetBloc, required this.isDuetVideo, }) : super(key: key); @override _VideoWidgetState createState() => _VideoWidgetState(); } class _VideoWidgetState extends State<VideoWidget> { late VideoPlayerController videoPlayerController; final _controllerStateStream = BehaviorSubject<int>.seeded(0); VoidCallback? _listener; StreamSubscription? _playPauseSubscription; StreamSubscription? _seekSubscription; @override void initState() { super.initState(); // 初始化播放器时优先加载本地缓存,无缓存则自动走网络请求 videoPlayerController = VideoPlayerController.network( widget.url, videoPlayerOptions: VideoPlayerOptions( allowBackgroundPlayback: false, ), ); videoPlayerController.initialize().then((_) { _controllerStateStream.add(1); _observeForPlayPause(); _observerForSeekDuration(); _listener = () { if (videoPlayerController.value.duration.inSeconds > 0 && videoPlayerController.value.position.inMilliseconds == videoPlayerController.value.duration.inMilliseconds) { widget.onVideoCompleted?.call(); } }; videoPlayerController.addListener(_listener!); }); } @override void dispose() { super.dispose(); _controllerStateStream.close(); _playPauseSubscription?.cancel(); _seekSubscription?.cancel(); try { if (_listener != null) { videoPlayerController.removeListener(_listener!); } videoPlayerController.dispose(); } catch (e) { print(e.toString()); } } @override Widget build(BuildContext context) { return StreamBuilder<int>( stream: _controllerStateStream, builder: (context, snapshot) { final isReady = (snapshot.data ?? 0) == 1; if (!isReady) { return _progressWidget(); } return Stack(children: <Widget>[ Container( child: Center( child: (widget.isDuetVideo || videoPlayerController.value.size.width > videoPlayerController.value.size.height) ? AspectRatio( child: VideoPlayer( videoPlayerController, ), aspectRatio: videoPlayerController.value.aspectRatio, ) : VideoPlayer( videoPlayerController, ), widthFactor: double.maxFinite, heightFactor: double.maxFinite, ), ), Visibility( visible: !widget.isDuetVideo, child: VideoPlayerCustomControlsWidget( controller: videoPlayerController, ), ), ]); }, ); } Center _progressWidget() { return Center( child: CircularProgressIndicator( color: AppStyles.primary500Color, ), ); } void _observeForPlayPause() { _playPauseSubscription = widget.homeWidgetBloc?.videoPlayPauseStream.listen((value) { if (value == PLAY) videoPlayerController.play(); else videoPlayerController.pause(); }); } void _observerForSeekDuration() { _seekSubscription = widget.homeWidgetBloc?.duetVideoSeekDurationZeroStream.listen((value) { if (value == true) videoPlayerController.seekTo(Duration.zero); widget.homeWidgetBloc?.duetVideoSeekDurationZeroStream.add(false); }); } }
3. 可选优化项
- 网络适配:仅在WiFi环境下开启预缓存,移动网络场景下仅缓存当前播放视频,降低用户流量消耗
- 缓存管理:设置缓存有效期、最大缓存容量,定期清理7天以上未访问的缓存文件,避免占用过多存储空间
- 队列优先级:下一条待播放视频的预缓存任务优先级设为最高,其余更远的内容按顺序低优先级加载,避免同时发起过多请求造成卡顿
内容的提问来源于stack exchange,提问作者Pratik Butani
相关产品推荐
相关产品推荐

