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

Flutter中PreloadPageView加载大体积API视频优化方案咨询

Flutter大体积MP4视频列表加载优化方案

问题根源

你的代码在initState中一次性初始化所有视频的VideoPlayerController,对于单文件100-200MB的MP4来说,会同时发起大量网络请求、占用极高内存,直接导致页面加载缓慢甚至卡顿。结合PreloadPageView的特性,我们可以从按需加载、控制预加载范围、优化控制器生命周期三个核心方向优化。

具体优化方法

1. 限制预加载数量

利用PreloadPageView的preloadPagesCount参数,只预加载当前页面前后1-2个视频(根据网络环境调整),避免无限制预加载。

2. 按需初始化/销毁控制器

仅保留当前页面、预加载页面的控制器,超出范围的控制器立即销毁释放资源。放弃一次性初始化所有控制器的逻辑,改为在页面即将显示时初始化,离开预加载范围时销毁。

3. 优化视频加载策略

给VideoPlayerController.network添加HTTP缓存头,或启用部分加载(Range请求),让播放器优先加载视频开头部分,快速进入可播放状态;同时可以前置获取视频缩略图,在加载完成前展示,提升用户体验。

4. 调整初始化时机

将控制器初始化逻辑从initState移到页面可见时触发,避免页面未渲染就发起大量请求。

修改后的完整代码

@RoutePage()
class _VideoScrollViewState extends State<VideoScrollView> {
  late final List<String> videoUrls;
  final Map<int, VideoPlayerController> _controllers = {}; // 改为Map存储,便于管理
  final Map<int, ValueNotifier<DurationState>> _durationNotifiers = {};
  late final PreloadPageController _pageController;
  int _currentPage = 0;
  Timer? _overlayTimer;
  bool _isOverlayVisible = true;
  static const int _preloadCount = 1; // 预加载前后1个页面

  @override
  void initState() {
    super.initState();
    _pageController = PreloadPageController();
    videoUrls = widget.state.episodesPageResponseModel.results
            ?.map((result) => result.videoFile ?? '')
            .where((url) => url.isNotEmpty)
            .toList() ??
        [];
    // 初始化当前页和预加载页的控制器
    _initializeControllersForRange(_currentPage - _preloadCount, _currentPage + _preloadCount);
  }

  @override
  void dispose() {
    // 销毁所有剩余控制器
    for (final controller in _controllers.values) {
      controller.dispose();
    }
    _durationNotifiers.values.forEach((notifier) => notifier.dispose());
    _pageController.dispose();
    _overlayTimer?.cancel();
    super.dispose();
  }

  // 初始化指定范围的控制器
  void _initializeControllersForRange(int start, int end) {
    start = start.clamp(0, videoUrls.length - 1);
    end = end.clamp(0, videoUrls.length - 1);

    for (var i = start; i <= end; i++) {
      if (!_controllers.containsKey(i)) {
        _createVideoController(i);
      }
    }

    // 播放当前页视频
    _controllers[_currentPage]?.play();
    _startOverlayTimer();
  }

  // 创建单个视频控制器
  void _createVideoController(int index) {
    final url = videoUrls[index];
    final controller = VideoPlayerController.network(
      url,
      httpHeaders: {
        // 添加缓存头,提升重复加载速度
        'Cache-Control': 'max-age=86400',
        // 启用部分加载,优先获取前1MB数据
        'Range': 'bytes=0-1048576',
      },
    );

    controller.initialize().timeout(
      const Duration(seconds: 30), // 缩短超时时间,避免长时间等待
      onTimeout: () {
        debugPrint("视频加载超时: $url");
        _showSnackBar('视频加载超时,请检查网络');
      },
    ).then((_) {
      setState(() {});
      // 初始化完成后取消Range限制,加载完整视频
      if (controller.value.isInitialized) {
        controller.setNetworkDataSource(url);
      }
    }).catchError((error) {
      debugPrint("视频加载错误: $error");
      _showSnackBar('视频加载失败');
    });

    _controllers[index] = controller;
    _durationNotifiers[index] = ValueNotifier(
      const DurationState(
        progress: Duration.zero,
        buffered: Duration.zero,
        total: Duration.zero,
      ),
    );

    controller.addListener(() {
      if (controller.value.isInitialized) {
        _durationNotifiers[index]!.value = DurationState(
          progress: controller.value.position,
          buffered: controller.value.buffered.isNotEmpty
              ? controller.value.buffered.last.end
              : Duration.zero,
          total: controller.value.duration,
        );

        if (controller.value.position >= controller.value.duration) {
          _onVideoEnd(index);
        }
      }
    });
  }

  // 销毁超出预加载范围的控制器
  void _disposeControllersOutsideRange(int currentIndex) {
    final start = currentIndex - _preloadCount;
    final end = currentIndex + _preloadCount;

    final keysToRemove = _controllers.keys.where((index) {
      return index < start || index > end;
    }).toList();

    for (final index in keysToRemove) {
      _controllers[index]?.dispose();
      _controllers.remove(index);
      _durationNotifiers[index]?.dispose();
      _durationNotifiers.remove(index);
    }
  }

