diff --git a/lib/http/download.dart b/lib/http/download.dart index 7d9f31943..5ba2e4d99 100644 --- a/lib/http/download.dart +++ b/lib/http/download.dart @@ -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, diff --git a/lib/models/common/video/cdn_type.dart b/lib/models/common/video/cdn_type.dart index 9bccbf35d..5bdfc4b9d 100644 --- a/lib/models/common/video/cdn_type.dart +++ b/lib/models/common/video/cdn_type.dart @@ -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} +// } \ No newline at end of file diff --git a/lib/models/common/video/live_quality.dart b/lib/models/common/video/live_quality.dart index e368c2237..6065c8e71 100644 --- a/lib/models/common/video/live_quality.dart +++ b/lib/models/common/video/live_quality.dart @@ -2,6 +2,7 @@ enum LiveQuality { dolby(30000, '杜比'), origin4K(25000, '4K 原画'), super4K(20000, '4K'), + super2K(15000, '2K'), origin(10000, '原画'), bluRay(400, '蓝光'), superHD(250, '超清'), diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index b138ddcaf..ae7b601b2 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -196,23 +196,17 @@ class Durl { backupUrl: (json['backup_url'] as List?)?.fromCast(), ); } -} -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 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? backupUrl; int? bandWidth; String? mimeType; String? codecs; @@ -243,14 +237,8 @@ abstract class BaseItem { BaseItem.fromJson(Map 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(); - 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 get playUrls sync* { + if (baseUrl?.isNotEmpty == true) yield baseUrl!; + if (backupUrl?.isNotEmpty == true) yield* backupUrl!; + } } class VideoItem extends BaseItem { diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index ec9ce1491..bc8f3081d 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -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 get playUrls sync* { + yield baseUrl; + yield* backupUrl; + } +} + +extension on ResponseUrl { + Iterable get playUrls sync* { + yield url; + yield* backupUrl; + } +} diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 7e0d996fb..6ead9a86b 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -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 { diff --git a/lib/pages/setting/models/video_settings.dart b/lib/pages/setting/models/video_settings.dart index c114f0cf8..0bc4864ae 100644 --- a/lib/pages/setting/models/video_settings.dart +++ b/lib/pages/setting/models/video_settings.dart @@ -54,9 +54,9 @@ List 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 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( + 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(); } }, diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 1f605117f..200ed822d 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -68,7 +68,7 @@ class SelectDialog extends StatelessWidget { } class CdnSelectDialog extends StatefulWidget { - final VideoItem? sample; + final BaseItem? sample; const CdnSelectDialog({ super.key, @@ -113,7 +113,7 @@ class _CdnSelectDialogState extends State { super.dispose(); } - Future _getSampleUrl() async { + Future _getSampleUrl() async { final result = await VideoHttp.videoUrl( cid: 196018899, bvid: 'BV1fK4y1t7hj', @@ -134,16 +134,19 @@ class _CdnSelectDialogState extends State { } } - Future _testAllCdnServices(VideoItem videoItem) async { + Future _testAllCdnServices(BaseItem videoItem) async { for (final item in CDNService.values) { if (!mounted) break; await _testSingleCdn(item, videoItem); } } - Future _testSingleCdn(CDNService item, VideoItem videoItem) async { + Future _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 { 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 { @override Widget build(BuildContext context) { - return SelectDialog( + return SelectDialog( 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) { diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 6993b5cba..864c7fef3 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -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) { diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 0f83552c4..453492d22 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -427,25 +427,23 @@ class HeaderControlState extends State { 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, diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 3193fcbe7..faaa277c7 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -224,7 +224,8 @@ abstract class SettingBoxKey { enableGradientBg = 'enableGradientBg', navBarSort = 'navBarSort', tempPlayerConf = 'tempPlayerConf', - reduceLuxColor = 'reduceLuxColor'; + reduceLuxColor = 'reduceLuxColor', + liveCdnUrl = 'liveCdnUrl'; } abstract class LocalCacheKey { diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 325bab6d0..44ce45bde 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -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); } diff --git a/lib/utils/video_utils.dart b/lib/utils/video_utils.dart index ec198e969..611b58d3f 100644 --- a/lib/utils/video_utils.dart +++ b/lib/utils/video_utils.dart @@ -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 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!; } }