You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 13:54:04