repeat_video_widget.dart 11 KB
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<ReadContentEntity?> videoUrls;

  @override
  State<StatefulWidget> createState() {
    return _RepeatVideoWidgetState();
  }
}

class _RepeatVideoWidgetState extends State<RepeatVideoWidget> {
  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<RepeatAfterContentBloc, RepeatAfterContentState>(
                  listener: (context, state) {
                    if (state is VideoPlayChangeState) {
                      unawaited(_setPlayback(
                        context.read<RepeatAfterContentBloc>().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<void> _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<void> _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<void> _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();
  }
}