import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:video_player/video_player.dart'; import 'package:wow_english/common/utils/video_playback_diagnostics.dart'; import 'package:wow_english/common/utils/video_rendering_preference.dart'; import 'package:wow_english/models/read_content_entity.dart'; import '../bloc/repeat_after_content_bloc.dart'; class RepeatVideoWidget extends StatefulWidget { const RepeatVideoWidget({super.key, this.videoUrl, required this.videoUrls}); final String? videoUrl; final List videoUrls; @override State createState() { return _RepeatVideoWidgetState(); } } class _RepeatVideoWidgetState extends State { VideoPlayerController? _controller; String _currentTime = '00:00'; String _totalTime = '00:00'; double _playDegree = 0.5; int _currentPlayIndex = 0; Object? _loadError; Timer? _bufferingTimer; VoidCallback? _controllerListener; bool _bufferingWarningShown = false; int _loadGeneration = 0; String formatDuration(Duration duration) { String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return "$minutes:$seconds"; } void _addListener() { _controllerListener = _onControllerChanged; _controller?.addListener(_controllerListener!); } void _onControllerChanged() { final controller = _controller; if (!mounted || controller == null) return; _updateBufferingWatch(controller); if (controller.value.hasError && _loadError == null) { final error = controller.value.errorDescription ?? '未知播放器错误'; debugPrint('课程儿歌/视频播放失败: $error'); VideoPlaybackDiagnostics.show( VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码'), ); setState(() => _loadError = error); return; } if (!controller.value.isInitialized || !controller.value.isPlaying) return; setState(() { final currentSecond = (controller.value.position.inMinutes.remainder(60) * 60 + controller.value.position.inSeconds.remainder(60)) .toDouble(); final totalSecond = controller.value.duration.inMinutes.remainder(60) * 60 + controller.value.duration.inSeconds.remainder(60); _currentTime = formatDuration(controller.value.position); _playDegree = totalSecond == 0 ? 0 : currentSecond / totalSecond; _playDegree = _playDegree.clamp(0.0, 1.0); }); } void _updateBufferingWatch(VideoPlayerController controller) { if (!controller.value.isBuffering) { _bufferingTimer?.cancel(); _bufferingTimer = null; _bufferingWarningShown = false; return; } if (_bufferingTimer != null || _bufferingWarningShown) return; _bufferingTimer = Timer(VideoPlaybackDiagnostics.bufferingWarningDelay, () { _bufferingTimer = null; if (!mounted || controller != _controller || !controller.value.isBuffering) { return; } _bufferingWarningShown = true; final error = controller.value.errorDescription; final message = error == null ? '视频持续缓冲,网络或设备解码可能异常' : VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码'); debugPrint('课程儿歌/视频持续缓冲: $message ${error ?? ''}'); VideoPlaybackDiagnostics.show(message); }); } @override void initState() { super.initState(); var videoUrl = ''; if (widget.videoUrls.isNotEmpty) { videoUrl = widget.videoUrls[_currentPlayIndex]?.videoUrl ?? ''; } unawaited(_initVideo(videoUrl)); } @override Widget build(BuildContext context) { return Center( child: _loadError != null ? Column( mainAxisSize: MainAxisSize.min, children: [ const Text('视频加载失败,请重试'), const SizedBox(height: 8), ElevatedButton( onPressed: () => unawaited(_initVideo( widget.videoUrls.isEmpty ? '' : widget.videoUrls[_currentPlayIndex]?.videoUrl ?? '', )), child: const Text('重试'), ), ], ) : _controller?.value.isInitialized == true ? BlocListener( listener: (context, state) { if (state is VideoPlayChangeState) { unawaited(_setPlayback( context.read().videoPlaying, )); } if (state is ChangeVideoPlayIndexState) { final nextIndex = state.isNext ? _currentPlayIndex + 1 : _currentPlayIndex - 1; if (nextIndex >= 0 && nextIndex < widget.videoUrls.length) { _currentPlayIndex = nextIndex; unawaited(_initVideo( widget.videoUrls[nextIndex]?.videoUrl ?? '', )); } } }, child: SizedBox( child: Column( children: [ Expanded( child: Stack( children: [ Container( width: double.infinity, height: double.infinity, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.black, border: Border.all( width: 1.0, color: const Color(0xFF140C10), ), borderRadius: BorderRadius.circular(5.r), ), child: AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), ), Positioned( left: 0, right: 0, bottom: 0, child: LinearProgressIndicator( color: const Color(0xFF78B72D), backgroundColor: const Color(0xFF7E756C), value: _playDegree, ), ), ], ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(_currentTime), Text(_totalTime)], ), ], ), ), ) : Container( color: Colors.white, child: const CircularProgressIndicator(), ), ); } Future _initVideo(String videoUrl) async { final generation = ++_loadGeneration; _bufferingTimer?.cancel(); _bufferingWarningShown = false; await _destroyController(); if (!mounted || generation != _loadGeneration) return; _loadError = null; final url = videoUrl.trim(); if (url.isEmpty) { _loadError = StateError('课程视频地址为空'); VideoPlaybackDiagnostics.show('课程视频地址为空'); if (mounted) setState(() {}); return; } final controller = VideoPlayerController.networkUrl( Uri.parse(url), viewType: VideoRenderingPreference.viewType, ); _controller = controller; if (mounted) setState(() {}); var stage = '视频初始化'; try { await controller .initialize() .timeout(VideoPlaybackDiagnostics.initializeTimeout); if (!mounted || generation != _loadGeneration || controller != _controller) { await controller.dispose(); return; } stage = '播放器配置'; await controller .setLooping(false) .timeout(VideoPlaybackDiagnostics.commandTimeout); await controller .setVolume(1) .timeout(VideoPlaybackDiagnostics.commandTimeout); _addListener(); _updateBufferingWatch(controller); await controller.play().timeout(VideoPlaybackDiagnostics.commandTimeout); if (!mounted || generation != _loadGeneration) return; setState(() { _currentTime = formatDuration(controller.value.position); _totalTime = formatDuration(controller.value.duration); }); } catch (error) { final failedCurrentController = controller == _controller; if (failedCurrentController) { _bufferingTimer?.cancel(); _bufferingTimer = null; } await VideoPlaybackDiagnostics.disposeController( controller, context: '课程儿歌/视频初始化失败($stage)', ); if (!mounted || generation != _loadGeneration || !failedCurrentController) { return; } _controller = null; final message = VideoPlaybackDiagnostics.messageFor(error, stage: stage); debugPrint('课程儿歌/视频加载失败(stage=$stage): $error'); VideoPlaybackDiagnostics.show(message); setState(() => _loadError = error); } } Future _setPlayback(bool shouldPlay) async { final controller = _controller; if (controller?.value.isInitialized != true) return; try { final operation = shouldPlay ? controller!.play() : controller!.pause(); await operation.timeout(VideoPlaybackDiagnostics.commandTimeout); } catch (error) { debugPrint('课程视频播放状态切换失败: $error'); VideoPlaybackDiagnostics.show( VideoPlaybackDiagnostics.messageFor(error, stage: '播放器控制'), ); } } Future _destroyController() async { final controller = _controller; final listener = _controllerListener; _controllerListener = null; _controller = null; if (controller != null) { if (listener != null) { controller.removeListener(listener); } await VideoPlaybackDiagnostics.disposeController( controller, context: '课程儿歌/视频切换', ); } } @override void dispose() { _loadGeneration++; _bufferingTimer?.cancel(); unawaited(_destroyController()); super.dispose(); } }