feat: 倍速、画质、音质调节
This commit is contained in:
91
lib/models/video/play/quality.dart
Normal file
91
lib/models/video/play/quality.dart
Normal 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];
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:pilipala/models/video/play/quality.dart';
|
||||||
|
|
||||||
class PlayUrlModel {
|
class PlayUrlModel {
|
||||||
PlayUrlModel({
|
PlayUrlModel({
|
||||||
this.from,
|
this.from,
|
||||||
@ -32,7 +34,7 @@ class PlayUrlModel {
|
|||||||
String? seekParam;
|
String? seekParam;
|
||||||
String? seekType;
|
String? seekType;
|
||||||
Dash? dash;
|
Dash? dash;
|
||||||
List? supportFormats;
|
List<FormatItem>? supportFormats;
|
||||||
// String? highFormat;
|
// String? highFormat;
|
||||||
int? lastPlayTime;
|
int? lastPlayTime;
|
||||||
int? lastPlayCid;
|
int? lastPlayCid;
|
||||||
@ -51,7 +53,11 @@ class PlayUrlModel {
|
|||||||
seekParam = json['seek_param'];
|
seekParam = json['seek_param'];
|
||||||
seekType = json['seek_type'];
|
seekType = json['seek_type'];
|
||||||
dash = Dash.fromJson(json['dash']);
|
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'];
|
lastPlayTime = json['last_play_time'];
|
||||||
lastPlayCid = json['last_play_cid'];
|
lastPlayCid = json['last_play_cid'];
|
||||||
}
|
}
|
||||||
@ -101,6 +107,7 @@ class VideoItem {
|
|||||||
this.startWithSap,
|
this.startWithSap,
|
||||||
this.segmentBase,
|
this.segmentBase,
|
||||||
this.codecid,
|
this.codecid,
|
||||||
|
this.quality,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? id;
|
int? id;
|
||||||
@ -116,6 +123,7 @@ class VideoItem {
|
|||||||
int? startWithSap;
|
int? startWithSap;
|
||||||
Map? segmentBase;
|
Map? segmentBase;
|
||||||
int? codecid;
|
int? codecid;
|
||||||
|
VideoQuality? quality;
|
||||||
|
|
||||||
VideoItem.fromJson(Map<String, dynamic> json) {
|
VideoItem.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
@ -131,6 +139,7 @@ class VideoItem {
|
|||||||
startWithSap = json['startWithSap'];
|
startWithSap = json['startWithSap'];
|
||||||
segmentBase = json['segmentBase'];
|
segmentBase = json['segmentBase'];
|
||||||
codecid = json['codecid'];
|
codecid = json['codecid'];
|
||||||
|
quality = VideoQuality.values.firstWhere((i) => i.code == json['id']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +158,7 @@ class AudioItem {
|
|||||||
this.startWithSap,
|
this.startWithSap,
|
||||||
this.segmentBase,
|
this.segmentBase,
|
||||||
this.codecid,
|
this.codecid,
|
||||||
|
this.quality,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? id;
|
int? id;
|
||||||
@ -164,6 +174,7 @@ class AudioItem {
|
|||||||
int? startWithSap;
|
int? startWithSap;
|
||||||
Map? segmentBase;
|
Map? segmentBase;
|
||||||
int? codecid;
|
int? codecid;
|
||||||
|
String? quality;
|
||||||
|
|
||||||
AudioItem.fromJson(Map<String, dynamic> json) {
|
AudioItem.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
@ -179,5 +190,31 @@ class AudioItem {
|
|||||||
startWithSap = json['startWithSap'];
|
startWithSap = json['startWithSap'];
|
||||||
segmentBase = json['segmentBase'];
|
segmentBase = json['segmentBase'];
|
||||||
codecid = json['codecid'];
|
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'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:pilipala/http/constants.dart';
|
import 'package:pilipala/http/constants.dart';
|
||||||
import 'package:pilipala/http/video.dart';
|
import 'package:pilipala/http/video.dart';
|
||||||
import 'package:pilipala/models/common/reply_type.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/play/url.dart';
|
||||||
import 'package:pilipala/models/video/reply/item.dart';
|
import 'package:pilipala/models/video/reply/item.dart';
|
||||||
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||||
@ -21,6 +22,11 @@ class VideoDetailController extends GetxController
|
|||||||
// 视频aid
|
// 视频aid
|
||||||
String bvid = Get.parameters['bvid']!;
|
String bvid = Get.parameters['bvid']!;
|
||||||
int cid = int.parse(Get.parameters['cid']!);
|
int cid = int.parse(Get.parameters['cid']!);
|
||||||
|
late PlayUrlModel data;
|
||||||
|
// 当前画质
|
||||||
|
late VideoQuality currentVideoQa;
|
||||||
|
// 当前音质
|
||||||
|
late AudioQuality currentAudioQa;
|
||||||
|
|
||||||
// 是否预渲染 骨架屏
|
// 是否预渲染 骨架屏
|
||||||
bool preRender = false;
|
bool preRender = false;
|
||||||
@ -86,6 +92,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,
|
playerInit(source, audioSource,
|
||||||
{Duration defaultST = Duration.zero, int duration = 0}) async {
|
{Duration defaultST = Duration.zero, int duration = 0}) async {
|
||||||
plPlayerController.setDataSource(
|
plPlayerController.setDataSource(
|
||||||
@ -111,14 +140,27 @@ class VideoDetailController extends GetxController
|
|||||||
queryVideoUrl() async {
|
queryVideoUrl() async {
|
||||||
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
PlayUrlModel data = result['data'];
|
data = result['data'];
|
||||||
// 指定质量的视频 -> 最高质量的视频
|
|
||||||
String videoUrl = data.dash!.video!.first.baseUrl!;
|
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
|
||||||
String audioUrl =
|
VideoItem firstVideo = data.dash!.video!.first;
|
||||||
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first.baseUrl! : '';
|
String videoUrl = firstVideo.baseUrl!;
|
||||||
playerInit(videoUrl, audioUrl,
|
//
|
||||||
|
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!),
|
defaultST: Duration(milliseconds: data.lastPlayTime!),
|
||||||
duration: data.timeLength ?? 0);
|
duration: data.timeLength ?? 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:pilipala/pages/video/detail/related/index.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
|
||||||
import 'widgets/app_bar.dart';
|
import 'widgets/app_bar.dart';
|
||||||
|
import 'widgets/header_control.dart';
|
||||||
|
|
||||||
class VideoDetailPage extends StatefulWidget {
|
class VideoDetailPage extends StatefulWidget {
|
||||||
const VideoDetailPage({Key? key}) : super(key: key);
|
const VideoDetailPage({Key? key}) : super(key: key);
|
||||||
@ -158,7 +159,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
.videoPlayerController !=
|
.videoPlayerController !=
|
||||||
null)
|
null)
|
||||||
PLVideoPlayer(
|
PLVideoPlayer(
|
||||||
controller: plPlayerController!),
|
controller: plPlayerController!,
|
||||||
|
headerControl: HeaderControl(
|
||||||
|
controller: plPlayerController,
|
||||||
|
videoDetailCtr: videoDetailController,
|
||||||
|
),
|
||||||
|
),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: isShowCover,
|
visible: isShowCover,
|
||||||
child: Positioned(
|
child: Positioned(
|
||||||
@ -194,14 +200,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Opacity(
|
||||||
|
opacity: 0,
|
||||||
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 0,
|
height: 0,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color:
|
color: Theme.of(context)
|
||||||
Theme.of(context).dividerColor.withOpacity(0.1),
|
.dividerColor
|
||||||
|
.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -227,6 +236,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: videoDetailController.tabCtr,
|
controller: videoDetailController.tabCtr,
|
||||||
|
|||||||
470
lib/pages/video/detail/widgets/header_control.dart
Normal file
470
lib/pages/video/detail/widgets/header_control.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/models/data_source.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:pilipala/utils/storage.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
import 'package:universal_platform/universal_platform.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 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import 'models/data_status.dart';
|
import 'models/data_status.dart';
|
||||||
|
import 'models/play_speed.dart';
|
||||||
import 'models/play_status.dart';
|
import 'models/play_status.dart';
|
||||||
|
|
||||||
Box videoStorage = GStrorage.video;
|
Box videoStorage = GStrorage.video;
|
||||||
@ -177,7 +179,7 @@ class PlPlayerController {
|
|||||||
DataSource dataSource, {
|
DataSource dataSource, {
|
||||||
bool autoplay = true,
|
bool autoplay = true,
|
||||||
// 默认不循环
|
// 默认不循环
|
||||||
PlaylistMode looping = PlaylistMode.single,
|
PlaylistMode looping = PlaylistMode.none,
|
||||||
// 初始化播放位置
|
// 初始化播放位置
|
||||||
Duration seekTo = Duration.zero,
|
Duration seekTo = Duration.zero,
|
||||||
// 初始化播放速度
|
// 初始化播放速度
|
||||||
@ -195,6 +197,7 @@ class PlPlayerController {
|
|||||||
_duration.value = duration ?? Duration.zero;
|
_duration.value = duration ?? Duration.zero;
|
||||||
// 初始化视频倍速
|
// 初始化视频倍速
|
||||||
_playbackSpeed.value = speed;
|
_playbackSpeed.value = speed;
|
||||||
|
// 初始化数据加载状态
|
||||||
dataStatus.status.value = DataStatus.loading;
|
dataStatus.status.value = DataStatus.loading;
|
||||||
|
|
||||||
if (_videoPlayerController != null &&
|
if (_videoPlayerController != null &&
|
||||||
@ -202,10 +205,12 @@ class PlPlayerController {
|
|||||||
await pause(notify: false);
|
await pause(notify: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置Player 音轨、字幕等等
|
||||||
_videoPlayerController = await _createVideoController(
|
_videoPlayerController = await _createVideoController(
|
||||||
dataSource, _looping, enableHA, width, height);
|
dataSource, _looping, enableHA, width, height);
|
||||||
|
// 获取视频时长 00:00
|
||||||
_duration.value = _videoPlayerController!.state.duration;
|
_duration.value = _videoPlayerController!.state.duration;
|
||||||
|
// 数据加载完成
|
||||||
dataStatus.status.value = DataStatus.loaded;
|
dataStatus.status.value = DataStatus.loaded;
|
||||||
|
|
||||||
await _initializePlayer(seekTo: seekTo);
|
await _initializePlayer(seekTo: seekTo);
|
||||||
@ -382,6 +387,8 @@ class PlPlayerController {
|
|||||||
position = Duration.zero;
|
position = Duration.zero;
|
||||||
}
|
}
|
||||||
_position.value = position;
|
_position.value = position;
|
||||||
|
print('seek 🌹duration : ${duration.value.inSeconds}');
|
||||||
|
|
||||||
if (duration.value.inSeconds != 0) {
|
if (duration.value.inSeconds != 0) {
|
||||||
// await _videoPlayerController!.stream.buffer.first;
|
// await _videoPlayerController!.stream.buffer.first;
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
@ -389,11 +396,13 @@ class PlPlayerController {
|
|||||||
// play();
|
// play();
|
||||||
// }
|
// }
|
||||||
} else {
|
} else {
|
||||||
|
print('🌹🌹');
|
||||||
_timerForSeek?.cancel();
|
_timerForSeek?.cancel();
|
||||||
_timerForSeek =
|
_timerForSeek =
|
||||||
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
||||||
//_timerForSeek = null;
|
//_timerForSeek = null;
|
||||||
if (duration.value.inSeconds != 0) {
|
if (duration.value.inSeconds != 0) {
|
||||||
|
print('🌹🌹🌹');
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
// if (playerStatus.stopped) {
|
// if (playerStatus.stopped) {
|
||||||
// play();
|
// play();
|
||||||
@ -413,11 +422,11 @@ class PlPlayerController {
|
|||||||
|
|
||||||
/// 设置倍速
|
/// 设置倍速
|
||||||
Future<void> togglePlaybackSpeed() async {
|
Future<void> togglePlaybackSpeed() async {
|
||||||
List<double> allowedSpeeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
|
List<double> allowedSpeeds =
|
||||||
if (allowedSpeeds.indexOf(_playbackSpeed.value) <
|
PlaySpeed.values.map<double>((e) => e.value).toList();
|
||||||
allowedSpeeds.length - 1) {
|
int index = allowedSpeeds.indexOf(_playbackSpeed.value);
|
||||||
setPlaybackSpeed(
|
if (index < allowedSpeeds.length - 1) {
|
||||||
allowedSpeeds[allowedSpeeds.indexOf(_playbackSpeed.value) + 1]);
|
setPlaybackSpeed(allowedSpeeds[index + 1]);
|
||||||
} else {
|
} else {
|
||||||
setPlaybackSpeed(allowedSpeeds[0]);
|
setPlaybackSpeed(allowedSpeeds[0]);
|
||||||
}
|
}
|
||||||
@ -451,6 +460,7 @@ class PlPlayerController {
|
|||||||
|
|
||||||
/// 更改播放状态
|
/// 更改播放状态
|
||||||
Future<void> togglePlay() async {
|
Future<void> togglePlay() async {
|
||||||
|
feedBack();
|
||||||
if (playerStatus.playing) {
|
if (playerStatus.playing) {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
@ -474,6 +484,7 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onChangedSliderStart() {
|
void onChangedSliderStart() {
|
||||||
|
feedBack();
|
||||||
_isSliderMoving = true;
|
_isSliderMoving = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,6 +611,7 @@ class PlPlayerController {
|
|||||||
|
|
||||||
/// 关闭控制栏
|
/// 关闭控制栏
|
||||||
void onCloseControl(bool val) {
|
void onCloseControl(bool val) {
|
||||||
|
feedBack();
|
||||||
_controlsClose.value = val;
|
_controlsClose.value = val;
|
||||||
showControls.value = !val;
|
showControls.value = !val;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,3 +5,5 @@ export './view.dart';
|
|||||||
export './models/data_source.dart';
|
export './models/data_source.dart';
|
||||||
export './models/play_status.dart';
|
export './models/play_status.dart';
|
||||||
export './models/data_status.dart';
|
export './models/data_status.dart';
|
||||||
|
export './widgets/common_btn.dart';
|
||||||
|
export './models/play_speed.dart';
|
||||||
|
|||||||
37
lib/plugin/pl_player/models/play_speed.dart
Normal file
37
lib/plugin/pl_player/models/play_speed.dart
Normal 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];
|
||||||
|
}
|
||||||
@ -10,12 +10,18 @@ import 'package:pilipala/utils/feed_back.dart';
|
|||||||
|
|
||||||
import 'widgets/bottom_control.dart';
|
import 'widgets/bottom_control.dart';
|
||||||
import 'widgets/common_btn.dart';
|
import 'widgets/common_btn.dart';
|
||||||
import 'widgets/header_control.dart';
|
|
||||||
|
|
||||||
class PLVideoPlayer extends StatefulWidget {
|
class PLVideoPlayer extends StatefulWidget {
|
||||||
final PlPlayerController controller;
|
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
|
@override
|
||||||
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
|
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
|
||||||
@ -97,6 +103,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
onVerticalDragUpdate: (DragUpdateDetails details) {},
|
onVerticalDragUpdate: (DragUpdateDetails details) {},
|
||||||
onVerticalDragEnd: (DragEndDetails details) {}),
|
onVerticalDragEnd: (DragEndDetails details) {}),
|
||||||
),
|
),
|
||||||
|
// 头部、底部控制条
|
||||||
if (_.controlsEnabled)
|
if (_.controlsEnabled)
|
||||||
Obx(
|
Obx(
|
||||||
() => Column(
|
() => Column(
|
||||||
@ -107,7 +114,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
controller: animationController,
|
controller: animationController,
|
||||||
visible: !_.controlsClose.value && _.showControls.value,
|
visible: !_.controlsClose.value && _.showControls.value,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
child: HeaderControl(controller: widget.controller),
|
child: widget.headerControl!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@ -166,7 +173,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
// print(details);
|
// print(details);
|
||||||
// },
|
// },
|
||||||
onSeek: (duration) {
|
onSeek: (duration) {
|
||||||
print(duration);
|
feedBack();
|
||||||
_.onChangedSlider(duration.inSeconds.toDouble());
|
_.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
_.seekTo(duration);
|
_.seekTo(duration);
|
||||||
},
|
},
|
||||||
@ -195,6 +202,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 锁
|
// 锁
|
||||||
|
if (_.controlsEnabled)
|
||||||
Obx(
|
Obx(
|
||||||
() => Align(
|
() => Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import 'package:get/get.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
|
||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
import 'common_btn.dart';
|
|
||||||
|
|
||||||
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
@ -67,21 +66,21 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
// Obx(
|
||||||
() => ComBtn(
|
// () => ComBtn(
|
||||||
icon: Icon(
|
// icon: Icon(
|
||||||
_.playerStatus.paused
|
// _.playerStatus.paused
|
||||||
? FontAwesomeIcons.play
|
// ? FontAwesomeIcons.play
|
||||||
: _.playerStatus.playing
|
// : _.playerStatus.playing
|
||||||
? FontAwesomeIcons.pause
|
// ? FontAwesomeIcons.pause
|
||||||
: FontAwesomeIcons.rotateRight,
|
// : FontAwesomeIcons.rotateRight,
|
||||||
size: 15,
|
// size: 15,
|
||||||
color: Colors.white,
|
// color: Colors.white,
|
||||||
),
|
// ),
|
||||||
fuc: () => _.togglePlay(),
|
// fuc: () => _.togglePlay(),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 6),
|
// const SizedBox(width: 6),
|
||||||
// 播放时间
|
// 播放时间
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Text(
|
return Text(
|
||||||
@ -104,33 +103,33 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// 倍速
|
// 倍速
|
||||||
Obx(
|
// Obx(
|
||||||
() => SizedBox(
|
// () => SizedBox(
|
||||||
width: 45,
|
// width: 45,
|
||||||
height: 34,
|
// height: 34,
|
||||||
child: TextButton(
|
// child: TextButton(
|
||||||
style: ButtonStyle(
|
// style: ButtonStyle(
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
// padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
),
|
// ),
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
_.togglePlaybackSpeed();
|
// _.togglePlaybackSpeed();
|
||||||
},
|
// },
|
||||||
child: Text(
|
// child: Text(
|
||||||
'${_.playbackSpeed.toString()}X',
|
// '${_.playbackSpeed.toString()}X',
|
||||||
style: textStyle,
|
// style: textStyle,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
ComBtn(
|
// ComBtn(
|
||||||
icon: const Icon(
|
// icon: const Icon(
|
||||||
Icons.fit_screen_sharp,
|
// Icons.fit_screen_sharp,
|
||||||
size: 18,
|
// size: 18,
|
||||||
color: Colors.white,
|
// color: Colors.white,
|
||||||
),
|
// ),
|
||||||
fuc: () => _.toggleVideoFit(),
|
// fuc: () => _.toggleVideoFit(),
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 4),
|
// const SizedBox(width: 4),
|
||||||
// 全屏
|
// 全屏
|
||||||
ComBtn(
|
ComBtn(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
|
|||||||
@ -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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user