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
相关产品推荐
相关产品推荐

