更新
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"ffmpeg-gui/internal/ffmpeg"
|
||||
"ffmpeg-gui/internal/gpu"
|
||||
"ffmpeg-gui/internal/hwaccel"
|
||||
"ffmpeg-gui/internal/media"
|
||||
"ffmpeg-gui/internal/platform"
|
||||
@@ -106,6 +107,14 @@ func (a *App) GetHardwareEncoders() ([]hwaccel.HWEncoder, error) {
|
||||
return a.hwDetect.DetectEncoders()
|
||||
}
|
||||
|
||||
// GetGPUInfo returns detected GPU models and their encoder capabilities.
|
||||
func (a *App) GetGPUInfo() []gpu.GPUInfo {
|
||||
if a.exec == nil {
|
||||
return nil
|
||||
}
|
||||
return gpu.DetectGPUs(a.exec)
|
||||
}
|
||||
|
||||
// GetAccelerators returns detected hardware acceleration methods.
|
||||
func (a *App) GetAccelerators() ([]hwaccel.Accelerator, error) {
|
||||
if a.hwDetect == nil {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo === FFmpeg GUI Build Script ===
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo [1/3] Installing frontend dependencies...
|
||||
cd frontend
|
||||
call npm install
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: npm install failed
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo [2/3] Building frontend...
|
||||
call npm run build
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: frontend build failed
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
cd ..
|
||||
|
||||
echo [3/3] Cleaning icon cache...
|
||||
if exist "build\windows\icon.ico" del /q "build\windows\icon.ico"
|
||||
if exist "*-res.syso" del /q "*-res.syso"
|
||||
|
||||
echo [4/4] Building Wails application...
|
||||
wails build -clean -platform windows/amd64
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: wails build failed
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo.
|
||||
echo === Build complete! ===
|
||||
echo Output: build\bin\ffmpeg-gui.exe
|
||||
echo.
|
||||
pause
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
+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,29 +24,23 @@
|
||||
</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">
|
||||
@@ -58,7 +48,6 @@
|
||||
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:16px">
|
||||
<label class="field-label">文件命名规则</label>
|
||||
<select v-model="namingRule">
|
||||
@@ -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 },
|
||||
]
|
||||
} finally { loading.value = 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()
|
||||
}
|
||||
})
|
||||
|
||||
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())
|
||||
})
|
||||
function onPrefChange() { syncAccel() }
|
||||
|
||||
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 })
|
||||
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 },
|
||||
]
|
||||
}]
|
||||
}
|
||||
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; }
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"ffmpeg-gui/internal/ffmpeg"
|
||||
"log"
|
||||
)
|
||||
|
||||
// GPUInfo holds detected GPU information.
|
||||
type GPUInfo struct {
|
||||
Name string `json:"name"`
|
||||
Vendor string `json:"vendor"`
|
||||
Encoders []EncoderCap `json:"encoders"`
|
||||
}
|
||||
|
||||
// EncoderCap describes an encoder capability.
|
||||
type EncoderCap struct {
|
||||
Name string `json:"name"`
|
||||
Codec string `json:"codec"`
|
||||
Label string `json:"label"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// knownEncoders defines all known hardware encoders by vendor.
|
||||
type knownEnc struct {
|
||||
Name, Vendor, Codec, Label string
|
||||
}
|
||||
|
||||
var allEncoders = []knownEnc{
|
||||
{"h264_nvenc", "nvidia", "h264", "H.264 NVENC"},
|
||||
{"hevc_nvenc", "nvidia", "hevc", "HEVC NVENC"},
|
||||
{"av1_nvenc", "nvidia", "av1", "AV1 NVENC"},
|
||||
{"h264_qsv", "intel", "h264", "H.264 QSV"},
|
||||
{"hevc_qsv", "intel", "hevc", "HEVC QSV"},
|
||||
{"av1_qsv", "intel", "av1", "AV1 QSV"},
|
||||
{"h264_amf", "amd", "h264", "H.264 AMF"},
|
||||
{"hevc_amf", "amd", "hevc", "HEVC AMF"},
|
||||
{"av1_amf", "amd", "av1", "AV1 AMF"},
|
||||
}
|
||||
|
||||
// DetectGPUs detects GPUs and their encoder capabilities.
|
||||
// Uses a single ffmpeg -encoders call for speed.
|
||||
func DetectGPUs(exec *ffmpeg.Executor) []GPUInfo {
|
||||
encList, err := exec.RunSync("-encoders")
|
||||
if err != nil {
|
||||
log.Printf("[gpu] ffmpeg -encoders failed: %v", err)
|
||||
return fallbackGPUs()
|
||||
}
|
||||
|
||||
// Group encoders by vendor
|
||||
vendorEncs := map[string][]EncoderCap{}
|
||||
for _, ke := range allEncoders {
|
||||
available := containsWord(encList, ke.Name)
|
||||
vendorEncs[ke.Vendor] = append(vendorEncs[ke.Vendor], EncoderCap{
|
||||
Name: ke.Name, Codec: ke.Codec, Label: ke.Label, Available: available,
|
||||
})
|
||||
}
|
||||
|
||||
// Get GPU names
|
||||
gpuNames := detectGPUNames()
|
||||
log.Printf("[gpu] detected GPU names: %v", gpuNames)
|
||||
|
||||
if len(gpuNames) == 0 {
|
||||
gpuNames = []gpuName{{Name: "Unknown GPU", Vendor: "unknown"}}
|
||||
}
|
||||
|
||||
var result []GPUInfo
|
||||
for _, gn := range gpuNames {
|
||||
encs := vendorEncs[gn.Vendor]
|
||||
if encs == nil {
|
||||
encs = []EncoderCap{}
|
||||
}
|
||||
result = append(result, GPUInfo{
|
||||
Name: gn.Name,
|
||||
Vendor: gn.Vendor,
|
||||
Encoders: encs,
|
||||
})
|
||||
}
|
||||
|
||||
// Add CPU entry
|
||||
result = append(result, GPUInfo{
|
||||
Name: "CPU",
|
||||
Vendor: "cpu",
|
||||
Encoders: []EncoderCap{
|
||||
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: containsWord(encList, "libx264")},
|
||||
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: containsWord(encList, "libx265")},
|
||||
{Name: "libsvtav1", Codec: "av1", Label: "AV1 (libsvtav1)", Available: containsWord(encList, "libsvtav1")},
|
||||
},
|
||||
})
|
||||
|
||||
log.Printf("[gpu] returning %d GPU entries", len(result))
|
||||
return result
|
||||
}
|
||||
|
||||
func containsWord(text, word string) bool {
|
||||
// Simple substring match — the encoder list has one encoder per line
|
||||
return len(text) > 0 && len(word) > 0 && containsLine(text, word)
|
||||
}
|
||||
|
||||
func containsLine(text, word string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
if i+len(word) > len(text) {
|
||||
break
|
||||
}
|
||||
if text[i:i+len(word)] == word {
|
||||
// Check word boundaries: should be preceded by space/newline and followed by space/newline
|
||||
before := i == 0 || text[i-1] == ' ' || text[i-1] == '\n' || text[i-1] == '\r'
|
||||
after := i+len(word) >= len(text) || text[i+len(word)] == ' ' || text[i+len(word)] == '\n' || text[i+len(word)] == '\r'
|
||||
if before && after {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fallbackGPUs() []GPUInfo {
|
||||
return []GPUInfo{
|
||||
{Name: "CPU", Vendor: "cpu", Encoders: []EncoderCap{
|
||||
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: true},
|
||||
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: true},
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package gpu
|
||||
|
||||
type gpuName struct {
|
||||
Name string
|
||||
Vendor string
|
||||
}
|
||||
|
||||
func detectGPUNames() []gpuName {
|
||||
return []gpuName{{Name: "Default GPU", Vendor: "unknown"}}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//go:build windows
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type gpuName struct {
|
||||
Name string
|
||||
Vendor string
|
||||
}
|
||||
|
||||
func detectGPUNames() []gpuName {
|
||||
// Try PowerShell first, then wmic
|
||||
gpus := psGPUList()
|
||||
if len(gpus) > 0 {
|
||||
log.Printf("[gpu] PowerShell found %d GPUs", len(gpus))
|
||||
for _, g := range gpus {
|
||||
log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor)
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
gpus = wmicGPUList()
|
||||
if len(gpus) > 0 {
|
||||
log.Printf("[gpu] WMIC found %d GPUs", len(gpus))
|
||||
for _, g := range gpus {
|
||||
log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor)
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
log.Printf("[gpu] No GPUs detected via PowerShell or WMIC")
|
||||
return nil
|
||||
}
|
||||
|
||||
func psGPUList() []gpuName {
|
||||
out, err := exec.Command("powershell", "-NoProfile", "-Command",
|
||||
"Get-CimInstance Win32_VideoController | Select-Object Name,AdapterCompatibility,DriverVersion | ConvertTo-Csv -NoTypeInformation",
|
||||
).CombinedOutput()
|
||||
log.Printf("[gpu] PS output:\n%s", string(out))
|
||||
if err != nil {
|
||||
log.Printf("[gpu] PS error: %v", err)
|
||||
return nil
|
||||
}
|
||||
return parseCSV(string(out))
|
||||
}
|
||||
|
||||
func wmicGPUList() []gpuName {
|
||||
out, err := exec.Command("wmic", "path", "win32_VideoController",
|
||||
"get", "Name,AdapterCompatibility", "/format:csv").CombinedOutput()
|
||||
log.Printf("[gpu] WMIC output:\n%s", string(out))
|
||||
if err != nil {
|
||||
log.Printf("[gpu] WMIC error: %v", err)
|
||||
return nil
|
||||
}
|
||||
return parseWMIC(string(out))
|
||||
}
|
||||
|
||||
func parseCSV(out string) []gpuName {
|
||||
var gpus []gpuName
|
||||
seen := map[string]bool{}
|
||||
lines := strings.Split(out, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "\"Name\"") || strings.HasPrefix(line, "Name") {
|
||||
continue
|
||||
}
|
||||
// Remove quotes: "Name","AdapterCompatibility"
|
||||
line = strings.ReplaceAll(line, "\"", "")
|
||||
parts := strings.SplitN(line, ",", 2)
|
||||
if len(parts) < 1 {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(parts[0])
|
||||
vendor := ""
|
||||
if len(parts) >= 2 {
|
||||
vendor = strings.TrimSpace(parts[1])
|
||||
}
|
||||
if name == "" || seen[name] {
|
||||
continue
|
||||
}
|
||||
if isVirtualGPU(name) {
|
||||
log.Printf("[gpu] SKIP virtual: %s", name)
|
||||
continue
|
||||
}
|
||||
vendor = classifyVendor(vendor, name)
|
||||
if vendor == "unknown" {
|
||||
log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name)
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
func parseWMIC(out string) []gpuName {
|
||||
var gpus []gpuName
|
||||
seen := map[string]bool{}
|
||||
lines := strings.Split(out, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "Node,") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ",", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(parts[2])
|
||||
vendor := strings.TrimSpace(parts[1])
|
||||
if name == "" || seen[name] {
|
||||
continue
|
||||
}
|
||||
if isVirtualGPU(name) {
|
||||
log.Printf("[gpu] SKIP virtual: %s", name)
|
||||
continue
|
||||
}
|
||||
vendor = classifyVendor(vendor, name)
|
||||
if vendor == "unknown" {
|
||||
log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name)
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
func classifyVendor(vendor, name string) string {
|
||||
lower := strings.ToLower(vendor + " " + name)
|
||||
switch {
|
||||
case strings.Contains(lower, "nvidia"):
|
||||
return "nvidia"
|
||||
case strings.Contains(lower, "intel") || strings.Contains(lower, "uhd graphics") ||
|
||||
strings.Contains(lower, "iris") || strings.Contains(lower, "hd graphics"):
|
||||
return "intel"
|
||||
case strings.Contains(lower, "amd") || strings.Contains(lower, "radeon") || strings.Contains(lower, "ati"):
|
||||
return "amd"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func isVirtualGPU(name string) bool {
|
||||
lower := strings.ToLower(name)
|
||||
// Filter out virtual/remote/display-only adapters that don't have encoders
|
||||
virtual := []string{
|
||||
"virtual", "remote", "rdp", "citrix", "vmware", "hyper-v",
|
||||
"mirror", "indirect", "parsec", "splashtop", "idm",
|
||||
"mirage", "vnc", "displayonly", "basicdisplay",
|
||||
"microsoft basic", "microsoft remote",
|
||||
}
|
||||
for _, v := range virtual {
|
||||
if strings.Contains(lower, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user