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'
let win: BrowserWindow | null = null
let encoder: Encoder | null = null
// Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js')
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
win.loadURL(url)
await win.loadURL(url)
// Open devTool if the app is not packaged
// win.webContents.openDevTools()
} else {
win.loadFile(indexHtml)
await win.loadFile(indexHtml)
}
// 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
win.webContents.setWindowOpenHandler(({url}) => {
console.log('setWindowOpenHandler', url)
if (url.startsWith('https:')) shell.openExternal(url)
return {action: 'deny'}
})
@@ -159,7 +161,7 @@ ipcMain.handle('quit', async () => app.quit())
ipcMain.handle('minimize', async () => win.minimize())
ipcMain.handle('encode', async (event: IpcMainInvokeEvent, ...args: any[]) => {
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),
onProgress: (id, progress) => event.sender.send('encode-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);
return encoder.encode();
})
ipcMain.handle('cancel-encode', async () => encoder.cancel());
ipcMain.handle('commitSettings', async (event: IpcMainInvokeEvent, ...args: any[]) => settingsRepository.commitSettings(args[0]))
settingsRepository.watch((settings: Settings) => emit('settings', settings));

View File

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

View File

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

View File

@@ -1,20 +1,34 @@
import defu from 'defu'
import {platform} from 'node:process'
import { defu } from 'defu'
import { platform } from 'node:process'
import * as fs from 'fs'
import {Credentials, Settings} from "../../shared/schema";
import {resolveUser} from "../main/helpers";
import { Credentials, Settings } from '../../shared/schema'
import { resolveUser } from '../main/helpers'
const defaults: Settings = {
version: '1.0.1',
credentials: null,
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 {
private settings: Settings | null
private readonly path: string
private readonly listeners: Array<(settings: Settings) => void>;
private readonly directory: string;
private readonly listeners: Array<(settings: Settings) => void>
private readonly directory: string
constructor() {
this.listeners = []
@@ -38,14 +52,14 @@ export class SettingsRepository {
const data = await fs.promises.readFile(this.path, {encoding: 'utf-8'})
if (data) {
const settings = JSON.parse(data) as Settings;
const settings = JSON.parse(data) as Settings
if (settings.version !== defaults.version) {
console.log('Settings version mismatch, resetting to defaults');
console.log('Settings version mismatch, resetting to defaults')
this.settings = defaults
} else {
console.log('Settings version match, merge with defaults');
this.settings = defu.defu(settings, defaults)
console.log('Settings version match, merge with defaults')
this.settings = defu(settings, defaults)
}
} else {
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
// if so, set settings.credentials to null
if (this.isExpired()) {
console.log('Credentials expired!');
console.log('Credentials expired!')
} else {
this.reloadUser();
this.reloadUser()
}
await this.save()
@@ -72,7 +86,7 @@ export class SettingsRepository {
async save() {
// call all listeners with the current settings
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)
@@ -89,7 +103,7 @@ export class SettingsRepository {
return process.env.HOME + '/Library/Preferences' + path
}
return process.env.HOME + "/.local/share" + path
return process.env.HOME + '/.local/share' + path
}
getSettings() {
@@ -106,6 +120,7 @@ export class SettingsRepository {
}
commitSettings(settings: Settings) {
console.log('Committing settings', settings)
this.settings = defu(settings, this.settings)
this.save()
}
@@ -122,7 +137,7 @@ export class SettingsRepository {
return expiresAt < now
}
return true;
return true
}
reloadUser() {
@@ -130,7 +145,7 @@ export class SettingsRepository {
console.debug('Reloading user')
resolveUser(
this.settings.credentials.access_token,
this.settings.credentials.token_type
this.settings.credentials.token_type,
).then((user) => {
this.settings.credentials.user = user
this.save()