feat: 支持设置 画质、音质、编码 的优先级

This commit is contained in:
lanyeeee
2025-09-05 05:06:38 +08:00
parent ae73e474c7
commit 5ed41ef7b6
10 changed files with 219 additions and 277 deletions
+2 -1
View File
@@ -21,7 +21,8 @@
"pinia": "^3.0.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vue": "^3.5.13"
"vue": "^3.5.13",
"vue-draggable-plus": "^0.6.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
+21
View File
@@ -44,6 +44,9 @@ importers:
vue:
specifier: ^3.5.13
version: 3.5.17(typescript@5.6.3)
vue-draggable-plus:
specifier: ^0.6.0
version: 0.6.0(@types/sortablejs@1.15.8)
devDependencies:
'@eslint/js':
specifier: ^9.30.1
@@ -738,6 +741,9 @@ packages:
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/sortablejs@1.15.8':
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
'@typescript-eslint/eslint-plugin@8.36.0':
resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1963,6 +1969,15 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-draggable-plus@0.6.0:
resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==}
peerDependencies:
'@types/sortablejs': ^1.15.0
'@vue/composition-api': '*'
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-eslint-parser@10.2.0:
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2576,6 +2591,8 @@ snapshots:
'@types/lodash@4.17.20': {}
'@types/sortablejs@1.15.8': {}
'@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -3966,6 +3983,10 @@ snapshots:
vscode-uri@3.1.0: {}
vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8):
dependencies:
'@types/sortablejs': 1.15.8
vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)):
dependencies:
debug: 4.4.1
+33 -104
View File
@@ -1,11 +1,13 @@
use std::path::{Path, PathBuf};
use num_enum::{FromPrimitive, IntoPrimitive};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{AppHandle, Manager};
use crate::danmaku_xml_to_ass::canvas::CanvasConfig;
use crate::{
danmaku_xml_to_ass::canvas::CanvasConfig,
types::{audio_quality::AudioQuality, codec_type::CodecType, video_quality::VideoQuality},
};
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[allow(clippy::struct_excessive_bools)]
@@ -14,9 +16,9 @@ pub struct Config {
pub download_dir: PathBuf,
pub enable_file_logger: bool,
pub sessdata: String,
pub prefer_video_quality: PreferVideoQuality,
pub prefer_codec_type: PreferCodecType,
pub prefer_audio_quality: PreferAudioQuality,
pub video_quality_priority: Vec<VideoQuality>,
pub codec_type_priority: Vec<CodecType>,
pub audio_quality_priority: Vec<AudioQuality>,
pub download_video: bool,
pub download_audio: bool,
pub auto_merge: bool,
@@ -96,13 +98,36 @@ impl Config {
fn default(app_data_dir: &Path) -> Config {
const DEFAULT_FMT_FOR_PART: &str =
"{collection_title}/{episode_title}/{episode_title}-P{part_order} {part_title}";
let default_video_quality_priority = vec![
VideoQuality::Video8K,
VideoQuality::VideoDolby,
VideoQuality::VideoHDR,
VideoQuality::Video4K,
VideoQuality::Video1080P60,
VideoQuality::Video1080PPlus,
VideoQuality::Video1080P,
VideoQuality::VideoAiRepair,
VideoQuality::Video720P60,
VideoQuality::Video720P,
VideoQuality::Video480P,
VideoQuality::Video360P,
VideoQuality::Video240P,
];
let default_audio_quality_priority = vec![
AudioQuality::AudioHiRes,
AudioQuality::AudioDolby,
AudioQuality::Audio192K,
AudioQuality::Audio132K,
AudioQuality::Audio64K,
];
Config {
download_dir: app_data_dir.join("视频下载"),
enable_file_logger: true,
sessdata: String::new(),
prefer_video_quality: PreferVideoQuality::Best,
prefer_codec_type: PreferCodecType::AVC,
prefer_audio_quality: PreferAudioQuality::Best,
video_quality_priority: default_video_quality_priority,
codec_type_priority: vec![CodecType::AVC, CodecType::HEVC, CodecType::AV1],
audio_quality_priority: default_audio_quality_priority,
download_video: true,
download_audio: true,
auto_merge: true,
@@ -130,102 +155,6 @@ impl Config {
}
}
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Serialize,
Deserialize,
Type,
IntoPrimitive,
FromPrimitive,
)]
#[repr(i64)]
pub enum PreferVideoQuality {
#[default]
Best = -1,
#[serde(rename = "240P")]
Video240P = 6,
#[serde(rename = "360P")]
Video360P = 16,
#[serde(rename = "480P")]
Video480P = 32,
#[serde(rename = "720P")]
Video720P = 64,
#[serde(rename = "720P60")]
Video720P60 = 74,
#[serde(rename = "1080P")]
Video1080P = 80,
#[serde(rename = "AiRepair")]
VideoAiRepair = 100,
#[serde(rename = "1080P+")]
Video1080PPlus = 112,
#[serde(rename = "1080P60")]
Video1080P60 = 116,
#[serde(rename = "4K")]
Video4K = 120,
#[serde(rename = "HDR")]
VideoHDR = 125,
#[serde(rename = "Dolby")]
VideoDolby = 126,
#[serde(rename = "8K")]
Video8K = 127,
}
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Serialize,
Deserialize,
Type,
IntoPrimitive,
FromPrimitive,
)]
#[repr(i64)]
#[allow(clippy::upper_case_acronyms)]
pub enum PreferCodecType {
#[default]
Unknown = -1,
AVC = 7,
HEVC = 12,
AV1 = 13,
}
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Serialize,
Deserialize,
Type,
IntoPrimitive,
FromPrimitive,
)]
#[repr(i64)]
pub enum PreferAudioQuality {
#[default]
Best = -1,
#[serde(rename = "64K")]
Audio64K = 30216,
#[serde(rename = "132K")]
Audio132K = 30232,
#[serde(rename = "192K")]
Audio192K = 30280,
#[serde(rename = "Dolby")]
AudioDolby = 30250,
#[serde(rename = "HiRes")]
AudioHiRes = 30251,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Type)]
pub enum ProxyMode {
#[default]
+21 -63
View File
@@ -1,5 +1,5 @@
use std::{
cmp::Reverse,
collections::HashMap,
fs::{File, OpenOptions},
sync::Arc,
};
@@ -114,11 +114,7 @@ impl AudioTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取音频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
@@ -174,11 +170,7 @@ impl AudioTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取音频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
@@ -234,37 +226,28 @@ impl AudioTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取音频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) {
medias.sort_by_key(|m| Reverse(m.id.to_audio_quality_for_prepare()));
let best_quality_id = medias[0].id;
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) -> anyhow::Result<()> {
if medias.is_empty() {
return Err(anyhow!("获取音频地址失败"));
}
let prefer_quality = app.get_config().read().prefer_audio_quality;
let prefer_quality_id: i64 = prefer_quality.into();
let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id);
let quality_filtered_medias: Vec<MediaForPrepare> = if prefer_quality_found {
// 如果用户指定质量存在,则使用用户指定的质量
medias
.into_iter()
.filter(|m| m.id == prefer_quality_id)
.collect()
} else {
// 否则使用最高质量
medias
.into_iter()
.filter(|m| m.id == best_quality_id)
.collect()
};
let quality_priority = app.get_config().read().audio_quality_priority.clone();
let priority_map: HashMap<&AudioQuality, usize> = quality_priority
.iter()
.enumerate()
.map(|(index, quality)| (quality, index))
.collect();
medias.sort_by_key(|media| {
let quality: AudioQuality = media.id.into();
priority_map.get(&quality).unwrap_or(&usize::MAX)
});
let media = &quality_filtered_medias[0];
let media = &medias[0];
self.audio_quality = media.id.into();
@@ -295,6 +278,8 @@ impl AudioTask {
self.content_length = content_length;
self.chunks = chunks;
}
Ok(())
}
pub fn mark_uncompleted(&mut self) {
@@ -427,30 +412,3 @@ struct MediaForPrepare {
pub id: i64,
pub url_with_content_length: Vec<(String, u64)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum AudioQualityForPrepare {
Audio64K,
Audio132K,
Audio192K,
AudioDolby,
AudioHiRes,
}
trait ToAudioQualityForPrepare {
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare>;
}
impl ToAudioQualityForPrepare for i64 {
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare> {
let audio_quality: AudioQuality = self.into();
match audio_quality {
AudioQuality::Audio64K => Some(AudioQualityForPrepare::Audio64K),
AudioQuality::Audio132K => Some(AudioQualityForPrepare::Audio132K),
AudioQuality::Audio192K => Some(AudioQualityForPrepare::Audio192K),
AudioQuality::AudioDolby => Some(AudioQualityForPrepare::AudioDolby),
AudioQuality::AudioHiRes => Some(AudioQualityForPrepare::AudioHiRes),
AudioQuality::Unknown => None,
}
}
}
+39 -43
View File
@@ -1,5 +1,5 @@
use std::{
cmp::Reverse,
collections::HashMap,
fs::{File, OpenOptions},
sync::Arc,
};
@@ -74,11 +74,7 @@ impl VideoTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取视频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
@@ -144,11 +140,7 @@ impl VideoTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取视频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
@@ -214,47 +206,49 @@ impl VideoTask {
}
}
if medias.is_empty() {
return Err(anyhow!("获取视频地址失败"));
}
self.prepare(app, medias);
self.prepare(app, medias)?;
Ok(())
}
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) {
medias.sort_by_key(|m| Reverse(m.id));
let best_quality_id = medias[0].id;
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) -> anyhow::Result<()> {
if medias.is_empty() {
return Err(anyhow!("获取音频地址失败"));
}
let (prefer_quality, prefer_codec_type) = {
let (video_quality_priority, codec_type_priority) = {
let config = app.get_config().inner().read();
(config.prefer_video_quality, config.prefer_codec_type)
(
config.video_quality_priority.clone(),
config.codec_type_priority.clone(),
)
};
let prefer_quality_id: i64 = prefer_quality.into();
let prefer_codec_id: i64 = prefer_codec_type.into();
let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id);
let mut quality_filtered_medias: Vec<MediaForPrepare> = if prefer_quality_found {
// 如果用户指定质量存在,则使用用户指定的质量
medias
.into_iter()
.filter(|m| m.id == prefer_quality_id)
.collect()
} else {
// 否则使用最高质量
medias
.into_iter()
.filter(|m| m.id == best_quality_id)
.collect()
};
// 按照 AVC > HEVC > AV1 的顺序排列
quality_filtered_medias.sort_by_key(|m| m.codecid);
let media = quality_filtered_medias
let video_priority_map: HashMap<&VideoQuality, usize> = video_quality_priority
.iter()
.find(|m| m.codecid == prefer_codec_id)
.unwrap_or(&quality_filtered_medias[0]);
.enumerate()
.map(|(index, quality)| (quality, index))
.collect();
medias.sort_by_key(|media| {
let quality: VideoQuality = media.id.into();
video_priority_map.get(&quality).unwrap_or(&usize::MAX)
});
let retain_id = medias[0].id;
medias.retain(|m| m.id == retain_id);
let codec_priority_map: HashMap<&CodecType, usize> = codec_type_priority
.iter()
.enumerate()
.map(|(index, codec_type)| (codec_type, index))
.collect();
medias.sort_by_key(|m| {
let codec_type: CodecType = m.codecid.into();
codec_priority_map.get(&codec_type).unwrap_or(&usize::MAX)
});
let media = &medias[0];
self.video_quality = media.id.into();
self.codec_type = media.codecid.into();
@@ -286,6 +280,8 @@ impl VideoTask {
self.content_length = content_length;
self.chunks = chunks;
}
Ok(())
}
pub fn mark_uncompleted(&mut self) {
+2
View File
@@ -7,6 +7,8 @@ use specta::Type;
Debug,
Clone,
Copy,
Hash,
Eq,
PartialEq,
Serialize,
Deserialize,
+3
View File
@@ -7,6 +7,8 @@ use specta::Type;
Debug,
Clone,
Copy,
Hash,
Eq,
PartialEq,
Serialize,
Deserialize,
@@ -19,6 +21,7 @@ use specta::Type;
pub enum CodecType {
#[default]
Unknown = -1,
Audio = 0,
AVC = 7,
HEVC = 12,
+2
View File
@@ -7,6 +7,8 @@ use specta::Type;
Debug,
Clone,
Copy,
Hash,
Eq,
PartialEq,
Serialize,
Deserialize,
+1 -4
View File
@@ -247,7 +247,7 @@ export type CntInfo = { collect: number; play: number; thumb_up: number; share:
export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string }
export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1"
export type CommandError = { err_title: string; err_message: string }
export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig }
export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig }
export type Consulting = { consulting_flag: boolean; consulting_url: string }
export type ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg }
export type ContentList = { bold: boolean; content: string; number: string }
@@ -331,9 +331,6 @@ export type PendantInCheese = { image: string; name: string; pid: number }
export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number }
export type PlayStrategy = { strategies: string[] }
export type Positive = { id: number; title: string }
export type PreferAudioQuality = "Best" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
export type PreferCodecType = "Unknown" | "AVC" | "HEVC" | "AV1"
export type PreferVideoQuality = "Best" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string }
export type Producer = { mid: number; type: number; is_contribute: number | null; title: string }
export type ProxyMode = "NoProxy" | "System" | "Custom"
@@ -1,34 +1,40 @@
<script setup lang="ts">
import { SelectBaseOption } from 'naive-ui/es/select/src/interface'
import { PreferAudioQuality, PreferVideoQuality } from '../../../bindings.ts'
import { AudioQuality, VideoQuality, CodecType } from '../../../bindings.ts'
import { useStore } from '../../../store.ts'
import { VueDraggable } from 'vue-draggable-plus'
import ColorfulTag from '../../../components/ColorfulTag.vue'
const store = useStore()
const videoQualitySelectOptions: SelectBaseOption<PreferVideoQuality>[] = [
{ label: '最高', value: 'Best' },
{ label: '240P', value: '240P' },
{ label: '360P', value: '360P' },
{ label: '480P', value: '480P' },
{ label: '720P', value: '720P' },
{ label: '720P 60帧', value: '720P60' },
{ label: '1080P', value: '1080P' },
{ label: '智能修复', value: 'AiRepair' },
{ label: '1080P 高码率', value: '1080P+' },
{ label: '1080P 60帧', value: '1080P60' },
{ label: '4K', value: '4K' },
{ label: 'HDR', value: 'HDR' },
{ label: '杜比视界', value: 'Dolby' },
{ label: '8K', value: '8K' },
]
const audioQualitySelectOptions: SelectBaseOption<PreferAudioQuality>[] = [
{ label: '最高', value: 'Best' },
{ label: '64K', value: '64K' },
{ label: '132K', value: '132K' },
{ label: '192K', value: '192K' },
{ label: '杜比全景声', value: 'Dolby' },
{ label: 'Hi-Res', value: 'HiRes' },
]
const videoQualityNameMap: Map<VideoQuality, string> = new Map([
['240P', '240P 极速'],
['360P', '360P 流畅'],
['480P', '480P 标清'],
['720P', '720P 准高清'],
['720P60', '720P 60帧'],
['1080P', '1080P 高清'],
['AiRepair', 'AI智能修复'],
['1080P+', '1080P 高码率'],
['1080P60', '1080P 60帧'],
['4K', '4K 超高清'],
['HDR', 'HDR 真彩色'],
['Dolby', '杜比视界'],
['8K', '8K 超高清'],
])
const audioQualityNameMap: Map<AudioQuality, string> = new Map([
['64K', '64K'],
['132K', '132K'],
['192K', '192K'],
['Dolby', '杜比全景声'],
['HiRes', 'Hi-Res 无损'],
])
const codecTypeNameMap: Map<CodecType, string> = new Map([
['AVC', 'AVC (H.264)'],
['HEVC', 'HEVC (H.265)'],
['AV1', 'AV1'],
])
</script>
<template>
@@ -87,44 +93,71 @@ const audioQualitySelectOptions: SelectBaseOption<PreferAudioQuality>[] = [
<n-checkbox class="w-22" v-model:checked="store.config.download_json">json刮削</n-checkbox>
</div>
<n-tooltip placement="left" trigger="hover" class="w-20vw">
<div>如果视频有对应的画质则使用对应的画质否则选择最高画质</div>
<template #trigger>
<div class="flex items-center">
<span class="mr-2 whitespace-nowrap font-bold">优先画质</span>
<n-select
size="small"
v-model:value="store.config.prefer_video_quality"
:options="videoQualitySelectOptions" />
<div class="flex flex-justify-between">
<div>
<span class="whitespace-nowrap font-bold">画质优先级</span>
<div class="overflow-auto overflow-x-hidden h-36">
<VueDraggable
:force-fallback="true"
:fallback-on-body="true"
:animation="300"
v-model="store.config.video_quality_priority"
ghostClass="draggable-ghost"
class="select-none flex flex-col gap-2">
<ColorfulTag
class="whitespace-nowrap cursor-move"
color="blue"
v-for="videoQuality in store.config.video_quality_priority"
:key="videoQuality">
{{ videoQualityNameMap.get(videoQuality) || videoQuality }}
</ColorfulTag>
</VueDraggable>
</div>
</template>
</n-tooltip>
</div>
<n-tooltip placement="left" trigger="hover" class="w-20vw">
<div>如果视频有对应的音质则使用对应的音质否则选择最高音质</div>
<template #trigger>
<div class="flex items-center">
<span class="mr-2 whitespace-nowrap font-bold">优先音质</span>
<n-select
size="small"
v-model:value="store.config.prefer_audio_quality"
:options="audioQualitySelectOptions" />
</div>
</template>
</n-tooltip>
<div>
<span class="whitespace-nowrap font-bold">音质优先级</span>
<VueDraggable
:force-fallback="true"
:fallback-on-body="true"
v-model="store.config.audio_quality_priority"
:animation="300"
ghostClass="draggable-ghost"
class="select-none flex flex-col gap-2">
<ColorfulTag
class="whitespace-nowrap cursor-move"
color="blue"
v-for="audioQuality in store.config.audio_quality_priority"
:key="audioQuality">
{{ audioQualityNameMap.get(audioQuality) || audioQuality }}
</ColorfulTag>
</VueDraggable>
</div>
<n-tooltip placement="left" trigger="hover" class="w-20vw">
<div>如果视频有对应的编码则使用对应的编码否则按照 AVC > HEVC > AV1 的顺序选择编码</div>
<template #trigger>
<div>
<span class="mr-2 font-bold">优先编码</span>
<n-radio-group v-model:value="store.config.prefer_codec_type" size="small">
<n-radio-button value="AVC">AVC</n-radio-button>
<n-radio-button value="HEVC">HEVC</n-radio-button>
<n-radio-button value="AV1">AV1</n-radio-button>
</n-radio-group>
</div>
</template>
</n-tooltip>
<div>
<span class="whitespace-nowrap font-bold">编码优先级</span>
<VueDraggable
:force-fallback="true"
:fallback-on-body="true"
v-model="store.config.codec_type_priority"
:animation="300"
ghostClass="draggable-ghost"
class="select-none flex flex-col gap-2">
<ColorfulTag
class="whitespace-nowrap cursor-move"
color="blue"
v-for="codecType in store.config.codec_type_priority"
:key="codecType">
{{ codecTypeNameMap.get(codecType) || codecType }}
</ColorfulTag>
</VueDraggable>
</div>
</div>
</div>
</template>
<style scoped>
.draggable-ghost {
opacity: 0.5;
}
</style>