Add encoder settings

Add encoder cancel button
This commit is contained in:
René Preuß
2023-08-06 17:22:05 +02:00
parent 4ba1f1fb5f
commit 5f96ca82ea
10 changed files with 392 additions and 79 deletions

View File

@@ -53,6 +53,7 @@ require('update-electron-app')({
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
let win: BrowserWindow | null = null let win: BrowserWindow | null = null
let encoder: Encoder | null = null
// Here, you can also use other preload // Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js') const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL const url = process.env.VITE_DEV_SERVER_URL
@@ -79,11 +80,11 @@ async function createWindow(settings: Settings) {
}) })
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298 if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
win.loadURL(url) await win.loadURL(url)
// Open devTool if the app is not packaged // Open devTool if the app is not packaged
// win.webContents.openDevTools() // win.webContents.openDevTools()
} else { } else {
win.loadFile(indexHtml) await win.loadFile(indexHtml)
} }
// Test actively push message to the Electron-Renderer // Test actively push message to the Electron-Renderer
@@ -93,6 +94,7 @@ async function createWindow(settings: Settings) {
// Make all links open with the browser, not with the application // Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({url}) => { win.webContents.setWindowOpenHandler(({url}) => {
console.log('setWindowOpenHandler', url)
if (url.startsWith('https:')) shell.openExternal(url) if (url.startsWith('https:')) shell.openExternal(url)
return {action: 'deny'} return {action: 'deny'}
}) })
@@ -159,7 +161,7 @@ ipcMain.handle('quit', async () => app.quit())
ipcMain.handle('minimize', async () => win.minimize()) ipcMain.handle('minimize', async () => win.minimize())
ipcMain.handle('encode', async (event: IpcMainInvokeEvent, ...args: any[]) => { ipcMain.handle('encode', async (event: IpcMainInvokeEvent, ...args: any[]) => {
const arg = args[0] as { id: string, input: string, options: EncoderOptions }; const arg = args[0] as { id: string, input: string, options: EncoderOptions };
const encoder = new Encoder(arg.id, arg.input, arg.options, { encoder = new Encoder(arg.id, arg.input, arg.options, {
onStart: (id) => event.sender.send('encode-start', id), onStart: (id) => event.sender.send('encode-start', id),
onProgress: (id, progress) => event.sender.send('encode-progress', id, progress), onProgress: (id, progress) => event.sender.send('encode-progress', id, progress),
onUploadProgress: (id, progress) => event.sender.send('encode-upload-progress', id, progress), onUploadProgress: (id, progress) => event.sender.send('encode-upload-progress', id, progress),
@@ -168,6 +170,7 @@ ipcMain.handle('encode', async (event: IpcMainInvokeEvent, ...args: any[]) => {
}, settingsRepository); }, settingsRepository);
return encoder.encode(); return encoder.encode();
}) })
ipcMain.handle('cancel-encode', async () => encoder.cancel());
ipcMain.handle('commitSettings', async (event: IpcMainInvokeEvent, ...args: any[]) => settingsRepository.commitSettings(args[0])) ipcMain.handle('commitSettings', async (event: IpcMainInvokeEvent, ...args: any[]) => settingsRepository.commitSettings(args[0]))
settingsRepository.watch((settings: Settings) => emit('settings', settings)); settingsRepository.watch((settings: Settings) => emit('settings', settings));

View File

@@ -161,6 +161,9 @@ window.api = {
logout: () => { logout: () => {
return ipcRenderer.invoke('logout') return ipcRenderer.invoke('logout')
}, },
cancelEncode: () => {
return ipcRenderer.invoke('cancel-encode')
},
encode: (id: string, input: string, options: EncoderOptions) => { encode: (id: string, input: string, options: EncoderOptions) => {
return ipcRenderer.invoke('encode', {id, input, options}) return ipcRenderer.invoke('encode', {id, input, options})
}, },

View File

@@ -1,60 +1,72 @@
import {EncoderListeners, EncoderOptions, Video} from "../../shared/schema"; import { EncoderListeners, EncoderOptions, Video } from '../../shared/schema'
import * as fs from "fs"; import * as fs from 'fs'
import axios, {AxiosInstance} from "axios"; import axios, { AxiosInstance } from 'axios'
import {SettingsRepository} from "./settings-repository"; import { SettingsRepository } from './settings-repository'
import * as path from "path"; import * as path from 'path'
import {path as ffmpegPath} from "@ffmpeg-installer/ffmpeg"; import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
import {upload} from "./file-uploader"; import { upload } from './file-uploader'
console.log('ffmpegPath', ffmpegPath)
const ffmpeg = require('fluent-ffmpeg')
ffmpeg.setFfmpegPath(ffmpegPath)
let ffmpegCommand: any = null
export interface Signal {
aborted: boolean
}
export class Encoder { export class Encoder {
private readonly id: string; private readonly id: string
private readonly input: string; private readonly input: string
private readonly output: string; private readonly output: string
private readonly options: EncoderOptions; private readonly options: EncoderOptions
private readonly listeners: EncoderListeners; private readonly listeners: EncoderListeners
private readonly settingsRepository: SettingsRepository; private readonly settingsRepository: SettingsRepository
private api: AxiosInstance; private api: AxiosInstance
private s3: AxiosInstance; private s3: AxiosInstance
private signal: Signal = {aborted: false}
constructor( constructor(
id: string, id: string,
input: string, input: string,
options: EncoderOptions, options: EncoderOptions,
listeners: EncoderListeners, listeners: EncoderListeners,
settingsRepository: SettingsRepository settingsRepository: SettingsRepository,
) { ) {
this.id = id; this.id = id
this.input = input; this.input = input
this.output = this.getOutputPath(input, 'flv'); this.output = this.getOutputPath(input, 'flv')
this.options = options; this.options = options
this.listeners = listeners; this.listeners = listeners
this.settingsRepository = settingsRepository; this.settingsRepository = settingsRepository
const settings = settingsRepository.getSettings(); const settings = settingsRepository.getSettings()
this.api = axios.create({ this.api = axios.create({
baseURL: settings.endpoint, baseURL: settings.endpoint,
headers: { headers: {
Authorization: `${settings.credentials.token_type} ${settings.credentials.access_token}` Authorization: `${settings.credentials.token_type} ${settings.credentials.access_token}`,
} },
}) })
this.s3 = axios.create({}); this.s3 = axios.create({})
} }
encode(): void { encode(): void {
this.signal.aborted = false
this.listeners.onStart(this.id) this.listeners.onStart(this.id)
if (this.requireEncoding()) { if (this.requireEncoding()) {
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path let totalTime = 0
console.log('ffmpegPath', ffmpegPath) const outputOptions = this.getOutputOptions()
const ffmpeg = require('fluent-ffmpeg')
ffmpeg.setFfmpegPath(ffmpegPath)
let totalTime = 0;
ffmpeg(this.input) console.log('Using the following output options:', outputOptions)
ffmpegCommand = ffmpeg(this.input)
ffmpegCommand
.output(this.output) .output(this.output)
.outputOptions(this.getOutputOptions()) .outputOptions(outputOptions)
.on('start', () => console.log('start')) .on('start', () => console.log('start'))
.on('codecData', data => totalTime = parseInt(data.duration.replace(/:/g, ''))) .on('codecData', data => totalTime = parseInt(data.duration.replace(/:/g, '')))
.on('progress', progress => { .on('progress', progress => {
@@ -75,23 +87,32 @@ export class Encoder {
} }
private getOutputOptions() { private getOutputOptions() {
const output = this.settingsRepository.getSettings().output
return [ return [
'-c:v libx264', `-c:v ${output.video.encoder}`, // libx264 Use NVIDIA NVENC for hardware-accelerated encoding
'-preset veryfast', `-profile:v ${output.profile}`, // Use the "main" profile for H264 (compatible with most browsers)
'-crf 23', '-x264opts keyint=60:no-scenecut', // Set the key frame interval to 60 seconds (same as the original video)
'-maxrate 6000k', `-preset ${output.preset}`, // Use the "fast" preset for faster transcoding
'-bufsize 6000k', `-crf ${output.crf}`, // Adjust the Constant Rate Factor for the desired quality (lower values mean higher quality, 18-28 is usually a good range)
'-c:a aac', '-sws_flags bilinear', // Use bilinear scaling algorithm for better image quality
'-b:a 128k', `-maxrate ${output.video.bitrate}k`,
`-bufsize ${output.video.bitrate}k`,
`-c:a ${output.audio.encoder}`,
`-b:a ${output.audio.bitrate}k`,
'-ac 2', '-ac 2',
'-f flv', '-f flv',
'-movflags +faststart', '-movflags +faststart',
'-y' '-y',
] ]
} }
private async requestUpload(filename: string): Promise<void> { private async requestUpload(filename: string): Promise<void> {
const user = this.settingsRepository.getSettings().credentials.user; if (this.signal.aborted) {
console.log('upload aborted, skipping upload request')
return
}
const user = this.settingsRepository.getSettings().credentials.user
const response = await this.api.post(`channels/${user.id}/videos`, { const response = await this.api.post(`channels/${user.id}/videos`, {
title: this.options.title, title: this.options.title,
size: fs.statSync(filename).size, size: fs.statSync(filename).size,
@@ -114,10 +135,11 @@ export class Encoder {
console.log('confirm', response.data) console.log('confirm', response.data)
return response.data as Video; return response.data as Video
} }
private async cancelUpload(video: Video): Promise<void> { private async cancelUpload(video: Video): Promise<void> {
console.log('cancel upload', video.id)
await this.api.delete(`videos/${video.id}/cancel`) await this.api.delete(`videos/${video.id}/cancel`)
} }
@@ -126,12 +148,18 @@ export class Encoder {
return return
} }
if (this.signal.aborted) {
console.log('upload aborted, skipping upload handle')
this.listeners.onError(this.id, `Upload Error: Upload aborted`)
return
}
try { try {
await upload(video.upload_url, filename, (progress: number) => { await upload(video.upload_url, filename, (progress: number) => {
this.listeners.onUploadProgress(this.id, progress) this.listeners.onUploadProgress(this.id, progress)
}) }, this.signal)
this.settingsRepository.reloadUser(); this.settingsRepository.reloadUser()
this.listeners.onUploadComplete(this.id, await this.confirmUpload(video)) this.listeners.onUploadComplete(this.id, await this.confirmUpload(video))
} catch (error) { } catch (error) {
@@ -162,4 +190,12 @@ export class Encoder {
private requireEncoding() { private requireEncoding() {
return path.parse(this.input).ext !== '.flv' return path.parse(this.input).ext !== '.flv'
} }
cancel() {
console.log('cancel rerun encode')
this.signal.aborted = true
if (ffmpegCommand) {
ffmpegCommand.kill()
}
}
} }

View File

@@ -1,8 +1,10 @@
import { Signal } from './encoder'
const fs = require('fs'); const fs = require('fs');
const https = require('https'); const https = require('https');
const {promisify} = require('util'); const {promisify} = require('util');
export async function upload(url, filename, onProgress): Promise<void> { export async function upload(url, filename, onProgress, signal: Signal): Promise<void> {
const fileStream = fs.createReadStream(filename); const fileStream = fs.createReadStream(filename);
const fileStats = await promisify(fs.stat)(filename); const fileStats = await promisify(fs.stat)(filename);
@@ -29,6 +31,12 @@ export async function upload(url, filename, onProgress): Promise<void> {
let uploadedBytes = 0; let uploadedBytes = 0;
fileStream.on('data', (chunk) => { fileStream.on('data', (chunk) => {
if (signal.aborted) {
console.log('upload aborted, skipping upload stream')
req.destroy();
fileStream.destroy();
return;
}
uploadedBytes += chunk.length; uploadedBytes += chunk.length;
onProgress(Math.round((uploadedBytes / fileStats.size) * 100)); onProgress(Math.round((uploadedBytes / fileStats.size) * 100));
}); });

View File

@@ -1,20 +1,34 @@
import defu from 'defu' import { defu } from 'defu'
import {platform} from 'node:process' import { platform } from 'node:process'
import * as fs from 'fs' import * as fs from 'fs'
import {Credentials, Settings} from "../../shared/schema"; import { Credentials, Settings } from '../../shared/schema'
import {resolveUser} from "../main/helpers"; import { resolveUser } from '../main/helpers'
const defaults: Settings = { const defaults: Settings = {
version: '1.0.1', version: '1.0.1',
credentials: null, credentials: null,
endpoint: 'https://api.rerunmanager.com/v1/', endpoint: 'https://api.rerunmanager.com/v1/',
deleteOnComplete: true,
output: {
video: {
encoder: 'libx264',
bitrate: 4500,
},
audio: {
encoder: 'aac',
bitrate: 128,
},
preset: 'fast',
profile: 'high',
crf: 23,
},
} }
export class SettingsRepository { export class SettingsRepository {
private settings: Settings | null private settings: Settings | null
private readonly path: string private readonly path: string
private readonly listeners: Array<(settings: Settings) => void>; private readonly listeners: Array<(settings: Settings) => void>
private readonly directory: string; private readonly directory: string
constructor() { constructor() {
this.listeners = [] this.listeners = []
@@ -38,14 +52,14 @@ export class SettingsRepository {
const data = await fs.promises.readFile(this.path, {encoding: 'utf-8'}) const data = await fs.promises.readFile(this.path, {encoding: 'utf-8'})
if (data) { if (data) {
const settings = JSON.parse(data) as Settings; const settings = JSON.parse(data) as Settings
if (settings.version !== defaults.version) { if (settings.version !== defaults.version) {
console.log('Settings version mismatch, resetting to defaults'); console.log('Settings version mismatch, resetting to defaults')
this.settings = defaults this.settings = defaults
} else { } else {
console.log('Settings version match, merge with defaults'); console.log('Settings version match, merge with defaults')
this.settings = defu.defu(settings, defaults) this.settings = defu(settings, defaults)
} }
} else { } else {
console.log('Settings file empty, resetting to defaults') console.log('Settings file empty, resetting to defaults')
@@ -59,9 +73,9 @@ export class SettingsRepository {
// check if settings.credentials.expires_at is in the past // check if settings.credentials.expires_at is in the past
// if so, set settings.credentials to null // if so, set settings.credentials to null
if (this.isExpired()) { if (this.isExpired()) {
console.log('Credentials expired!'); console.log('Credentials expired!')
} else { } else {
this.reloadUser(); this.reloadUser()
} }
await this.save() await this.save()
@@ -72,7 +86,7 @@ export class SettingsRepository {
async save() { async save() {
// call all listeners with the current settings // call all listeners with the current settings
this.listeners.forEach( this.listeners.forEach(
(listener: (settings: Settings) => void) => listener(this.settings) (listener: (settings: Settings) => void) => listener(this.settings),
) )
const pretty = JSON.stringify(this.settings, null, 2) const pretty = JSON.stringify(this.settings, null, 2)
@@ -89,7 +103,7 @@ export class SettingsRepository {
return process.env.HOME + '/Library/Preferences' + path return process.env.HOME + '/Library/Preferences' + path
} }
return process.env.HOME + "/.local/share" + path return process.env.HOME + '/.local/share' + path
} }
getSettings() { getSettings() {
@@ -106,6 +120,7 @@ export class SettingsRepository {
} }
commitSettings(settings: Settings) { commitSettings(settings: Settings) {
console.log('Committing settings', settings)
this.settings = defu(settings, this.settings) this.settings = defu(settings, this.settings)
this.save() this.save()
} }
@@ -122,7 +137,7 @@ export class SettingsRepository {
return expiresAt < now return expiresAt < now
} }
return true; return true
} }
reloadUser() { reloadUser() {
@@ -130,7 +145,7 @@ export class SettingsRepository {
console.debug('Reloading user') console.debug('Reloading user')
resolveUser( resolveUser(
this.settings.credentials.access_token, this.settings.credentials.access_token,
this.settings.credentials.token_type this.settings.credentials.token_type,
).then((user) => { ).then((user) => {
this.settings.credentials.user = user this.settings.credentials.user = user
this.save() this.save()

View File

@@ -2,7 +2,7 @@
"name": "rerun-encoder", "name": "rerun-encoder",
"private": true, "private": true,
"productName": "Rerun Encoder", "productName": "Rerun Encoder",
"version": "1.0.4", "version": "1.1.0",
"description": "Official Rerun Encoder App for Rerun Manager", "description": "Official Rerun Encoder App for Rerun Manager",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"scripts": { "scripts": {
@@ -59,6 +59,7 @@
"dependencies": { "dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffmpeg-installer/ffmpeg": "^1.1.0",
"@koa/cors": "^4.0.0", "@koa/cors": "^4.0.0",
"@types/cookie": "^0.5.1",
"axios": "^1.3.4", "axios": "^1.3.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"defu": "^6.1.2", "defu": "^6.1.2",

18
shared/schema.d.ts vendored
View File

@@ -31,8 +31,22 @@ export interface Credentials {
*/ */
export interface Settings { export interface Settings {
version?: string // version of the settings schema (diff will force a reset) version?: string // version of the settings schema (diff will force a reset)
credentials?: Credentials | null, credentials?: Credentials | null
endpoint?: string | null, endpoint?: string | null
deleteOnComplete?: boolean
output?: {
video: {
encoder: 'libx264' | 'nvenc_h264'
bitrate: number
}
audio: {
encoder: 'aac'
bitrate: number
}
preset: 'default' | 'fast' | 'medium' | 'slow'
profile: 'high' | 'main' | 'baseline'
crf: number
}
} }
export interface VerifiedGame { export interface VerifiedGame {

View File

@@ -1,14 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref} from "vue"; import { computed, onMounted, ref } from 'vue'
import ProgressBar from "./components/ProgressBar.vue"; import ProgressBar from './components/ProgressBar.vue'
import AppTitleBar from "./components/AppTitleBar.vue"; import AppTitleBar from './components/AppTitleBar.vue'
import VButton from "./components/VButton.vue"; import VButton from './components/VButton.vue'
import {Settings, Video} from "../shared/schema"; import { Settings, Video } from '../shared/schema'
import AppEncoderOptions from './components/AppEncoderOptions.vue'
console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`) console.log('[App.vue]', `Hello world from Electron ${process.versions.electron}!`)
const isEncoding = ref(false) const isEncoding = ref(false)
const isCompleted = ref(false) const isCompleted = ref(false)
const isOptionsOpen = ref(false)
const encodingProgress = ref(0) const encodingProgress = ref(0)
const encodingProgressStage = ref<string>('unknown') const encodingProgressStage = ref<string>('unknown')
const encodingError = ref<string | null>(null) const encodingError = ref<string | null>(null)
@@ -83,7 +85,7 @@ const requestEncode = (event: any) => {
} }
} }
const settings = ref<Settings | null>(null); const settings = ref<Settings | null>(null)
const fileInput = ref<HTMLInputElement | null>(null) const fileInput = ref<HTMLInputElement | null>(null)
// Authorization Code Grant with PKCE // Authorization Code Grant with PKCE
@@ -107,6 +109,15 @@ const logout = () => {
window.api.logout() window.api.logout()
} }
const cancelEncode = () => {
// @ts-ignore
window.api.cancelEncode()
}
const openOptions = () => {
isOptionsOpen.value = true
}
const storageUpgradeRequired = computed(() => { const storageUpgradeRequired = computed(() => {
if (!settings.value) return false if (!settings.value) return false
if (!settings.value.credentials) return false if (!settings.value.credentials) return false
@@ -132,15 +143,25 @@ onMounted(() => {
<div> <div>
<app-title-bar/> <app-title-bar/>
<div class="px-6 flex"> <div class="px-6 flex" v-if="!isOptionsOpen">
<div> <div>
<img src="./assets/ffmpeg.svg" class="h-16" alt="ffmpeg"> <img src="./assets/ffmpeg.svg" class="h-16" alt="ffmpeg">
</div> </div>
<div> <div>
<div class="font-bold text-xl mt-2">Rerun Encoder</div> <div class="font-bold text-xl mt-2">Rerun Encoder</div>
<div class="text-sm text-zinc-400">Powered by FFmpeg</div> <div class="text-sm text-zinc-400">
Powered by FFmpeg ({{ settings?.output?.video?.encoder }})
</div>
<div class="flex gap-4 mt-4"> <div class="flex gap-4 mt-4">
<a
class="text-rose-400"
href="#"
@click.prevent="openOptions"
>
<i class="far fa-gear"></i>
Options
</a>
<a <a
class="text-rose-400" class="text-rose-400"
href="https://www.bitinflow.com/legal/privacy/" href="https://www.bitinflow.com/legal/privacy/"
@@ -160,9 +181,15 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<app-encoder-options
v-if="settings && isOptionsOpen"
:settings="settings"
@close="isOptionsOpen = false"
/>
</div> </div>
<div> <div>
<div> <div v-if="!isOptionsOpen">
<div v-if="settings && settings.credentials" class="p-4 flex justify-between bg-base-900"> <div v-if="settings && settings.credentials" class="p-4 flex justify-between bg-base-900">
<div class="test flex gap-2"> <div class="test flex gap-2">
<div> <div>
@@ -212,6 +239,11 @@ onMounted(() => {
</div> </div>
<template v-else-if="isEncoding"> <template v-else-if="isEncoding">
<progress-bar :progress="encodingProgress" :stage="encodingProgressStage"/> <progress-bar :progress="encodingProgress" :stage="encodingProgressStage"/>
<div>
<button class="text-rose-400/80 text-sm" @click="cancelEncode">
Cancel
</button>
</div>
</template> </template>
<template v-else> <template v-else>
<input <input

View File

@@ -0,0 +1,201 @@
<script setup lang="ts">
import { PropType } from 'vue'
import { Settings } from '../../shared/schema'
const props = defineProps({
settings: Object as PropType<Settings>,
isOptionsOpen: Boolean,
onOptionsOpen: Function as PropType<() => void>,
onOptionsClose: Function as PropType<() => void>,
})
const emits = defineEmits(['close', 'commit-settings'])
// watch for changes to the settings object
const commitSettings = () => {
const clone = JSON.parse(JSON.stringify(props.settings))
// commit the settings object to the main process
// @ts-ignore
window.api.commitSettings(clone)
console.log('settings updated', clone)
}
</script>
<template>
<div
class="px-6 pb-6 no-drag grid gap-2"
style="max-height: 220px; overflow-y: auto;"
>
<div>
<button
type="button"
class="text-white/50"
@click="emits('close')"
>
<i class="fal fa-arrow-left mr-1"></i>
Go Back
</button>
</div>
<div class="font-bold text-sm text-white/80">Encoding</div>
<div
v-if="props.settings && props.settings.output"
class="grid gap-3"
>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div>
<div class="text-sm">Video Encoder</div>
<div class="text-xs text-white/50">
If available, use hardware encoding.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model="props.settings.output.video.encoder"
@change="commitSettings"
>
<option value="libx264">Software (libx264)</option>
<option value="nvenc_h264">Hardware (NVENC, H.264)</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">Video Bitrate</div>
<div class="text-xs text-white/50">
Select your desired video bitrate.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model.number="props.settings.output.video.bitrate"
@change="commitSettings"
>
<option value="6000">6000 kbps (1080p @ 60 FPS)</option>
<option value="4500">4500 kbps (1080p @ 30 FPS)</option>
<option value="4500">4500 kbps (720p @ 60 FPS)</option>
<option value="3000">3000 kbps (720p @ 30 FPS)</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">Audio Encoder</div>
<div class="text-xs text-white/50">
Select your desired audio encoder.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model="props.settings.output.audio.encoder"
@change="commitSettings"
>
<option value="aac">AAC</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">Audio Bitrate</div>
<div class="text-xs text-white/50">
We recommend using 192 kbps.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model.number="props.settings.output.audio.bitrate"
@change="commitSettings"
>
<option value="64">64 kbps</option>
<option value="96">96 kbps</option>
<option value="128">128 kbps</option>
<option value="160">160 kbps</option>
<option value="192" selected>192 kbps</option>
<option value="224">224 kbps</option>
<option value="256">256 kbps</option>
<option value="288">288 kbps</option>
<option value="320">320 kbps</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">Preset</div>
<div class="text-xs text-white/50">
We recommend using the fast preset.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model="props.settings.output.preset"
@change="commitSettings"
>
<option value="default">Default</option>
<option value="fast">Fast</option>
<option value="medium">Medium</option>
<option value="slow">Slow</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">Profile</div>
<div class="text-xs text-white/50">
We recommend using the high profile.
</div>
</div>
<div>
<select
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model="props.settings.output.profile"
@change="commitSettings"
>
<option value="high">High</option>
<option value="main">Main</option>
<option value="baseline">Baseline</option>
</select>
</div>
</div>
<div class="bg-base-600/40 rounded flex items-center p-3 gap-4 justify-between">
<div class="self-center">
<div class="text-sm">CRF</div>
<div class="text-xs text-white/50">
Lower values mean higher quality, 17-28 is usually a good range.
</div>
</div>
<div>
<input
type="number"
min="0"
max="51"
step="1"
class="w-full rounded bg-base-700 text-white/50 border-0 py-1.5"
v-model.number="props.settings.output.crf"
@change="commitSettings"
>
</div>
</div>
</div>
</div>
</template>
<style scoped>
::-webkit-scrollbar {
width: 4px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
</style>

View File

@@ -7,6 +7,6 @@ body {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
button, a, input, label { button, a, input, label, .no-drag {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }