Files
PiliPlus/lib/pages/setting/models/video_settings.dart
My-Responsitories 22f37074b1 feat: custom buffer size (#2370)
* feat: custom buffer size

* cache buffer params

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-06-10 09:54:32 +08:00

572 lines
17 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'package:PiliPlus/models/common/video/audio_quality.dart';
import 'package:PiliPlus/models/common/video/cdn_type.dart';
import 'package:PiliPlus/models/common/video/live_quality.dart';
import 'package:PiliPlus/models/common/video/video_decode_type.dart';
import 'package:PiliPlus/models/common/video/video_quality.dart';
import 'package:PiliPlus/pages/setting/models/model.dart';
import 'package:PiliPlus/pages/setting/widgets/ordered_multi_select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/plugin/pl_player/models/audio_output_type.dart';
import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart';
import 'package:PiliPlus/utils/filtering_text.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/video_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
List<SettingsModel> get videoSettings => [
const SwitchModel(
title: '开启硬解',
subtitle: '以较低功耗播放视频,若异常卡死请关闭',
leading: Icon(Icons.flash_on_outlined),
setKey: SettingBoxKey.enableHA,
defaultVal: true,
),
const SwitchModel(
title: '免登录1080P',
subtitle: '免登录查看1080P视频',
leading: Icon(Icons.hd_outlined),
setKey: SettingBoxKey.p1080,
defaultVal: true,
),
NormalModel(
title: 'B站定向流量支持',
subtitle: '若套餐含B站定向流量则会自动使用。可查阅运营商的流量记录确认。',
leading: const Icon(Icons.perm_data_setting_outlined),
getTrailing: (theme) => IgnorePointer(
child: Transform.scale(
scale: 0.8,
alignment: Alignment.centerRight,
child: Switch(
value: true,
onChanged: (_) {},
thumbIcon: WidgetStateProperty.all(
const Icon(Icons.lock_outline_rounded),
),
),
),
),
),
NormalModel(
title: 'CDN 设置',
leading: const Icon(MdiIcons.cloudPlusOutline),
getSubtitle: () =>
'当前使用:${VideoUtils.cdnService.desc},部分 CDN 可能失效,如无法播放请尝试切换',
onTap: _showCDNDialog,
),
NormalModel(
title: '直播 CDN 设置',
leading: const Icon(MdiIcons.cloudPlusOutline),
getSubtitle: () => '当前使用:${Pref.liveCdnUrl ?? "默认"}',
onTap: _showLiveCDNDialog,
),
const SwitchModel(
title: 'CDN 测速',
leading: Icon(Icons.speed),
subtitle: '测速通过模拟加载视频实现,注意流量消耗,结果仅供参考',
setKey: SettingBoxKey.cdnSpeedTest,
defaultVal: true,
),
SwitchModel(
title: '音频不跟随 CDN 设置',
subtitle: '直接采用备用 URL可解决部分视频无声',
leading: const Icon(MdiIcons.musicNotePlus),
setKey: SettingBoxKey.disableAudioCDN,
defaultVal: false,
onChanged: (value) => VideoUtils.disableAudioCDN = value,
),
NormalModel(
title: '默认画质',
leading: const Icon(Icons.video_settings_outlined),
getSubtitle: () =>
'当前画质:${VideoQuality.fromCode(Pref.defaultVideoQa).desc}',
onTap: _showVideoQaDialog,
),
NormalModel(
title: '蜂窝网络画质',
leading: const Icon(Icons.video_settings_outlined),
getSubtitle: () =>
'当前画质:${VideoQuality.fromCode(Pref.defaultVideoQaCellular).desc}',
onTap: _showVideoCellularQaDialog,
),
NormalModel(
title: '默认音质',
leading: const Icon(Icons.music_video_outlined),
getSubtitle: () =>
'当前音质:${AudioQuality.fromCode(Pref.defaultAudioQa).desc}',
onTap: _showAudioQaDialog,
),
NormalModel(
title: '蜂窝网络音质',
leading: const Icon(Icons.music_video_outlined),
getSubtitle: () =>
'当前音质:${AudioQuality.fromCode(Pref.defaultAudioQaCellular).desc}',
onTap: _showAudioCellularQaDialog,
),
NormalModel(
title: '直播默认画质',
leading: const Icon(Icons.video_settings_outlined),
getSubtitle: () => '当前画质:${LiveQuality.fromCode(Pref.liveQuality)?.desc}',
onTap: _showLiveQaDialog,
),
NormalModel(
title: '蜂窝网络直播默认画质',
leading: const Icon(Icons.video_settings_outlined),
getSubtitle: () =>
'当前画质:${LiveQuality.fromCode(Pref.liveQualityCellular)?.desc}',
onTap: _showLiveCellularQaDialog,
),
NormalModel(
title: '首选解码格式',
leading: const Icon(Icons.movie_creation_outlined),
getSubtitle: () =>
'首选解码格式:${VideoDecodeFormatType.fromCode(Pref.defaultDecode).description},请根据设备支持情况与需求调整',
onTap: _showDecodeDialog,
),
NormalModel(
title: '次选解码格式',
getSubtitle: () =>
'非杜比视频次选:${VideoDecodeFormatType.fromCode(Pref.secondDecode).description},仍无则选择首个提供的解码格式',
leading: const Icon(Icons.swap_horizontal_circle_outlined),
onTap: _showSecondDecodeDialog,
),
if (kDebugMode || Platform.isAndroid)
NormalModel(
title: '音频输出设备',
leading: const Icon(Icons.speaker_outlined),
getSubtitle: () => '当前:${Pref.audioOutput}',
onTap: _showAudioOutputDialog,
),
NormalModel(
title: '缓冲大小',
leading: const Icon(Icons.storage_outlined),
getSubtitle: () =>
'当前:${Pref.bufferSize}MB。同时为前向和后向缓冲区大小。对于直播流无后向缓冲大小全部转给前向此选项即mpv的--demuxer-max-bytes--demuxer-max-back-bytes',
onTap: _showBufferSizeDialog,
),
NormalModel(
title: '缓冲时长',
leading: const Icon(Icons.av_timer),
getSubtitle: () =>
'当前:${Pref.bufferSec}s。实际缓冲为二者最小值。对于直播流该选项无效此选项即mpv的--cache-secs',
onTap: _showBufferSecDialog,
),
NormalModel(
title: '自动同步',
leading: const Icon(Icons.sync_rounded),
getSubtitle: () => '当前:${Pref.autosync}此项即mpv的--autosync',
onTap: _showAutoSyncDialog,
),
NormalModel(
title: '视频同步',
leading: const Icon(Icons.view_timeline_outlined),
getSubtitle: () => '当前:${Pref.videoSync}此项即mpv的--video-sync',
onTap: _showVideoSyncDialog,
),
NormalModel(
title: '硬解模式',
leading: const Icon(Icons.memory_outlined),
getSubtitle: () => '当前:${Pref.hardwareDecoding}此项即mpv的--hwdec',
onTap: _showHwDecDialog,
),
];
Future<void> _showCDNDialog(BuildContext context, VoidCallback setState) async {
final res = await showDialog<CDNService>(
context: context,
builder: (context) => const CdnSelectDialog(),
);
if (res != null) {
VideoUtils.cdnService = res;
await GStorage.setting.put(SettingBoxKey.CDNService, res.name);
setState();
}
}
Future<void> _showLiveCDNDialog(
BuildContext context,
VoidCallback setState,
) async {
String host = Pref.liveCdnUrl ?? '';
String? res = await showDialog<String>(
context: context,
builder: (context) => 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 (res != null) {
if (res.isEmpty) {
res = null;
await GStorage.setting.delete(SettingBoxKey.liveCdnUrl);
} else {
if (!res.startsWith('http')) {
res = 'https://$res';
}
await GStorage.setting.put(SettingBoxKey.liveCdnUrl, res);
}
VideoUtils.liveCdnUrl = res;
setState();
}
}
Future<void> _showVideoQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '默认画质',
value: Pref.defaultVideoQa,
values: VideoQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.defaultVideoQa, res);
setState();
}
}
Future<void> _showVideoCellularQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '蜂窝网络画质',
value: Pref.defaultVideoQaCellular,
values: VideoQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(
SettingBoxKey.defaultVideoQaCellular,
res,
);
setState();
}
}
Future<void> _showAudioQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '默认音质',
value: Pref.defaultAudioQa,
values: AudioQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.defaultAudioQa, res);
setState();
}
}
Future<void> _showAudioCellularQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '蜂窝网络音质',
value: Pref.defaultAudioQaCellular,
values: AudioQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(
SettingBoxKey.defaultAudioQaCellular,
res,
);
setState();
}
}
Future<void> _showLiveQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '直播默认画质',
value: Pref.liveQuality,
values: LiveQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.liveQuality, res);
setState();
}
}
Future<void> _showLiveCellularQaDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<int>(
context: context,
builder: (context) => SelectDialog<int>(
title: '蜂窝网络直播默认画质',
value: Pref.liveQualityCellular,
values: LiveQuality.values.map((e) => (e.code, e.desc)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.liveQualityCellular, res);
setState();
}
}
Future<void> _showDecodeDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<String>(
context: context,
builder: (context) => SelectDialog<String>(
title: '默认解码格式',
value: Pref.defaultDecode,
values: VideoDecodeFormatType.values
.map((e) => (e.codes.first, e.description))
.toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.defaultDecode, res);
setState();
}
}
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();
}
}
Future<void> _showAudioOutputDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<List<String>>(
context: context,
builder: (context) => OrderedMultiSelectDialog<String>(
title: '音频输出设备',
initValues: Pref.audioOutput.split(','),
values: {
for (final e in AudioOutput.values) e.name: e.label,
},
),
);
if (res != null && res.isNotEmpty) {
await GStorage.setting.put(
SettingBoxKey.audioOutput,
res.join(','),
);
setState();
}
}
Future<void> _showVideoSyncDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<String>(
context: context,
builder: (context) => SelectDialog<String>(
title: '视频同步',
value: Pref.videoSync,
values: const [
'audio',
'display-resample',
'display-resample-vdrop',
'display-resample-desync',
'display-tempo',
'display-vdrop',
'display-adrop',
'display-desync',
'desync',
].map((e) => (e, e)).toList(),
),
);
if (res != null) {
await GStorage.setting.put(SettingBoxKey.videoSync, res);
setState();
}
}
Future<void> _showHwDecDialog(
BuildContext context,
VoidCallback setState,
) async {
final res = await showDialog<List<String>>(
context: context,
builder: (context) => OrderedMultiSelectDialog<String>(
title: '硬解模式',
initValues: Pref.hardwareDecoding.split(','),
values: {
for (final e in HwDecType.values) e.hwdec: '${e.hwdec}\n${e.desc}',
},
),
);
if (res != null && res.isNotEmpty) {
await GStorage.setting.put(
SettingBoxKey.hardwareDecoding,
res.join(','),
);
setState();
}
}
void _showAutoSyncDialog(BuildContext context, VoidCallback setState) {
String autosync = Pref.autosync.toString();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('自动同步'),
content: TextFormField(
autofocus: true,
initialValue: autosync,
keyboardType: TextInputType.number,
onChanged: (value) => autosync = value,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: ColorScheme.of(context).outline),
),
),
TextButton(
onPressed: () async {
try {
// validate
int.parse(autosync);
Get.back();
await GStorage.setting.put(SettingBoxKey.autosync, autosync);
setState();
} catch (e) {
SmartDialog.showToast(e.toString());
}
},
child: const Text('确定'),
),
],
),
);
}
void _showDecimalDialog(
BuildContext context,
VoidCallback setState, {
required String key,
required double defVal,
required String title,
required String? suffix,
}) {
String value = (GStorage.setting.get(key) ?? defVal).toString();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: TextFormField(
autofocus: true,
initialValue: value,
keyboardType: const .numberWithOptions(decimal: true),
onChanged: (val) => value = val,
inputFormatters: FilteringText.decimal,
decoration: suffix == null ? null : InputDecoration(suffixText: suffix),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: ColorScheme.of(context).outline),
),
),
TextButton(
onPressed: () async {
try {
final val = double.parse(value);
Get.back();
await GStorage.setting.put(key, val);
setState();
} catch (e) {
SmartDialog.showToast(e.toString());
}
},
child: const Text('确定'),
),
],
),
);
}
void _showBufferSizeDialog(BuildContext context, VoidCallback setState) =>
_showDecimalDialog(
context,
setState,
key: SettingBoxKey.bufferSize,
defVal: Pref.bufferSize,
title: '缓冲大小',
suffix: 'MB',
);
void _showBufferSecDialog(BuildContext context, VoidCallback setState) =>
_showDecimalDialog(
context,
setState,
key: SettingBoxKey.bufferSec,
defVal: Pref.bufferSec,
title: '缓冲时长',
suffix: 's',
);