Merge pull request #7 from guozhigq/feature-media_kit

Feature media kit
This commit is contained in:
Infinite
2023-08-01 22:52:45 +08:00
committed by GitHub
15 changed files with 1394 additions and 208 deletions

View File

@ -0,0 +1,91 @@
enum VideoQuality {
speed240,
flunt360,
clear480,
high720,
high72060,
high1080,
high1080plus,
high108060,
super4K,
hdr,
dolbyVision,
super8k
}
extension VideoQualityCode on VideoQuality {
static final List<int> _codeList = [
6,
16,
32,
64,
74,
80,
112,
116,
120,
125,
126,
127,
];
int get code => _codeList[index];
static VideoQuality? fromCode(int code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return VideoQuality.values[index];
}
return null;
}
}
extension VideoQualityDesc on VideoQuality {
static final List<String> _descList = [
'240P 极速',
'360P 流畅',
'480P 清晰',
'720P 高清',
'720P60 高帧率',
'1080P 高清',
'1080P+ 高码率',
'1080P60 高帧率',
'4K 超清',
'HDR 真彩色',
'杜比视界',
'8K 超高清'
];
get description => _descList[index];
}
///
enum AudioQuality { k64, k132, k192, dolby, hiRes }
extension AudioQualityCode on AudioQuality {
static final List<int> _codeList = [
30216,
30232,
30280,
30250,
30251,
];
int get code => _codeList[index];
static AudioQuality? fromCode(int code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return AudioQuality.values[index];
}
return null;
}
}
extension AudioQualityDesc on AudioQuality {
static final List<String> _descList = [
'64K',
'132K',
'192K',
'杜比全景声',
'Hi-Res无损',
];
get description => _descList[index];
}

View File

@ -1,3 +1,5 @@
import 'package:pilipala/models/video/play/quality.dart';
class PlayUrlModel {
PlayUrlModel({
this.from,
@ -32,7 +34,7 @@ class PlayUrlModel {
String? seekParam;
String? seekType;
Dash? dash;
List? supportFormats;
List<FormatItem>? supportFormats;
// String? highFormat;
int? lastPlayTime;
int? lastPlayCid;
@ -51,7 +53,11 @@ class PlayUrlModel {
seekParam = json['seek_param'];
seekType = json['seek_type'];
dash = Dash.fromJson(json['dash']);
supportFormats = json['support_formats'];
supportFormats = json['support_formats'] != null
? json['support_formats']
.map<FormatItem>((e) => FormatItem.fromJson(e))
.toList()
: [];
lastPlayTime = json['last_play_time'];
lastPlayCid = json['last_play_cid'];
}
@ -101,6 +107,7 @@ class VideoItem {
this.startWithSap,
this.segmentBase,
this.codecid,
this.quality,
});
int? id;
@ -116,6 +123,7 @@ class VideoItem {
int? startWithSap;
Map? segmentBase;
int? codecid;
VideoQuality? quality;
VideoItem.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -131,6 +139,7 @@ class VideoItem {
startWithSap = json['startWithSap'];
segmentBase = json['segmentBase'];
codecid = json['codecid'];
quality = VideoQuality.values.firstWhere((i) => i.code == json['id']);
}
}
@ -149,6 +158,7 @@ class AudioItem {
this.startWithSap,
this.segmentBase,
this.codecid,
this.quality,
});
int? id;
@ -164,6 +174,7 @@ class AudioItem {
int? startWithSap;
Map? segmentBase;
int? codecid;
String? quality;
AudioItem.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -179,5 +190,31 @@ class AudioItem {
startWithSap = json['startWithSap'];
segmentBase = json['segmentBase'];
codecid = json['codecid'];
quality =
AudioQuality.values.firstWhere((i) => i.code == json['id']).description;
}
}
class FormatItem {
FormatItem({
this.quality,
this.format,
this.newDesc,
this.displayDesc,
this.codecs,
});
int? quality;
String? format;
String? newDesc;
String? displayDesc;
List? codecs;
FormatItem.fromJson(Map<String, dynamic> json) {
quality = json['quality'];
format = json['format'];
newDesc = json['new_description'];
displayDesc = json['display_desc'];
codecs = json['codecs'];
}
}

View File

@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
@ -21,6 +22,11 @@ class VideoDetailController extends GetxController
// 视频aid
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
late PlayUrlModel data;
// 当前画质
late VideoQuality currentVideoQa;
// 当前音质
late AudioQuality currentAudioQa;
// 是否预渲染 骨架屏
bool preRender = false;
@ -44,6 +50,10 @@ class VideoDetailController extends GetxController
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
@override
void onInit() {
@ -86,6 +96,29 @@ class VideoDetailController extends GetxController
});
}
/// 更新画质、音质
/// TODO 继续进度播放
updatePlayer() {
Duration position = plPlayerController.position.value;
plPlayerController.removeListeners();
plPlayerController.isBuffering.value = false;
plPlayerController.buffered.value = Duration.zero;
/// 暂不匹配解码规则
/// 根据currentVideoQa 重新设置videoUrl
VideoItem firstVideo =
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
String videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
String audioUrl = firstAudio.baseUrl ?? '';
playerInit(videoUrl, audioUrl, defaultST: position);
}
playerInit(source, audioSource,
{Duration defaultST = Duration.zero, int duration = 0}) async {
plPlayerController.setDataSource(
@ -101,24 +134,42 @@ class VideoDetailController extends GetxController
),
// 硬解
enableHA: true,
autoplay: true,
autoplay: autoPlay.value,
seekTo: defaultST,
duration: Duration(milliseconds: duration),
);
}
// 手动点击播放
handlePlay() {
plPlayerController.togglePlay();
}
// 视频链接
queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
if (result['status']) {
PlayUrlModel data = result['data'];
// 指定质量的视频 -> 最高质量的视频
String videoUrl = data.dash!.video!.first.baseUrl!;
String audioUrl =
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first.baseUrl! : '';
playerInit(videoUrl, audioUrl,
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
VideoItem firstVideo = data.dash!.video!.first;
String videoUrl = firstVideo.baseUrl!;
//
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
AudioItem firstAudio =
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
String audioUrl = firstAudio.baseUrl ?? '';
//
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
playerInit(
videoUrl,
audioUrl,
defaultST: Duration(milliseconds: data.lastPlayTime!),
duration: data.timeLength ?? 0);
duration: data.timeLength ?? 0,
);
}
}

