diff --git a/lib/models/video/play/quality.dart b/lib/models/video/play/quality.dart new file mode 100644 index 00000000..7536b971 --- /dev/null +++ b/lib/models/video/play/quality.dart @@ -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 _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 _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 _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 _descList = [ + '64K', + '132K', + '192K', + '杜比全景声', + 'Hi-Res无损', + ]; + get description => _descList[index]; +} diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index a879fefb..8944d797 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -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? 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((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 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 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 json) { + quality = json['quality']; + format = json['format']; + newDesc = json['new_description']; + displayDesc = json['display_desc']; + codecs = json['codecs']; } } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 3051ba55..6a218596 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -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; @@ -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, {Duration defaultST = Duration.zero, int duration = 0}) async { plPlayerController.setDataSource( @@ -111,14 +140,27 @@ class VideoDetailController extends GetxController 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, - defaultST: Duration(milliseconds: data.lastPlayTime!), - duration: data.timeLength ?? 0); + 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, + ); } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 056c7629..64e14db4 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -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); @@ -158,7 +159,12 @@ class _VideoDetailPageState extends State .videoPlayerController != null) PLVideoPlayer( - controller: plPlayerController!), + controller: plPlayerController!, + headerControl: HeaderControl( + controller: plPlayerController, + videoDetailCtr: videoDetailController, + ), + ), Visibility( visible: isShowCover, child: Positioned( @@ -194,37 +200,41 @@ class _VideoDetailPageState extends State color: Theme.of(context).colorScheme.background, child: Column( children: [ - Container( - width: double.infinity, - height: 0, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).dividerColor.withOpacity(0.1), - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - width: 280, - margin: const EdgeInsets.only(left: 20), - child: Obx( - () => TabBar( - controller: videoDetailController.tabCtr, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).colorScheme.background, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), + Opacity( + opacity: 0, + child: Container( + width: double.infinity, + height: 0, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context) + .dividerColor + .withOpacity(0.1), ), ), - ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 280, + margin: const EdgeInsets.only(left: 20), + child: Obx( + () => TabBar( + controller: videoDetailController.tabCtr, + dividerColor: Colors.transparent, + indicatorColor: + Theme.of(context).colorScheme.background, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), + ), + ), + ), + ], + ), ), ), Expanded( diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart new file mode 100644 index 00000000..b4a50749 --- /dev/null +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -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 createState() => _HeaderControlState(); + + @override + Size get preferredSize => throw UnimplementedError(); +} + +class _HeaderControlState extends State { + late PlayUrlModel videoInfo; + List 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( + (Set 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 videoFormat = videoInfo.supportFormats!; + VideoQuality currentVideoQa = widget.videoDetailCtr!.currentVideoQa; + + /// 总质量分类 + int totalQaSam = videoFormat.length; + + /// 可用的质量分类 + int userfulQaSam = 0; + List video = videoInfo.dash!.video!; + Set 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 videoFormat = videoInfo.supportFormats!; + AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa; + + List 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(), + ), + ], + ), + ); + } +} diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 94b5354b..af44d4fc 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -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; @@ -177,7 +179,7 @@ class PlPlayerController { DataSource dataSource, { bool autoplay = true, // 默认不循环 - PlaylistMode looping = PlaylistMode.single, + PlaylistMode looping = PlaylistMode.none, // 初始化播放位置 Duration seekTo = Duration.zero, // 初始化播放速度 @@ -195,6 +197,7 @@ class PlPlayerController { _duration.value = duration ?? Duration.zero; // 初始化视频倍速 _playbackSpeed.value = speed; + // 初始化数据加载状态 dataStatus.status.value = DataStatus.loading; if (_videoPlayerController != null && @@ -202,10 +205,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); @@ -382,6 +387,8 @@ class PlPlayerController { position = Duration.zero; } _position.value = position; + print('seek 🌹duration : ${duration.value.inSeconds}'); + if (duration.value.inSeconds != 0) { // await _videoPlayerController!.stream.buffer.first; await _videoPlayerController?.seek(position); @@ -389,11 +396,13 @@ class PlPlayerController { // play(); // } } else { + print('🌹🌹'); _timerForSeek?.cancel(); _timerForSeek = Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { //_timerForSeek = null; if (duration.value.inSeconds != 0) { + print('🌹🌹🌹'); await _videoPlayerController?.seek(position); // if (playerStatus.stopped) { // play(); @@ -413,11 +422,11 @@ class PlPlayerController { /// 设置倍速 Future togglePlaybackSpeed() async { - List 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 allowedSpeeds = + PlaySpeed.values.map((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 +460,7 @@ class PlPlayerController { /// 更改播放状态 Future togglePlay() async { + feedBack(); if (playerStatus.playing) { pause(); } else { @@ -474,6 +484,7 @@ class PlPlayerController { } void onChangedSliderStart() { + feedBack(); _isSliderMoving = true; } @@ -600,6 +611,7 @@ class PlPlayerController { /// 关闭控制栏 void onCloseControl(bool val) { + feedBack(); _controlsClose.value = val; showControls.value = !val; } diff --git a/lib/plugin/pl_player/index.dart b/lib/plugin/pl_player/index.dart index cab7264e..997776f9 100644 --- a/lib/plugin/pl_player/index.dart +++ b/lib/plugin/pl_player/index.dart @@ -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'; diff --git a/lib/plugin/pl_player/models/play_speed.dart b/lib/plugin/pl_player/models/play_speed.dart new file mode 100644 index 00000000..01226ed5 --- /dev/null +++ b/lib/plugin/pl_player/models/play_speed.dart @@ -0,0 +1,37 @@ +enum PlaySpeed { + pointTwoFive, + pointFive, + pointSevenFive, + one, + onePointTwoFive, + onePointFive, + onePointSevenFive, + two +} + +extension PlaySpeedExtension on PlaySpeed { + static final List _descList = [ + '0.25倍', + '0.5倍', + '0.75倍', + '正常速度', + '1.25倍', + '1.5倍', + '1.75倍', + '2.0倍', + ]; + get description => _descList[index]; + + static final List _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]; +} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 854c7277..5709007c 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -10,12 +10,18 @@ import 'package:pilipala/utils/feed_back.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; -import 'widgets/header_control.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 createState() => _PLVideoPlayerState(); @@ -97,6 +103,7 @@ class _PLVideoPlayerState extends State onVerticalDragUpdate: (DragUpdateDetails details) {}, onVerticalDragEnd: (DragEndDetails details) {}), ), + // 头部、底部控制条 if (_.controlsEnabled) Obx( () => Column( @@ -107,7 +114,7 @@ class _PLVideoPlayerState extends State controller: animationController, visible: !_.controlsClose.value && _.showControls.value, position: 'top', - child: HeaderControl(controller: widget.controller), + child: widget.headerControl!, ), ), const Spacer(), @@ -166,7 +173,7 @@ class _PLVideoPlayerState extends State // print(details); // }, onSeek: (duration) { - print(duration); + feedBack(); _.onChangedSlider(duration.inSeconds.toDouble()); _.seekTo(duration); }, @@ -195,27 +202,28 @@ class _PLVideoPlayerState extends State ), ), // 锁 - Obx( - () => Align( - alignment: Alignment.centerLeft, - child: FractionalTranslation( - translation: const Offset(0.5, 0.0), - child: Visibility( - visible: _.showControls.value, - child: ComBtn( - icon: Icon( - _.controlsClose.value - ? FontAwesomeIcons.lock - : FontAwesomeIcons.lockOpen, - size: 15, - color: Colors.white, + 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( + _.controlsClose.value + ? FontAwesomeIcons.lock + : FontAwesomeIcons.lockOpen, + size: 15, + color: Colors.white, + ), + fuc: () => _.onCloseControl(!_.controlsClose.value), ), - fuc: () => _.onCloseControl(!_.controlsClose.value), ), ), ), ), - ), // Obx(() { if (_.dataStatus.loading || _.isBuffering.value) { diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index e1a50768..4f0c7a75 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -5,7 +5,6 @@ import 'package:get/get.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import '../utils.dart'; -import 'common_btn.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; @@ -67,21 +66,21 @@ 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, - ), - fuc: () => _.togglePlay(), - ), - ), - const SizedBox(width: 6), + // Obx( + // () => ComBtn( + // icon: Icon( + // _.playerStatus.paused + // ? FontAwesomeIcons.play + // : _.playerStatus.playing + // ? FontAwesomeIcons.pause + // : FontAwesomeIcons.rotateRight, + // size: 15, + // color: Colors.white, + // ), + // fuc: () => _.togglePlay(), + // ), + // ), + // const SizedBox(width: 6), // 播放时间 Obx(() { return Text( @@ -104,33 +103,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( diff --git a/lib/plugin/pl_player/widgets/header_control.dart b/lib/plugin/pl_player/widgets/header_control.dart deleted file mode 100644 index ade38f80..00000000 --- a/lib/plugin/pl_player/widgets/header_control.dart +++ /dev/null @@ -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(), - ), - ], - ), - ); - } -}