diff --git a/lib/pages/player/player_controller.dart b/lib/pages/player/player_controller.dart index 8c99ebd..b61b8c5 100644 --- a/lib/pages/player/player_controller.dart +++ b/lib/pages/player/player_controller.dart @@ -56,6 +56,8 @@ abstract class _PlayerController with Store { late AudioItem firstAudio; late String videoUrl; late String audioUrl; + String subUrl = ''; + String subContext = ''; late Duration defaultST; late Player mediaPlayer; @@ -105,6 +107,7 @@ abstract class _PlayerController with Store { // Todo videoSource: videoUrl, audioSource: audioUrl, + subFiles: subUrl, type: DataSourceType.network, httpHeaders: { 'user-agent': @@ -238,12 +241,13 @@ abstract class _PlayerController with Store { // 字幕 if (dataSource.subFiles != '' && dataSource.subFiles != null) { - await pp.setProperty( - 'sub-files', - UniversalPlatform.isWindows - ? dataSource.subFiles!.replaceAll(';', '\\;') - : dataSource.subFiles!.replaceAll(':', '\\:'), - ); + debugPrint('发现可用字幕, 尝试加载'); + subContext = await VideoRequest.getSub(subUrl); + debugPrint('字幕转换并加载完成'); + // await pp.setProperty( + // 'sub-files', + // subContext + // ); await pp.setProperty("subs-with-matching-audio", "no"); await pp.setProperty("sub-forced-only", "yes"); await pp.setProperty("blend-subtitles", "video"); @@ -273,6 +277,18 @@ abstract class _PlayerController with Store { Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders), play: false, ); + + // 设定字幕 + await mediaPlayer.setSubtitleTrack(SubtitleTrack.data(subContext)); + // debugPrint('待设定字幕为 $subContext'); + // await mediaPlayer.setSubtitleTrack( + // SubtitleTrack.uri( + // 'https://www.iandevlin.com/html5test/webvtt/upc-video-subtitles-en.vtt', + // title: 'English', + // language: 'en', + // ), + // ); + // 音轨 // player.setAudioTrack( // AudioTrack.uri(dataSource.audioSource!), @@ -284,7 +300,13 @@ abstract class _PlayerController with Store { //获得视频详细 Future queryVideoUrl() async { var result = await VideoRequest.videoUrl(cid: cid, bvid: bvid); + try { + subUrl = await VideoRequest.subUrl(cid: cid, bvid: bvid); + } catch (e) { + debugPrint(e.toString()); + } debugPrint('已从服务器得到响应'); + debugPrint('播放器加载字幕 $subUrl'); if (result['status']) { data = result['data']; debugPrint('响应合法'); diff --git a/lib/pages/player/player_item.dart b/lib/pages/player/player_item.dart index c6e8773..d2b1f14 100644 --- a/lib/pages/player/player_item.dart +++ b/lib/pages/player/player_item.dart @@ -45,7 +45,33 @@ class _PlayerItemState extends State { ? MediaQuery.of(context).size.width * 9.0 / 32.0 : MediaQuery.of(context).size.width * 9.0 / 16.0, child: playerController.dataStatus == 'loaded' - ? Video(controller: playerController.videoController) + ? Video( + controller: playerController.videoController, + subtitleViewConfiguration: SubtitleViewConfiguration( + style: TextStyle( + color: Colors.pink, // 深粉色字体 + fontSize: 48.0, // 较大的字号 + background: Paint()..color = Colors.transparent, // 背景透明 + decoration: TextDecoration.none, // 无下划线 + fontWeight: FontWeight.bold, // 字体加粗 + shadows: const [ + // 显眼的包边 + Shadow( + offset: Offset(1.0, 1.0), + blurRadius: 3.0, + color: Color.fromARGB(255, 255, 255, 255), + ), + Shadow( + offset: Offset(-1.0, -1.0), + blurRadius: 3.0, + color: Color.fromARGB(125, 255, 255, 255), + ), + ], + ), + textAlign: TextAlign.center, + padding: const EdgeInsets.all(24.0), + ), + ) : SizedBox( child: Center( child: CircularProgressIndicator(), diff --git a/lib/request/api.dart b/lib/request/api.dart index 9853f51..fd10d66 100644 --- a/lib/request/api.dart +++ b/lib/request/api.dart @@ -13,6 +13,9 @@ class Api { // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md static const String videoUrl = '/x/player/wbi/playurl'; + // 字幕 + static const String subUrl = '/x/player/v2'; + // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 // https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端) @@ -316,7 +319,7 @@ class Api { 'https://api.github.com/repos/Predidit/BiliNeo/releases/latest'; // 当前版本 - static const String version = '1.0.0'; + static const String version = '1.0.1'; static const sourceUrl = "https://github.com/Predidit/BiliNeo"; diff --git a/lib/request/video.dart b/lib/request/video.dart index 4457ed4..bd291b4 100644 --- a/lib/request/video.dart +++ b/lib/request/video.dart @@ -1,10 +1,54 @@ +import 'package:flutter/material.dart'; +import 'dart:convert'; + import 'api.dart'; import 'request.dart'; import 'package:hive/hive.dart'; import 'package:bilineo/utils/wbisign.dart'; import 'package:bilineo/pages/player/player_url.dart'; +import 'package:bilineo/utils/utils.dart'; class VideoRequest { +// Todo 获取字幕 + static Future subUrl({int? avid, String? bvid, required int cid}) async { + Map data = { + 'cid': cid, + 'bvid': bvid, + }; + if (avid != null) { + data['avid'] = avid; + } + Map params = await WbiSign().makSign({ + ...data, + 'fourk': 1, + 'voice_balance': 1, + 'gaia_source': 'pre-load', + 'web_location': 1550101, + }); + try { + var res = await Request().get(Api.subUrl, data: params); + Map jsonMap = json.decode(res.toString()); + List subtitles = jsonMap['data']['subtitle']['subtitles']; + String subtitleUrl = subtitles.firstWhere( + (subtitle) => subtitle['lan'].startsWith('zh'), + orElse: () => null)['subtitle_url']; + subtitleUrl = 'https:' + subtitleUrl; + debugPrint(subtitleUrl); + return subtitleUrl; + } catch (e) { + debugPrint('查询字幕失败 ${e.toString()}'); + return ''; + } + } + + static Future getSub(String subUrl) async{ + var res = await Request().get(subUrl); + final jsonData = json.decode(res.toString()); + final webvttString = Utils.jsonToWebVTT(jsonData); + // debugPrint(webvttString); + return webvttString; + } + static Future videoUrl( {int? avid, String? bvid, required int cid, int? qn}) async { Map data = { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index cbdcca2..0d06ac4 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -228,12 +228,10 @@ class Utils { // 检查更新 static Future checkUpdata() async { return true; - } + } // 下载适用于当前系统的安装包 - static Future matchVersion(data) async { - - } + static Future matchVersion(data) async {} static Future latest() async { var resp = await Dio().get>(Api.latestApp); @@ -242,7 +240,7 @@ class Utils { } else { throw resp.data?["message"]; } - } + } // 时间戳转时间 static tampToSeektime(number) { @@ -270,13 +268,33 @@ class Utils { } static List generateRandomBytes(int minLength, int maxLength) { - return List.generate( - random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20 - ); + return List.generate(random.nextInt(maxLength - minLength + 1), + (_) => random.nextInt(0x60) + 0x20); } static String base64EncodeRandomString(int minLength, int maxLength) { List randomBytes = generateRandomBytes(minLength, maxLength); return base64.encode(randomBytes); } + + static String jsonToWebVTT(Map json) { + var webvttContent = 'WEBVTT FILE\n\n'; + int i = 1; + for (var entry in json['body']) { + final startTime = formatTime(entry['from']); + final endTime = formatTime(entry['to']); + final content = entry['content']; + webvttContent += + '${i.toString()}\n$startTime --> $endTime\n$content\n\n'; + i = i + 1; + } + return webvttContent; + } + + static String formatTime(double seconds) { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + final secs = seconds % 60; + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toStringAsFixed(3).replaceAll('.', '.')}'; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 7e7a7af..46d7457 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.0.1+1 environment: sdk: '>=3.2.6 <4.0.0'