* 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:
My-Responsitories
2025-11-15 20:12:21 +08:00
committed by GitHub
parent e589f27195
commit 27ae296b28
13 changed files with 297 additions and 156 deletions

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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();
}
},

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,