View File

@ -13,6 +13,7 @@ import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'widgets/app_bar.dart';
import 'widgets/header_control.dart';
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@ -54,7 +55,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
isPlay = true;
setState(() {});
// 播放完成停止 or 切换下一个
if (status == PlayerStatus.completed) {}
if (status == PlayerStatus.completed) {
// 当只有1p或多p未打开自动播放时播放完成还原进度条展示控制栏
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.onLockControl(false);
}
}
},
);
@ -158,7 +163,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.videoPlayerController !=
null)
PLVideoPlayer(
controller: plPlayerController!),
controller: plPlayerController!,
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
),
),
Visibility(
visible: isShowCover,
child: Positioned(
@ -174,6 +184,38 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
),
),
/// 关闭自动播放时 手动播放
Obx(
() => Visibility(
visible: isShowCover &&
videoDetailController
.isEffective.value &&
!videoDetailController.autoPlay.value,
child: Positioned(
right: 12,
bottom: 6,
child: TextButton.icon(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty
.resolveWith((states) {
return Theme.of(context)
.colorScheme
.primaryContainer;
}),
),
onPressed: () => videoDetailController
.handlePlay(),
icon: const Icon(
Icons.play_circle_outline,
size: 20,
),
label: const Text('Play'),
),
),
),
),
],
),
);
@ -194,14 +236,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
Opacity(
opacity: 0,
child: Container(
width: double.infinity,
height: 0,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color:
Theme.of(context).dividerColor.withOpacity(0.1),
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
),
),
@ -227,6 +272,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
],
),
),
),
Expanded(
child: TabBarView(
controller: videoDetailController.tabCtr,

View File

@ -0,0 +1,470 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr;
const HeaderControl({
this.controller,
this.videoDetailCtr,
Key? key,
}) : super(key: key);
@override
State<HeaderControl> createState() => _HeaderControlState();
@override
Size get preferredSize => throw UnimplementedError();
}
class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
void initState() {
super.initState();
videoInfo = widget.videoDetailCtr!.data;
}
/// 设置面板
void showSettingSheet() {
TextStyle subTitleStyle =
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.outline);
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) {
return Container(
width: double.infinity,
height: 420,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: EdgeInsets.only(
left: 12,
right: 12,
bottom: MediaQuery.of(context).padding.bottom + 23,
),
child: Column(
children: [
SizedBox(
height: 35,
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.5),
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
Expanded(
child: Material(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading:
const Icon(Icons.network_cell_outlined, size: 20),
title: const Text('省流模式'),
subtitle: Text('低画质 减少视频缓存', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.75,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: false,
onChanged: (value) => {},
),
),
),
Obx(
() => ListTile(
onTap: () => {Get.back(), showSetSpeedSheet()},
dense: true,
leading: const Icon(Icons.speed_outlined, size: 20),
title: const Text('播放速度'),
subtitle: Text(
'当前倍速 x${widget.controller!.playbackSpeed}',
style: subTitleStyle),
),
),
ListTile(
onTap: () => {Get.back(), showSetVideoQa()},
dense: true,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('选择画质'),
subtitle: Text(
'当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: const Text('选择音质'),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('播放设置'),
),
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20),
title: const Text('弹幕设置'),
),
],
),
))
],
),
);
},
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
}
/// 选择倍速
void showSetSpeedSheet() {
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 450,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: EdgeInsets.only(
left: 12,
right: 12,
bottom: MediaQuery.of(context).padding.bottom,
),
child: Material(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [
const SizedBox(
height: 45,
child: Center(
child: Text('播放速度'),
),
),
for (var i in playSpeed) ...[
ListTile(
onTap: () {
widget.controller!.setPlaybackSpeed(i.value);
Get.back(result: {'playbackSpeed': i.value});
},
dense: true,
contentPadding: const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: i.value == widget.controller!.playbackSpeed
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: null,
),
]
],
),
),
);
},
);
}
/// 选择画质
void showSetVideoQa() {
List<FormatItem> videoFormat = videoInfo.supportFormats!;
VideoQuality currentVideoQa = widget.videoDetailCtr!.currentVideoQa;
/// 总质量分类
int totalQaSam = videoFormat.length;
/// 可用的质量分类
int userfulQaSam = 0;
List<VideoItem> video = videoInfo.dash!.video!;
Set<int> idSet = {};
for (var item in video) {
int id = item.id!;
if (!idSet.contains(id)) {
idSet.add(id);
userfulQaSam++;
}
}
TextStyle subTitleStyle =
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.outline);
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 310,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: EdgeInsets.only(
left: 12,
right: 12,
bottom: MediaQuery.of(context).padding.bottom,
),
child: Column(
children: [
SizedBox(
height: 45,
child: GestureDetector(
onTap: () {
SmartDialog.showToast('标灰画质可能需要bilibili会员');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('选择画质'),
const SizedBox(width: 4),
Icon(
Icons.info_outline,
size: 16,
color: Theme.of(context).colorScheme.outline,
)
],
),
),
),
Expanded(
child: Material(
child: Scrollbar(
thumbVisibility: true,
child: ListView(
children: [
for (var i = 0; i < totalQaSam; i++) ...[
ListTile(
onTap: () {
final int quality = videoFormat[i].quality!;
widget.videoDetailCtr!.currentVideoQa =
VideoQualityCode.fromCode(quality)!;
widget.videoDetailCtr!.updatePlayer();
Get.back();
},
dense: true,
// 可能包含会员解锁画质
enabled: i >= totalQaSam - userfulQaSam,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(videoFormat[i].newDesc!),
subtitle: Text(
videoFormat[i].format!,
style: subTitleStyle,
),
trailing: currentVideoQa.code ==
videoFormat[i].quality
? Icon(
Icons.done,
color:
Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
),
]
],
),
),
),
),
],
),
);
},
);
}
/// 选择音质
void showSetAudioQa() {
List<FormatItem> videoFormat = videoInfo.supportFormats!;
AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa;
List<AudioItem> audio = videoInfo.dash!.audio!;
TextStyle subTitleStyle =
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.outline);
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: EdgeInsets.only(
left: 12,
right: 12,
bottom: MediaQuery.of(context).padding.bottom,
),
child: Column(
children: [
const SizedBox(height: 45, child: Center(child: Text('选择音质'))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in audio) ...[
ListTile(
onTap: () {
final int quality = i.id!;
widget.videoDetailCtr!.currentAudioQa =
AudioQualityCode.fromCode(quality)!;
widget.videoDetailCtr!.updatePlayer();
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(i.quality!),
subtitle: Text(
i.codecs!,
style: subTitleStyle,
),
trailing: currentAudioQa.code == i.id
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
),
]
],
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final _ = widget.controller!;
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
centerTitle: false,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Row(
children: [
ComBtn(
icon: const Icon(
FontAwesomeIcons.arrowLeft,
size: 15,
color: Colors.white,
),
fuc: () => Get.back(),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.house,
size: 15,
color: Colors.white,
),
fuc: () => Get.offAll(const HomePage()),
),
const Spacer(),
// ComBtn(
// icon: const Icon(
// FontAwesomeIcons.cropSimple,
// size: 15,
// color: Colors.white,
// ),
// fuc: () => _.screenshot(),
// ),
Obx(
() => SizedBox(
width: 45,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.togglePlaybackSpeed();
},
child: Text(
'${_.playbackSpeed.toString()}X',
style: textStyle,
),
),
),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.sliders,
size: 15,
color: Colors.white,
),
fuc: () => showSettingSheet(),
),
],
),
);
}
}

