mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-06 10:40:09 +08:00
refa: cdn (#1743)
* refa: cdn * feat: live cdn (WIP) * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * add live quality [skip ci] Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * mod: replace durl host * tweak [skip ci] Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
e589f27195
commit
27ae296b28
@@ -109,7 +109,7 @@ abstract final class DownloadHttp {
|
||||
orElse: () => videosList.first,
|
||||
);
|
||||
|
||||
final videoUrl = VideoUtils.getCdnUrl(videoDash);
|
||||
final videoUrl = VideoUtils.getCdnUrl(videoDash.playUrls);
|
||||
|
||||
final Type2File videoFile = Type2File(
|
||||
id: videoDash.id!,
|
||||
@@ -143,7 +143,10 @@ abstract final class DownloadHttp {
|
||||
(e) => e.id == closestNumber,
|
||||
orElse: () => audioDashList.first,
|
||||
);
|
||||
final audioUrl = VideoUtils.getCdnUrl(audioDash);
|
||||
final audioUrl = VideoUtils.getCdnUrl(
|
||||
audioDash.playUrls,
|
||||
isAudio: true,
|
||||
);
|
||||
audioFileList = [
|
||||
Type2File(
|
||||
id: audioDash.id!,
|
||||
@@ -177,7 +180,7 @@ abstract final class DownloadHttp {
|
||||
md5: '',
|
||||
metaUrl: '',
|
||||
order: first.order!,
|
||||
url: first.backupUrl?.lastOrNull ?? first.url!,
|
||||
url: VideoUtils.getCdnUrl(first.playUrls),
|
||||
),
|
||||
];
|
||||
final FormatItem? formatItem = data.supportFormats?.firstWhereOrNull(
|
||||
@@ -189,6 +192,7 @@ abstract final class DownloadHttp {
|
||||
formatItem?.quality ?? VideoQuality.clear480.code;
|
||||
|
||||
entry
|
||||
..mediaType = 1
|
||||
..typeTag = targetVideoQa.toString()
|
||||
..videoQuality = targetVideoQa
|
||||
..preferedVideoQuality = targetVideoQa
|
||||
@@ -204,7 +208,7 @@ abstract final class DownloadHttp {
|
||||
useIjkMediaCodec: false,
|
||||
),
|
||||
];
|
||||
entry.mediaType = 1;
|
||||
|
||||
return Type1(
|
||||
from: pageData?.from ?? ep?.from,
|
||||
quality: entry.preferedVideoQuality,
|
||||
|
||||
@@ -27,11 +27,84 @@ enum CDNService {
|
||||
hk_bcache('hk_bcache(Bilibili海外)', 'cn-hk-eq-bcache-01.bilivideo.com')
|
||||
;
|
||||
|
||||
String get code => name;
|
||||
static final fromCode = values.byName;
|
||||
|
||||
final String desc;
|
||||
final String host;
|
||||
final String? host;
|
||||
|
||||
const CDNService(this.desc, [this.host = '']);
|
||||
const CDNService(this.desc, [this.host]);
|
||||
}
|
||||
// from https://rec.danmuji.org/dev/cdn-info/
|
||||
// {
|
||||
// 'cn-ahwh-ct-': {'01': 16},
|
||||
// 'cn-cq-ct-': {'01': 35, '02': 2},
|
||||
// 'cn-gddg-ct-': {'01': 36},
|
||||
// 'cn-gdfs-ct-': {'01': 28},
|
||||
// 'cn-hblf-ct-': {'01': 21},
|
||||
// 'cn-hbyc-ct-': {'02': 35},
|
||||
// 'cn-hljheb-ct-': {'01': 12},
|
||||
// 'cn-hnld-ct-': {'01': 56},
|
||||
// 'cn-jsnt-ct-': {'01': 52},
|
||||
// 'cn-jsyz-ct-': {'03': 52},
|
||||
// 'cn-jxjj-ct-': {'01': 14},
|
||||
// 'cn-sccd-ct-': {'01': 32},
|
||||
// 'cn-sxxa-ct-': {'03': 14},
|
||||
// 'cn-xj-ct-': {'01': 6},
|
||||
// 'cn-zjjh-ct-': {'04': 37},
|
||||
// 'cn-gddg-cu-': {'01': 15},
|
||||
// 'cn-hncs-cu-': {'01': 14, 'v': 6},
|
||||
// 'cn-hnly-cu-': {'01': 35},
|
||||
// 'cn-jlcc-cu-': {'03': 16},
|
||||
// 'cn-jstz-cu-': {'01': 14},
|
||||
// 'cn-lnsy-cu-': {'01': 9, 'v': 4},
|
||||
// 'cn-nmghhht-cu-': {'01': 15, 'v': 11},
|
||||
// 'cn-sccd-cu-': {'01': 13},
|
||||
// 'cn-sdqd-cu-': {'01': 25},
|
||||
// 'cn-sxty-cu-': {'03': 10},
|
||||
// 'cn-sxxa-cu-': {'02': 8},
|
||||
// 'cn-zjhz-cu-': {'01': 8, 'v': 6},
|
||||
// 'cn-cq-cm-': {'01': 30},
|
||||
// 'cn-fjqz-cm-': {'01': 10},
|
||||
// 'cn-gddg-cm-': {'01': 14},
|
||||
// 'cn-gdst-cm-': {'01': 17},
|
||||
// 'cn-hbsjz-cm-': {'02': 16},
|
||||
// 'cn-hbwh-cm-': {'01': 23},
|
||||
// 'cn-hncs-cm-': {'03': 24},
|
||||
// 'cn-hnzz-cm-': {'01': 16},
|
||||
// 'cn-jssz-cm-': {'01': 24, '02': 62},
|
||||
// 'cn-jxnc-cm-': {'01': 20},
|
||||
// 'cn-lnsy-cm-': {'01': 11},
|
||||
// 'cn-nmghhht-cm-': {'01': 5},
|
||||
// 'cn-sccd-cm-': {'03': 26},
|
||||
// 'cn-sdjn-cm-': {'02': 14},
|
||||
// 'cn-sxxa-cm-': {'01': 14},
|
||||
// 'cn-tj-cm-': {'02': 16},
|
||||
// 'cn-xj-cm-': {'02': 6},
|
||||
// 'cn-zjhz-cm-': {'01': 29},
|
||||
// 'cn-cq-gd-': {'01': 20},
|
||||
// 'cn-gdgz-gd-': {'01': 20},
|
||||
// 'cn-gzgy-gd-': {'01': 6},
|
||||
// 'cn-hb-gd-': {'01': 4},
|
||||
// 'cn-hbwh-gd-': {'01': 6},
|
||||
// 'cn-hljheb-gd-': {'01': 2},
|
||||
// 'cn-hncs-gd-': {'01': 8},
|
||||
// 'cn-jlcc-gd-': {'01': 5},
|
||||
// 'cn-jsnj-gd-': {'01': 8},
|
||||
// 'cn-zjhz-gd-': {'02': 2},
|
||||
// 'cn-bj-fx-': {'01': 6},
|
||||
// 'cn-fjfz-fx-': {'01': 6},
|
||||
// 'cn-gdgz-fx-': {'01': 18},
|
||||
// 'cn-hbwh-fx-': {'01': 16},
|
||||
// 'cn-hncs-fx-': {'01': 6},
|
||||
// 'cn-hnzz-fx-': {'01': 8},
|
||||
// 'cn-jsnj-fx-': {'02': 6},
|
||||
// 'cn-sccd-fx-': {'01': 6},
|
||||
// 'cn-sdjn-fx-': {'01': 6},
|
||||
// 'cn-sh-fx-': {'01': 10},
|
||||
// 'cn-tj-fx-': {'01': 6},
|
||||
// 'cn-bj-se-': {'01': 8},
|
||||
// 'cn-bj-cc-': {'03': 18},
|
||||
// 'cn-gdfs-cc-': {'02': 21},
|
||||
// 'cn-sh-cc-': {'01': 15},
|
||||
// 'cn-zjhz-wasu-': {'03': 21, '04': 12},
|
||||
// 'cn-sh-ix-': {'01': 13},
|
||||
// 'cn-hk-eq-': {'01': 14}
|
||||
// }
|
||||
@@ -2,6 +2,7 @@ enum LiveQuality {
|
||||
dolby(30000, '杜比'),
|
||||
origin4K(25000, '4K 原画'),
|
||||
super4K(20000, '4K'),
|
||||
super2K(15000, '2K'),
|
||||
origin(10000, '原画'),
|
||||
bluRay(400, '蓝光'),
|
||||
superHD(250, '超清'),
|
||||
|
||||
@@ -196,23 +196,17 @@ class Durl {
|
||||
backupUrl: (json['backup_url'] as List?)?.fromCast<String>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _ipRegExp = RegExp(r'^https?://\d{1,3}\.\d{1,3}');
|
||||
|
||||
bool _isMCDNorPCDN(String url) {
|
||||
return url.contains("upos-sz-302") ||
|
||||
url.contains("nexusedgeio.com") ||
|
||||
url.contains("ahdohpiechei.com") ||
|
||||
url.contains("szbdyd.com") ||
|
||||
url.contains(".mcdn.bilivideo") ||
|
||||
_ipRegExp.hasMatch(url);
|
||||
Iterable<String> get playUrls sync* {
|
||||
if (url?.isNotEmpty == true) yield url!;
|
||||
if (backupUrl?.isNotEmpty == true) yield* backupUrl!;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseItem {
|
||||
int? id;
|
||||
String? baseUrl;
|
||||
String? backupUrl;
|
||||
List<String>? backupUrl;
|
||||
int? bandWidth;
|
||||
String? mimeType;
|
||||
String? codecs;
|
||||
@@ -243,14 +237,8 @@ abstract class BaseItem {
|
||||
BaseItem.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
baseUrl = json['baseUrl'] ?? json['base_url'];
|
||||
final backupUrls = ((json['backupUrl'] ?? json['backup_url']) as List?)
|
||||
backupUrl = ((json['backupUrl'] ?? json['backup_url']) as List?)
|
||||
?.fromCast<String>();
|
||||
backupUrl = backupUrls != null && backupUrls.isNotEmpty
|
||||
? backupUrls.firstWhere(
|
||||
(i) => !_isMCDNorPCDN(i),
|
||||
orElse: () => backupUrls.first,
|
||||
)
|
||||
: null;
|
||||
bandWidth = json['bandWidth'] ?? json['bandwidth'];
|
||||
mimeType = json['mime_type'];
|
||||
codecs = json['codecs'];
|
||||
@@ -262,6 +250,11 @@ abstract class BaseItem {
|
||||
segmentBase = json['segmentBase'] ?? json['segment_base'];
|
||||
codecid = json['codecid'];
|
||||
}
|
||||
|
||||
Iterable<String> get playUrls sync* {
|
||||
if (baseUrl?.isNotEmpty == true) yield baseUrl!;
|
||||
if (backupUrl?.isNotEmpty == true) yield* backupUrl!;
|
||||
}
|
||||
}
|
||||
|
||||
class VideoItem extends BaseItem {
|
||||
|
||||
@@ -10,7 +10,9 @@ import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'
|
||||
PlaylistSource,
|
||||
PlayInfo,
|
||||
ThumbUpReq_ThumbType,
|
||||
ListOrder;
|
||||
ListOrder,
|
||||
DashItem,
|
||||
ResponseUrl;
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/ua_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_intro_controller.dart'
|
||||
@@ -226,7 +228,7 @@ class AudioController extends GetxController
|
||||
(e) => e.id <= cacheAudioQa,
|
||||
(a, b) => a.id > b.id ? a : b,
|
||||
);
|
||||
_onOpenMedia(VideoUtils.getCdnUrl(audio));
|
||||
_onOpenMedia(VideoUtils.getCdnUrl(audio.playUrls));
|
||||
} else if (playInfo.hasPlayUrl()) {
|
||||
final playUrl = playInfo.playUrl;
|
||||
final durls = playUrl.durl;
|
||||
@@ -235,7 +237,7 @@ class AudioController extends GetxController
|
||||
}
|
||||
final durl = durls.first;
|
||||
position.value = Duration.zero;
|
||||
_onOpenMedia(VideoUtils.getDurlCdnUrl(durl));
|
||||
_onOpenMedia(VideoUtils.getCdnUrl(durl.playUrls));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,3 +710,17 @@ class AudioController extends GetxController
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
extension on DashItem {
|
||||
Iterable<String> get playUrls sync* {
|
||||
yield baseUrl;
|
||||
yield* backupUrl;
|
||||
}
|
||||
}
|
||||
|
||||
extension on ResponseUrl {
|
||||
Iterable<String> get playUrls sync* {
|
||||
yield url;
|
||||
yield* backupUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class LiveRoomController extends GetxController {
|
||||
}).toList();
|
||||
currentQnDesc.value =
|
||||
LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString();
|
||||
videoUrl = VideoUtils.getCdnUrl(item);
|
||||
videoUrl = VideoUtils.getLiveCdnUrl(item);
|
||||
await playerInit();
|
||||
isLoaded.value = true;
|
||||
} else {
|
||||
|
||||
@@ -54,9 +54,9 @@ List<SettingsModel> get videoSettings => [
|
||||
title: 'CDN 设置',
|
||||
leading: const Icon(MdiIcons.cloudPlusOutline),
|
||||
getSubtitle: () =>
|
||||
'当前使用:${CDNService.fromCode(VideoUtils.cdnService).desc},部分 CDN 可能失效,如无法播放请尝试切换',
|
||||
'当前使用:${VideoUtils.cdnService.desc},部分 CDN 可能失效,如无法播放请尝试切换',
|
||||
onTap: (setState) async {
|
||||
String? result = await showDialog(
|
||||
CDNService? result = await showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return const CdnSelectDialog();
|
||||
@@ -64,7 +64,57 @@ List<SettingsModel> get videoSettings => [
|
||||
);
|
||||
if (result != null) {
|
||||
VideoUtils.cdnService = result;
|
||||
await GStorage.setting.put(SettingBoxKey.CDNService, result);
|
||||
await GStorage.setting.put(SettingBoxKey.CDNService, result.name);
|
||||
setState();
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsModel(
|
||||
settingsType: SettingsType.normal,
|
||||
title: '直播 CDN 设置',
|
||||
leading: const Icon(MdiIcons.cloudPlusOutline),
|
||||
getSubtitle: () => '当前使用:${Pref.liveCdnUrl ?? "默认"}',
|
||||
onTap: (setState) async {
|
||||
String? result = await showDialog<String>(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
String host = Pref.liveCdnUrl ?? '';
|
||||
return AlertDialog(
|
||||
title: const Text('输入CDN host'),
|
||||
content: TextFormField(
|
||||
initialValue: host,
|
||||
autofocus: true,
|
||||
onChanged: (value) => host = value,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: ColorScheme.of(context).outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: host),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result != null) {
|
||||
if (result.isEmpty) {
|
||||
result = null;
|
||||
await GStorage.setting.delete(SettingBoxKey.liveCdnUrl);
|
||||
} else {
|
||||
if (!result.startsWith('http')) {
|
||||
result = 'https://${result}';
|
||||
}
|
||||
await GStorage.setting.put(SettingBoxKey.liveCdnUrl, result);
|
||||
}
|
||||
VideoUtils.liveCdnUrl = result;
|
||||
setState();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@ class SelectDialog<T> extends StatelessWidget {
|
||||
}
|
||||
|
||||
class CdnSelectDialog extends StatefulWidget {
|
||||
final VideoItem? sample;
|
||||
final BaseItem? sample;
|
||||
|
||||
const CdnSelectDialog({
|
||||
super.key,
|
||||
@@ -113,7 +113,7 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<VideoItem> _getSampleUrl() async {
|
||||
Future<BaseItem> _getSampleUrl() async {
|
||||
final result = await VideoHttp.videoUrl(
|
||||
cid: 196018899,
|
||||
bvid: 'BV1fK4y1t7hj',
|
||||
@@ -134,16 +134,19 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testAllCdnServices(VideoItem videoItem) async {
|
||||
Future<void> _testAllCdnServices(BaseItem videoItem) async {
|
||||
for (final item in CDNService.values) {
|
||||
if (!mounted) break;
|
||||
await _testSingleCdn(item, videoItem);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testSingleCdn(CDNService item, VideoItem videoItem) async {
|
||||
Future<void> _testSingleCdn(CDNService item, BaseItem videoItem) async {
|
||||
try {
|
||||
final cdnUrl = VideoUtils.getCdnUrl(videoItem, item.code);
|
||||
final cdnUrl = VideoUtils.getCdnUrl(
|
||||
videoItem.playUrls,
|
||||
defaultCDNService: item,
|
||||
);
|
||||
await _measureDownloadSpeed(cdnUrl, item.index);
|
||||
} catch (e) {
|
||||
_handleSpeedTestError(e, item.index);
|
||||
@@ -211,7 +214,17 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
|
||||
|
||||
if (kDebugMode) debugPrint('CDN speed test error: $error');
|
||||
if (!mounted) return;
|
||||
var message = error.toString();
|
||||
String message;
|
||||
if (error is DioException) {
|
||||
final statusCode = error.response?.statusCode;
|
||||
if (statusCode != null && 400 <= statusCode && statusCode < 500) {
|
||||
message = '此视频可能无法替换为该CDN';
|
||||
} else {
|
||||
message = error.toString();
|
||||
}
|
||||
} else {
|
||||
message = error.toString();
|
||||
}
|
||||
if (message.isEmpty) {
|
||||
message = '测速失败';
|
||||
}
|
||||
@@ -220,9 +233,9 @@ class _CdnSelectDialogState extends State<CdnSelectDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectDialog<String>(
|
||||
return SelectDialog<CDNService>(
|
||||
title: 'CDN 设置',
|
||||
values: CDNService.values.map((i) => (i.code, i.desc)).toList(),
|
||||
values: CDNService.values.map((i) => (i, i.desc)).toList(),
|
||||
value: VideoUtils.cdnService,
|
||||
subtitleBuilder: _cdnSpeedTest
|
||||
? (context, index) {
|
||||
|
||||
@@ -1125,7 +1125,7 @@ class VideoDetailController extends GetxController
|
||||
currentDecodeFormats = VideoDecodeFormatType.fromString(video.codecs!);
|
||||
}
|
||||
firstVideo = video;
|
||||
videoUrl = VideoUtils.getCdnUrl(firstVideo);
|
||||
videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls);
|
||||
|
||||
/// 根据currentAudioQa 重新设置audioUrl
|
||||
if (currentAudioQa != null) {
|
||||
@@ -1133,7 +1133,7 @@ class VideoDetailController extends GetxController
|
||||
(i) => i.id == currentAudioQa!.code,
|
||||
orElse: () => data.dash!.audio!.first,
|
||||
);
|
||||
audioUrl = VideoUtils.getCdnUrl(firstAudio);
|
||||
audioUrl = VideoUtils.getCdnUrl(firstAudio.playUrls, isAudio: true);
|
||||
}
|
||||
|
||||
playerInit();
|
||||
@@ -1308,7 +1308,7 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
if (data.dash == null && data.durl != null) {
|
||||
final first = data.durl!.first;
|
||||
videoUrl = first.backupUrl?.lastOrNull ?? first.url!;
|
||||
videoUrl = VideoUtils.getCdnUrl(first.playUrls);
|
||||
audioUrl = '';
|
||||
|
||||
// 实际为FLV/MP4格式,但已被淘汰,这里仅做兜底处理
|
||||
@@ -1395,7 +1395,7 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
setVideoHeight();
|
||||
|
||||
videoUrl = VideoUtils.getCdnUrl(firstVideo);
|
||||
videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls);
|
||||
|
||||
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
|
||||
AudioItem? firstAudio;
|
||||
@@ -1414,7 +1414,7 @@ class VideoDetailController extends GetxController
|
||||
(e) => e.id == closestNumber,
|
||||
orElse: () => audioList.first,
|
||||
);
|
||||
audioUrl = VideoUtils.getCdnUrl(firstAudio);
|
||||
audioUrl = VideoUtils.getCdnUrl(firstAudio.playUrls, isAudio: true);
|
||||
if (firstAudio.id case final int id?) {
|
||||
currentAudioQa = AudioQuality.fromCode(id);
|
||||
}
|
||||
@@ -2001,13 +2001,14 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (res.isSuccess) {
|
||||
final PlayUrlModel data = res.data;
|
||||
final data = res.data;
|
||||
final first = data.durl?.firstOrNull;
|
||||
final url = first?.backupUrl?.lastOrNull ?? first?.url;
|
||||
if (url == null || url.isEmpty) {
|
||||
if (first == null || first.playUrls.isEmpty) {
|
||||
SmartDialog.showToast('不支持投屏');
|
||||
return;
|
||||
}
|
||||
final url = VideoUtils.getCdnUrl(first.playUrls);
|
||||
|
||||
String? title;
|
||||
try {
|
||||
if (isUgc) {
|
||||
|
||||
@@ -427,25 +427,23 @@ class HeaderControlState extends State<HeaderControl> {
|
||||
title: const Text('CDN 设置', style: titleStyle),
|
||||
leading: const Icon(MdiIcons.cloudPlusOutline, size: 20),
|
||||
subtitle: Text(
|
||||
'当前:${CDNService.fromCode(VideoUtils.cdnService).desc},无法播放请切换',
|
||||
'当前:${VideoUtils.cdnService.desc},无法播放请切换',
|
||||
style: subTitleStyle,
|
||||
),
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
String? result = await showDialog(
|
||||
CDNService? result = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CdnSelectDialog(
|
||||
sample: videoInfo.dash?.video?.first,
|
||||
sample: videoInfo.dash?.video?.firstOrNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result != null) {
|
||||
VideoUtils.cdnService = result;
|
||||
setting.put(SettingBoxKey.CDNService, result);
|
||||
SmartDialog.showToast(
|
||||
'已设置为 ${CDNService.fromCode(result).desc},正在重载视频',
|
||||
);
|
||||
setting.put(SettingBoxKey.CDNService, result.name);
|
||||
SmartDialog.showToast('已设置为 ${result.desc},正在重载视频');
|
||||
videoDetailCtr.queryVideoUrl(
|
||||
defaultST: videoDetailCtr.playedTime,
|
||||
fromReset: true,
|
||||
|
||||
@@ -224,7 +224,8 @@ abstract class SettingBoxKey {
|
||||
enableGradientBg = 'enableGradientBg',
|
||||
navBarSort = 'navBarSort',
|
||||
tempPlayerConf = 'tempPlayerConf',
|
||||
reduceLuxColor = 'reduceLuxColor';
|
||||
reduceLuxColor = 'reduceLuxColor',
|
||||
liveCdnUrl = 'liveCdnUrl';
|
||||
}
|
||||
|
||||
abstract class LocalCacheKey {
|
||||
|
||||
@@ -238,10 +238,12 @@ abstract class Pref {
|
||||
static String get videoSync =>
|
||||
_setting.get(SettingBoxKey.videoSync, defaultValue: 'display-resample');
|
||||
|
||||
static String get defaultCDNService => _setting.get(
|
||||
SettingBoxKey.CDNService,
|
||||
defaultValue: CDNService.backupUrl.code,
|
||||
);
|
||||
static CDNService get defaultCDNService {
|
||||
if (_setting.get(SettingBoxKey.CDNService) case final String cdnName) {
|
||||
return CDNService.values.byName(cdnName);
|
||||
}
|
||||
return CDNService.backupUrl;
|
||||
}
|
||||
|
||||
static String get banWordForRecommend =>
|
||||
_setting.get(SettingBoxKey.banWordForRecommend, defaultValue: '');
|
||||
@@ -870,4 +872,6 @@ abstract class Pref {
|
||||
_setting.get(SettingBoxKey.setSystemBrightness, defaultValue: false);
|
||||
|
||||
static String? get downloadPath => _setting.get(SettingBoxKey.downloadPath);
|
||||
|
||||
static String? get liveCdnUrl => _setting.get(SettingBoxKey.liveCdnUrl);
|
||||
}
|
||||
|
||||
@@ -1,109 +1,96 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'
|
||||
show DashItem, ResponseUrl;
|
||||
import 'package:PiliPlus/models/common/video/cdn_type.dart';
|
||||
import 'package:PiliPlus/models/video/play/url.dart';
|
||||
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract final class VideoUtils {
|
||||
static String cdnService = Pref.defaultCDNService;
|
||||
static CDNService cdnService = Pref.defaultCDNService;
|
||||
static String? liveCdnUrl = Pref.liveCdnUrl;
|
||||
static bool disableAudioCDN = Pref.disableAudioCDN;
|
||||
|
||||
/// [DashItem] audio
|
||||
/// [VideoItem] [AudioItem] video
|
||||
/// [CodecItem] live
|
||||
static String getCdnUrl(dynamic item, [String? defaultCDNService]) {
|
||||
String? backupUrl;
|
||||
String? videoUrl;
|
||||
static const _proxyTf = 'proxy-tf-all-ws.bilivideo.com';
|
||||
|
||||
static final _mirrorRegex = RegExp(
|
||||
r'^https?://(?:upos-\w+-(?!302)\w+|(?:upos|proxy)-tf-[^/]+)\.(?:bilivideo|akamaized)\.(?:com|net)/upgcxcode',
|
||||
);
|
||||
|
||||
static final _mCdnTfRegex = RegExp(
|
||||
r'^https?://(?:(?:(?:\d{1,3}\.){3}\d{1,3}|[^/]+\.mcdn\.bilivideo\.(?:com|cn|net))(?:\:\d{1,5})?/v\d/resource)',
|
||||
);
|
||||
|
||||
static String getCdnUrl(
|
||||
Iterable<String> urls, {
|
||||
CDNService? defaultCDNService,
|
||||
bool isAudio = false,
|
||||
}) {
|
||||
defaultCDNService ??= cdnService;
|
||||
if (item case final AudioItem e) {
|
||||
if (disableAudioCDN) {
|
||||
return e.backupUrl?.isNotEmpty == true ? e.backupUrl! : e.baseUrl ?? '';
|
||||
|
||||
if (defaultCDNService == CDNService.baseUrl) {
|
||||
return urls.first;
|
||||
}
|
||||
|
||||
String? mcdnTf;
|
||||
String? mcdnUpgcxcode;
|
||||
|
||||
String last = '';
|
||||
for (var url in urls) {
|
||||
last = url;
|
||||
if (_mirrorRegex.hasMatch(url)) {
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.queryParameters['os'] == 'mcdn') {
|
||||
// upos-sz-mirrorcoso1.bilivideo.com os=mcdn
|
||||
mcdnUpgcxcode = url;
|
||||
} else {
|
||||
if (defaultCDNService == CDNService.backupUrl ||
|
||||
(isAudio && disableAudioCDN)) {
|
||||
return url;
|
||||
}
|
||||
return uri.replace(host: defaultCDNService.host).toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (_mCdnTfRegex.hasMatch(url)) {
|
||||
mcdnTf = url;
|
||||
continue;
|
||||
}
|
||||
|
||||
// upos-\w*-302.* & bcache & mcdn host but upgcxcode path
|
||||
if (url.contains('/upgcxcode/')) {
|
||||
mcdnUpgcxcode = url;
|
||||
continue;
|
||||
}
|
||||
|
||||
// may be deprecated
|
||||
if (url.contains('szbdyd.com')) {
|
||||
final uri = Uri.parse(url);
|
||||
final hostname =
|
||||
uri.queryParameters['xy_usource'] ?? defaultCDNService.host;
|
||||
return uri
|
||||
.replace(scheme: 'https', host: hostname, port: 443)
|
||||
.toString();
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('unknown cdn type: $url');
|
||||
}
|
||||
}
|
||||
if (defaultCDNService == CDNService.baseUrl.code) {
|
||||
if (item case final BaseItem e) {
|
||||
return e.baseUrl?.isNotEmpty == true ? e.baseUrl! : e.backupUrl ?? '';
|
||||
}
|
||||
}
|
||||
if (item case final CodecItem e) {
|
||||
backupUrl = e.urlInfo!.first.host! + e.baseUrl! + e.urlInfo!.first.extra!;
|
||||
} else if (item case final DashItem e) {
|
||||
backupUrl = e.backupUrl.lastOrNull;
|
||||
} else {
|
||||
backupUrl = item.backupUrl;
|
||||
}
|
||||
if (defaultCDNService == CDNService.backupUrl.code) {
|
||||
return backupUrl?.isNotEmpty == true ? backupUrl! : item.baseUrl ?? '';
|
||||
}
|
||||
videoUrl = backupUrl?.isNotEmpty == true ? backupUrl : item.baseUrl;
|
||||
|
||||
if (videoUrl == null || videoUrl.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
// if (kDebugMode) debugPrint('videoUrl:$videoUrl');
|
||||
|
||||
String defaultCDNHost = CDNService.fromCode(defaultCDNService).host;
|
||||
// if (kDebugMode) debugPrint('defaultCDNHost:$defaultCDNHost');
|
||||
if (videoUrl.contains('szbdyd.com')) {
|
||||
final uri = Uri.parse(videoUrl);
|
||||
String hostname = uri.queryParameters['xy_usource'] ?? defaultCDNHost;
|
||||
videoUrl = uri.replace(host: hostname, port: 443).toString();
|
||||
} else if (videoUrl.contains('.mcdn.bilivideo') ||
|
||||
videoUrl.contains('/upgcxcode/')) {
|
||||
videoUrl = Uri.parse(
|
||||
videoUrl,
|
||||
).replace(host: defaultCDNHost, port: 443).toString();
|
||||
// videoUrl =
|
||||
// 'https://proxy-tf-all-ws.bilivideo.com/?url=${Uri.encodeComponent(videoUrl)}';
|
||||
}
|
||||
// if (kDebugMode) debugPrint('videoUrl:$videoUrl');
|
||||
|
||||
// /// 先获取backupUrl 一般是upgcxcode地址 播放更稳定
|
||||
// if (item is VideoItem) {
|
||||
// backupUrl = item.backupUrl ?? '';
|
||||
// videoUrl = backupUrl.contains('http') ? backupUrl : (item.baseUrl ?? '');
|
||||
// } else if (item is AudioItem) {
|
||||
// backupUrl = item.backupUrl ?? '';
|
||||
// videoUrl = backupUrl.contains('http') ? backupUrl : (item.baseUrl ?? '');
|
||||
// } else if (item is CodecItem) {
|
||||
// backupUrl = (item.urlInfo?.first.host)! +
|
||||
// item.baseUrl! +
|
||||
// item.urlInfo!.first.extra!;
|
||||
// videoUrl = backupUrl.contains('http') ? backupUrl : (item.baseUrl ?? '');
|
||||
// } else {
|
||||
// return '';
|
||||
// }
|
||||
//
|
||||
// /// issues #70
|
||||
// if (videoUrl.contains('.mcdn.bilivideo')) {
|
||||
// videoUrl =
|
||||
// 'https://proxy-tf-all-ws.bilivideo.com/?url=${Uri.encodeComponent(videoUrl)}';
|
||||
// } else if (videoUrl.contains('/upgcxcode/')) {
|
||||
// //CDN列表
|
||||
// var cdnList = {
|
||||
// 'ali': 'upos-sz-mirrorali.bilivideo.com',
|
||||
// 'cos': 'upos-sz-mirrorcos.bilivideo.com',
|
||||
// 'hw': 'upos-sz-mirrorhw.bilivideo.com',
|
||||
// };
|
||||
// //取一个CDN
|
||||
// var cdn = cdnList['cos'] ?? '';
|
||||
// var reg = RegExp(r'(http|https)://(.*?)/upgcxcode/');
|
||||
// videoUrl = videoUrl.replaceAll(reg, 'https://$cdn/upgcxcode/');
|
||||
// }
|
||||
|
||||
return videoUrl;
|
||||
return mcdnUpgcxcode == null
|
||||
? mcdnTf == null
|
||||
? last
|
||||
: Uri(
|
||||
scheme: 'https',
|
||||
host: _proxyTf,
|
||||
queryParameters: {'url': mcdnTf},
|
||||
).toString()
|
||||
: Uri.parse(mcdnUpgcxcode)
|
||||
.replace(host: defaultCDNService.host ?? CDNService.ali.host)
|
||||
.toString();
|
||||
}
|
||||
|
||||
static String getDurlCdnUrl(ResponseUrl item) {
|
||||
if (disableAudioCDN || cdnService == CDNService.backupUrl.code) {
|
||||
return item.backupUrl.lastOrNull ?? item.url;
|
||||
}
|
||||
if (cdnService == CDNService.baseUrl.code) {
|
||||
return item.url;
|
||||
}
|
||||
return Uri.parse(
|
||||
item.backupUrl.lastOrNull ?? item.url,
|
||||
).replace(host: CDNService.fromCode(cdnService).host, port: 443).toString();
|
||||
static String getLiveCdnUrl(CodecItem e) {
|
||||
return (liveCdnUrl ?? e.urlInfo!.first.host!) +
|
||||
e.baseUrl! +
|
||||
e.urlInfo!.first.extra!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user