refa: subtitle

Closes #553

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-03-28 15:56:45 +08:00
parent 5655e6ccdf
commit f36f8d69fc
8 changed files with 98 additions and 149 deletions

View File

@@ -1005,23 +1005,18 @@ class VideoHttp {
*/
return {
'status': true,
'data': data['subtitle']['subtitles'],
'subtitles': data['subtitle']['subtitles'],
'view_points': data['view_points'],
// 'last_play_time': data['last_play_time'],
'last_play_cid': data['last_play_cid'],
'interaction': data['interaction'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
static Future vttSubtitles(List subtitlesJson) async {
if (subtitlesJson.isEmpty) {
return [];
}
List<Map<String, String>> subtitlesVtt = [];
static Future vttSubtitles(subtile) async {
String subtitleTimecode(num seconds) {
int h = (seconds / 3600).floor();
int m = ((seconds % 3600) / 60).floor();
@@ -1039,56 +1034,12 @@ class VideoHttp {
});
}
for (var i in subtitlesJson) {
var res =
await Request().get("https://${i['subtitle_url'].split('//')[1]}");
/*
{
"font_size": 0.4,
"font_color": "#FFFFFF",
"background_alpha": 0.5,
"background_color": "#9C27B0",
"Stroke": "none",
"type": "AIsubtitle",
"lang": "zh",
"version": "v1.6.0.4",
"body": [
{
"from": 0.5,
"to": 1.58,
"sid": 1,
"location": 2,
"content": "很多人可能不知道",
"music": 0.0
},
……,
{
"from": 558.629,
"to": 560.22,
"sid": 280,
"location": 2,
"content": "我们下期再见",
"music": 0.0
}
]
}
*/
if (res.data != null && res.data?['body'] is List) {
String vttData = await compute(processList, res.data['body'] as List);
subtitlesVtt.add({
'language': i['lan'],
'title': i['lan_doc'],
'text': vttData,
});
} else {
// SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}");
debugPrint('字幕${i['lan_doc']}加载失败, ${res.data['message']}');
}
var res = await Request().get("https:${subtile['subtitle_url']}");
if (res.data?['body'] is List) {
return await compute(processList, res.data['body'] as List);
}
if (subtitlesVtt.isNotEmpty) {
subtitlesVtt.insert(0, {'language': '', 'title': '关闭字幕', 'text': ""});
}
return subtitlesVtt;
return null;
}
// 视频排行

View File

@@ -11,7 +11,7 @@ extension SubtitlePreferenceDesc on SubtitlePreference {
extension SubtitlePreferenceCode on SubtitlePreference {
static final List<String> _codeList = ['off', 'on', 'withoutAi'];
get code => _codeList[index];
String get code => _codeList[index];
static SubtitlePreference? fromCode(String code) {
final index = _codeList.indexOf(code);

View File

@@ -41,6 +41,7 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/video_utils.dart';
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
@@ -1068,8 +1069,6 @@ class VideoDetailController extends GetxController
),
segmentList: segmentProgressList,
viewPointList: viewPointList,
vttSubtitles: _vttSubtitles,
vttSubtitlesIndex: vttSubtitlesIndex,
showVP: showVP,
dmTrend: dmTrend,
// 硬解
@@ -1092,12 +1091,13 @@ class VideoDetailController extends GetxController
if (videoState.value is! Success) {
videoState.value = LoadingState.success(null);
}
setSubtitle(vttSubtitlesIndex.value);
},
);
initSkip();
if (vttSubtitlesIndex == null) {
if (vttSubtitlesIndex.value == -1) {
_getSubtitle();
}
@@ -1358,43 +1358,47 @@ class VideoDetailController extends GetxController
}
}
dynamic subtitles;
late List<Map<String, String>> _vttSubtitles = <Map<String, String>>[];
int? vttSubtitlesIndex;
RxList subtitles = [].obs;
late final Map<int, String> _vttSubtitles = {};
late final RxInt vttSubtitlesIndex = (-1).obs;
late bool showVP = true;
void _getSubtitle() {
_vttSubtitles.clear();
viewPointList.clear();
_querySubtitles().then((value) {
if (_vttSubtitles.isNotEmpty) {
String preference = setting.get(
SettingBoxKey.subtitlePreference,
defaultValue: SubtitlePreference.values.first.code,
);
vttSubtitlesIndex = 0;
if (preference == 'on') {
vttSubtitlesIndex = 1;
} else if (preference == 'withoutAi') {
for (int i = 1; i < _vttSubtitles.length; i++) {
if (_vttSubtitles[i]['language']!.startsWith('ai')) {
continue;
}
vttSubtitlesIndex = i;
break;
}
}
if (plPlayerController.vttSubtitles.isEmpty) {
plPlayerController.vttSubtitles.value = _vttSubtitles;
if (vttSubtitlesIndex != null) {
plPlayerController.vttSubtitlesIndex.value = vttSubtitlesIndex!;
if (vttSubtitlesIndex != 0) {
plPlayerController.setSubtitle(vttSubtitlesIndex!);
}
}
}
_querySubtitles();
}
// 设定字幕轨道
setSubtitle(int index) async {
if (index <= 0) {
plPlayerController.videoPlayerController
?.setSubtitleTrack(SubtitleTrack.no());
vttSubtitlesIndex.value = index;
return;
}
void setSub(subtitle) {
plPlayerController.videoPlayerController?.setSubtitleTrack(
SubtitleTrack.data(
subtitle,
title: subtitles[index - 1]['lan_doc'],
language: subtitles[index - 1]['lan'],
),
);
vttSubtitlesIndex.value = index;
}
String? subtitle = _vttSubtitles[index - 1];
if (subtitle != null) {
setSub(subtitle);
} else {
var result = await VideoHttp.vttSubtitles(subtitles[index - 1]);
if (result != null) {
_vttSubtitles[index - 1] = result;
setSub(result);
}
});
}
}
// interactive video
@@ -1484,15 +1488,26 @@ class VideoDetailController extends GetxController
}
}
if (res["data"] is List && res["data"].isNotEmpty) {
subtitles = res["data"];
var result = await VideoHttp.vttSubtitles(res["data"]);
if (result != null) {
_vttSubtitles = result;
if (res["subtitles"] is List && res["subtitles"].isNotEmpty) {
vttSubtitlesIndex.value = 0;
subtitles.value = res["subtitles"];
String preference = setting.get(
SettingBoxKey.subtitlePreference,
defaultValue: SubtitlePreference.values.first.code,
);
if (preference == 'on') {
vttSubtitlesIndex.value = 1;
} else if (preference == 'withoutAi') {
for (int i = 0; i < subtitles.length; i++) {
if (subtitles[i]['lan']!.startsWith('ai')) {
continue;
}
vttSubtitlesIndex.value = i + 1;
break;
}
}
// if (_vttSubtitles.isEmpty) {
// SmartDialog.showToast('字幕均加载失败');
// }
setSubtitle(vttSubtitlesIndex.value);
}
}
}
@@ -1556,8 +1571,8 @@ class VideoDetailController extends GetxController
savedDanmaku = null;
// subtitle
subtitles = null;
vttSubtitlesIndex = null;
subtitles.clear();
vttSubtitlesIndex.value = -1;
_vttSubtitles.clear();
// view point

View File

@@ -410,8 +410,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// }
if (plPlayerController != null) {
videoDetailController.makeHeartBeat();
videoDetailController.vttSubtitlesIndex =
plPlayerController!.vttSubtitlesIndex.value;
videoDetailController.showVP = plPlayerController!.showVP.value;
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.removePositionListener(positionListener);

View File

@@ -449,8 +449,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
// }
if (plPlayerController != null) {
videoDetailController.makeHeartBeat();
videoDetailController.vttSubtitlesIndex =
plPlayerController!.vttSubtitlesIndex.value;
videoDetailController.showVP = plPlayerController!.showVP.value;
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.removePositionListener(positionListener);

View File

@@ -401,8 +401,7 @@ class HeaderControlState extends State<HeaderControl> {
leading: const Icon(Icons.subtitles_outlined, size: 20),
title: const Text('字幕设置', style: titleStyle),
),
if (videoDetailCtr.subtitles is List &&
videoDetailCtr.subtitles.isNotEmpty)
if (videoDetailCtr.subtitles.isNotEmpty)
ListTile(
dense: true,
onTap: () => {Get.back(), onExportSubtitle()},
@@ -1103,7 +1102,7 @@ class HeaderControlState extends State<HeaderControl> {
title: const Text('保存字幕'),
content: SingleChildScrollView(
child: Column(
children: (videoDetailCtr.subtitles as List)
children: videoDetailCtr.subtitles
.map(
(item) => ListTile(
dense: true,

View File

@@ -111,9 +111,6 @@ class PlPlayerController {
bool _enableHeart = true;
late DataSource dataSource;
// 视频字幕
final RxList<Map<String, String>> vttSubtitles = <Map<String, String>>[].obs;
final RxInt vttSubtitlesIndex = 0.obs;
Timer? _timer;
Timer? _timerForSeek;
@@ -471,8 +468,6 @@ class PlPlayerController {
DataSource dataSource, {
List<Segment>? segmentList,
List<Segment>? viewPointList,
List<Map<String, String>>? vttSubtitles,
int? vttSubtitlesIndex,
bool? showVP,
List? dmTrend,
bool autoplay = true,
@@ -504,8 +499,6 @@ class PlPlayerController {
this.dataSource = dataSource;
this.segmentList.value = segmentList ?? <Segment>[];
this.viewPointList.value = viewPointList ?? <Segment>[];
this.vttSubtitles.value = vttSubtitles ?? <Map<String, String>>[];
this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0;
this.showVP.value = showVP ?? true;
this.dmTrend.value = dmTrend ?? [];
_autoPlay = autoplay;
@@ -555,7 +548,6 @@ class PlPlayerController {
startListeners();
}
await _initializePlayer();
setSubtitle(this.vttSubtitlesIndex.value);
} catch (err, stackTrace) {
dataStatus.status.value = DataStatus.error;
debugPrint(stackTrace.toString());
@@ -1574,22 +1566,6 @@ class PlPlayerController {
}
}
// 设定字幕轨道
setSubtitle(int index) {
if (index == 0) {
_videoPlayerController?.setSubtitleTrack(SubtitleTrack.no());
vttSubtitlesIndex.value = 0;
return;
}
Map<String, String> s = vttSubtitles[index];
_videoPlayerController?.setSubtitleTrack(SubtitleTrack.data(
s['text']!,
title: s['title']!,
language: s['language']!,
));
vttSubtitlesIndex.value = index;
}
static void updatePlayCount() {
if (_instance?._playerCount.value == 1) {
_instance?.dispose();

View File

@@ -550,40 +550,52 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 字幕
BottomControlType.subtitle: Obx(
() => plPlayerController.vttSubtitles.isEmpty
() => widget.videoDetailController?.subtitles.isEmpty == true
? const SizedBox.shrink()
: SizedBox(
width: widgetWidth,
height: 30,
child: PopupMenuButton<int>(
initialValue: plPlayerController.vttSubtitles.length <
plPlayerController.vttSubtitlesIndex.value
? 0
: plPlayerController.vttSubtitlesIndex.value,
initialValue: widget
.videoDetailController!.vttSubtitlesIndex.value
.clamp(0, widget.videoDetailController!.subtitles.length),
color: Colors.black.withOpacity(0.8),
itemBuilder: (BuildContext context) {
return plPlayerController.vttSubtitles
.asMap()
.entries
.map((entry) {
return PopupMenuItem<int>(
value: entry.key,
return [
PopupMenuItem<int>(
value: 0,
onTap: () {
plPlayerController.setSubtitle(entry.key);
widget.videoDetailController!.setSubtitle(0);
},
child: Text(
"${entry.value['title']}",
"关闭字幕",
style: const TextStyle(color: Colors.white),
),
);
}).toList();
),
...widget.videoDetailController!.subtitles
.asMap()
.entries
.map((entry) {
return PopupMenuItem<int>(
value: entry.key + 1,
onTap: () {
widget.videoDetailController!
.setSubtitle(entry.key + 1);
},
child: Text(
"${entry.value['lan_doc']}",
style: const TextStyle(color: Colors.white),
),
);
})
];
},
child: Container(
width: 35,
height: 30,
alignment: Alignment.center,
child: Icon(
plPlayerController.vttSubtitlesIndex.value == 0
widget.videoDetailController!.vttSubtitlesIndex.value == 0
? Icons.closed_caption_off_outlined
: Icons.closed_caption_off_rounded,
size: 22,