flutter_integrated_whb/lib/customWidget/video_player_widget.dart

478 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<VideoPlayerWidget> {
bool _visibleControls = true;
Timer? _hideTimer;
Timer? _positionTimer;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
bool _isPlaying = false;
final ValueNotifier<double> _sliderValue = ValueNotifier<double>(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<void> _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<double>(
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)}';
}
}