From ca0eb1716f673def880bff5afca61f90f552c389 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Wed, 8 Oct 2025 21:22:02 +0800 Subject: [PATCH] feat: pgc skip Signed-off-by: bggRGjQaUbCoE --- lib/http/video.dart | 2 + .../common/sponsor_block/segment_model.dart | 3 +- lib/models/video/play/url.dart | 21 +++ .../sponsor_block/segment_item.dart | 19 ++ lib/pages/setting/models/extra_settings.dart | 65 ++++++- lib/pages/video/ai_conclusion/view.dart | 1 + lib/pages/video/controller.dart | 177 +++++++++++------- lib/pages/video/view_point/view.dart | 9 +- lib/pages/video/widgets/header_control.dart | 2 + lib/plugin/pl_player/controller.dart | 16 +- lib/plugin/pl_player/view.dart | 4 +- lib/utils/storage_key.dart | 3 +- lib/utils/storage_pref.dart | 4 + 13 files changed, 232 insertions(+), 94 deletions(-) diff --git a/lib/http/video.dart b/lib/http/video.dart index 0d9f91c44..ea70f6aff 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -229,11 +229,13 @@ class VideoHttp { switch (videoType) { case VideoType.ugc: data = PlayUrlModel.fromJson(res.data['data']); + case VideoType.pugv: var result = res.data['data']; data = PlayUrlModel.fromJson(result) ..lastPlayTime = result?['play_view_business_info']?['user_status']?['watch_progress']?['current_watch_progress']; + case VideoType.pgc: var result = res.data['result']; data = PlayUrlModel.fromJson(result['video_info']) diff --git a/lib/models/common/sponsor_block/segment_model.dart b/lib/models/common/sponsor_block/segment_model.dart index 5258ab4a5..1468cc40e 100644 --- a/lib/models/common/sponsor_block/segment_model.dart +++ b/lib/models/common/sponsor_block/segment_model.dart @@ -10,11 +10,10 @@ class SegmentModel { required this.segmentType, required this.segment, required this.skipType, - this.hasSkipped = false, }); String UUID; SegmentType segmentType; Pair segment; SkipType skipType; - bool hasSkipped; + late bool hasSkipped = false; } diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index 65e6590fd..8639b565d 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -2,6 +2,7 @@ import 'dart:math' show max, min; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; +import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart'; import 'package:PiliPlus/utils/extension.dart'; class PlayUrlModel { @@ -44,6 +45,7 @@ class PlayUrlModel { int? lastPlayCid; String? curLanguage; Language? language; + List? clipInfoList; PlayUrlModel.fromJson(Map json) { from = json['from']; @@ -72,6 +74,25 @@ class PlayUrlModel { language = json['language'] == null ? null : Language.fromJson(json['language']); + // debug + // final clipInfoList = [ + // { + // "start": 0, + // "end": 150, + // "clipType": "CLIP_TYPE_OP", + // }, + // { + // "start": timeLength! ~/ 1000 - 150, + // "end": timeLength! ~/ 1000, + // "clipType": "CLIP_TYPE_ED", + // }, + // ]; + final List? clipInfoList = json['clip_info_list']; + if (clipInfoList != null && clipInfoList.isNotEmpty) { + this.clipInfoList = clipInfoList + .map((e) => SegmentItemModel.fromPgcJson(e, timeLength)) + .toList(); + } } } diff --git a/lib/models_new/sponsor_block/segment_item.dart b/lib/models_new/sponsor_block/segment_item.dart index 0c280da15..359db7931 100644 --- a/lib/models_new/sponsor_block/segment_item.dart +++ b/lib/models_new/sponsor_block/segment_item.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; + class SegmentItemModel { String? cid; String category; @@ -28,4 +30,21 @@ class SegmentItemModel { ? null : (json["videoDuration"] as num) * 1000, ); + + factory SegmentItemModel.fromPgcJson( + Map json, + num? videoDuration, + ) => SegmentItemModel( + category: switch (json['clipType']) { + 'CLIP_TYPE_OP' => SegmentType.intro.name, + 'CLIP_TYPE_ED' => SegmentType.outro.name, + _ => SegmentType.sponsor.name, + }, + segment: [ + ((json['start'] as num) * 1000).round(), + ((json['end'] as num) * 1000).round(), + ], + uuid: '', + videoDuration: videoDuration, + ); } diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index 7c5d5de7a..683005848 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -13,6 +13,7 @@ import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart'; import 'package:PiliPlus/models/common/member/tab_type.dart'; import 'package:PiliPlus/models/common/reply/reply_sort_type.dart'; import 'package:PiliPlus/models/common/settings_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; @@ -71,6 +72,54 @@ List get extraSettings => [ ], ), ), + SettingsModel( + settingsType: SettingsType.normal, + leading: const Icon(MdiIcons.debugStepOver), + title: '番剧片头/片尾跳过类型', + getTrailing: () => Builder( + builder: (context) { + final pgcSkipType = Pref.pgcSkipType; + final colorScheme = ColorScheme.of(context); + final color = pgcSkipType == SkipType.disable + ? colorScheme.outline + : colorScheme.secondary; + return PopupMenuButton( + initialValue: pgcSkipType, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + pgcSkipType.title, + style: TextStyle(fontSize: 14, height: 1, color: color), + strutStyle: const StrutStyle( + leading: 0, + height: 1, + fontSize: 14, + ), + ), + Icon( + MdiIcons.unfoldMoreHorizontal, + size: MediaQuery.textScalerOf(context).scale(14), + color: color, + ), + ], + ), + ), + onSelected: (value) async { + await GStorage.setting.put(SettingBoxKey.pgcSkipType, value.index); + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + }, + itemBuilder: (context) => SkipType.values + .map((e) => PopupMenuItem(value: e, child: Text(e.title))) + .toList(), + ); + }, + ), + ), SettingsModel( settingsType: SettingsType.sw1tch, title: '检查未读动态', @@ -185,6 +234,14 @@ List get extraSettings => [ setKey: SettingBoxKey.horizontalMemberPage, defaultVal: false, ), + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '横屏在侧栏打开图片预览', + leading: const Icon(Icons.photo_outlined), + setKey: SettingBoxKey.horizontalPreview, + defaultVal: false, + onChanged: (value) => CustomGridView.horizontalPreview = value, + ), SettingsModel( settingsType: SettingsType.normal, title: '评论折叠行数', @@ -331,14 +388,6 @@ List get extraSettings => [ setKey: SettingBoxKey.continuePlayingPart, defaultVal: true, ), - SettingsModel( - settingsType: SettingsType.sw1tch, - title: '横屏在侧栏打开图片预览', - leading: const Icon(Icons.photo_outlined), - setKey: SettingBoxKey.horizontalPreview, - defaultVal: false, - onChanged: (value) => CustomGridView.horizontalPreview = value, - ), getBanwordModel( context: Get.context!, title: '评论关键词过滤', diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index e96806492..737c25194 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -142,6 +142,7 @@ class _AiDetailState extends State tag: Get.arguments['heroTag'], ).plPlayerController.seekTo( Duration(seconds: item.timestamp!), + isSeek: false, ); } catch (_) {} }, diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 7b94c16bc..89a715dea 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -586,7 +586,7 @@ class VideoDetailController extends GetxController (item) => ListTile( onTap: () { Get.back(); - _showVoteDialog(context, item); + if (isUgc) _showVoteDialog(context, item); }, dense: true, title: Text.rich( @@ -635,7 +635,8 @@ class VideoDetailController extends GetxController Get.back(); onSkip( item, - item.skipType != SkipType.showOnly, + isSkip: item.skipType != SkipType.showOnly, + isSeek: false, ); }, style: IconButton.styleFrom( @@ -677,6 +678,7 @@ class VideoDetailController extends GetxController Future _querySponsorBlock() async { positionSubscription?.cancel(); + positionSubscription = null; videoLabel.value = ''; segmentList.clear(); segmentProgressList.clear(); @@ -695,9 +697,10 @@ class VideoDetailController extends GetxController } } - void handleSBData(List list) { + Future handleSBData(List list) async { if (list.isNotEmpty) { try { + Completer? completer; final duration = list.first.videoDuration ?? data.timeLength!; // segmentList segmentList.addAll( @@ -714,15 +717,20 @@ class VideoDetailController extends GetxController videoLabel.value += '${videoLabel.value.isNotEmpty ? '/' : ''}${segmentType.title}'; } - var skipType = plPlayerController - .blockSettings[segmentType.index] - .second; - if (skipType != SkipType.showOnly) { - if (item.segment[1] == item.segment[0] || - item.segment[1] - item.segment[0] < - plPlayerController.blockLimit) { - skipType = SkipType.showOnly; + SkipType skipType; + if (isUgc) { + skipType = plPlayerController + .blockSettings[segmentType.index] + .second; + if (skipType != SkipType.showOnly) { + if (item.segment[1] == item.segment[0] || + item.segment[1] - item.segment[0] < + plPlayerController.blockLimit) { + skipType = SkipType.showOnly; + } } + } else { + skipType = Pref.pgcSkipType; } final segmentModel = SegmentModel( @@ -739,32 +747,44 @@ class VideoDetailController extends GetxController autoPlay.value && plPlayerController.videoPlayerController != null) { final currPost = + defaultST?.inMilliseconds ?? plPlayerController.position.value.inMilliseconds; + if (currPost >= segmentModel.segment.first && currPost < segmentModel.segment.second) { _lastPos = currPost; - if (segmentModel.skipType == SkipType.alwaysSkip) { - plPlayerController - .videoPlayerController! - .stream - .buffer - .first - .whenComplete(() { - onSkip(segmentModel); + + switch (segmentModel.skipType) { + case SkipType.alwaysSkip: + case SkipType.skipOnce: + segmentModel.hasSkipped = true; + completer = Completer(); + final videoPlayerController = + plPlayerController.videoPlayerController!; + if (videoPlayerController.state.playing) { + onSkip( + segmentModel, + ).whenComplete(completer!.complete); + } else { + videoPlayerController.stream.playing.firstWhere(( + e, + ) { + if (e) { + onSkip( + segmentModel, + ).whenComplete(completer!.complete); + return true; + } + return false; }); - } else if (segmentModel.skipType == SkipType.skipOnce) { - segmentModel.hasSkipped = true; - plPlayerController - .videoPlayerController! - .stream - .buffer - .first - .whenComplete(() { - onSkip(segmentModel); - }); - } else if (segmentModel.skipType == - SkipType.skipManually) { - onAddItem(segmentModel); + } + + break; + case SkipType.skipManually: + onAddItem(segmentModel); + break; + default: + break; } } } @@ -785,6 +805,7 @@ class VideoDetailController extends GetxController if (positionSubscription == null && (autoPlay.value || plPlayerController.preInitPlayer)) { + await completer?.future; initSkip(); } } catch (e) { @@ -815,14 +836,21 @@ class VideoDetailController extends GetxController // } if (msPos <= item.segment.first && item.segment.first <= msPos + 1000) { - if (item.skipType == SkipType.alwaysSkip) { - onSkip(item); - } else if (item.skipType == SkipType.skipOnce && - !item.hasSkipped) { - item.hasSkipped = true; - onSkip(item); - } else if (item.skipType == SkipType.skipManually) { - onAddItem(item); + switch (item.skipType) { + case SkipType.alwaysSkip: + onSkip(item, isSeek: false); + break; + case SkipType.skipOnce: + if (!item.hasSkipped) { + item.hasSkipped = true; + onSkip(item, isSeek: false); + } + break; + case SkipType.skipManually: + onAddItem(item); + break; + default: + break; } break; } @@ -909,7 +937,7 @@ class VideoDetailController extends GetxController } onRemoveItem(listData.indexOf(item), item); } else if (item is SegmentModel) { - onSkip(item); + onSkip(item, isSeek: false); onRemoveItem(listData.indexOf(item), item); } }, @@ -920,17 +948,21 @@ class VideoDetailController extends GetxController ); } - Future onSkip(SegmentModel item, [bool isSkip = true]) async { + Future onSkip( + SegmentModel item, { + bool isSkip = true, + bool isSeek = true, + }) async { try { - plPlayerController.danmakuController?.clear(); - await plPlayerController.videoPlayerController?.seek( + await plPlayerController.seekTo( Duration(milliseconds: item.segment.second), + isSeek: isSeek, ); if (isSkip) { if (Pref.blockToast) { _showBlockToast('已跳过${item.segmentType.shortTitle}片段'); } - if (Pref.blockTrack) { + if (isUgc && Pref.blockTrack) { Request().post( '$blockServer/api/viewedVideoSponsorTime', queryParameters: {'UUID': item.UUID}, @@ -1031,9 +1063,10 @@ class VideoDetailController extends GetxController void updatePlayer() { autoPlay.value = true; playedTime = plPlayerController.position.value; - plPlayerController.removeListeners(); - plPlayerController.isBuffering.value = false; - plPlayerController.buffered.value = Duration.zero; + plPlayerController + ..removeListeners() + ..isBuffering.value = false + ..buffered.value = Duration.zero; final video = findVideoByQa(currentVideoQa.value.code); if (firstVideo.codecs != video.codecs) { @@ -1142,7 +1175,9 @@ class VideoDetailController extends GetxController return; } isQuerying = true; - if (plPlayerController.enableSponsorBlock && !fromReset) { + if (plPlayerController.enableSponsorBlock && + (isUgc || !plPlayerController.enablePgcSkip) && + !fromReset) { _querySponsorBlock(); } if (plPlayerController.cacheVideoQa == null) { @@ -1174,6 +1209,26 @@ class VideoDetailController extends GetxController volume = data.volume; + final progress = args['progress']; + if (progress != null) { + this.defaultST = Duration(milliseconds: progress); + args['progress'] = null; + } else { + this.defaultST = + defaultST ?? + (data.lastPlayTime == null + ? Duration.zero + : Duration(milliseconds: data.lastPlayTime!)); + } + + if (!isUgc && !fromReset && plPlayerController.enablePgcSkip) { + if (data.clipInfoList case final clipInfoList?) { + positionSubscription?.cancel(); + positionSubscription = null; + handleSBData(clipInfoList); + } + } + if (data.acceptDesc?.contains('试看') == true) { SmartDialog.showToast( '该视频为专属视频,仅提供试看', @@ -1183,13 +1238,7 @@ class VideoDetailController extends GetxController if (data.dash == null && data.durl != null) { videoUrl = data.durl!.first.url!; audioUrl = ''; - final progress = args['progress']; - if (progress != null) { - this.defaultST = Duration(milliseconds: progress); - args['progress'] = null; - } else { - this.defaultST = defaultST ?? Duration.zero; - } + // 实际为FLV/MP4格式,但已被淘汰,这里仅做兜底处理 firstVideo = VideoItem( id: data.quality!, @@ -1305,18 +1354,6 @@ class VideoDetailController extends GetxController firstAudio = AudioItem(); audioUrl = ''; } - // - final progress = args['progress']; - if (progress != null) { - this.defaultST = Duration(milliseconds: progress); - args['progress'] = null; - } else { - this.defaultST = - defaultST ?? - (data.lastPlayTime == null - ? Duration.zero - : Duration(milliseconds: data.lastPlayTime!)); - } if (autoPlay.value || plPlayerController.preInitPlayer) { await playerInit(); } @@ -1578,6 +1615,7 @@ class VideoDetailController extends GetxController void onReset({bool isStein = false}) { playedTime = null; + defaultST = null; videoUrl = null; audioUrl = null; @@ -1609,6 +1647,7 @@ class VideoDetailController extends GetxController // sponsor block if (plPlayerController.enableSponsorBlock) { + _lastPos = null; positionSubscription?.cancel(); positionSubscription = null; videoLabel.value = ''; @@ -1699,7 +1738,7 @@ class VideoDetailController extends GetxController try { if (plPlayerController.enableSponsorBlock) { if (listData.lastOrNull case SegmentModel item) { - onSkip(item); + onSkip(item, isSeek: false); onRemoveItem(listData.indexOf(item), item); return true; } diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index a70017324..e8be3a6ab 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -130,11 +130,10 @@ class _ViewPointsPageState extends State onTap: segment.from != null ? () { Get.back(); - plPlayerController - ?..danmakuController?.clear() - ..videoPlayerController?.seek( - Duration(seconds: segment.from!), - ); + plPlayerController?.seekTo( + Duration(seconds: segment.from!), + isSeek: false, + ); } : null, child: Padding( diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 149d9fda3..aef001108 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -684,6 +684,7 @@ class HeaderControlState extends State { final int quality = item.quality!; final newQa = VideoQuality.fromCode(quality); videoDetailCtr + ..plPlayerController.cacheVideoQa = newQa.code ..currentVideoQa.value = newQa ..updatePlayer(); @@ -762,6 +763,7 @@ class HeaderControlState extends State { final int quality = i.id!; final newQa = AudioQuality.fromCode(quality); videoDetailCtr + ..plPlayerController.cacheAudioQa = newQa.code ..currentAudioQa = newQa ..updatePlayer(); diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index eaa21328d..5ddefcfeb 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -361,8 +361,10 @@ class PlPlayerController { late double subtitleStrokeWidth = Pref.subtitleStrokeWidth; late int subtitleFontWeight = Pref.subtitleFontWeight; + late final pgcSkipType = Pref.pgcSkipType; + late final enablePgcSkip = Pref.pgcSkipType != SkipType.disable; // sponsor block - late final bool enableSponsorBlock = Pref.enableSponsorBlock; + late final bool enableSponsorBlock = Pref.enableSponsorBlock || enablePgcSkip; late final double blockLimit = Pref.blockLimit; late final blockSettings = Pref.blockSettings; late final List blockColor = Pref.blockColor; @@ -1253,14 +1255,12 @@ class PlPlayerController { /// 暂停播放 Future pause({bool notify = true, bool isInterrupt = false}) async { - if (videoPlayerController?.state.playing ?? false) { - await _videoPlayerController?.playOrPause(); - playerStatus.status.value = PlayerStatus.paused; + await _videoPlayerController?.pause(); + playerStatus.status.value = PlayerStatus.paused; - // 主动暂停时让出音频焦点 - if (!isInterrupt) { - audioSessionHandler?.setActive(false); - } + // 主动暂停时让出音频焦点 + if (!isInterrupt) { + audioSessionHandler?.setActive(false); } } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index b6bb7fa84..248c4a52c 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -682,6 +682,7 @@ class _PLVideoPlayerState extends State final int quality = item.quality!; final newQa = VideoQuality.fromCode(quality); videoDetailController + ..plPlayerController.cacheVideoQa = newQa.code ..currentVideoQa.value = newQa ..updatePlayer(); @@ -1756,7 +1757,8 @@ class _PLVideoPlayerState extends State Obx(() { if (plPlayerController.dataStatus.loading || - plPlayerController.isBuffering.value) { + (plPlayerController.isBuffering.value && + plPlayerController.playerStatus.playing)) { return Center( child: GestureDetector( onTap: plPlayerController.refreshPlayer, diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 4fb76125d..8ec40a60d 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -26,7 +26,8 @@ abstract class SettingBoxKey { enableOnlineTotal = 'enableOnlineTotal', showSuperChat = 'showSuperChat', keyboardControl = 'keyboardControl', - pauseOnMinimize = 'pauseOnMinimize'; + pauseOnMinimize = 'pauseOnMinimize', + pgcSkipType = 'pgcSkipType'; static const String enableVerticalExpand = 'enableVerticalExpand', feedBackEnable = 'feedBackEnable', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 060523092..3e2fa91af 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -847,4 +847,8 @@ abstract class Pref { static double get desktopVolume => _setting.get(SettingBoxKey.desktopVolume, defaultValue: 1.0); + + static SkipType get pgcSkipType => + SkipType.values[_setting.get(SettingBoxKey.pgcSkipType) ?? + SkipType.skipOnce.index]; }