import 'dart:async'; import 'package:chewie/chewie.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; import 'package:wow_english/common/request/dao/video_dao.dart'; import 'package:wow_english/common/utils/video_progress_cache.dart'; import 'video_zone_item.dart'; class VideoZonePlayerPage extends StatefulWidget { const VideoZonePlayerPage({ super.key, required this.items, this.initialVideoId, }) : assert(items.length > 0, '视频播放列表不能为空'); final List items; final int? initialVideoId; @override State createState() => _VideoZonePlayerPageState(); } class _VideoZonePlayerPageState extends State with WidgetsBindingObserver { VideoPlayerController? _controller; ChewieController? _chewieController; Timer? _cacheTimer; late int _currentIndex; double _playbackSpeed = 1; bool _switchingVideo = false; bool _handlingCompletion = false; bool? _lastIsPlaying; int? _completedReportedVideoId; Object? _loadError; VideoZoneItem get _currentItem => widget.items[_currentIndex]; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); unawaited( SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky), ); final index = widget.initialVideoId == null ? 0 : widget.items.indexWhere((item) => item.id == widget.initialVideoId); _currentIndex = index < 0 ? 0 : index; _loadCurrentVideo(); } Future _loadCurrentVideo() async { final useCupertinoControls = defaultTargetPlatform == TargetPlatform.iOS; _cacheTimer?.cancel(); final oldController = _controller; final oldChewieController = _chewieController; if (oldController?.value.isInitialized == true) { _playbackSpeed = oldController!.value.playbackSpeed; } oldController?.removeListener(_onVideoChanged); _lastIsPlaying = null; final controller = VideoPlayerController.networkUrl(Uri.parse(_currentItem.videoUrl)); _controller = controller; _chewieController = null; _loadError = null; if (mounted) { setState(() {}); } oldChewieController?.dispose(); await oldController?.dispose(); try { await controller.initialize().timeout(const Duration(seconds: 20)); if (!mounted || controller != _controller) { await controller.dispose(); return; } await controller.setLooping(false); await controller.setPlaybackSpeed(_playbackSpeed); final savedProgress = await _resumeProgressFor(_currentItem.id); if (!mounted || controller != _controller) { await controller.dispose(); return; } if (savedProgress > 0 && savedProgress < controller.value.duration.inSeconds) { await controller.seekTo(Duration(seconds: savedProgress)); } _lastIsPlaying = controller.value.isPlaying; controller.addListener(_onVideoChanged); _chewieController = ChewieController( videoPlayerController: controller, autoPlay: true, looping: false, allowFullScreen: false, allowPlaybackSpeedChanging: true, draggableProgressBar: true, allowedScreenSleep: false, playbackSpeeds: const [0.5, 0.75, 1, 1.25, 1.5, 2], customControls: useCupertinoControls ? null : const _VideoMaterialControls(), optionsTranslation: OptionsTranslation( playbackSpeedButtonText: '播放速度', cancelButtonText: '取消', ), materialProgressColors: ChewieProgressColors( playedColor: const Color(0xFF00B6F1), handleColor: const Color(0xFF00B6F1), bufferedColor: Colors.white54, backgroundColor: Colors.white24, ), cupertinoProgressColors: ChewieProgressColors( playedColor: const Color(0xFF00B6F1), handleColor: const Color(0xFF00B6F1), bufferedColor: Colors.white54, backgroundColor: Colors.white24, ), placeholder: const ColoredBox(color: Colors.black), errorBuilder: (_, __) => const Center( child: Text( '视频播放失败', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ); _handlingCompletion = false; _cacheTimer = Timer.periodic( const Duration(seconds: 30), (_) { if (_controller?.value.isPlaying == true) { unawaited(_cacheCurrentProgress()); } }, ); setState(() {}); } catch (error) { if (mounted && controller == _controller) { setState(() => _loadError = error); } } } void _onVideoChanged() { final controller = _controller; if (!mounted || controller == null) return; final isPlaying = controller.value.isPlaying; if (_lastIsPlaying != null && _lastIsPlaying != isPlaying) { unawaited(_cacheCurrentProgress()); } _lastIsPlaying = isPlaying; if (controller.value.isCompleted && !_switchingVideo && !_handlingCompletion && _completedReportedVideoId != _currentItem.id) { _handlingCompletion = true; unawaited(_handleVideoCompleted()); } } Future _playNext({bool currentCompleted = false}) async { if (_switchingVideo || _currentIndex >= widget.items.length - 1) return; _switchingVideo = true; await _reportCurrentProgress(completed: currentCompleted); setState(() => _currentIndex++); await _loadCurrentVideo(); _switchingVideo = false; } Future _handleVideoCompleted() async { if (_currentIndex < widget.items.length - 1) { await _playNext(currentCompleted: true); } else { await _reportCurrentProgress(completed: true); } _handlingCompletion = false; } @override void dispose() { WidgetsBinding.instance.removeObserver(this); unawaited( SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky), ); _cacheTimer?.cancel(); _controller?.removeListener(_onVideoChanged); unawaited(_reportCurrentProgress()); _chewieController?.dispose(); _controller?.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { unawaited( SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky), ); } else if (state == AppLifecycleState.inactive || state == AppLifecycleState.paused || state == AppLifecycleState.detached) { unawaited(_cacheCurrentProgress()); } } Future _reportCurrentProgress({bool? completed}) async { final controller = _controller; if (controller == null || !controller.value.isInitialized) return; final currentItem = _currentItem; final durationSeconds = controller.value.duration.inSeconds; final progressSeconds = controller.value.position.inSeconds .clamp(0, durationSeconds > 0 ? durationSeconds : 0); final isCompleted = completed ?? controller.value.isCompleted; await _persistCachedProgress( videoId: currentItem.id, progressSeconds: progressSeconds, durationSeconds: durationSeconds, completed: isCompleted, ); if (isCompleted && _completedReportedVideoId == currentItem.id) return; try { await VideoDao.savePlayRecord( videoId: currentItem.id, progressSeconds: progressSeconds, durationSeconds: durationSeconds, completed: isCompleted, ); if (isCompleted) _completedReportedVideoId = currentItem.id; } catch (error) { debugPrint('保存视频播放记录失败: $error'); } } Future _resumeProgressFor(int videoId) async { try { final cached = await VideoProgressCache.get(videoId); if (cached != null) return cached.progressSeconds; } catch (error) { debugPrint('读取本地视频播放进度失败: $error'); } return 0; } Future _cacheCurrentProgress() async { final controller = _controller; if (controller == null || !controller.value.isInitialized) return; final currentItem = _currentItem; final durationSeconds = controller.value.duration.inSeconds; final progressSeconds = controller.value.position.inSeconds .clamp(0, durationSeconds > 0 ? durationSeconds : 0); await _persistCachedProgress( videoId: currentItem.id, progressSeconds: progressSeconds, durationSeconds: durationSeconds, completed: controller.value.isCompleted, ); } Future _persistCachedProgress({ required int videoId, required int progressSeconds, required int durationSeconds, required bool completed, }) async { try { if (completed) { await VideoProgressCache.remove(videoId); } else { await VideoProgressCache.save( videoId: videoId, progressSeconds: progressSeconds, durationSeconds: durationSeconds, ); } } catch (error) { debugPrint('保存本地视频播放进度失败: $error'); } } @override Widget build(BuildContext context) { final controller = _controller; final chewieController = _chewieController; return MediaQuery.removePadding( context: context, removeTop: true, child: Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ if (controller?.value.isInitialized == true && chewieController != null) Chewie(controller: chewieController) else if (_loadError != null) Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('视频加载失败', style: TextStyle(color: Colors.white, fontSize: 18)), const SizedBox(height: 12), FilledButton( onPressed: _loadCurrentVideo, child: const Text('重试')), ], ), ) else const Center(child: CircularProgressIndicator()), SafeArea( top: false, child: Align( alignment: Alignment.topLeft, child: Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), ), Expanded( child: Text( _currentItem.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white, fontSize: 20), ), ), ], ), ), ), ], ), ), ); } } class _VideoMaterialControls extends StatelessWidget { const _VideoMaterialControls(); @override Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( iconButtonTheme: IconButtonThemeData( style: IconButton.styleFrom( minimumSize: const Size.square(72), padding: const EdgeInsets.all(20), ), ), ), child: const MaterialControls(), ); } }