Files
PiliPlus/lib/pages/setting/models/video_settings.dart
2026-01-29 18:12:08 +08:00

497 lines
14 KiB
Dart
Raw 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/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,
),
const SwitchModel(
title: '扩大缓冲区',
leading: Icon(Icons.storage_outlined),
subtitle: '默认缓冲区为视频4MB/直播16MB开启后为32MB/64MB加载时间变长',
setKey: SettingBoxKey.expandBuffer,
defaultVal: false,
),
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('确定'),
),
],
),
);
}