  void _startOverlayTimer() {
    _overlayTimer?.cancel();
    _overlayTimer = Timer(const Duration(seconds: 3), () {
      setState(() {
        _isOverlayVisible = false;
      });
    });
  }

  void _toggleOverlayVisibility() {
    setState(() {
      _isOverlayVisible = !_isOverlayVisible;
    });
    if (_isOverlayVisible && _controllers[_currentPage]?.value.isPlaying == true) {
      _startOverlayTimer();
    } else {
      _overlayTimer?.cancel();
    }
  }

  void _onPageChanged(int index) {
    // 暂停当前页视频
    _controllers[_currentPage]?.pause();
    // 销毁超出预加载范围的控制器
    _disposeControllersOutsideRange(index);
    // 初始化新的预加载范围控制器
    _initializeControllersForRange(index - _preloadCount, index + _preloadCount);

    setState(() {
      _currentPage = index;
    });

    // 播放新页面视频
    _controllers[_currentPage]?.play();
    _startOverlayTimer();
  }

  void _onVideoEnd(int currentIndex) {
    if (currentIndex + 1 < videoUrls.length) {
      _controllers[currentIndex]?.pause();
      _pageController.animateToPage(
        currentIndex + 1,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    } else {
      debugPrint("所有视频已播放完毕。");
    }
  }

  void _togglePlayPause() {
    final controller = _controllers[_currentPage];
    if (controller == null) return;

    setState(() {
      if (controller.value.isPlaying) {
        controller.pause();
        _isOverlayVisible = true;
        _overlayTimer?.cancel();
      } else {
        controller.play();
        _isOverlayVisible = false;
        _startOverlayTimer();
      }
    });
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    if (videoUrls.isEmpty) {
      return const Scaffold(
        body: Center(
          child: Text(
            '未找到视频',
            style: TextStyle(color: Colors.white),
          ),
        ),
      );
    }

    return Scaffold(
      backgroundColor: Colors.black,
      body: PreloadPageView.builder(
        scrollDirection: Axis.vertical,
        controller: _pageController,
        onPageChanged: _onPageChanged,
        itemCount: videoUrls.length,
        preloadPagesCount: _preloadCount, // 设置预加载数量
        itemBuilder: (context, index) {
          final controller = _controllers[index];
          final durationNotifier = _durationNotifiers[index];
          final videoData = widget.state.episodesPageResponseModel.results?[index];

          return GestureDetector(
            onTap: _toggleOverlayVisibility,
            child: Stack(
              children: [
                // 优先展示缩略图(可从videoData中获取前置页已拿到的封面)
                if (videoData?.coverUrl != null)
                  Image.network(
                    videoData!.coverUrl!,
                    fit: BoxFit.cover,
                    width: double.infinity,
                    height: double.infinity,
                  ),
                // 视频加载完成后覆盖缩略图
                if (controller?.value.isInitialized == true)
                  SizedBox(
                    width: double.infinity,
                    height: double.infinity,
                    child: AspectRatio(
                      aspectRatio: controller!.value.aspectRatio,
                      child: VideoPlayer(controller),
                    ),
                  )
                else if (controller != null) // 正在加载中
                  const Center(
                    child: CircularProgressIndicator(color: Colors.white),
                  ),
                if (_isOverlayVisible)
                  Center(
                    child: GestureDetector(
                      onTap: _togglePlayPause,
                      child: Icon(
                        controller?.value.isPlaying == true
                            ? Icons.pause
                            : Icons.play_arrow,
                        size: 64,
                        color: Colors.white,
                      ),
                    ),
                  ),
                if (durationNotifier != null)
                  Positioned(
                    bottom: 20,
                    left: 20,
                    right: 20,
                    child: ValueListenableBuilder<DurationState>(
                      valueListenable: durationNotifier,
                      builder: (context, value, child) {
                        final progressValue = value.total.inMilliseconds > 0
                            ? value.progress.inMilliseconds / value.total.inMilliseconds
                            : 0.0;

                        return Column(
                          children: [
                            LinearProgressIndicator(
                              value: progressValue,
                              color: Colors.red,
                              backgroundColor: Colors.grey.withOpacity(0.5),
                            ),
                            const SizedBox(height: 10),
                            Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                Text(
                                  _formatDuration(value.progress),
                                  style: const TextStyle(color: Colors.white),
                                ),
                                Text(
                                  _formatDuration(value.total),
                                  style: const TextStyle(color: Colors.white),
                                ),
                              ],
                            ),
                          ],
                        );
                      },
                    ),
                  ),
              ],
            ),
          );
        },
      ),
    );
  }

  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes.remainder(60);
    final seconds = duration.inSeconds.remainder(60);
    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  }
}

额外优化建议

  • 视频转码优化:后端将大体积MP4转成HLS(m3u8)格式,实现分段加载,播放器可以边播边加载,大幅降低初始加载时间。
  • 内存监控:添加内存监听,在内存不足时主动销毁非必要控制器,避免OOM。
  • 错误重试:给加载失败的视频添加重试按钮,提升容错性。

内容的提问来源于stack exchange,提问作者vkc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:15:55