更新
This commit is contained in:
+34
-3
@@ -12,9 +12,9 @@
|
||||
|
||||
<div class="workspace-column">
|
||||
<main class="workspace">
|
||||
<EncodePage v-if="currentView === 'encode'" @taskAdded="onTaskAdded" />
|
||||
<RemuxPage v-if="currentView === 'remux'" @taskAdded="onTaskAdded" />
|
||||
<BurnPage v-if="currentView === 'burn'" @taskAdded="onTaskAdded" />
|
||||
<EncodePage v-if="currentView === 'encode'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||
<RemuxPage v-if="currentView === 'remux'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||
<BurnPage v-if="currentView === 'burn'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||
<SettingsPage v-if="currentView === 'settings'" />
|
||||
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
|
||||
</main>
|
||||
@@ -41,12 +41,15 @@ import type { Task, Progress } from './types'
|
||||
const currentView = ref('encode')
|
||||
const theme = ref<'dark'|'light'>('light')
|
||||
const tasks = ref<Task[]>([])
|
||||
const gpuInfo = ref<any[]>([])
|
||||
|
||||
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('contextmenu', e => e.preventDefault())
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
loadTasks()
|
||||
loadGPUInfo()
|
||||
|
||||
onTaskUpdated((task: Task) => {
|
||||
const found = tasks.value.find(t => t.id === task.id)
|
||||
@@ -90,6 +93,34 @@ async function loadTasks() {
|
||||
try { tasks.value = await api.getTasks() } catch { /* not connected */ }
|
||||
}
|
||||
|
||||
async function loadGPUInfo() {
|
||||
try {
|
||||
const app = (window as any).go?.main?.App
|
||||
if (app?.GetGPUInfo) {
|
||||
gpuInfo.value = await app.GetGPUInfo() || []
|
||||
} else {
|
||||
const encs = await api.getHardwareEncoders()
|
||||
gpuInfo.value = groupByVendor(encs)
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
const encs = await api.getHardwareEncoders()
|
||||
gpuInfo.value = groupByVendor(encs)
|
||||
} catch { gpuInfo.value = [] }
|
||||
}
|
||||
}
|
||||
|
||||
function groupByVendor(encs: any[]): any[] {
|
||||
const groups: Record<string, any> = {}
|
||||
for (const e of encs) {
|
||||
if (!groups[e.type]) {
|
||||
groups[e.type] = { name: e.type, vendor: e.type, encoders: [] }
|
||||
}
|
||||
groups[e.type].encoders.push({ name: e.name, codec: e.codec, label: e.label, available: e.available })
|
||||
}
|
||||
return [...Object.values(groups), { name: 'CPU', vendor: 'cpu', encoders: [{ name:'libx264',codec:'h264',label:'H.264',available:true },{ name:'libx265',codec:'hevc',label:'HEVC',available:true }]}]
|
||||
}
|
||||
|
||||
function onTaskAdded() { loadTasks() }
|
||||
|
||||
async function handleCancel(id: string) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785291728333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1480" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M861.257143 54.857143c27.428571 0 51.2 21.942857 53.028571 49.371428v446.171429h-69.485714V124.342857H142.628571v791.771429h316.342858V987.428571H126.171429c-27.428571 0-51.2-21.942857-53.028572-49.371428V107.885714C73.142857 78.628571 95.085714 56.685714 122.514286 54.857143H861.257143zM592.457143 499.2c3.657143 0 7.314286 1.828571 9.142857 3.657143L950.857143 725.942857c9.142857 5.485714 10.971429 16.457143 5.485714 25.6l-5.485714 5.485714-347.428572 221.257143c-9.142857 5.485714-20.114286 3.657143-25.6-5.485714-1.828571-3.657143-3.657143-5.485714-3.657142-9.142857V517.485714c0-10.971429 9.142857-18.285714 18.285714-18.285714z m54.857143 117.028571v245.028572L841.142857 738.742857l-193.828571-122.514286zM457.142857 548.571429c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h182.857143z m292.571429-256c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h475.428572z" fill="#1296db" p-id="1481"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<header class="app-header" @dblclick="maximize">
|
||||
<div class="header-left" style="--wails-draggable: drag">
|
||||
<svg class="app-logo" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
<line x1="1" y1="9" x2="15" y2="9"/>
|
||||
</svg>
|
||||
<img :src="logoSrc" class="app-logo" width="22" height="22" alt="" />
|
||||
<span class="app-name">FFmpeg GUI</span>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +47,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import logoSrc from '../assets/icon/app-logo.svg'
|
||||
|
||||
defineProps<{ currentView: string; theme: string }>()
|
||||
defineEmits<{
|
||||
navigate: [view: string]
|
||||
@@ -94,7 +92,7 @@ function closeWindow() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-logo { color: var(--accent); flex-shrink: 0; }
|
||||
.app-logo { flex-shrink: 0; }
|
||||
.app-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -62,19 +62,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Encode Settings (simplified) -->
|
||||
<!-- Hardware Acceleration -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>编码设置</h3></div>
|
||||
<div class="card-header"><h3>编码硬件与编码器</h3></div>
|
||||
<div class="fields-row">
|
||||
<div class="field">
|
||||
<label class="field-label">硬件加速</label>
|
||||
<select v-model="hwAccel">
|
||||
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">视频编码器</label>
|
||||
<select v-model="encodeSettings.videoCodec">
|
||||
<option value="libx264">H.264 (libx264)</option>
|
||||
<option value="libx265">H.265 / HEVC (libx265)</option>
|
||||
<option value="h264_nvenc">H.264 NVENC</option>
|
||||
<option value="hevc_nvenc">HEVC NVENC</option>
|
||||
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
|
||||
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
|
||||
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Encode Settings -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>编码参数</h3></div>
|
||||
<div class="fields-row">
|
||||
<div class="field">
|
||||
<label class="field-label">质量 / CRF</label>
|
||||
<input type="number" v-model.number="encodeSettings.crf" placeholder="23" min="0" max="51" />
|
||||
@@ -113,8 +127,37 @@ import { ref, computed } from 'vue'
|
||||
import { api } from '../api/wails'
|
||||
import type { MediaInfo, StreamInfo, EncodeSettings, SubTrack } from '../types'
|
||||
|
||||
const props = defineProps<{ gpuInfo: any[] }>()
|
||||
const emit = defineEmits<{ taskAdded: [] }>()
|
||||
|
||||
const hwAccel = ref('')
|
||||
const hwEncoders = computed(() => {
|
||||
const result: { label: string; options: { value: string; label: string; avail: boolean }[] }[] = []
|
||||
for (const g of props.gpuInfo || []) {
|
||||
if (!g.encoders?.length) continue
|
||||
result.push({
|
||||
label: g.vendor === 'cpu' ? '软件编码' : g.name,
|
||||
options: g.encoders.map((e: any) => ({ value: e.name, label: e.label, avail: e.available }))
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
const hwAccelOptions = computed(() => {
|
||||
const opts: { value: string; label: string }[] = [{ value: '', label: 'CPU 软件编码' }]
|
||||
for (const g of props.gpuInfo || []) {
|
||||
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
|
||||
switch (g.vendor) {
|
||||
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
|
||||
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
|
||||
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
|
||||
}
|
||||
}
|
||||
if (opts.length === 1) {
|
||||
opts.push({ value: 'd3d11va', label: 'Direct3D 11' }, { value: 'dxva2', label: 'DirectX VA2' })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
|
||||
const inputFile = ref('')
|
||||
const outputFile = ref('')
|
||||
const mediaInfo = ref<MediaInfo | null>(null)
|
||||
@@ -165,6 +208,7 @@ async function addTask() {
|
||||
remux: {} as any, subtitle: { subtitles: [...subtitles.value] },
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
await api.setHWAccel(hwAccel.value)
|
||||
await api.startTask(taskId)
|
||||
emit('taskAdded')
|
||||
resetForm()
|
||||
@@ -184,8 +228,10 @@ function resetForm() {
|
||||
}
|
||||
|
||||
function streamIcon(s: StreamInfo) {
|
||||
if (s.codec_type === 'video') return '🎥'; if (s.codec_type === 'audio') return '🔊'
|
||||
if (s.codec_type === 'subtitle') return '💬'; return '📄'
|
||||
if (s.codec_type === 'video') return 'V'
|
||||
if (s.codec_type === 'audio') return 'A'
|
||||
if (s.codec_type === 'subtitle') return 'S'
|
||||
return '?'
|
||||
}
|
||||
function streamLabel(s: StreamInfo) {
|
||||
if (s.codec_type === 'video') return `视频: ${s.codec_name} ${s.width||''}x${s.height||''}`
|
||||
|
||||
@@ -28,35 +28,16 @@
|
||||
<div class="field">
|
||||
<label class="field-label">硬件加速</label>
|
||||
<select v-model="hwAccel">
|
||||
<option value="">CPU 软件编码</option>
|
||||
<option value="cuda">NVIDIA CUDA</option>
|
||||
<option value="d3d11va">Direct3D 11 (DXVA)</option>
|
||||
<option value="dxva2">DirectX VA2</option>
|
||||
<option value="qsv">Intel Quick Sync</option>
|
||||
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">视频编码器</label>
|
||||
<select v-model="encodeSettings.videoCodec">
|
||||
<optgroup label="软件编码">
|
||||
<option value="libx264">H.264 / AVC (libx264)</option>
|
||||
<option value="libx265">H.265 / HEVC (libx265)</option>
|
||||
<option value="libsvtav1">AV1 (libsvtav1)</option>
|
||||
</optgroup>
|
||||
<optgroup label="NVIDIA NVENC">
|
||||
<option value="h264_nvenc">H.264 NVENC</option>
|
||||
<option value="hevc_nvenc">HEVC NVENC</option>
|
||||
<option value="av1_nvenc">AV1 NVENC</option>
|
||||
</optgroup>
|
||||
<optgroup label="Intel QSV">
|
||||
<option value="h264_qsv">H.264 QSV</option>
|
||||
<option value="hevc_qsv">HEVC QSV</option>
|
||||
<option value="av1_qsv">AV1 QSV</option>
|
||||
</optgroup>
|
||||
<optgroup label="AMD AMF">
|
||||
<option value="h264_amf">H.264 AMF</option>
|
||||
<option value="hevc_amf">HEVC AMF</option>
|
||||
<option value="av1_amf">AV1 AMF</option>
|
||||
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
|
||||
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
|
||||
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
@@ -182,8 +163,38 @@ import { ref, computed } from 'vue'
|
||||
import { api } from '../api/wails'
|
||||
import type { MediaInfo, StreamInfo, EncodeSettings } from '../types'
|
||||
|
||||
const props = defineProps<{ gpuInfo: any[] }>()
|
||||
const emit = defineEmits<{ taskAdded: [] }>()
|
||||
|
||||
// Filtered dropdown options based on detected hardware
|
||||
const hwEncoders = computed(() => {
|
||||
const result: { label: string; options: { value: string; label: string; avail: boolean }[] }[] = []
|
||||
for (const g of props.gpuInfo || []) {
|
||||
if (!g.encoders?.length) continue
|
||||
result.push({
|
||||
label: g.vendor === 'cpu' ? '软件编码' : g.name,
|
||||
options: g.encoders.map((e: any) => ({ value: e.name, label: e.label, avail: e.available }))
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const hwAccelOptions = computed(() => {
|
||||
const opts: { value: string; label: string }[] = [{ value: '', label: 'CPU 软件编码' }]
|
||||
for (const g of props.gpuInfo || []) {
|
||||
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
|
||||
switch (g.vendor) {
|
||||
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
|
||||
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
|
||||
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
|
||||
}
|
||||
}
|
||||
if (opts.length === 1) {
|
||||
opts.push({ value: 'd3d11va', label: 'Direct3D 11 (DXVA)' }, { value: 'dxva2', label: 'DirectX VA2' })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
|
||||
const inputFile = ref('')
|
||||
const outputFile = ref('')
|
||||
const mediaInfo = ref<MediaInfo | null>(null)
|
||||
|
||||
@@ -35,11 +35,7 @@
|
||||
<div class="field" style="max-width:300px">
|
||||
<label class="field-label">硬件解码(加速输入读取)</label>
|
||||
<select v-model="hwAccel">
|
||||
<option value="">不使用硬件加速</option>
|
||||
<option value="cuda">NVIDIA CUDA</option>
|
||||
<option value="d3d11va">Direct3D 11 (DXVA)</option>
|
||||
<option value="dxva2">DirectX VA2</option>
|
||||
<option value="qsv">Intel Quick Sync</option>
|
||||
<option v-for="o in hwAccelOpts" :key="o.value" :value="o.value">{{ o.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,12 +79,28 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { api } from '../api/wails'
|
||||
import type { MediaInfo, StreamInfo } from '../types'
|
||||
|
||||
const props = defineProps<{ gpuInfo: any[] }>()
|
||||
const emit = defineEmits<{ taskAdded: [] }>()
|
||||
|
||||
const inputFile = ref('')
|
||||
const outputFile = ref('')
|
||||
const outputFormat = ref('mp4')
|
||||
const hwAccel = ref('')
|
||||
const hwAccelOpts = computed(() => {
|
||||
const opts: { value: string; label: string }[] = [{ value: '', label: '不使用硬件加速' }]
|
||||
for (const g of props.gpuInfo || []) {
|
||||
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
|
||||
switch (g.vendor) {
|
||||
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
|
||||
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
|
||||
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
|
||||
}
|
||||
}
|
||||
if (opts.length === 1) {
|
||||
opts.push({ value: 'd3d11va', label: 'Direct3D 11' }, { value: 'dxva2', label: 'DirectX VA2' })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
const mediaInfo = ref<MediaInfo | null>(null)
|
||||
const selectedStreams = reactive<Record<number, boolean>>({})
|
||||
|
||||
|
||||
@@ -2,25 +2,21 @@
|
||||
<div class="settings-page">
|
||||
<h2>设置</h2>
|
||||
|
||||
<!-- Hardware Detection Card -->
|
||||
<!-- GPU Detection Card -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>硬件检测</h3></div>
|
||||
|
||||
<div v-if="loading" class="loading-hint">正在检测硬件编码器...</div>
|
||||
|
||||
<div v-else class="hw-cards">
|
||||
<div v-for="gpu in gpuGroups" :key="gpu.type" class="gpu-card">
|
||||
<div v-else class="gpu-cards">
|
||||
<div v-for="gpu in gpus" :key="gpu.name" class="gpu-card">
|
||||
<div class="gpu-header">
|
||||
<span class="gpu-dot" :class="gpu.type"></span>
|
||||
<span class="gpu-name">{{ gpu.label }}</span>
|
||||
<span v-if="gpu.hasAvailable" class="gpu-badge avail">可用</span>
|
||||
<span class="gpu-dot" :class="gpu.vendor"></span>
|
||||
<span class="gpu-name">{{ gpu.name }}</span>
|
||||
<span v-if="hasAvailable(gpu)" class="gpu-badge avail">可用</span>
|
||||
<span v-else class="gpu-badge unavail">不可用</span>
|
||||
</div>
|
||||
<div class="gpu-encoders">
|
||||
<div v-for="enc in gpu.encoders" :key="enc.name" class="encoder-row">
|
||||
<span :class="['enc-check', { avail: enc.available }]">
|
||||
{{ enc.available ? '✓' : '✗' }}
|
||||
</span>
|
||||
<span :class="['enc-check', { avail: enc.available }]">{{ enc.available ? '✓' : '✗' }}</span>
|
||||
<span class="enc-name">{{ enc.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,35 +24,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hardware Priority Card -->
|
||||
<!-- Preferred Hardware Card -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>硬件优先级</h3></div>
|
||||
<p class="field-hint" style="margin-bottom:12px">拖拽调整编码器优先级,编解码时优先使用排在前面的硬件</p>
|
||||
<div class="priority-list">
|
||||
<div
|
||||
v-for="(item, i) in priorities"
|
||||
:key="item.key"
|
||||
class="priority-item"
|
||||
:class="{ disabled: !item.available }"
|
||||
>
|
||||
<span class="priority-rank">{{ i + 1 }}</span>
|
||||
<span class="priority-dot" :class="item.key"></span>
|
||||
<span class="priority-label">{{ item.label }}</span>
|
||||
<span v-if="item.available" class="priority-check">✓</span>
|
||||
</div>
|
||||
<div class="card-header"><h3>首选硬件加速</h3></div>
|
||||
<div class="field" style="max-width:360px">
|
||||
<label class="field-label">默认使用的硬件加速器</label>
|
||||
<select v-model="preferredAccel" @change="onPrefChange">
|
||||
<option v-for="item in allOpts" :key="item.key" :value="item.key"
|
||||
:disabled="!item.available">
|
||||
{{ item.label }}{{ item.available ? '' : ' (不可用)' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Settings Card -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>输出设置</h3></div>
|
||||
<div class="fields-row">
|
||||
<div class="field">
|
||||
<label class="field-label">默认输出目录</label>
|
||||
<div class="input-row">
|
||||
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
|
||||
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">默认输出目录</label>
|
||||
<div class="input-row">
|
||||
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
|
||||
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:16px">
|
||||
@@ -74,55 +63,81 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { api } from '../api/wails'
|
||||
import type { HWEncoder } from '../types'
|
||||
|
||||
interface EncoderCap { name: string; codec: string; label: string; available: boolean }
|
||||
interface GPUInfo { name: string; vendor: string; encoders: EncoderCap[] }
|
||||
|
||||
const loading = ref(true)
|
||||
const encoders = ref<HWEncoder[]>([])
|
||||
const gpus = ref<GPUInfo[]>([])
|
||||
const outputDir = ref('')
|
||||
const namingRule = ref('{name}_{codec}')
|
||||
|
||||
const preferredAccel = ref('')
|
||||
|
||||
interface SelectOpt { key: string; label: string; available: boolean }
|
||||
const allOpts = computed<SelectOpt[]>(() => {
|
||||
const opts: SelectOpt[] = []
|
||||
for (const g of gpus.value) {
|
||||
opts.push({ key: g.vendor, label: g.name, available: hasAvailable(g) })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
encoders.value = await api.getHardwareEncoders()
|
||||
const app = (window as any).go?.main?.App
|
||||
if (app?.GetGPUInfo) {
|
||||
gpus.value = await app.GetGPUInfo() || []
|
||||
} else {
|
||||
const encoders = await api.getHardwareEncoders()
|
||||
gpus.value = buildFromEncoders(encoders)
|
||||
}
|
||||
} catch {
|
||||
encoders.value = [
|
||||
{ name:'h264_nvenc',label:'H.264 NVENC',type:'nvidia',codec:'h264',available:true },
|
||||
{ name:'hevc_nvenc',label:'HEVC NVENC',type:'nvidia',codec:'hevc',available:true },
|
||||
{ name:'av1_nvenc', label:'AV1 NVENC', type:'nvidia',codec:'av1', available:true },
|
||||
{ name:'h264_qsv', label:'H.264 QSV', type:'intel', codec:'h264',available:false },
|
||||
{ name:'hevc_qsv', label:'HEVC QSV', type:'intel', codec:'hevc',available:false },
|
||||
{ name:'h264_amf', label:'H.264 AMF', type:'amd', codec:'h264',available:false },
|
||||
try {
|
||||
const encoders = await api.getHardwareEncoders()
|
||||
gpus.value = buildFromEncoders(encoders)
|
||||
} catch { gpus.value = [] }
|
||||
} finally {
|
||||
loading.value = false
|
||||
// Default to first available hardware GPU
|
||||
const firstAvail = gpus.value.find(g => g.vendor !== 'cpu' && hasAvailable(g))
|
||||
preferredAccel.value = firstAvail ? firstAvail.vendor : 'cpu'
|
||||
syncAccel()
|
||||
}
|
||||
})
|
||||
|
||||
function onPrefChange() { syncAccel() }
|
||||
|
||||
function syncAccel() {
|
||||
const g = gpus.value.find(g => g.vendor === preferredAccel.value)
|
||||
if (g && g.vendor !== 'cpu' && hasAvailable(g)) {
|
||||
api.setHWAccel(vendorToAccel(g.vendor))
|
||||
} else {
|
||||
api.setHWAccel('')
|
||||
}
|
||||
}
|
||||
|
||||
function vendorToAccel(v: string): string {
|
||||
switch (v) { case 'nvidia': return 'cuda'; case 'intel': return 'qsv'; case 'amd': return 'd3d11va'; default: return '' }
|
||||
}
|
||||
|
||||
function hasAvailable(g: GPUInfo) { return g.encoders.some(e => e.available) }
|
||||
|
||||
function buildFromEncoders(encoders: any[]): GPUInfo[] {
|
||||
const groups: Record<string, any> = {}
|
||||
for (const e of encoders) {
|
||||
if (!groups[e.type]) {
|
||||
groups[e.type] = { name: e.type, vendor: e.type, encoders: [] }
|
||||
}
|
||||
groups[e.type].encoders.push({ name: e.name, codec: e.codec, label: e.label, available: e.available })
|
||||
}
|
||||
return [...Object.values(groups), {
|
||||
name: 'CPU', vendor: 'cpu', encoders: [
|
||||
{ name: 'libx264', codec: 'h264', label: 'H.264 (libx264)', available: true },
|
||||
{ name: 'libx265', codec: 'hevc', label: 'H.265/HEVC (libx265)', available: true },
|
||||
]
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
|
||||
const gpuGroups = computed(() => {
|
||||
const map = new Map<string, { type: string; label: string; encoders: HWEncoder[]; hasAvailable: boolean }>()
|
||||
const groups = [
|
||||
{ type: 'nvidia', label: 'NVIDIA GPU' },
|
||||
{ type: 'intel', label: 'Intel GPU' },
|
||||
{ type: 'amd', label: 'AMD GPU' },
|
||||
]
|
||||
for (const g of groups) {
|
||||
const encs = encoders.value.filter(e => e.type === g.type)
|
||||
map.set(g.type, {
|
||||
type: g.type,
|
||||
label: g.label,
|
||||
encoders: encs,
|
||||
hasAvailable: encs.some(e => e.available),
|
||||
})
|
||||
}
|
||||
return Array.from(map.values())
|
||||
})
|
||||
|
||||
const priorities = computed(() => {
|
||||
const items: { key: string; label: string; available: boolean }[] = []
|
||||
for (const g of gpuGroups.value) {
|
||||
items.push({ key: g.type, label: g.label, available: g.hasAvailable })
|
||||
}
|
||||
items.push({ key: 'cpu', label: 'CPU (软件编码)', available: true })
|
||||
return items
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
async function selectOutputDir() {
|
||||
try {
|
||||
@@ -133,66 +148,27 @@ async function selectOutputDir() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.settings-page { display: flex; flex-direction: column; gap: 20px; }
|
||||
.settings-page > h2 { margin-bottom: 4px; }
|
||||
|
||||
.loading-hint { color: var(--text-dim); padding: 12px 0; }
|
||||
|
||||
/* GPU Cards */
|
||||
.hw-cards { display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.gpu-card {
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
.gpu-cards { display: flex; flex-direction: column; gap: 12px; }
|
||||
.gpu-card { border: 1px solid var(--border-light); border-radius: var(--radius); padding: 16px; background: var(--bg-surface); }
|
||||
.gpu-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.gpu-dot.nvidia { background: #76b900; }
|
||||
.gpu-dot.intel { background: #00aaff; }
|
||||
.gpu-dot.amd { background: #ed1c24; }
|
||||
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.gpu-dot.nvidia { background: #76b900; } .gpu-dot.intel { background: #00aaff; }
|
||||
.gpu-dot.amd { background: #ed1c24; } .gpu-dot.cpu,.gpu-dot.unknown { background: var(--text-dim); }
|
||||
.gpu-name { font-size: 14px; font-weight: 600; flex: 1; }
|
||||
.gpu-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
|
||||
.gpu-badge.avail { background: #e6f4ea; color: var(--success); }
|
||||
.gpu-badge.unavail { background: var(--bg-input); color: var(--text-dim); }
|
||||
|
||||
.gpu-encoders { display: flex; flex-direction: column; gap: 6px; }
|
||||
.encoder-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.enc-check { width: 18px; font-size: 12px; font-weight: 600; color: var(--text-dim); }
|
||||
.enc-check.avail { color: var(--success); }
|
||||
.enc-name { color: var(--text-secondary); }
|
||||
|
||||
/* Priority */
|
||||
.priority-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.priority-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
.priority-item.disabled { opacity: 0.5; }
|
||||
.priority-rank {
|
||||
font-size: 12px; font-weight: 700; color: var(--text-dim);
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: var(--bg-input); display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.priority-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.priority-dot.nvidia { background: #76b900; }
|
||||
.priority-dot.intel { background: #00aaff; }
|
||||
.priority-dot.amd { background: #ed1c24; }
|
||||
.priority-dot.cpu { background: var(--text-dim); }
|
||||
.priority-label { flex: 1; font-size: 13px; font-weight: 500; }
|
||||
.priority-check { font-size: 14px; color: var(--success); font-weight: 600; }
|
||||
|
||||
/* Output */
|
||||
.input-row { display: flex; gap: 8px; width: 100%; }
|
||||
.input-row input { flex: 1; }
|
||||
|
||||
Reference in New Issue
Block a user