View File

@ -7,6 +7,7 @@ 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/models/data_source.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:universal_platform/universal_platform.dart';
@ -14,6 +15,7 @@ import 'package:volume_controller/volume_controller.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'models/data_status.dart';
import 'models/play_speed.dart';
import 'models/play_status.dart';
Box videoStorage = GStrorage.video;
@ -37,6 +39,7 @@ class PlPlayerController {
// 播放位置
final Rx<Duration> _position = Rx(Duration.zero);
final Rx<Duration> _sliderPosition = Rx(Duration.zero);
final Rx<Duration> _sliderTempPosition = Rx(Duration.zero);
final Rx<Duration> _duration = Rx(Duration.zero);
final Rx<Duration> _buffered = Rx(Duration.zero);
@ -49,13 +52,13 @@ class PlPlayerController {
final Rx<bool> _showVolumeStatus = false.obs;
final Rx<bool> _showBrightnessStatus = false.obs;
final Rx<bool> _doubleSpeedStatus = false.obs;
final Rx<bool> _controlsClose = false.obs;
final Rx<bool> _controlsLock = false.obs;
Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
///
bool _isSliderMoving = false;
Rx<bool> _isSliderMoving = false.obs;
PlaylistMode _looping = PlaylistMode.none;
bool _autoPlay = false;
final bool _listenersInitialized = false;
@ -110,10 +113,15 @@ class PlPlayerController {
/// [videoController] instace of Player
VideoController? get videoController => _videoController;
Rx<bool> get isSliderMoving => _isSliderMoving;
/// 进度条位置及监听
Rx<Duration> get sliderPosition => _sliderPosition;
Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
Rx<Duration> get sliderTempPosition => _sliderTempPosition;
// Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
/// 是否展示控制条及监听
Rx<bool> get showControls => _showControls;
Stream<bool> get onShowControlsChanged => _showControls.stream;
@ -149,9 +157,11 @@ class PlPlayerController {
Rx<bool> isBuffering = true.obs;
Rx<bool> get controlsClose => _controlsClose;
/// 屏幕锁 为true时关闭控制栏
Rx<bool> get controlsLock => _controlsLock;
PlPlayerController({
// 直播间 传false 关闭控制栏
this.controlsEnabled = true,
this.fits = const [
BoxFit.contain,
@ -177,7 +187,7 @@ class PlPlayerController {
DataSource dataSource, {
bool autoplay = true,
// 默认不循环
PlaylistMode looping = PlaylistMode.single,
PlaylistMode looping = PlaylistMode.none,
// 初始化播放位置
Duration seekTo = Duration.zero,
// 初始化播放速度
@ -195,6 +205,7 @@ class PlPlayerController {
_duration.value = duration ?? Duration.zero;
// 初始化视频倍速
_playbackSpeed.value = speed;
// 初始化数据加载状态
dataStatus.status.value = DataStatus.loading;
if (_videoPlayerController != null &&
@ -202,10 +213,12 @@ class PlPlayerController {
await pause(notify: false);
}
// 配置Player 音轨、字幕等等
_videoPlayerController = await _createVideoController(
dataSource, _looping, enableHA, width, height);
// 获取视频时长 00:00
_duration.value = _videoPlayerController!.state.duration;
// 数据加载完成
dataStatus.status.value = DataStatus.loaded;
await _initializePlayer(seekTo: seekTo);
@ -344,7 +357,7 @@ class PlPlayerController {
}),
videoPlayerController!.stream.position.listen((event) {
_position.value = event;
if (!_isSliderMoving) {
if (!isSliderMoving.value) {
_sliderPosition.value = event;
}
}),
@ -413,11 +426,11 @@ class PlPlayerController {
/// 设置倍速
Future<void> togglePlaybackSpeed() async {
List<double> allowedSpeeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
if (allowedSpeeds.indexOf(_playbackSpeed.value) <
allowedSpeeds.length - 1) {
setPlaybackSpeed(
allowedSpeeds[allowedSpeeds.indexOf(_playbackSpeed.value) + 1]);
List<double> allowedSpeeds =
PlaySpeed.values.map<double>((e) => e.value).toList();
int index = allowedSpeeds.indexOf(_playbackSpeed.value);
if (index < allowedSpeeds.length - 1) {
setPlaybackSpeed(allowedSpeeds[index + 1]);
} else {
setPlaybackSpeed(allowedSpeeds[0]);
}
@ -451,6 +464,7 @@ class PlPlayerController {
/// 更改播放状态
Future<void> togglePlay() async {
feedBack();
if (playerStatus.playing) {
pause();
} else {
@ -461,7 +475,7 @@ class PlPlayerController {
/// 隐藏控制条
void _hideTaskControls() {
_timer = Timer(const Duration(milliseconds: 3000), () {
if (!_isSliderMoving) {
if (!isSliderMoving.value) {
controls = false;
}
_timer = null;
@ -474,11 +488,16 @@ class PlPlayerController {
}
void onChangedSliderStart() {
_isSliderMoving = true;
feedBack();
_isSliderMoving.value = true;
}
void onUodatedSliderProgress(value) {
_sliderTempPosition.value = value;
}
void onChangedSliderEnd() {
_isSliderMoving = false;
_isSliderMoving.value = false;
_hideTaskControls();
}
@ -599,8 +618,9 @@ class PlPlayerController {
}
/// 关闭控制栏
void onCloseControl(bool val) {
_controlsClose.value = val;
void onLockControl(bool val) {
feedBack();
_controlsLock.value = val;
showControls.value = !val;
}
@ -630,10 +650,12 @@ class PlPlayerController {
_position.close();
_playerEventSubs?.cancel();
_sliderPosition.close();
_sliderTempPosition.close();
_isSliderMoving.close();
_duration.close();
_buffered.close();
_showControls.close();
_controlsClose.close();
_controlsLock.close();
playerStatus.status.close();
dataStatus.status.close();

View File

@ -5,3 +5,5 @@ export './view.dart';
export './models/data_source.dart';
export './models/play_status.dart';
export './models/data_status.dart';
export './widgets/common_btn.dart';
export './models/play_speed.dart';

View File

@ -0,0 +1,29 @@
extension DurationExtension on Duration {
/// Returns clamp of [Duration] between [min] and [max].
Duration clamp(Duration min, Duration max) {
if (this < min) return min;
if (this > max) return max;
return this;
}
/// Returns a [String] representation of [Duration].
String label({Duration? reference}) {
reference ??= this;
if (reference > const Duration(days: 1)) {
final days = inDays.toString().padLeft(3, '0');
final hours = (inHours - (inDays * 24)).toString().padLeft(2, '0');
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$days:$hours:$minutes:$seconds';
} else if (reference > const Duration(hours: 1)) {
final hours = inHours.toString().padLeft(2, '0');
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
} else {
final minutes = inMinutes.toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}
}

View File

@ -0,0 +1,37 @@
enum PlaySpeed {
pointTwoFive,
pointFive,
pointSevenFive,
one,
onePointTwoFive,
onePointFive,
onePointSevenFive,
two
}
extension PlaySpeedExtension on PlaySpeed {
static final List<String> _descList = [
'0.25倍',
'0.5倍',
'0.75倍',
'正常速度',
'1.25倍',
'1.5倍',
'1.75倍',
'2.0倍',
];
get description => _descList[index];
static final List<double> _valueList = [
0.25,
0.5,
0.75,
1.0,
1.25,
1.5,
1.75,
2.0
];
get value => _valueList[index];
get defaultValue => _valueList[3];
}

View File

@ -2,20 +2,32 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/common/widgets/app_bar_ani.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/play_status.dart';
import 'package:pilipala/plugin/pl_player/utils.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'widgets/backward_seek.dart';
import 'widgets/bottom_control.dart';
import 'widgets/common_btn.dart';
import 'widgets/header_control.dart';
import 'widgets/forward_seek.dart';
import 'widgets/play_pause_btn.dart';
class PLVideoPlayer extends StatefulWidget {
final PlPlayerController controller;
final PreferredSizeWidget? headerControl;
final Widget? danmuWidget;
const PLVideoPlayer({required this.controller, super.key});
const PLVideoPlayer({
required this.controller,
this.headerControl,
this.danmuWidget,
super.key,
});
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -26,6 +38,23 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late AnimationController animationController;
late VideoController videoController;
bool _mountSeekBackwardButton = false;
bool _mountSeekForwardButton = false;
bool _hideSeekBackwardButton = false;
bool _hideSeekForwardButton = false;
void onDoubleTapSeekBackward() {
setState(() {
_mountSeekBackwardButton = true;
});
}
void onDoubleTapSeekForward() {
setState(() {
_mountSeekForwardButton = true;
});
}
@override
void initState() {
super.initState();
@ -53,10 +82,33 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
fontWeight: FontWeight.normal,
backgroundColor: Color(0xaa000000),
);
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return Stack(
clipBehavior: Clip.hardEdge,
fit: StackFit.passthrough,
children: [
// Wrap [Video] widget with [MaterialVideoControlsTheme].
// MaterialVideoControlsTheme(
// normal: MaterialVideoControlsThemeData(
// // Modify theme options:
// buttonBarButtonSize: 24.0,
// buttonBarButtonColor: Colors.white,
// ),
// fullscreen: const MaterialVideoControlsThemeData(
// // Modify theme options:
// displaySeekBar: false,
// automaticallyImplySkipNextButton: false,
// automaticallyImplySkipPreviousButton: false,
// ),
// child: Scaffold(
// body: Video(
// controller: videoController,
// ),
// ),
// ),
Video(
controller: videoController,
controls: NoVideoControls,
@ -72,12 +124,30 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
onTap: () {
_.controls = !_.showControls.value;
},
onDoubleTap: () {
// onDoubleTap: () {
// if (_.playerStatus.status.value == PlayerStatus.playing) {
// _.togglePlay();
// } else {
// _.play();
// }
// },
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();
@ -97,6 +167,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
onVerticalDragUpdate: (DragUpdateDetails details) {},
onVerticalDragEnd: (DragEndDetails details) {}),
),
// 头部、底部控制条
if (_.controlsEnabled)
Obx(
() => Column(
@ -105,9 +176,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsClose.value && _.showControls.value,
visible: !_.controlsLock.value && _.showControls.value,
position: 'top',
child: HeaderControl(controller: widget.controller),
child: widget.headerControl!,
),
),
const Spacer(),
@ -115,7 +186,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
clipBehavior: Clip.hardEdge,
child: AppBarAni(
controller: animationController,
visible: !_.controlsClose.value && _.showControls.value,
visible: !_.controlsLock.value && _.showControls.value,
position: 'bottom',
child: BottomControl(controller: widget.controller),
),
@ -133,7 +204,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
return Container();
}
return Positioned(
bottom: -4,
bottom: -3,
left: 0,
right: 0,
child: SlideTransition(
@ -154,22 +225,22 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Theme.of(context).colorScheme.primary.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3,
barHeight: 2,
thumbRadius: 0.0,
onDragStart: (duration) {
_.onChangedSliderStart();
},
onDragEnd: () {
_.onChangedSliderEnd();
},
// onDragStart: (duration) {
// _.onChangedSliderStart();
// },
// onDragEnd: () {
// _.onChangedSliderEnd();
// },
// onDragUpdate: (details) {
// print(details);
// },
onSeek: (duration) {
print(duration);
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(duration);
},
// onSeek: (duration) {
// feedBack();
// _.onChangedSlider(duration.inSeconds.toDouble());
// _.seekTo(duration);
// },
)),
);
},
@ -195,6 +266,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
// 锁
if (_.controlsEnabled)
Obx(
() => Align(
alignment: Alignment.centerLeft,
@ -204,13 +276,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
visible: _.showControls.value,
child: ComBtn(
icon: Icon(
_.controlsClose.value
_.controlsLock.value
? FontAwesomeIcons.lock
: FontAwesomeIcons.lockOpen,
size: 15,
color: Colors.white,
),
fuc: () => _.onCloseControl(!_.controlsClose.value),
fuc: () => _.onLockControl(!_.controlsLock.value),
),
),
),
@ -229,6 +301,141 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
return Container();
}
}),
// 时间进度
/// TDDO 样式
Obx(
() => Align(
alignment: Alignment.topCenter,
child: FractionalTranslation(
translation: const Offset(0.0, 2.5), // 上下偏移量(负数向上偏移)
child: Visibility(
visible: _.isSliderMoving.value,
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,
),
),
],
),
),
),
),
),
// 点击 快进/快退
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) {
print(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(),
),
],
),
),
],
);
}
@ -248,5 +455,3 @@ class MSliderTrackShape extends RoundedRectSliderTrackShape {
return Rect.fromLTWH(trackLeft, -1, trackWidth, 3);
}
}
class PLPlayerCtr extends GetxController {}

View File

@ -0,0 +1,84 @@
import 'dart:async';
import 'package:flutter/material.dart';
class BackwardSeekIndicator extends StatefulWidget {
final void Function(Duration) onChanged;
final void Function(Duration) onSubmitted;
const BackwardSeekIndicator({
Key? key,
required this.onChanged,
required this.onSubmitted,
}) : super(key: key);
@override
State<BackwardSeekIndicator> createState() => BackwardSeekIndicatorState();
}
class BackwardSeekIndicatorState extends State<BackwardSeekIndicator> {
Duration value = const Duration(seconds: 10);
Timer? timer;
@override
void initState() {
super.initState();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
}
void increment() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
widget.onChanged.call(value);
// 重复点击 快退秒数累加10
setState(() {
value += const Duration(seconds: 10);
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0x88767676),
Color(0x00767676),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: InkWell(
splashColor: const Color(0x44767676),
onTap: increment,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.fast_rewind,
size: 24.0,
color: Color(0xFFFFFFFF),
),
const SizedBox(height: 8.0),
Text(
'快退${value.inSeconds}',
style: const TextStyle(
fontSize: 12.0,
color: Color(0xFFFFFFFF),
),
),
],
),
),
),
);
}
}

View File

@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
import '../utils.dart';
import 'common_btn.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
@ -57,6 +57,9 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
onDragStart: (duration) {
_.onChangedSliderStart();
},
onDragUpdate: (duration) {
_.onUodatedSliderProgress(duration.timeStamp);
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
@ -67,21 +70,24 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
),
Row(
children: [
Obx(
() => ComBtn(
icon: Icon(
_.playerStatus.paused
? FontAwesomeIcons.play
: _.playerStatus.playing
? FontAwesomeIcons.pause
: FontAwesomeIcons.rotateRight,
size: 15,
color: Colors.white,
// Obx(
// () => ComBtn(
// icon: Icon(
// _.playerStatus.paused
// ? FontAwesomeIcons.play
// : _.playerStatus.playing
// ? FontAwesomeIcons.pause
// : FontAwesomeIcons.rotateRight,
// size: 15,
// color: Colors.white,
// ),
// fuc: () => _.togglePlay(),
// ),
// ),
PlayOrPauseButton(
controller: _,
),
fuc: () => _.togglePlay(),
),
),
const SizedBox(width: 6),
const SizedBox(width: 4),
// 播放时间
Obx(() {
return Text(
@ -104,33 +110,33 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
),
const Spacer(),
// 倍速
Obx(
() => SizedBox(
width: 45,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.togglePlaybackSpeed();
},
child: Text(
'${_.playbackSpeed.toString()}X',
style: textStyle,
),
),
),
),
ComBtn(
icon: const Icon(
Icons.fit_screen_sharp,
size: 18,
color: Colors.white,
),
fuc: () => _.toggleVideoFit(),
),
const SizedBox(width: 4),
// Obx(
// () => SizedBox(
// width: 45,
// height: 34,
// child: TextButton(
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () {
// _.togglePlaybackSpeed();
// },
// child: Text(
// '${_.playbackSpeed.toString()}X',
// style: textStyle,
// ),
// ),
// ),
// ),
// ComBtn(
// icon: const Icon(
// Icons.fit_screen_sharp,
// size: 18,
// color: Colors.white,
// ),
// fuc: () => _.toggleVideoFit(),
// ),
// const SizedBox(width: 4),
// 全屏
ComBtn(
icon: const Icon(

View File

@ -0,0 +1,84 @@
import 'dart:async';
import 'package:flutter/material.dart';
class ForwardSeekIndicator extends StatefulWidget {
final void Function(Duration) onChanged;
final void Function(Duration) onSubmitted;
const ForwardSeekIndicator({
Key? key,
required this.onChanged,
required this.onSubmitted,
}) : super(key: key);
@override
State<ForwardSeekIndicator> createState() => ForwardSeekIndicatorState();
}
class ForwardSeekIndicatorState extends State<ForwardSeekIndicator> {
Duration value = const Duration(seconds: 10);
Timer? timer;
@override
void initState() {
super.initState();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
}
void increment() {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () {
widget.onSubmitted.call(value);
});
widget.onChanged.call(value);
// 重复点击 快进秒数累加10
setState(() {
value += const Duration(seconds: 10);
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0x00767676),
Color(0x88767676),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: InkWell(
splashColor: const Color(0x44767676),
onTap: increment,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.fast_forward,
size: 24.0,
color: Color(0xFFFFFFFF),
),
const SizedBox(height: 8.0),
Text(
'快进${value.inSeconds}',
style: const TextStyle(
fontSize: 12.0,
color: Color(0xFFFFFFFF),
),
),
],
),
),
),
);
}
}

View File

@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'common_btn.dart';
class HeaderControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
const HeaderControl({this.controller, Key? key}) : super(key: key);
@override
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
Widget build(BuildContext context) {
final _ = controller!;
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
centerTitle: false,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Row(
children: [
ComBtn(
icon: const Icon(
FontAwesomeIcons.arrowLeft,
size: 15,
color: Colors.white,
),
fuc: () => Get.back(),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.house,
size: 15,
color: Colors.white,
),
fuc: () => Get.back(),
),
const Spacer(),
ComBtn(
icon: const Icon(
FontAwesomeIcons.cropSimple,
size: 15,
color: Colors.white,
),
fuc: () => _.screenshot(),
),
const SizedBox(width: 4),
ComBtn(
icon: const Icon(
FontAwesomeIcons.sliders,
size: 15,
color: Colors.white,
),
fuc: () => _.screenshot(),
),
],
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
class PlayOrPauseButton extends StatefulWidget {
final double? iconSize;
final Color? iconColor;
final PlPlayerController? controller;
const PlayOrPauseButton({
super.key,
this.iconSize,
this.iconColor,
this.controller,
});
@override
PlayOrPauseButtonState createState() => PlayOrPauseButtonState();
}
class PlayOrPauseButtonState extends State<PlayOrPauseButton>
with SingleTickerProviderStateMixin {
late final AnimationController animation;
StreamSubscription<bool>? subscription;
late Player player;
bool isOpacity = false;
@override
void initState() {
super.initState();
player = widget.controller!.videoPlayerController!;
animation = AnimationController(
vsync: this,
value: player.state.playing ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
subscription ??= player.stream.playing.listen((event) {
if (event) {
animation.forward().then((value) => {
isOpacity = true,
});
} else {
animation.reverse().then((value) => {isOpacity = false});
}
setState(() {});
});
}
@override
void dispose() {
animation.dispose();
subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: player.playOrPause,
color: Colors.white,
iconSize: 20,
// iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,
// color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
icon: AnimatedIcon(
progress: animation,
icon: AnimatedIcons.play_pause,
color: Colors.white,
size: 20,
// size: widget.iconSize ?? _theme(context).buttonBarButtonSize,
// color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
),
),
);
}
}