更新
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"ffmpeg-gui/internal/ffmpeg"
|
"ffmpeg-gui/internal/ffmpeg"
|
||||||
|
"ffmpeg-gui/internal/gpu"
|
||||||
"ffmpeg-gui/internal/hwaccel"
|
"ffmpeg-gui/internal/hwaccel"
|
||||||
"ffmpeg-gui/internal/media"
|
"ffmpeg-gui/internal/media"
|
||||||
"ffmpeg-gui/internal/platform"
|
"ffmpeg-gui/internal/platform"
|
||||||
@@ -106,6 +107,14 @@ func (a *App) GetHardwareEncoders() ([]hwaccel.HWEncoder, error) {
|
|||||||
return a.hwDetect.DetectEncoders()
|
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.
|
// GetAccelerators returns detected hardware acceleration methods.
|
||||||
func (a *App) GetAccelerators() ([]hwaccel.Accelerator, error) {
|
func (a *App) GetAccelerators() ([]hwaccel.Accelerator, error) {
|
||||||
if a.hwDetect == nil {
|
if a.hwDetect == nil {
|
||||||
@@ -200,7 +209,7 @@ func (a *App) SelectInputFile() (string, error) {
|
|||||||
// SelectOutputFile opens a save file dialog.
|
// SelectOutputFile opens a save file dialog.
|
||||||
func (a *App) SelectOutputFile(defaultName string) (string, error) {
|
func (a *App) SelectOutputFile(defaultName string) (string, error) {
|
||||||
return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||||
Title: "选择输出文件",
|
Title: "选择输出文件",
|
||||||
DefaultFilename: defaultName,
|
DefaultFilename: defaultName,
|
||||||
Filters: []runtime.FileFilter{
|
Filters: []runtime.FileFilter{
|
||||||
{DisplayName: "MP4 (*.mp4)", Pattern: "*.mp4"},
|
{DisplayName: "MP4 (*.mp4)", Pattern: "*.mp4"},
|
||||||
|
|||||||
@@ -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">
|
<div class="workspace-column">
|
||||||
<main class="workspace">
|
<main class="workspace">
|
||||||
<EncodePage v-if="currentView === 'encode'" @taskAdded="onTaskAdded" />
|
<EncodePage v-if="currentView === 'encode'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||||
<RemuxPage v-if="currentView === 'remux'" @taskAdded="onTaskAdded" />
|
<RemuxPage v-if="currentView === 'remux'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||||
<BurnPage v-if="currentView === 'burn'" @taskAdded="onTaskAdded" />
|
<BurnPage v-if="currentView === 'burn'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
|
||||||
<SettingsPage v-if="currentView === 'settings'" />
|
<SettingsPage v-if="currentView === 'settings'" />
|
||||||
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
|
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
|
||||||
</main>
|
</main>
|
||||||
@@ -41,12 +41,15 @@ import type { Task, Progress } from './types'
|
|||||||
const currentView = ref('encode')
|
const currentView = ref('encode')
|
||||||
const theme = ref<'dark'|'light'>('light')
|
const theme = ref<'dark'|'light'>('light')
|
||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
|
const gpuInfo = ref<any[]>([])
|
||||||
|
|
||||||
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
|
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
document.addEventListener('contextmenu', e => e.preventDefault())
|
||||||
document.documentElement.setAttribute('data-theme', theme.value)
|
document.documentElement.setAttribute('data-theme', theme.value)
|
||||||
loadTasks()
|
loadTasks()
|
||||||
|
loadGPUInfo()
|
||||||
|
|
||||||
onTaskUpdated((task: Task) => {
|
onTaskUpdated((task: Task) => {
|
||||||
const found = tasks.value.find(t => t.id === task.id)
|
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 */ }
|
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() }
|
function onTaskAdded() { loadTasks() }
|
||||||
|
|
||||||
async function handleCancel(id: string) {
|
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>
|
<template>
|
||||||
<header class="app-header" @dblclick="maximize">
|
<header class="app-header" @dblclick="maximize">
|
||||||
<div class="header-left" style="--wails-draggable: drag">
|
<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">
|
<img :src="logoSrc" class="app-logo" width="22" height="22" alt="" />
|
||||||
<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>
|
|
||||||
<span class="app-name">FFmpeg GUI</span>
|
<span class="app-name">FFmpeg GUI</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,6 +47,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import logoSrc from '../assets/icon/app-logo.svg'
|
||||||
|
|
||||||
defineProps<{ currentView: string; theme: string }>()
|
defineProps<{ currentView: string; theme: string }>()
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
navigate: [view: string]
|
navigate: [view: string]
|
||||||
@@ -94,7 +92,7 @@ function closeWindow() {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-logo { color: var(--accent); flex-shrink: 0; }
|
.app-logo { flex-shrink: 0; }
|
||||||
.app-name {
|
.app-name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -62,19 +62,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Encode Settings (simplified) -->
|
<!-- Hardware Acceleration -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><h3>编码设置</h3></div>
|
<div class="card-header"><h3>编码硬件与编码器</h3></div>
|
||||||
<div class="fields-row">
|
<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">
|
<div class="field">
|
||||||
<label class="field-label">视频编码器</label>
|
<label class="field-label">视频编码器</label>
|
||||||
<select v-model="encodeSettings.videoCodec">
|
<select v-model="encodeSettings.videoCodec">
|
||||||
<option value="libx264">H.264 (libx264)</option>
|
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
|
||||||
<option value="libx265">H.265 / HEVC (libx265)</option>
|
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
|
||||||
<option value="h264_nvenc">H.264 NVENC</option>
|
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
|
||||||
<option value="hevc_nvenc">HEVC NVENC</option>
|
</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Encode Settings -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h3>编码参数</h3></div>
|
||||||
|
<div class="fields-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label">质量 / CRF</label>
|
<label class="field-label">质量 / CRF</label>
|
||||||
<input type="number" v-model.number="encodeSettings.crf" placeholder="23" min="0" max="51" />
|
<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 { api } from '../api/wails'
|
||||||
import type { MediaInfo, StreamInfo, EncodeSettings, SubTrack } from '../types'
|
import type { MediaInfo, StreamInfo, EncodeSettings, SubTrack } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{ gpuInfo: any[] }>()
|
||||||
const emit = defineEmits<{ taskAdded: [] }>()
|
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 inputFile = ref('')
|
||||||
const outputFile = ref('')
|
const outputFile = ref('')
|
||||||
const mediaInfo = ref<MediaInfo | null>(null)
|
const mediaInfo = ref<MediaInfo | null>(null)
|
||||||
@@ -165,6 +208,7 @@ async function addTask() {
|
|||||||
remux: {} as any, subtitle: { subtitles: [...subtitles.value] },
|
remux: {} as any, subtitle: { subtitles: [...subtitles.value] },
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
|
await api.setHWAccel(hwAccel.value)
|
||||||
await api.startTask(taskId)
|
await api.startTask(taskId)
|
||||||
emit('taskAdded')
|
emit('taskAdded')
|
||||||
resetForm()
|
resetForm()
|
||||||
@@ -184,8 +228,10 @@ function resetForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function streamIcon(s: StreamInfo) {
|
function streamIcon(s: StreamInfo) {
|
||||||
if (s.codec_type === 'video') return '🎥'; if (s.codec_type === 'audio') return '🔊'
|
if (s.codec_type === 'video') return 'V'
|
||||||
if (s.codec_type === 'subtitle') return '💬'; return '📄'
|
if (s.codec_type === 'audio') return 'A'
|
||||||
|
if (s.codec_type === 'subtitle') return 'S'
|
||||||
|
return '?'
|
||||||
}
|
}
|
||||||
function streamLabel(s: StreamInfo) {
|
function streamLabel(s: StreamInfo) {
|
||||||
if (s.codec_type === 'video') return `视频: ${s.codec_name} ${s.width||''}x${s.height||''}`
|
if (s.codec_type === 'video') return `视频: ${s.codec_name} ${s.width||''}x${s.height||''}`
|
||||||
|
|||||||
@@ -28,35 +28,16 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label">硬件加速</label>
|
<label class="field-label">硬件加速</label>
|
||||||
<select v-model="hwAccel">
|
<select v-model="hwAccel">
|
||||||
<option value="">CPU 软件编码</option>
|
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</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>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label">视频编码器</label>
|
<label class="field-label">视频编码器</label>
|
||||||
<select v-model="encodeSettings.videoCodec">
|
<select v-model="encodeSettings.videoCodec">
|
||||||
<optgroup label="软件编码">
|
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
|
||||||
<option value="libx264">H.264 / AVC (libx264)</option>
|
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
|
||||||
<option value="libx265">H.265 / HEVC (libx265)</option>
|
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
|
||||||
<option value="libsvtav1">AV1 (libsvtav1)</option>
|
</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>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,8 +163,38 @@ import { ref, computed } from 'vue'
|
|||||||
import { api } from '../api/wails'
|
import { api } from '../api/wails'
|
||||||
import type { MediaInfo, StreamInfo, EncodeSettings } from '../types'
|
import type { MediaInfo, StreamInfo, EncodeSettings } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{ gpuInfo: any[] }>()
|
||||||
const emit = defineEmits<{ taskAdded: [] }>()
|
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 inputFile = ref('')
|
||||||
const outputFile = ref('')
|
const outputFile = ref('')
|
||||||
const mediaInfo = ref<MediaInfo | null>(null)
|
const mediaInfo = ref<MediaInfo | null>(null)
|
||||||
|
|||||||
@@ -35,11 +35,7 @@
|
|||||||
<div class="field" style="max-width:300px">
|
<div class="field" style="max-width:300px">
|
||||||
<label class="field-label">硬件解码(加速输入读取)</label>
|
<label class="field-label">硬件解码(加速输入读取)</label>
|
||||||
<select v-model="hwAccel">
|
<select v-model="hwAccel">
|
||||||
<option value="">不使用硬件加速</option>
|
<option v-for="o in hwAccelOpts" :key="o.value" :value="o.value">{{ o.label }}</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>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,12 +79,28 @@ import { ref, reactive, computed } from 'vue'
|
|||||||
import { api } from '../api/wails'
|
import { api } from '../api/wails'
|
||||||
import type { MediaInfo, StreamInfo } from '../types'
|
import type { MediaInfo, StreamInfo } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{ gpuInfo: any[] }>()
|
||||||
const emit = defineEmits<{ taskAdded: [] }>()
|
const emit = defineEmits<{ taskAdded: [] }>()
|
||||||
|
|
||||||
const inputFile = ref('')
|
const inputFile = ref('')
|
||||||
const outputFile = ref('')
|
const outputFile = ref('')
|
||||||
const outputFormat = ref('mp4')
|
const outputFormat = ref('mp4')
|
||||||
const hwAccel = ref('')
|
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 mediaInfo = ref<MediaInfo | null>(null)
|
||||||
const selectedStreams = reactive<Record<number, boolean>>({})
|
const selectedStreams = reactive<Record<number, boolean>>({})
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,21 @@
|
|||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
<h2>设置</h2>
|
<h2>设置</h2>
|
||||||
|
|
||||||
<!-- Hardware Detection Card -->
|
<!-- GPU Detection Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><h3>硬件检测</h3></div>
|
<div class="card-header"><h3>硬件检测</h3></div>
|
||||||
|
|
||||||
<div v-if="loading" class="loading-hint">正在检测硬件编码器...</div>
|
<div v-if="loading" class="loading-hint">正在检测硬件编码器...</div>
|
||||||
|
<div v-else class="gpu-cards">
|
||||||
<div v-else class="hw-cards">
|
<div v-for="gpu in gpus" :key="gpu.name" class="gpu-card">
|
||||||
<div v-for="gpu in gpuGroups" :key="gpu.type" class="gpu-card">
|
|
||||||
<div class="gpu-header">
|
<div class="gpu-header">
|
||||||
<span class="gpu-dot" :class="gpu.type"></span>
|
<span class="gpu-dot" :class="gpu.vendor"></span>
|
||||||
<span class="gpu-name">{{ gpu.label }}</span>
|
<span class="gpu-name">{{ gpu.name }}</span>
|
||||||
<span v-if="gpu.hasAvailable" class="gpu-badge avail">可用</span>
|
<span v-if="hasAvailable(gpu)" class="gpu-badge avail">可用</span>
|
||||||
<span v-else class="gpu-badge unavail">不可用</span>
|
<span v-else class="gpu-badge unavail">不可用</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gpu-encoders">
|
<div class="gpu-encoders">
|
||||||
<div v-for="enc in gpu.encoders" :key="enc.name" class="encoder-row">
|
<div v-for="enc in gpu.encoders" :key="enc.name" class="encoder-row">
|
||||||
<span :class="['enc-check', { avail: enc.available }]">
|
<span :class="['enc-check', { avail: enc.available }]">{{ enc.available ? '✓' : '✗' }}</span>
|
||||||
{{ enc.available ? '✓' : '✗' }}
|
|
||||||
</span>
|
|
||||||
<span class="enc-name">{{ enc.label }}</span>
|
<span class="enc-name">{{ enc.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,35 +24,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hardware Priority Card -->
|
<!-- Preferred Hardware Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><h3>硬件优先级</h3></div>
|
<div class="card-header"><h3>首选硬件加速</h3></div>
|
||||||
<p class="field-hint" style="margin-bottom:12px">拖拽调整编码器优先级,编解码时优先使用排在前面的硬件</p>
|
<div class="field" style="max-width:360px">
|
||||||
<div class="priority-list">
|
<label class="field-label">默认使用的硬件加速器</label>
|
||||||
<div
|
<select v-model="preferredAccel" @change="onPrefChange">
|
||||||
v-for="(item, i) in priorities"
|
<option v-for="item in allOpts" :key="item.key" :value="item.key"
|
||||||
:key="item.key"
|
:disabled="!item.available">
|
||||||
class="priority-item"
|
{{ item.label }}{{ item.available ? '' : ' (不可用)' }}
|
||||||
:class="{ disabled: !item.available }"
|
</option>
|
||||||
>
|
</select>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Output Settings Card -->
|
<!-- Output Settings Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><h3>输出设置</h3></div>
|
<div class="card-header"><h3>输出设置</h3></div>
|
||||||
<div class="fields-row">
|
<div class="field">
|
||||||
<div class="field">
|
<label class="field-label">默认输出目录</label>
|
||||||
<label class="field-label">默认输出目录</label>
|
<div class="input-row">
|
||||||
<div class="input-row">
|
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
|
||||||
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
|
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
|
||||||
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" style="margin-top:16px">
|
<div class="field" style="margin-top:16px">
|
||||||
@@ -74,55 +63,81 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { api } from '../api/wails'
|
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 loading = ref(true)
|
||||||
const encoders = ref<HWEncoder[]>([])
|
const gpus = ref<GPUInfo[]>([])
|
||||||
const outputDir = ref('')
|
const outputDir = ref('')
|
||||||
const namingRule = ref('{name}_{codec}')
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
encoders.value = [
|
try {
|
||||||
{ name:'h264_nvenc',label:'H.264 NVENC',type:'nvidia',codec:'h264',available:true },
|
const encoders = await api.getHardwareEncoders()
|
||||||
{ name:'hevc_nvenc',label:'HEVC NVENC',type:'nvidia',codec:'hevc',available:true },
|
gpus.value = buildFromEncoders(encoders)
|
||||||
{ name:'av1_nvenc', label:'AV1 NVENC', type:'nvidia',codec:'av1', available:true },
|
} catch { gpus.value = [] }
|
||||||
{ name:'h264_qsv', label:'H.264 QSV', type:'intel', codec:'h264',available:false },
|
} finally {
|
||||||
{ name:'hevc_qsv', label:'HEVC QSV', type:'intel', codec:'hevc',available:false },
|
loading.value = false
|
||||||
{ name:'h264_amf', label:'H.264 AMF', type:'amd', codec:'h264',available: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() {
|
async function selectOutputDir() {
|
||||||
try {
|
try {
|
||||||
@@ -133,66 +148,27 @@ async function selectOutputDir() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.settings-page {
|
.settings-page { display: flex; flex-direction: column; gap: 20px; }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-page > h2 { margin-bottom: 4px; }
|
.settings-page > h2 { margin-bottom: 4px; }
|
||||||
|
|
||||||
.loading-hint { color: var(--text-dim); padding: 12px 0; }
|
.loading-hint { color: var(--text-dim); padding: 12px 0; }
|
||||||
|
|
||||||
/* GPU Cards */
|
/* GPU Cards */
|
||||||
.hw-cards { display: flex; flex-direction: column; gap: 12px; }
|
.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-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-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; }
|
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.gpu-dot.nvidia { background: #76b900; }
|
.gpu-dot.nvidia { background: #76b900; } .gpu-dot.intel { background: #00aaff; }
|
||||||
.gpu-dot.intel { background: #00aaff; }
|
.gpu-dot.amd { background: #ed1c24; } .gpu-dot.cpu,.gpu-dot.unknown { background: var(--text-dim); }
|
||||||
.gpu-dot.amd { background: #ed1c24; }
|
|
||||||
.gpu-name { font-size: 14px; font-weight: 600; flex: 1; }
|
.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 { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
|
||||||
.gpu-badge.avail { background: #e6f4ea; color: var(--success); }
|
.gpu-badge.avail { background: #e6f4ea; color: var(--success); }
|
||||||
.gpu-badge.unavail { background: var(--bg-input); color: var(--text-dim); }
|
.gpu-badge.unavail { background: var(--bg-input); color: var(--text-dim); }
|
||||||
|
|
||||||
.gpu-encoders { display: flex; flex-direction: column; gap: 6px; }
|
.gpu-encoders { display: flex; flex-direction: column; gap: 6px; }
|
||||||
.encoder-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
.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 { width: 18px; font-size: 12px; font-weight: 600; color: var(--text-dim); }
|
||||||
.enc-check.avail { color: var(--success); }
|
.enc-check.avail { color: var(--success); }
|
||||||
.enc-name { color: var(--text-secondary); }
|
.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 */
|
/* Output */
|
||||||
.input-row { display: flex; gap: 8px; width: 100%; }
|
.input-row { display: flex; gap: 8px; width: 100%; }
|
||||||
.input-row input { flex: 1; }
|
.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
|
||||||
|
}
|
||||||
@@ -25,8 +25,9 @@ func main() {
|
|||||||
AssetServer: &assetserver.Options{
|
AssetServer: &assetserver.Options{
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
},
|
},
|
||||||
OnStartup: app.startup,
|
EnableDefaultContextMenu: false,
|
||||||
OnShutdown: app.shutdown,
|
OnStartup: app.startup,
|
||||||
|
OnShutdown: app.shutdown,
|
||||||
Bind: []any{
|
Bind: []any{
|
||||||
app,
|
app,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user