Files
pilipala/lib/plugin/pl_player/view.dart
2023-08-14 18:13:08 +08:00

798 lines
27 KiB
Dart

import 'dart:async';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/plugin/pl_player/controller.dart';
import 'package:pilipala/plugin/pl_player/models/duration.dart';
import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
import 'package:pilipala/plugin/pl_player/utils.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:volume_controller/volume_controller.dart';
import 'utils/fullscreen.dart';
import 'widgets/app_bar_ani.dart';
import 'widgets/backward_seek.dart';
import 'widgets/bottom_control.dart';
import 'widgets/common_btn.dart';
import 'widgets/forward_seek.dart';
class PLVideoPlayer extends StatefulWidget {
final PlPlayerController controller;
final PreferredSizeWidget? headerControl;
final Widget? danmuWidget;
const PLVideoPlayer({
required this.controller,
this.headerControl,
this.danmuWidget,
super.key,
});
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
}
class _PLVideoPlayerState extends State<PLVideoPlayer>
with TickerProviderStateMixin {
late AnimationController animationController;
late VideoController videoController;
bool _mountSeekBackwardButton = false;
bool _mountSeekForwardButton = false;
bool _hideSeekBackwardButton = false;
bool _hideSeekForwardButton = false;
double _brightnessValue = 0.0;
bool _brightnessIndicator = false;
Timer? _brightnessTimer;
double _volumeValue = 0.0;
bool _volumeIndicator = false;
Timer? _volumeTimer;
double _distance = 0.0;
// 初始手指落下位置
double _initTapPositoin = 0.0;
bool _volumeInterceptEventStream = false;
Box setting = GStrorage.setting;
late FullScreenMode mode;
void onDoubleTapSeekBackward() {
setState(() {
_mountSeekBackwardButton = true;
});
}
void onDoubleTapSeekForward() {
setState(() {
_mountSeekForwardButton = true;
});
}
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
Future.microtask(() async {
try {
VolumeController().showSystemUI = false;
_volumeValue = await VolumeController().getVolume();
VolumeController().listener((value) {
if (mounted && !_volumeInterceptEventStream) {
setState(() {
_volumeValue = value;
});
}
});
} catch (_) {}
});
Future.microtask(() async {
try {
_brightnessValue = await ScreenBrightness().current;
ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
if (mounted) {
setState(() {
_brightnessValue = value;
});
}
});
} catch (_) {}
});
}
Future<void> setVolume(double value) async {
try {
VolumeController().setVolume(value);
} catch (_) {}
setState(() {
_volumeValue = value;
_volumeIndicator = true;
_volumeInterceptEventStream = true;
});
_volumeTimer?.cancel();
_volumeTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
setState(() {
_volumeIndicator = false;
_volumeInterceptEventStream = false;
});
}
});
}
Future<void> setBrightness(double value) async {
try {
await ScreenBrightness().setScreenBrightness(value);
} catch (_) {}
setState(() {
_brightnessIndicator = true;
});
_brightnessTimer?.cancel();
_brightnessTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) {
setState(() {
_brightnessIndicator = false;
});
}
});
}
Future<void> triggerFullScreen() async {
PlPlayerController _ = widget.controller;
mode = FullScreenModeCode.fromCode(
setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
if (!_.isFullScreen.value) {
/// 按照视频宽高比决定全屏方向
switch (mode) {
case FullScreenMode.auto:
if (_.direction.value == 'horizontal') {
/// 进入全屏
await enterFullScreen();
// 横屏
// await landScape();
} else {
// 竖屏
await verticalScreen();
}
break;
case FullScreenMode.vertical:
/// 进入全屏
await enterFullScreen();
// 横屏
// await landScape();
break;
case FullScreenMode.horizontal:
/// 进入全屏
await enterFullScreen();
// 横屏
// await landScape();
break;
}
_.toggleFullScreen(true);
var result = await showDialog(
context: Get.context!,
useSafeArea: false,
builder: (context) => Dialog.fullscreen(
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
primary: false,
toolbarHeight: 0,
backgroundColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
body: SafeArea(
bottom: true,
child: PLVideoPlayer(
controller: _,
headerControl: _.headerControl,
),
),
),
),
);
if (result == null) {
// 退出全屏
exitFullScreen();
await verticalScreen();
_.toggleFullScreen(false);
}
} else {
Get.back();
exitFullScreen();
await verticalScreen();
_.toggleFullScreen(false);
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final _ = widget.controller;
Color colorTheme = Theme.of(context).colorScheme.primary;
TextStyle subTitleStyle = const TextStyle(
height: 1.5,
fontSize: 40.0,
letterSpacing: 0.0,
wordSpacing: 0.0,
color: Color(0xffffffff),
fontWeight: FontWeight.normal,
backgroundColor: Color(0xaa000000),
);
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return Stack(
clipBehavior: Clip.hardEdge,
fit: StackFit.passthrough,
children: [
Video(
controller: videoController,
controls: NoVideoControls,
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
textAlign: TextAlign.center,
padding: const EdgeInsets.all(24.0),
),
),
/// 长按倍速
Obx(
() => Align(
alignment: Alignment.topCenter,
child: FractionalTranslation(
translation: const Offset(0.0, 1), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
width: 86.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(width: 3),
Image.asset(
'assets/images/run-pokemon.gif',
height: 20,
),
const Text(
'倍速中',
style: TextStyle(color: Colors.white, fontSize: 12),
),
const SizedBox(width: 4),
],
),
),
),
),
),
),
/// 时间进度
Obx(
() => Align(
alignment: Alignment.topCenter,
child: FractionalTranslation(
translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _.isSliderMoving.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
width: 100.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
return Text(
_.sliderTempPosition.value.inMinutes >= 60
? printDurationWithHours(
_.sliderTempPosition.value)
: printDuration(_.sliderTempPosition.value),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.duration.value.inMinutes >= 60
? printDurationWithHours(_.duration.value)
: printDuration(_.duration.value),
style: textStyle,
),
),
],
),
),
),
),
),
),
/// 音量🔊 控制条展示
Align(
alignment: Alignment.center,
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _volumeIndicator ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
width: 70.0,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 34.0,
width: 28.0,
alignment: Alignment.centerRight,
child: Icon(
_volumeValue == 0.0
? Icons.volume_off
: _volumeValue < 0.5
? Icons.volume_down
: Icons.volume_up,
color: const Color(0xFFFFFFFF),
size: 20.0,
),
),
Expanded(
child: Text(
'${(_volumeValue * 100.0).round()}%',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.0,
color: Color(0xFFFFFFFF),
),
),
),
const SizedBox(width: 6.0),
],
),
),
),
),
/// 亮度🌞 控制条展示
Align(
alignment: Alignment.center,
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _brightnessIndicator ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
width: 70.0,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 30.0,
width: 28.0,
alignment: Alignment.centerRight,
child: Icon(
_brightnessValue < 1.0 / 3.0
? Icons.brightness_low
: _brightnessValue < 2.0 / 3.0
? Icons.brightness_medium
: Icons.brightness_high,
color: const Color(0xFFFFFFFF),
size: 18.0,
),
),
const SizedBox(width: 2.0),
Expanded(
child: Text(
'${(_brightnessValue * 100.0).round()}%',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13.0,
color: Color(0xFFFFFFFF),
),
),
),
const SizedBox(width: 6.0),
],
),
),
),
),
Obx(() {
if (_.buffered.value == Duration.zero) {
return Positioned.fill(
child: Container(
color: Colors.black,
child: Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
),
),
);
} else {
return Container();
}
}),
/// 手势
Positioned.fill(
left: 16,
top: 25,
right: 15,
bottom: 15,
child: GestureDetector(
onTap: () {
_.controls = !_.showControls.value;
},
onDoubleTapDown: (details) {
final totalWidth = MediaQuery.of(context).size.width;
final tapPosition = details.localPosition.dx;
final sectionWidth = totalWidth / 3;
if (tapPosition < sectionWidth) {
// 双击左边区域 👈
onDoubleTapSeekBackward();
} else if (tapPosition < sectionWidth * 2) {
if (_.playerStatus.status.value == PlayerStatus.playing) {
_.togglePlay();
} else {
_.play();
}
} else {
// 双击右边区域 👈
onDoubleTapSeekForward();
}
},
onLongPressStart: (detail) {
feedBack();
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(true);
_.setPlaybackSpeed(currentSpeed * 2);
},
onLongPressEnd: (details) {
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(false);
_.setPlaybackSpeed(currentSpeed / 2);
},
// 水平位置 快进
onHorizontalDragUpdate: (DragUpdateDetails details) {
final tapPosition = details.localPosition.dx;
int curSliderPosition = _.sliderPosition.value.inSeconds;
late int result;
if (tapPosition - _initTapPositoin > 0) {
// 快进
/// TODO 优化屏幕越小效果越明显
result = (curSliderPosition + 1)
.clamp(0, _.duration.value.inSeconds);
} else {
// 快退
result = (curSliderPosition - 1)
.clamp(0, _.duration.value.inSeconds);
}
_.onUodatedSliderProgress(Duration(seconds: result));
_.onChangedSliderStart();
_initTapPositoin = tapPosition;
},
onHorizontalDragEnd: (DragEndDetails details) {
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value);
},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) async {
final totalWidth = MediaQuery.of(context).size.width;
final tapPosition = details.localPosition.dx;
final sectionWidth = totalWidth / 3;
final delta = details.delta.dy;
if (tapPosition < sectionWidth) {
// 左边区域 👈
final brightness = _brightnessValue - delta / 100.0;
final result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (tapPosition < sectionWidth * 2) {
// 全屏
final double dy = details.delta.dy;
const double threshold = 7.0; // 滑动阈值
if (dy > _distance && dy > threshold) {
if (_.isFullScreen.value) {
// 下滑退出全屏
await triggerFullScreen();
}
_distance = 0.0;
} else if (dy < _distance && dy < -threshold) {
if (!_.isFullScreen.value) {
// 上滑进入全屏
await triggerFullScreen();
}
_distance = 0.0;
}
_distance = dy;
// triggerFullScreen();
} else {
// 右边区域 👈
final volume = _volumeValue - delta / 100.0;
final result = volume.clamp(0.0, 1.0);
setVolume(result);
}
},
onVerticalDragEnd: (DragEndDetails details) {},
),
),
// 头部、底部控制条
if (_.controlsEnabled)
Obx(
() => Column(
children: [
if (widget.headerControl != null)
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsLock.value && _.showControls.value,
position: 'top',
child: widget.headerControl!,
),
),
const Spacer(),
ClipRect(
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsLock.value && _.showControls.value,
position: 'bottom',
child: BottomControl(
controller: widget.controller,
triggerFullScreen: triggerFullScreen),
),
),
],
),
),
// 进度条
Obx(
() {
final int value = _.sliderPosition.value.inSeconds;
final int max = _.duration.value.inSeconds;
final int buffer = _.buffered.value.inSeconds;
if (value > max || max <= 0) {
return Container();
}
return Positioned(
bottom: -3.5,
left: 0,
right: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, -1),
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeInOut,
)),
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor:
Theme.of(context).colorScheme.primary.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 2,
thumbRadius: 0.0,
// onDragStart: (duration) {
// _.onChangedSliderStart();
// },
// onDragEnd: () {
// _.onChangedSliderEnd();
// },
// onDragUpdate: (details) {
// print(details);
// },
// onSeek: (duration) {
// feedBack();
// _.onChangedSlider(duration.inSeconds.toDouble());
// _.seekTo(duration);
// },
)),
);
},
),
// 锁
if (_.controlsEnabled)
Obx(
() => Align(
alignment: Alignment.centerLeft,
child: FractionalTranslation(
translation: const Offset(0.5, 0.0),
child: Visibility(
visible: _.showControls.value,
child: ComBtn(
icon: Icon(
_.controlsLock.value
? FontAwesomeIcons.lock
: FontAwesomeIcons.lockOpen,
size: 15,
color: Colors.white,
),
fuc: () => _.onLockControl(!_.controlsLock.value),
),
),
),
),
),
//
Obx(() {
if (_.dataStatus.loading || _.isBuffering.value) {
return Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
);
} else {
return Container();
}
}),
/// 点击 快进/快退
if (_mountSeekBackwardButton || _mountSeekForwardButton)
Positioned.fill(
child: Row(
children: [
Expanded(
child: _mountSeekBackwardButton
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: _hideSeekBackwardButton ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 500),
builder: (context, value, child) => Opacity(
opacity: value,
child: child,
),
onEnd: () {
if (_hideSeekBackwardButton) {
setState(() {
_hideSeekBackwardButton = false;
_mountSeekBackwardButton = false;
});
}
},
child: BackwardSeekIndicator(
onChanged: (value) {
// _seekBarDeltaValueNotifier.value = -value;
},
onSubmitted: (value) {
setState(() {
_hideSeekBackwardButton = true;
});
Player player =
widget.controller.videoPlayerController!;
var result = player.state.position - value;
result = result.clamp(
Duration.zero,
player.state.duration,
);
player.seek(result);
widget.controller.play();
},
),
)
: const SizedBox(),
),
Expanded(
child: SizedBox(
width: MediaQuery.of(context).size.width / 4,
),
),
Expanded(
child: _mountSeekForwardButton
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0.0,
end: _hideSeekForwardButton ? 0.0 : 1.0,
),
duration: const Duration(milliseconds: 500),
builder: (context, value, child) => Opacity(
opacity: value,
child: child,
),
onEnd: () {
if (_hideSeekForwardButton) {
setState(() {
_hideSeekForwardButton = false;
_mountSeekForwardButton = false;
});
}
},
child: ForwardSeekIndicator(
onChanged: (value) {
// _seekBarDeltaValueNotifier.value = value;
},
onSubmitted: (value) {
setState(() {
_hideSeekForwardButton = true;
});
Player player =
widget.controller.videoPlayerController!;
var result = player.state.position + value;
result = result.clamp(
Duration.zero,
player.state.duration,
);
player.seek(result);
widget.controller.play();
},
),
)
: const SizedBox(),
),
],
),
),
],
);
}
}