2026-07-29 02:17:57 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="encode-page">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<h2>视频转码</h2>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
<p class="page-desc">重新编码视频和音频流,可调整编码器、质量、分辨率等参数</p>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Input File Card -->
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-header"><h3>输入文件</h3></div>
|
|
|
|
|
|
<div class="input-row">
|
|
|
|
|
|
<input :value="inputFile" readonly placeholder="选择要转码的视频文件..." @click="browseInput" />
|
|
|
|
|
|
<button class="btn-secondary" @click="browseInput">选择文件</button>
|
|
|
|
|
|
</div>
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<StreamInfoPanel :info="mediaInfo" />
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Hardware + Encoder Card -->
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-header"><h3>编码硬件与编码器</h3></div>
|
|
|
|
|
|
<div class="fields-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">硬件加速</label>
|
|
|
|
|
|
<select v-model="hwAccel">
|
2026-07-29 10:46:19 +08:00
|
|
|
|
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">视频编码器</label>
|
|
|
|
|
|
<select v-model="encodeSettings.videoCodec">
|
2026-07-29 10:46:19 +08:00
|
|
|
|
<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>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</optgroup>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="fields-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">音频编码器</label>
|
|
|
|
|
|
<select v-model="encodeSettings.audioCodec">
|
|
|
|
|
|
<option value="aac">AAC</option>
|
|
|
|
|
|
<option value="opus">Opus</option>
|
|
|
|
|
|
<option value="mp3">MP3</option>
|
|
|
|
|
|
<option value="copy">复制(不重新编码)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">Preset</label>
|
|
|
|
|
|
<select v-model="encodeSettings.preset">
|
|
|
|
|
|
<template v-if="isHardwareCodec">
|
|
|
|
|
|
<option value="p1">P1 — 最快(低画质)</option>
|
|
|
|
|
|
<option value="p2">P2</option>
|
|
|
|
|
|
<option value="p3">P3</option>
|
|
|
|
|
|
<option value="p4">P4 — 中等</option>
|
|
|
|
|
|
<option value="p5">P5</option>
|
|
|
|
|
|
<option value="p6">P6</option>
|
|
|
|
|
|
<option value="p7">P7 — 最慢(高画质)</option>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<option value="ultrafast">ultrafast</option>
|
|
|
|
|
|
<option value="superfast">superfast</option>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
<option value="veryfast">veryfast</option>
|
|
|
|
|
|
<option value="faster">faster</option>
|
|
|
|
|
|
<option value="fast">fast</option>
|
|
|
|
|
|
<option value="medium">medium — 中等</option>
|
|
|
|
|
|
<option value="slow">slow</option>
|
|
|
|
|
|
<option value="slower">slower</option>
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<option value="veryslow">veryslow</option>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Encoding Settings Card -->
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-header"><h3>编码参数</h3></div>
|
|
|
|
|
|
<div class="fields-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">码率控制</label>
|
|
|
|
|
|
<select v-model="rateControl">
|
|
|
|
|
|
<option value="crf">CRF / CQ (质量优先)</option>
|
|
|
|
|
|
<option value="bitrate">固定码率</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field" v-if="rateControl === 'crf'">
|
|
|
|
|
|
<label class="field-label">CRF / CQ (越小质量越高)</label>
|
|
|
|
|
|
<div class="crf-group">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<input type="range" min="1" max="51" v-model.number="encodeSettings.crf"
|
2026-07-29 02:17:57 +08:00
|
|
|
|
style="height:auto;padding:0;box-shadow:none;flex:1" />
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<span class="crf-value">{{ encodeSettings.crf ?? 23 }}</span>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="range-hint"><span>高质量</span><span>低质量</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field" v-else>
|
|
|
|
|
|
<label class="field-label">视频码率</label>
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<div class="bitrate-combo">
|
|
|
|
|
|
<input type="number" v-model.number="videoBitrateNum" placeholder="5000" min="1" />
|
|
|
|
|
|
<select v-model="videoBitrateUnit">
|
|
|
|
|
|
<option value="k">kbps</option>
|
|
|
|
|
|
<option value="M">Mbps</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="fields-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">音频码率</label>
|
|
|
|
|
|
<select v-model="encodeSettings.audioBitrate">
|
|
|
|
|
|
<option value="128k">128 kbps</option>
|
|
|
|
|
|
<option value="192k">192 kbps</option>
|
|
|
|
|
|
<option value="256k">256 kbps</option>
|
|
|
|
|
|
<option value="320k">320 kbps</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<label class="field-label">宽度 (0=保持原始)</label>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
<input type="number" v-model.number="encodeSettings.width" placeholder="1920" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<label class="field-label">高度 (0=保持原始)</label>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
<input type="number" v-model.number="encodeSettings.height" placeholder="1080" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="fields-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label class="field-label">帧率 (0=保持原始)</label>
|
|
|
|
|
|
<select v-model.number="encodeSettings.fps">
|
|
|
|
|
|
<option :value="0">保持原始</option>
|
|
|
|
|
|
<option :value="23.976">23.976</option>
|
|
|
|
|
|
<option :value="24">24</option>
|
|
|
|
|
|
<option :value="25">25</option>
|
|
|
|
|
|
<option :value="30">30</option>
|
|
|
|
|
|
<option :value="60">60</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Output Card -->
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-header"><h3>输出位置</h3></div>
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<div class="output-options">
|
|
|
|
|
|
<label class="radio-label"><input type="radio" value="default" v-model="outputMode" @change="autoOutput" /> 默认目录</label>
|
|
|
|
|
|
<label class="radio-label"><input type="radio" value="custom" v-model="outputMode" /> 自定义路径</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field" style="margin-bottom:8px">
|
|
|
|
|
|
<label class="field-label">输出格式</label>
|
|
|
|
|
|
<select v-model="outputFormat" @change="autoOutput" style="max-width:120px">
|
|
|
|
|
|
<option value="mp4">MP4</option>
|
|
|
|
|
|
<option value="mkv">MKV</option>
|
|
|
|
|
|
<option value="webm">WebM</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<span v-if="outputFormat === 'webm'" class="field-hint">WebM 仅支持 VP9/AV1 + Opus,请确认编码器兼容</span>
|
|
|
|
|
|
</div>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
<div class="input-row">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
<input :value="outputFile" readonly :placeholder="outputMode === 'custom' ? '选择输出文件...' : '自动生成'" @click="outputMode === 'custom' && browseOutput()" />
|
|
|
|
|
|
<button v-if="outputMode === 'custom'" class="btn-secondary" @click="browseOutput">浏览</button>
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Start Button -->
|
|
|
|
|
|
<div class="submit-area">
|
|
|
|
|
|
<span v-if="!canSubmit" class="field-hint submit-hint">请先选择输入和输出文件</span>
|
|
|
|
|
|
<button class="btn-primary btn-lg" @click="addTask" :disabled="!canSubmit">
|
|
|
|
|
|
开始任务
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-29 19:35:47 +08:00
|
|
|
|
import { ref, computed, watch } from 'vue'
|
2026-07-29 02:17:57 +08:00
|
|
|
|
import { api } from '../api/wails'
|
|
|
|
|
|
import type { MediaInfo, StreamInfo, EncodeSettings } from '../types'
|
2026-07-29 19:35:47 +08:00
|
|
|
|
import StreamInfoPanel from '../components/StreamInfo.vue'
|
2026-07-29 02:17:57 +08:00
|
|
|
|
|
2026-07-29 10:46:19 +08:00
|
|
|
|
const props = defineProps<{ gpuInfo: any[] }>()
|
2026-07-29 02:17:57 +08:00
|
|
|
|
const emit = defineEmits<{ taskAdded: [] }>()
|
|
|
|
|
|
|
2026-07-29 10:46:19 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-29 02:17:57 +08:00
|
|
|
|
const inputFile = ref('')
|
|
|
|
|
|
const outputFile = ref('')
|
2026-07-29 19:35:47 +08:00
|
|
|
|
const outputDir = ref('')
|
|
|
|
|
|
const namingRule = ref('{name}_{codec}')
|
|
|
|
|
|
const outputFormat = ref('mp4')
|
|
|
|
|
|
const outputMode = ref<'default' | 'custom'>('default')
|
|
|
|
|
|
|
|
|
|
|
|
async function loadOutputDir() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const app = (window as any).go?.main?.App
|
|
|
|
|
|
if (app?.GetConfig) {
|
|
|
|
|
|
const cfg = await app.GetConfig()
|
|
|
|
|
|
outputDir.value = cfg.outputDir || ''
|
|
|
|
|
|
namingRule.value = cfg.namingRule || '{name}_{codec}'
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyNamingRule(originalPath: string, codec: string): string {
|
|
|
|
|
|
const name = originalPath.replace(/^.*[\\/]/, '').replace(/\.[^.]+$/, '')
|
|
|
|
|
|
const dir = outputDir.value || originalPath.replace(/[\\/][^\\/]+$/, '')
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const date = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`
|
|
|
|
|
|
const ext = '.' + outputFormat.value
|
|
|
|
|
|
let result = namingRule.value
|
|
|
|
|
|
.replace('{name}', name)
|
|
|
|
|
|
.replace('{codec}', codecLabelForNaming(codec))
|
|
|
|
|
|
.replace('{date}', date)
|
|
|
|
|
|
return dir.replace(/[\\/]$/, '') + '\\' + result + ext
|
|
|
|
|
|
}
|
2026-07-29 02:17:57 +08:00
|
|
|
|
const mediaInfo = ref<MediaInfo | null>(null)
|
|
|
|
|
|
const hwAccel = ref('')
|
|
|
|
|
|
const rateControl = ref<'crf' | 'bitrate'>('crf')
|
|
|
|
|
|
|
|
|
|
|
|
const encodeSettings = ref<EncodeSettings>({
|
|
|
|
|
|
videoCodec: 'libx264', audioCodec: 'aac', hwEncoder: '',
|
|
|
|
|
|
width: 0, height: 0, fps: 0,
|
|
|
|
|
|
videoBitrate: '', audioBitrate: '192k', crf: 23, preset: 'medium', pixelFormat: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const isHardwareCodec = computed(() => {
|
|
|
|
|
|
const c = encodeSettings.value.videoCodec
|
|
|
|
|
|
return c.includes('nvenc') || c.includes('qsv') || c.includes('amf')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-29 19:35:47 +08:00
|
|
|
|
// When switching rate control mode, clear the unused field
|
|
|
|
|
|
watch(rateControl, (mode) => {
|
|
|
|
|
|
if (mode === 'bitrate') {
|
|
|
|
|
|
encodeSettings.value.crf = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
encodeSettings.value.videoBitrate = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Combine number + unit into the bitrate string ffmpeg expects
|
|
|
|
|
|
const videoBitrateNum = ref(5000)
|
|
|
|
|
|
const videoBitrateUnit = ref<'k' | 'M'>('k')
|
|
|
|
|
|
watch([videoBitrateNum, videoBitrateUnit], () => {
|
|
|
|
|
|
if (videoBitrateNum.value > 0) {
|
|
|
|
|
|
encodeSettings.value.videoBitrate = videoBitrateNum.value + videoBitrateUnit.value
|
|
|
|
|
|
} else {
|
|
|
|
|
|
encodeSettings.value.videoBitrate = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-29 02:17:57 +08:00
|
|
|
|
const canSubmit = computed(() => inputFile.value && outputFile.value)
|
|
|
|
|
|
|
|
|
|
|
|
async function browseInput() {
|
|
|
|
|
|
try { const p = await api.selectInputFile(); if (p) { inputFile.value = p; await analyzeMedia() } } catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function browseOutput() {
|
|
|
|
|
|
try { const p = await api.selectOutputFile('output.mp4'); if (p) outputFile.value = p } catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function analyzeMedia() {
|
|
|
|
|
|
if (!inputFile.value) return
|
2026-07-29 19:35:47 +08:00
|
|
|
|
await loadOutputDir()
|
2026-07-29 02:17:57 +08:00
|
|
|
|
try {
|
|
|
|
|
|
mediaInfo.value = await api.getMediaInfo(inputFile.value)
|
2026-07-29 19:35:47 +08:00
|
|
|
|
autoOutput()
|
2026-07-29 02:17:57 +08:00
|
|
|
|
} catch { mediaInfo.value = null }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 19:35:47 +08:00
|
|
|
|
function autoOutput() {
|
|
|
|
|
|
if (!inputFile.value) return
|
|
|
|
|
|
if (outputMode.value === 'custom') return
|
|
|
|
|
|
outputFile.value = applyNamingRule(inputFile.value, encodeSettings.value.videoCodec)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Regenerate output filename when encoder or format changes
|
|
|
|
|
|
watch([() => encodeSettings.value.videoCodec, outputFormat], () => {
|
|
|
|
|
|
if (inputFile.value && outputMode.value === 'default') autoOutput()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// When switching between HW/software encoder, reset preset and hwaccel
|
|
|
|
|
|
watch(() => encodeSettings.value.videoCodec, (codec) => {
|
|
|
|
|
|
if (codec.includes('nvenc')) {
|
|
|
|
|
|
if (!encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'p4'
|
|
|
|
|
|
hwAccel.value = 'cuda'
|
|
|
|
|
|
} else if (codec.includes('qsv')) {
|
|
|
|
|
|
if (!encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'p4'
|
|
|
|
|
|
hwAccel.value = 'qsv'
|
|
|
|
|
|
} else if (codec.includes('amf')) {
|
|
|
|
|
|
if (!encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'p4'
|
|
|
|
|
|
hwAccel.value = 'd3d11va'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'medium'
|
|
|
|
|
|
// For software encoders, keep hwaccel as-is (user's choice for decode only)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function codecLabelForNaming(c: string): string {
|
|
|
|
|
|
const m: Record<string, string> = {
|
|
|
|
|
|
libx264: 'H264', libx265: 'HEVC', libsvtav1: 'AV1',
|
|
|
|
|
|
h264_nvenc: 'H264-NV', hevc_nvenc: 'HEVC-NV', av1_nvenc: 'AV1-NV',
|
|
|
|
|
|
h264_qsv: 'H264-QSV', hevc_qsv: 'HEVC-QSV', av1_qsv: 'AV1-QSV',
|
|
|
|
|
|
h264_amf: 'H264-AMF', hevc_amf: 'HEVC-AMF', av1_amf: 'AV1-AMF',
|
|
|
|
|
|
libvpx: 'VP8', libvpx_vp9: 'VP9',
|
|
|
|
|
|
mpeg4: 'MPEG4', libaom_av1: 'AV1',
|
|
|
|
|
|
}
|
|
|
|
|
|
return m[c] || c.replace(/^lib/, '').replace(/_/g, '-')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 02:17:57 +08:00
|
|
|
|
async function addTask() {
|
|
|
|
|
|
if (!canSubmit.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const taskId = await api.addTask({
|
|
|
|
|
|
id: '', type: 'encode', inputFile: inputFile.value, outputFile: outputFile.value,
|
|
|
|
|
|
status: 'pending', progress: {} as any,
|
|
|
|
|
|
encode: { ...encodeSettings.value },
|
|
|
|
|
|
remux: {} as any, subtitle: {} as any,
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
})
|
|
|
|
|
|
await api.setHWAccel(hwAccel.value)
|
|
|
|
|
|
await api.startTask(taskId)
|
|
|
|
|
|
emit('taskAdded')
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
} catch (e: any) { alert('添加失败: ' + (e?.message || e)) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetForm() {
|
|
|
|
|
|
inputFile.value = ''
|
|
|
|
|
|
outputFile.value = ''
|
|
|
|
|
|
mediaInfo.value = null
|
|
|
|
|
|
hwAccel.value = ''
|
|
|
|
|
|
rateControl.value = 'crf'
|
|
|
|
|
|
encodeSettings.value = {
|
|
|
|
|
|
videoCodec: 'libx264', audioCodec: 'aac', hwEncoder: '',
|
|
|
|
|
|
width: 0, height: 0, fps: 0,
|
|
|
|
|
|
videoBitrate: '', audioBitrate: '192k', crf: 23, preset: 'medium', pixelFormat: '',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.encode-page { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
|
|
.encode-page > h2 { margin-bottom: 0; }
|
|
|
|
|
|
.page-desc { font-size: 13px; color: var(--text-dim); margin-top: -8px; }
|
|
|
|
|
|
|
|
|
|
|
|
.input-row { display: flex; gap: 8px; }
|
|
|
|
|
|
.input-row input { flex: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
.stream-tags { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 8px; }
|
|
|
|
|
|
.stream-tag {
|
|
|
|
|
|
font-size: 12px; padding: 3px 8px; line-height: 1.5;
|
|
|
|
|
|
border-radius: 4px; background: var(--bg-input); color: var(--text-secondary);
|
|
|
|
|
|
display: flex; align-items: center; gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stream-type {
|
|
|
|
|
|
font-size: 10px; font-weight: 700; padding: 0 4px;
|
|
|
|
|
|
border-radius: 2px; background: var(--accent); color: white;
|
|
|
|
|
|
min-width: 16px; text-align: center; line-height: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stream-tag.dur { font-weight: 500; }
|
|
|
|
|
|
|
|
|
|
|
|
.crf-group { display: flex; align-items: center; gap: 10px; }
|
|
|
|
|
|
.crf-value { font-size: 16px; font-weight: 700; color: var(--accent); min-width: 28px; }
|
|
|
|
|
|
.range-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); }
|
|
|
|
|
|
input[type="range"] { accent-color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
|
|
.submit-area { display: flex; align-items: center; justify-content: flex-end; gap: 16px; padding-top: 4px; }
|
|
|
|
|
|
.submit-hint { margin-right: auto; }
|
2026-07-29 19:35:47 +08:00
|
|
|
|
|
|
|
|
|
|
.bitrate-combo {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.bitrate-combo input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.bitrate-combo select {
|
|
|
|
|
|
width: 72px;
|
|
|
|
|
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
|
|
|
|
border-left: none;
|
|
|
|
|
|
}
|
2026-07-29 02:17:57 +08:00
|
|
|
|
</style>
|