feat: pgc skip

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-10-08 21:22:02 +08:00
parent 06d8296939
commit ca0eb1716f
13 changed files with 232 additions and 94 deletions

View File

@@ -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'])

View File

@@ -10,11 +10,10 @@ class SegmentModel {
required this.segmentType,
required this.segment,
required this.skipType,
this.hasSkipped = false,
});
String UUID;
SegmentType segmentType;
Pair<int, int> segment;
SkipType skipType;
bool hasSkipped;
late bool hasSkipped = false;
}

View File

@@ -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<SegmentItemModel>? clipInfoList;
PlayUrlModel.fromJson(Map<String, dynamic> 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();
}
}
}

View File

@@ -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<String, dynamic> 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,
);
}

View File

@@ -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<SettingsModel> 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<SkipType>(
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<SettingsModel> 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<SettingsModel> 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: '评论关键词过滤',

View File

@@ -142,6 +142,7 @@ class _AiDetailState extends State<AiConclusionPanel>
tag: Get.arguments['heroTag'],
).plPlayerController.seekTo(
Duration(seconds: item.timestamp!),
isSeek: false,
);
} catch (_) {}
},

View File

@@ -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<void> _querySponsorBlock() async {
positionSubscription?.cancel();
positionSubscription = null;
videoLabel.value = '';
segmentList.clear();
segmentProgressList.clear();
@@ -695,9 +697,10 @@ class VideoDetailController extends GetxController
}
}
void handleSBData(List<SegmentItemModel> list) {
Future<void> handleSBData(List<SegmentItemModel> 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<void> onSkip(SegmentModel item, [bool isSkip = true]) async {
Future<void> 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;
}

View File

@@ -130,11 +130,10 @@ class _ViewPointsPageState extends State<ViewPointsPage>
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(

View File

@@ -684,6 +684,7 @@ class HeaderControlState extends State<HeaderControl> {
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<HeaderControl> {
final int quality = i.id!;
final newQa = AudioQuality.fromCode(quality);
videoDetailCtr
..plPlayerController.cacheAudioQa = newQa.code
..currentAudioQa = newQa
..updatePlayer();

View File

@@ -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<Color> blockColor = Pref.blockColor;
@@ -1253,14 +1255,12 @@ class PlPlayerController {
/// 暂停播放
Future<void> 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);
}
}

View File

@@ -682,6 +682,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
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<PLVideoPlayer>
Obx(() {
if (plPlayerController.dataStatus.loading ||
plPlayerController.isBuffering.value) {
(plPlayerController.isBuffering.value &&
plPlayerController.playerStatus.playing)) {
return Center(
child: GestureDetector(
onTap: plPlayerController.refreshPlayer,

View File

@@ -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',

View File

@@ -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];
}