mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
refa: subtitle
Closes #553 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -1005,23 +1005,18 @@ class VideoHttp {
|
|||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
'status': true,
|
'status': true,
|
||||||
'data': data['subtitle']['subtitles'],
|
'subtitles': data['subtitle']['subtitles'],
|
||||||
'view_points': data['view_points'],
|
'view_points': data['view_points'],
|
||||||
// 'last_play_time': data['last_play_time'],
|
// 'last_play_time': data['last_play_time'],
|
||||||
'last_play_cid': data['last_play_cid'],
|
'last_play_cid': data['last_play_cid'],
|
||||||
'interaction': data['interaction'],
|
'interaction': data['interaction'],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
return {'status': false, 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future vttSubtitles(List subtitlesJson) async {
|
static Future vttSubtitles(subtile) async {
|
||||||
if (subtitlesJson.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
List<Map<String, String>> subtitlesVtt = [];
|
|
||||||
|
|
||||||
String subtitleTimecode(num seconds) {
|
String subtitleTimecode(num seconds) {
|
||||||
int h = (seconds / 3600).floor();
|
int h = (seconds / 3600).floor();
|
||||||
int m = ((seconds % 3600) / 60).floor();
|
int m = ((seconds % 3600) / 60).floor();
|
||||||
@@ -1039,56 +1034,12 @@ class VideoHttp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i in subtitlesJson) {
|
var res = await Request().get("https:${subtile['subtitle_url']}");
|
||||||
var res =
|
|
||||||
await Request().get("https://${i['subtitle_url'].split('//')[1]}");
|
if (res.data?['body'] is List) {
|
||||||
/*
|
return await compute(processList, res.data['body'] as List);
|
||||||
{
|
|
||||||
"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']}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (subtitlesVtt.isNotEmpty) {
|
return null;
|
||||||
subtitlesVtt.insert(0, {'language': '', 'title': '关闭字幕', 'text': ""});
|
|
||||||
}
|
|
||||||
return subtitlesVtt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频排行
|
// 视频排行
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension SubtitlePreferenceDesc on SubtitlePreference {
|
|||||||
|
|
||||||
extension SubtitlePreferenceCode on SubtitlePreference {
|
extension SubtitlePreferenceCode on SubtitlePreference {
|
||||||
static final List<String> _codeList = ['off', 'on', 'withoutAi'];
|
static final List<String> _codeList = ['off', 'on', 'withoutAi'];
|
||||||
get code => _codeList[index];
|
String get code => _codeList[index];
|
||||||
|
|
||||||
static SubtitlePreference? fromCode(String code) {
|
static SubtitlePreference? fromCode(String code) {
|
||||||
final index = _codeList.indexOf(code);
|
final index = _codeList.indexOf(code);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import 'package:PiliPlus/utils/utils.dart';
|
|||||||
import 'package:PiliPlus/utils/video_utils.dart';
|
import 'package:PiliPlus/utils/video_utils.dart';
|
||||||
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
|
||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
@@ -1068,8 +1069,6 @@ class VideoDetailController extends GetxController
|
|||||||
),
|
),
|
||||||
segmentList: segmentProgressList,
|
segmentList: segmentProgressList,
|
||||||
viewPointList: viewPointList,
|
viewPointList: viewPointList,
|
||||||
vttSubtitles: _vttSubtitles,
|
|
||||||
vttSubtitlesIndex: vttSubtitlesIndex,
|
|
||||||
showVP: showVP,
|
showVP: showVP,
|
||||||
dmTrend: dmTrend,
|
dmTrend: dmTrend,
|
||||||
// 硬解
|
// 硬解
|
||||||
@@ -1092,12 +1091,13 @@ class VideoDetailController extends GetxController
|
|||||||
if (videoState.value is! Success) {
|
if (videoState.value is! Success) {
|
||||||
videoState.value = LoadingState.success(null);
|
videoState.value = LoadingState.success(null);
|
||||||
}
|
}
|
||||||
|
setSubtitle(vttSubtitlesIndex.value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
initSkip();
|
initSkip();
|
||||||
|
|
||||||
if (vttSubtitlesIndex == null) {
|
if (vttSubtitlesIndex.value == -1) {
|
||||||
_getSubtitle();
|
_getSubtitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1358,43 +1358,47 @@ class VideoDetailController extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic subtitles;
|
RxList subtitles = [].obs;
|
||||||
late List<Map<String, String>> _vttSubtitles = <Map<String, String>>[];
|
late final Map<int, String> _vttSubtitles = {};
|
||||||
int? vttSubtitlesIndex;
|
late final RxInt vttSubtitlesIndex = (-1).obs;
|
||||||
late bool showVP = true;
|
late bool showVP = true;
|
||||||
|
|
||||||
void _getSubtitle() {
|
void _getSubtitle() {
|
||||||
_vttSubtitles.clear();
|
_vttSubtitles.clear();
|
||||||
viewPointList.clear();
|
viewPointList.clear();
|
||||||
_querySubtitles().then((value) {
|
_querySubtitles();
|
||||||
if (_vttSubtitles.isNotEmpty) {
|
}
|
||||||
String preference = setting.get(
|
|
||||||
SettingBoxKey.subtitlePreference,
|
// 设定字幕轨道
|
||||||
defaultValue: SubtitlePreference.values.first.code,
|
setSubtitle(int index) async {
|
||||||
);
|
if (index <= 0) {
|
||||||
vttSubtitlesIndex = 0;
|
plPlayerController.videoPlayerController
|
||||||
if (preference == 'on') {
|
?.setSubtitleTrack(SubtitleTrack.no());
|
||||||
vttSubtitlesIndex = 1;
|
vttSubtitlesIndex.value = index;
|
||||||
} else if (preference == 'withoutAi') {
|
return;
|
||||||
for (int i = 1; i < _vttSubtitles.length; i++) {
|
}
|
||||||
if (_vttSubtitles[i]['language']!.startsWith('ai')) {
|
|
||||||
continue;
|
void setSub(subtitle) {
|
||||||
}
|
plPlayerController.videoPlayerController?.setSubtitleTrack(
|
||||||
vttSubtitlesIndex = i;
|
SubtitleTrack.data(
|
||||||
break;
|
subtitle,
|
||||||
}
|
title: subtitles[index - 1]['lan_doc'],
|
||||||
}
|
language: subtitles[index - 1]['lan'],
|
||||||
if (plPlayerController.vttSubtitles.isEmpty) {
|
),
|
||||||
plPlayerController.vttSubtitles.value = _vttSubtitles;
|
);
|
||||||
if (vttSubtitlesIndex != null) {
|
vttSubtitlesIndex.value = index;
|
||||||
plPlayerController.vttSubtitlesIndex.value = vttSubtitlesIndex!;
|
}
|
||||||
if (vttSubtitlesIndex != 0) {
|
|
||||||
plPlayerController.setSubtitle(vttSubtitlesIndex!);
|
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
|
// interactive video
|
||||||
@@ -1484,15 +1488,26 @@ class VideoDetailController extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res["data"] is List && res["data"].isNotEmpty) {
|
if (res["subtitles"] is List && res["subtitles"].isNotEmpty) {
|
||||||
subtitles = res["data"];
|
vttSubtitlesIndex.value = 0;
|
||||||
var result = await VideoHttp.vttSubtitles(res["data"]);
|
subtitles.value = res["subtitles"];
|
||||||
if (result != null) {
|
|
||||||
_vttSubtitles = result;
|
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) {
|
setSubtitle(vttSubtitlesIndex.value);
|
||||||
// SmartDialog.showToast('字幕均加载失败');
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1556,8 +1571,8 @@ class VideoDetailController extends GetxController
|
|||||||
savedDanmaku = null;
|
savedDanmaku = null;
|
||||||
|
|
||||||
// subtitle
|
// subtitle
|
||||||
subtitles = null;
|
subtitles.clear();
|
||||||
vttSubtitlesIndex = null;
|
vttSubtitlesIndex.value = -1;
|
||||||
_vttSubtitles.clear();
|
_vttSubtitles.clear();
|
||||||
|
|
||||||
// view point
|
// view point
|
||||||
|
|||||||
@@ -410,8 +410,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
// }
|
// }
|
||||||
if (plPlayerController != null) {
|
if (plPlayerController != null) {
|
||||||
videoDetailController.makeHeartBeat();
|
videoDetailController.makeHeartBeat();
|
||||||
videoDetailController.vttSubtitlesIndex =
|
|
||||||
plPlayerController!.vttSubtitlesIndex.value;
|
|
||||||
videoDetailController.showVP = plPlayerController!.showVP.value;
|
videoDetailController.showVP = plPlayerController!.showVP.value;
|
||||||
plPlayerController!.removeStatusLister(playerListener);
|
plPlayerController!.removeStatusLister(playerListener);
|
||||||
plPlayerController!.removePositionListener(positionListener);
|
plPlayerController!.removePositionListener(positionListener);
|
||||||
|
|||||||
@@ -449,8 +449,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
|||||||
// }
|
// }
|
||||||
if (plPlayerController != null) {
|
if (plPlayerController != null) {
|
||||||
videoDetailController.makeHeartBeat();
|
videoDetailController.makeHeartBeat();
|
||||||
videoDetailController.vttSubtitlesIndex =
|
|
||||||
plPlayerController!.vttSubtitlesIndex.value;
|
|
||||||
videoDetailController.showVP = plPlayerController!.showVP.value;
|
videoDetailController.showVP = plPlayerController!.showVP.value;
|
||||||
plPlayerController!.removeStatusLister(playerListener);
|
plPlayerController!.removeStatusLister(playerListener);
|
||||||
plPlayerController!.removePositionListener(positionListener);
|
plPlayerController!.removePositionListener(positionListener);
|
||||||
|
|||||||
@@ -401,8 +401,7 @@ class HeaderControlState extends State<HeaderControl> {
|
|||||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||||
title: const Text('字幕设置', style: titleStyle),
|
title: const Text('字幕设置', style: titleStyle),
|
||||||
),
|
),
|
||||||
if (videoDetailCtr.subtitles is List &&
|
if (videoDetailCtr.subtitles.isNotEmpty)
|
||||||
videoDetailCtr.subtitles.isNotEmpty)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
onTap: () => {Get.back(), onExportSubtitle()},
|
onTap: () => {Get.back(), onExportSubtitle()},
|
||||||
@@ -1103,7 +1102,7 @@ class HeaderControlState extends State<HeaderControl> {
|
|||||||
title: const Text('保存字幕'),
|
title: const Text('保存字幕'),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: (videoDetailCtr.subtitles as List)
|
children: videoDetailCtr.subtitles
|
||||||
.map(
|
.map(
|
||||||
(item) => ListTile(
|
(item) => ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
|
|||||||
@@ -111,9 +111,6 @@ class PlPlayerController {
|
|||||||
bool _enableHeart = true;
|
bool _enableHeart = true;
|
||||||
|
|
||||||
late DataSource dataSource;
|
late DataSource dataSource;
|
||||||
// 视频字幕
|
|
||||||
final RxList<Map<String, String>> vttSubtitles = <Map<String, String>>[].obs;
|
|
||||||
final RxInt vttSubtitlesIndex = 0.obs;
|
|
||||||
|
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
Timer? _timerForSeek;
|
Timer? _timerForSeek;
|
||||||
@@ -471,8 +468,6 @@ class PlPlayerController {
|
|||||||
DataSource dataSource, {
|
DataSource dataSource, {
|
||||||
List<Segment>? segmentList,
|
List<Segment>? segmentList,
|
||||||
List<Segment>? viewPointList,
|
List<Segment>? viewPointList,
|
||||||
List<Map<String, String>>? vttSubtitles,
|
|
||||||
int? vttSubtitlesIndex,
|
|
||||||
bool? showVP,
|
bool? showVP,
|
||||||
List? dmTrend,
|
List? dmTrend,
|
||||||
bool autoplay = true,
|
bool autoplay = true,
|
||||||
@@ -504,8 +499,6 @@ class PlPlayerController {
|
|||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.segmentList.value = segmentList ?? <Segment>[];
|
this.segmentList.value = segmentList ?? <Segment>[];
|
||||||
this.viewPointList.value = viewPointList ?? <Segment>[];
|
this.viewPointList.value = viewPointList ?? <Segment>[];
|
||||||
this.vttSubtitles.value = vttSubtitles ?? <Map<String, String>>[];
|
|
||||||
this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0;
|
|
||||||
this.showVP.value = showVP ?? true;
|
this.showVP.value = showVP ?? true;
|
||||||
this.dmTrend.value = dmTrend ?? [];
|
this.dmTrend.value = dmTrend ?? [];
|
||||||
_autoPlay = autoplay;
|
_autoPlay = autoplay;
|
||||||
@@ -555,7 +548,6 @@ class PlPlayerController {
|
|||||||
startListeners();
|
startListeners();
|
||||||
}
|
}
|
||||||
await _initializePlayer();
|
await _initializePlayer();
|
||||||
setSubtitle(this.vttSubtitlesIndex.value);
|
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
dataStatus.status.value = DataStatus.error;
|
dataStatus.status.value = DataStatus.error;
|
||||||
debugPrint(stackTrace.toString());
|
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() {
|
static void updatePlayCount() {
|
||||||
if (_instance?._playerCount.value == 1) {
|
if (_instance?._playerCount.value == 1) {
|
||||||
_instance?.dispose();
|
_instance?.dispose();
|
||||||
|
|||||||
@@ -550,40 +550,52 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
|
|
||||||
/// 字幕
|
/// 字幕
|
||||||
BottomControlType.subtitle: Obx(
|
BottomControlType.subtitle: Obx(
|
||||||
() => plPlayerController.vttSubtitles.isEmpty
|
() => widget.videoDetailController?.subtitles.isEmpty == true
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
width: widgetWidth,
|
width: widgetWidth,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: PopupMenuButton<int>(
|
child: PopupMenuButton<int>(
|
||||||
initialValue: plPlayerController.vttSubtitles.length <
|
initialValue: widget
|
||||||
plPlayerController.vttSubtitlesIndex.value
|
.videoDetailController!.vttSubtitlesIndex.value
|
||||||
? 0
|
.clamp(0, widget.videoDetailController!.subtitles.length),
|
||||||
: plPlayerController.vttSubtitlesIndex.value,
|
|
||||||
color: Colors.black.withOpacity(0.8),
|
color: Colors.black.withOpacity(0.8),
|
||||||
itemBuilder: (BuildContext context) {
|
itemBuilder: (BuildContext context) {
|
||||||
return plPlayerController.vttSubtitles
|
return [
|
||||||
.asMap()
|
PopupMenuItem<int>(
|
||||||
.entries
|
value: 0,
|
||||||
.map((entry) {
|
|
||||||
return PopupMenuItem<int>(
|
|
||||||
value: entry.key,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
plPlayerController.setSubtitle(entry.key);
|
widget.videoDetailController!.setSubtitle(0);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entry.value['title']}",
|
"关闭字幕",
|
||||||
style: const TextStyle(color: Colors.white),
|
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(
|
child: Container(
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 30,
|
height: 30,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
plPlayerController.vttSubtitlesIndex.value == 0
|
widget.videoDetailController!.vttSubtitlesIndex.value == 0
|
||||||
? Icons.closed_caption_off_outlined
|
? Icons.closed_caption_off_outlined
|
||||||
: Icons.closed_caption_off_rounded,
|
: Icons.closed_caption_off_rounded,
|
||||||
size: 22,
|
size: 22,
|
||||||
|
|||||||
Reference in New Issue
Block a user