import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:video_player/video_player.dart'; class VideoPlayerWidget extends StatefulWidget { final VideoPlayerController? controller; final String coverUrl; final double aspectRatio; final bool allowSeek; final bool isFullScreen; const VideoPlayerWidget({ Key? key, required this.controller, required this.coverUrl, required this.aspectRatio, this.allowSeek = true, this.isFullScreen = false, }) : super(key: key); @override _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); } class _VideoPlayerWidgetState extends State { bool _visibleControls = true; Timer? _hideTimer; Timer? _positionTimer; Duration _currentPosition = Duration.zero; Duration _totalDuration = Duration.zero; bool _isPlaying = false; final ValueNotifier _sliderValue = ValueNotifier(0.0); // 判断是否有controller,避免被销毁后访问 bool get _hasController => widget.controller != null; /// 安全判断 controller 是否初始化,避免抛异常 bool _controllerInitializedSafe() { try { final c = widget.controller; if (c == null) return false; return c.value.isInitialized; } catch (e) { return false; } } @override void initState() { super.initState(); // 初始化时立即启动隐藏控制栏的定时器 _startHideTimer(); // 如果有controller并且初始化了,则启动进度定时器 _maybeStartPositionTimer(); // 给controller添加监听 _addControllerListenerSafely(); // 如果controller已经初始化,获取初始值 if (_controllerInitializedSafe()) { _updateControllerValuesSafe(); } } @override void didUpdateWidget(covariant VideoPlayerWidget oldWidget) { super.didUpdateWidget(oldWidget); // 如果controller发生变化,需要移除旧的监听,添加新的监听 if (oldWidget.controller != widget.controller) { _removeControllerListenerSafely(oldWidget.controller); _addControllerListenerSafely(); _restartPositionTimer(); _updateControllerValuesSafe(); } } void _addControllerListenerSafely() { try { widget.controller?.addListener(_controllerListener); } catch (_) { // 忽略已经被销毁的情况 } } void _removeControllerListenerSafely([VideoPlayerController? ctrl]) { final c = ctrl ?? widget.controller; if (c == null) return; try { c.removeListener(_controllerListener); } catch (_) { // 忽略 } } void _controllerListener() { if (!mounted) return; _updateControllerValuesSafe(); } // 更新播放状态、进度等信息 void _updateControllerValuesSafe() { final c = widget.controller; if (c == null) { // 没有controller时清空数据 if (mounted) { setState(() { _isPlaying = false; _totalDuration = Duration.zero; _currentPosition = Duration.zero; _sliderValue.value = 0; }); } return; } try { final value = c.value; if (!value.isInitialized) { if (mounted) { setState(() { _isPlaying = false; _totalDuration = Duration.zero; _currentPosition = Duration.zero; _sliderValue.value = 0; }); } return; } final pos = value.position; final dur = value.duration; final playing = value.isPlaying; if (mounted) { setState(() { _isPlaying = playing; _totalDuration = dur; _currentPosition = pos; _sliderValue.value = pos.inMilliseconds.toDouble(); }); } } catch (e) { // controller被销毁时忽略 } } // 启动隐藏控制栏的定时器(3秒后隐藏) void _startHideTimer() { _hideTimer?.cancel(); _hideTimer = Timer(const Duration(seconds: 3), () { if (mounted) setState(() => _visibleControls = false); }); } // 启动进度定时器(定时刷新播放进度) void _maybeStartPositionTimer() { if (_positionTimer != null && _positionTimer!.isActive) return; if (!_hasController) return; _positionTimer = Timer.periodic(const Duration(milliseconds: 300), (_) { if (!mounted) return; if (!_controllerInitializedSafe()) return; try { final c = widget.controller!; final pos = c.value.position; final dur = c.value.duration; final playing = c.value.isPlaying; setState(() { _currentPosition = pos; _totalDuration = dur; _isPlaying = playing; _sliderValue.value = pos.inMilliseconds.toDouble(); }); } catch (_) { // controller销毁时忽略 } }); } void _restartPositionTimer() { _positionTimer?.cancel(); _maybeStartPositionTimer(); } void _stopPositionTimer() { try { _positionTimer?.cancel(); } catch (_) {} _positionTimer = null; } // 点击切换控制栏显示/隐藏 void _toggleControls() { setState(() => _visibleControls = !_visibleControls); if (_visibleControls) _startHideTimer(); else _hideTimer?.cancel(); } // 播放/暂停切换 void _togglePlayPause() { final c = widget.controller; if (c == null) return; try { if (_isPlaying) { c.pause(); setState(() => _isPlaying = false); } else { c.play(); setState(() => _isPlaying = true); } _startHideTimer(); } catch (_) {} } // 进入全屏播放 Future _enterFullScreen() async { try { // 设置横屏、沉浸式UI await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); await NativeOrientation.setLandscape(); } catch (_) {} // 跳转到全屏页面 await Navigator.of(context).push( MaterialPageRoute( builder: (ctx) => Scaffold( backgroundColor: Colors.black, body: SafeArea( top: false, bottom: false, child: VideoPlayerWidget( controller: widget.controller, coverUrl: widget.coverUrl, aspectRatio: max( widget.aspectRatio, MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height, ), allowSeek: widget.allowSeek, isFullScreen: true, ), ), ), ), ); // 返回后恢复竖屏和UI try { await NativeOrientation.setPortrait(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } catch (_) {} if (!mounted) return; setState(() {}); // 强制刷新,修复退出全屏后UI问题 } @override void dispose() { _hideTimer?.cancel(); _stopPositionTimer(); _sliderValue.dispose(); _removeControllerListenerSafely(); super.dispose(); } @override Widget build(BuildContext context) { final media = MediaQuery.of(context); final screenW = media.size.width; final screenH = media.size.height; // 非全屏高度:按照宽高比计算,但不超过屏幕一半高度 final preferredNonFullHeight = min( screenW / (widget.aspectRatio <= 0 ? (16 / 9) : widget.aspectRatio), screenH * 0.5, ); final containerW = screenW; final containerH = widget.isFullScreen ? screenH : preferredNonFullHeight; return Center( child: SizedBox( width: containerW, height: containerH, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: _toggleControls, child: Stack( fit: StackFit.expand, children: [ // 视频画面或封面图 _buildVideoOrCover(containerW, containerH), // 全屏模式时左上角显示返回按钮 if (widget.isFullScreen) Positioned( top: MediaQuery.of(context).padding.top + 8, left: 8, child: SafeArea( top: true, bottom: false, child: ClipOval( child: Material( color: Colors.black38, child: InkWell( onTap: () async { try { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } catch (_) {} Navigator.of(context).maybePop(); }, child: const Padding( padding: EdgeInsets.all(8.0), child: Icon( Icons.arrow_back, color: Colors.white, size: 22, ), ), ), ), ), ), ), // 控制栏 if (_visibleControls) _buildControls(), ], ), ), ), ); } // 构建视频画面或封面图 Widget _buildVideoOrCover(double containerW, double containerH) { final c = widget.controller; if (c != null) { try { if (c.value.isInitialized) { final vidAspect = (c.value.size.height > 0) ? (c.value.size.width / c.value.size.height) : widget.aspectRatio; return Center( child: AspectRatio( aspectRatio: vidAspect > 0 ? vidAspect : widget.aspectRatio, child: VideoPlayer(c), ), ); } } catch (_) {} } if (widget.coverUrl.isNotEmpty) { return Image.network( widget.coverUrl, width: containerW, height: containerH, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: Colors.black), ); } return Container(color: Colors.black); } // 构建底部控制栏 Widget _buildControls() { final totalMs = _totalDuration.inMilliseconds.toDouble(); final sliderMax = totalMs > 0 ? totalMs : 1.0; final sliderValue = _sliderValue.value.clamp(0.0, sliderMax).toDouble(); return Positioned( bottom: 0, left: 0, right: 0, child: Container( height: 50, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ IconButton( padding: EdgeInsets.zero, icon: Icon( _isPlaying ? Icons.pause : Icons.play_arrow, size: 28, color: Colors.white, ), onPressed: _togglePlayPause, ), Expanded( child: ValueListenableBuilder( valueListenable: _sliderValue, builder: (_, value, __) => SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, inactiveTrackColor: Colors.white54, thumbColor: Colors.white, overlayColor: Colors.white24, trackHeight: 2, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8), ), child: Slider( value: sliderValue, min: 0, max: sliderMax, onChanged: (v) { if (widget.allowSeek && widget.controller != null) { try { widget.controller!.seekTo(Duration(milliseconds: v.toInt())); setState(() => _currentPosition = Duration(milliseconds: v.toInt())); _sliderValue.value = v; _startHideTimer(); } catch (_) {} } }, ), ), ), ), SizedBox( width: 110, child: Text( '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', style: TextStyle( color: Colors.white, fontSize: 12, fontFeatures: [FontFeature.tabularFigures()], ), ), ), IconButton( padding: EdgeInsets.zero, icon: Icon( widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, size: 28, color: Colors.white, ), onPressed: () { if (widget.isFullScreen) { Navigator.of(context).maybePop(); } else { _enterFullScreen(); } }, ), ], ), ), ); } // 格式化时间 String _formatDuration(Duration d) { String twoDigits(int n) => n.toString().padLeft(2, '0'); final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60); if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; return '${twoDigits(m)}:${twoDigits(s)}'; } }