feat: codec list options

This commit is contained in:
My-Responsitories
2026-06-24 01:07:11 +08:00
parent cd3b0547a2
commit dee3580c4a
7 changed files with 140 additions and 170 deletions

View File

@@ -2,12 +2,12 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/account_type.dart';
import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart';
import 'package:PiliPlus/models/common/video/video_decode_type.dart';
import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart';
import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart';
import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models/video/play/url.dart';
import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
import 'package:PiliPlus/models_new/download/bili_download_media_file_info.dart'; import 'package:PiliPlus/models_new/download/bili_download_media_file_info.dart';
import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
@@ -40,9 +40,9 @@ abstract final class DownloadHttp {
}, },
); );
if (res case Success(:final response)) { if (res case Success(:final response)) {
final Dash? dash = response.dash; final dash = response.dash;
if (dash != null) { if (dash != null) {
final List<VideoItem> videoList = dash.video!; final videoList = dash.video!;
final curHighestVideoQa = videoList.first.quality.code; final curHighestVideoQa = videoList.first.quality.code;
final preferVideoQa = entry.preferedVideoQuality; final preferVideoQa = entry.preferedVideoQuality;
int targetVideoQa = curHighestVideoQa; int targetVideoQa = curHighestVideoQa;
@@ -55,19 +55,18 @@ abstract final class DownloadHttp {
); );
} }
/// 取出符合当前画质的videoList
final List<VideoItem> videosList = videoList
.where((e) => e.quality.code == targetVideoQa)
.toList();
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
final List<FormatItem> supportFormats = response.supportFormats!; final supportFormats = response.supportFormats!;
// 根据画质选编码格式 // 根据画质选编码格式
final FormatItem targetSupportFormats = supportFormats.firstWhere( final targetSupportFormats = supportFormats.firstWhere(
(e) => e.quality == targetVideoQa, (e) => e.quality == targetVideoQa,
orElse: () => supportFormats.first, orElse: () => supportFormats.first,
); );
final List<String> supportDecodeFormats = targetSupportFormats.codecs!;
final currentDecodeFormats = VideoDetailController.selectCodec(
targetSupportFormats.codecs!,
Pref.preferCodecs,
);
entry entry
..typeTag = targetVideoQa.toString() ..typeTag = targetVideoQa.toString()
@@ -77,31 +76,10 @@ abstract final class DownloadHttp {
targetSupportFormats.newDesc ?? targetSupportFormats.newDesc ??
VideoQuality.fromCode(targetVideoQa).desc; VideoQuality.fromCode(targetVideoQa).desc;
String preferDecode = Pref.defaultDecode; // def avc /// 取出符合当前画质的videoList
String preferSecondDecode = Pref.secondDecode; // def av1 final videosList = videoList
.where((e) => e.quality.code == targetVideoQa)
// 默认从设置中取AV1 .toList();
VideoDecodeFormatType currentDecodeFormats =
VideoDecodeFormatType.fromString(preferDecode);
VideoDecodeFormatType secondDecodeFormats =
VideoDecodeFormatType.fromString(preferSecondDecode);
// 当前视频没有对应格式返回第一个
int flag = 0;
for (final e in supportDecodeFormats) {
if (currentDecodeFormats.codes.any(e.startsWith)) {
flag = 1;
break;
} else if (secondDecodeFormats.codes.any(e.startsWith)) {
flag = 2;
}
}
if (flag == 2) {
currentDecodeFormats = secondDecodeFormats;
} else if (flag == 0) {
currentDecodeFormats = VideoDecodeFormatType.fromString(
supportDecodeFormats.first,
);
}
/// 取出符合当前解码格式的videoItem /// 取出符合当前解码格式的videoItem
final videoDash = videosList.firstWhere( final videoDash = videosList.firstWhere(

View File

@@ -12,9 +12,6 @@ enum VideoDecodeFormatType {
const VideoDecodeFormatType(this.codes); const VideoDecodeFormatType(this.codes);
static VideoDecodeFormatType fromCode(String code) =>
values.firstWhere((i) => i.codes.contains(code));
static VideoDecodeFormatType fromString(String val) => static VideoDecodeFormatType fromString(String val) =>
values.firstWhere((i) => i.codes.any(val.startsWith)); values.firstWhere((i) => i.codes.any(val.startsWith));
} }

View File

@@ -127,16 +127,11 @@ List<SettingsModel> get videoSettings => [
NormalModel( NormalModel(
title: '首选解码格式', title: '首选解码格式',
leading: const Icon(Icons.movie_creation_outlined), leading: const Icon(Icons.movie_creation_outlined),
getSubtitle: () => getSubtitle: () {
'首选解码格式:${VideoDecodeFormatType.fromCode(Pref.defaultDecode).description},请根据设备支持情况与需求调整', final list = Pref.preferCodecs;
onTap: _showDecodeDialog, return '首选解码格式:${(list.isEmpty ? '第一个可用' : list.map((i) => i.name).join(","))},请根据设备支持情况与需求调整';
), },
NormalModel( onTap: _showCodecsDialog,
title: '次选解码格式',
getSubtitle: () =>
'非杜比视频次选:${VideoDecodeFormatType.fromCode(Pref.secondDecode).description},仍无则选择首个提供的解码格式',
leading: const Icon(Icons.swap_horizontal_circle_outlined),
onTap: _showSecondDecodeDialog,
), ),
if (kDebugMode || Platform.isAndroid) if (kDebugMode || Platform.isAndroid)
NormalModel( NormalModel(
@@ -349,42 +344,25 @@ Future<void> _showLiveCellularQaDialog(
} }
} }
Future<void> _showDecodeDialog( Future<void> _showCodecsDialog(
BuildContext context, BuildContext context,
VoidCallback setState, VoidCallback setState,
) async { ) async {
final res = await showDialog<String>( final res = await showDialog<List<VideoDecodeFormatType>>(
context: context, context: context,
builder: (context) => SelectDialog<String>( builder: (context) => OrderedMultiSelectDialog<VideoDecodeFormatType>(
title: '默认解码格式', title: '首选解码格式',
value: Pref.defaultDecode, initValues: Pref.preferCodecs,
values: VideoDecodeFormatType.values values: {for (final e in VideoDecodeFormatType.values) e: e.name},
.map((e) => (e.codes.first, e.description))
.toList(),
), ),
); );
if (res != null) { if (res != null) {
await GStorage.setting.put(SettingBoxKey.defaultDecode, res); await (res.isEmpty
setState(); ? GStorage.setting.delete(SettingBoxKey.preferCodecs)
} : GStorage.setting.put(
} SettingBoxKey.preferCodecs,
res.map((i) => i.name).toList(),
Future<void> _showSecondDecodeDialog( ));
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<String>(
context: context,
builder: (context) => SelectDialog<String>(
title: '次选解码格式',
value: Pref.secondDecode,
values: VideoDecodeFormatType.values
.map((e) => (e.codes.first, e.description))
.toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.secondDecode, res);
setState(); setState();
} }
} }

View File

@@ -143,8 +143,7 @@ class VideoDetailController extends GetxController
Box setting = GStorage.setting; Box setting = GStorage.setting;
// 预设的解码格式 // 预设的解码格式
late String cacheDecode = Pref.defaultDecode; // def avc late List<VideoDecodeFormatType> preferCodecs = Pref.preferCodecs;
late String cacheSecondDecode = Pref.secondDecode; // def av1
bool get showReply => isFileSource bool get showReply => isFileSource
? false ? false
@@ -669,31 +668,38 @@ class VideoDetailController extends GetxController
} }
} }
VideoItem findVideoByQa(int qa) { VideoItem findVideoByQa(int qa, {bool setCodecs = false}) {
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
final videoList = data.dash!.video!.where((i) => i.id == qa).toList(); final videoList = data.dash!.video!.where((i) => i.id == qa).toList();
final currentDecodeFormats = this.currentDecodeFormats.codes; final currentCodes = currentDecodeFormats.codes;
final defaultDecodeFormats = VideoDecodeFormatType.fromString( VideoItem? bestVideo;
cacheDecode, int bestIndex = preferCodecs.length;
).codes; for (final video in videoList) {
final secondDecodeFormats = VideoDecodeFormatType.fromString( final c = video.codecs!;
cacheSecondDecode, if (currentCodes.any(c.startsWith)) {
).codes; return video;
}
VideoItem? video; for (int i = 0; i < bestIndex; i++) {
for (final i in videoList) { if (preferCodecs[i].codes.any(c.startsWith)) {
final codec = i.codecs!; bestIndex = i;
if (currentDecodeFormats.any(codec.startsWith)) { bestVideo = video;
video = i; break;
break; }
} else if (defaultDecodeFormats.any(codec.startsWith)) {
video = i;
} else if (video == null && secondDecodeFormats.any(codec.startsWith)) {
video = i;
} }
} }
return video ?? videoList.first;
if (setCodecs) {
if (bestIndex < preferCodecs.length) {
currentDecodeFormats = preferCodecs[bestIndex];
} else {
currentDecodeFormats = VideoDecodeFormatType.fromString(
videoList.first.codecs!,
);
}
}
return bestVideo ?? videoList.first;
} }
/// 更新画质、音质 /// 更新画质、音质
@@ -706,11 +712,7 @@ class VideoDetailController extends GetxController
..isBuffering.value = false ..isBuffering.value = false
..buffered.value = Duration.zero; ..buffered.value = Duration.zero;
final video = findVideoByQa(currentVideoQa.code); firstVideo = findVideoByQa(currentVideoQa.code, setCodecs: true);
if (firstVideo.codecs != video.codecs) {
currentDecodeFormats = VideoDecodeFormatType.fromString(video.codecs!);
}
firstVideo = video;
videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls); videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls);
/// 根据currentAudioQa 重新设置audioUrl /// 根据currentAudioQa 重新设置audioUrl
@@ -814,9 +816,34 @@ class VideoDetailController extends GetxController
queryVideoUrl(fromReset: true); queryVideoUrl(fromReset: true);
} }
static VideoDecodeFormatType selectCodec(
Iterable<String> codecs,
List<VideoDecodeFormatType> preferCodecs,
) {
if (preferCodecs.isNotEmpty) {
int bestIndex = preferCodecs.length;
for (final e in codecs) {
for (int i = 0; i < bestIndex; i++) {
if (preferCodecs[i].codes.any(e.startsWith)) {
bestIndex = i;
if (bestIndex == 0) {
return preferCodecs[0];
}
break;
}
}
}
if (bestIndex < preferCodecs.length) {
return preferCodecs[bestIndex];
}
}
return VideoDecodeFormatType.fromString(codecs.first);
}
Volume? volume; Volume? volume;
// 视频链接 // 视频链接
/// TODO: merge [DownloadHttp.getVideoUrl].
Future<void> queryVideoUrl({ Future<void> queryVideoUrl({
bool fromReset = false, bool fromReset = false,
bool autoFullScreenFlag = false, bool autoFullScreenFlag = false,
@@ -897,7 +924,7 @@ class VideoDetailController extends GetxController
quality: videoQuality, quality: videoQuality,
); );
_setVideoHeight(); _setVideoHeight();
currentDecodeFormats = VideoDecodeFormatType.fromString('avc1'); currentDecodeFormats = VideoDecodeFormatType.AVC;
currentVideoQa.value = videoQuality; currentVideoQa.value = videoQuality;
await _initPlayerIfNeeded(autoFullScreenFlag); await _initPlayerIfNeeded(autoFullScreenFlag);
isQuerying = false; isQuerying = false;
@@ -929,42 +956,25 @@ class VideoDetailController extends GetxController
} }
currentVideoQa.value = VideoQuality.fromCode(targetVideoQa); currentVideoQa.value = VideoQuality.fromCode(targetVideoQa);
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
final supportFormats = data.supportFormats!;
// 根据画质选编码格式
currentDecodeFormats = selectCodec(
supportFormats
.firstWhere(
(e) => e.quality == targetVideoQa,
orElse: () => supportFormats.first,
)
.codecs!,
preferCodecs,
);
/// 取出符合当前画质的videoList /// 取出符合当前画质的videoList
final List<VideoItem> videosList = videoList final videosList = videoList
.where((e) => e.quality.code == targetVideoQa) .where((e) => e.quality.code == targetVideoQa)
.toList(); .toList();
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
final List<FormatItem> supportFormats = data.supportFormats!;
// 根据画质选编码格式
final List<String> supportDecodeFormats = supportFormats
.firstWhere(
(e) => e.quality == targetVideoQa,
orElse: () => supportFormats.first,
)
.codecs!;
// 默认从设置中取AV1
currentDecodeFormats = VideoDecodeFormatType.fromString(cacheDecode);
VideoDecodeFormatType secondDecodeFormats =
VideoDecodeFormatType.fromString(cacheSecondDecode);
// 当前视频没有对应格式返回第一个
int flag = 0;
for (final e in supportDecodeFormats) {
if (currentDecodeFormats.codes.any(e.startsWith)) {
flag = 1;
break;
} else if (secondDecodeFormats.codes.any(e.startsWith)) {
flag = 2;
}
}
if (flag == 2) {
currentDecodeFormats = secondDecodeFormats;
} else if (flag == 0) {
currentDecodeFormats = VideoDecodeFormatType.fromString(
supportDecodeFormats.first,
);
}
/// 取出符合当前解码格式的videoItem /// 取出符合当前解码格式的videoItem
firstVideo = videosList.firstWhere( firstVideo = videosList.firstWhere(
(e) => currentDecodeFormats.codes.any(e.codecs!.startsWith), (e) => currentDecodeFormats.codes.any(e.codecs!.startsWith),

View File

@@ -1069,28 +1069,26 @@ class HeaderControlState extends State<HeaderControl>
// 选择解码格式 // 选择解码格式
void showSetDecodeFormats() { void showSetDecodeFormats() {
final VideoItem firstVideo = videoDetailCtr.firstVideo; final firstCode = videoDetailCtr.firstVideo.quality.code;
// 当前视频可用的解码格式 // 当前视频可用的解码格式
final List<FormatItem> videoFormat = videoInfo.supportFormats!; final videoFormat = videoInfo.supportFormats!;
final List<String>? list = videoFormat
.firstWhere((FormatItem e) => e.quality == firstVideo.quality.code) final list = videoFormat.firstWhere((e) => e.quality == firstCode).codecs;
.codecs;
if (list == null) { if (list == null) {
SmartDialog.showToast('当前视频不支持选择解码格式'); SmartDialog.showToast('当前视频不支持选择解码格式');
return; return;
} }
// 当前选中的解码格式 // 当前选中的解码格式
final VideoDecodeFormatType currentDecodeFormats = final curCodecs = videoDetailCtr.currentDecodeFormats.codes;
videoDetailCtr.currentDecodeFormats;
showBottomSheet( showBottomSheet(
(context, setState) { (context, setState) {
final theme = Theme.of(context); final colorScheme = ColorScheme.of(context);
return Padding( return Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Material( child: Material(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
color: theme.colorScheme.surface, color: colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Column( child: Column(
children: [ children: [
@@ -1108,30 +1106,22 @@ class HeaderControlState extends State<HeaderControl>
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
final format = VideoDecodeFormatType.fromString(item); final format = VideoDecodeFormatType.fromString(item);
final isCurr = currentDecodeFormats.codes.any( final isCurr = curCodecs.any(item.startsWith);
item.startsWith,
);
return ListTile( return ListTile(
dense: true, dense: true,
onTap: () { onTap: () {
if (isCurr) { if (isCurr) return;
return;
}
Get.back(); Get.back();
videoDetailCtr videoDetailCtr
..currentDecodeFormats = format ..currentDecodeFormats = format
..updatePlayer(); ..updatePlayer();
SmartDialog.showToast("解码已变为:${format.name}");
}, },
contentPadding: const EdgeInsets.symmetric( contentPadding: const .symmetric(horizontal: 20),
horizontal: 20,
),
title: Text(format.description), title: Text(format.description),
subtitle: Text(item, style: subTitleStyle), subtitle: Text(item, style: subTitleStyle),
trailing: isCurr trailing: isCurr
? Icon( ? Icon(Icons.done, color: colorScheme.primary)
Icons.done,
color: theme.colorScheme.primary,
)
: null, : null,
); );
}, },

View File

@@ -8,8 +8,7 @@ abstract final class SettingBoxKey {
defaultAudioQaCellular = 'defaultAudioQaCellular', defaultAudioQaCellular = 'defaultAudioQaCellular',
autoPlayEnable = 'autoPlayEnable', autoPlayEnable = 'autoPlayEnable',
fullScreenMode = 'fullScreenMode', fullScreenMode = 'fullScreenMode',
defaultDecode = 'defaultDecode', preferCodecs = 'preferCodecs',
secondDecode = 'secondDecode',
defaultToastOp = 'defaultToastOp', defaultToastOp = 'defaultToastOp',
defaultPicQa = 'defaultPicQa', defaultPicQa = 'defaultPicQa',
enableHA = 'enableHA', enableHA = 'enableHA',

View File

@@ -245,15 +245,33 @@ abstract final class Pref {
defaultValue: AudioQuality.k192.code, defaultValue: AudioQuality.k192.code,
); );
static String get defaultDecode => _setting.get( static List<VideoDecodeFormatType> get preferCodecs {
SettingBoxKey.defaultDecode, // TODO: remove next 2 version
defaultValue: VideoDecodeFormatType.AVC.codes.first, if (_setting.get('defaultDecode') case String codecStr) {
); String? codecStr2 = _setting.get('secondDecode');
_setting.deleteAll(const ['defaultDecode', 'secondDecode']);
final codecs = [
VideoDecodeFormatType.values.firstWhere(
(i) => i.codes.contains(codecStr),
),
if (codecStr2 != null && codecStr2 != codecStr)
VideoDecodeFormatType.values.firstWhere(
(i) => i.codes.contains(codecStr2),
),
];
_setting.put(
SettingBoxKey.preferCodecs,
codecs.map((i) => i.name).toList(),
);
return codecs;
}
static String get secondDecode => _setting.get( final codecs = _setting.get(SettingBoxKey.preferCodecs);
SettingBoxKey.secondDecode, if (codecs is List && codecs.isNotEmpty) {
defaultValue: VideoDecodeFormatType.AV1.codes.first, return codecs.map((i) => VideoDecodeFormatType.values.byName(i)).toList();
); }
return const [];
}
static String get hardwareDecoding => _setting.get( static String get hardwareDecoding => _setting.get(
SettingBoxKey.hardwareDecoding, SettingBoxKey.hardwareDecoding,