mirror of
https://github.com/bitinflow/rerun-encoder.git
synced 2026-03-13 21:56:01 +00:00
first commit
This commit is contained in:
147
electron/rerun-manager/encoder.ts
Normal file
147
electron/rerun-manager/encoder.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
|
||||
import {EncoderListeners, EncoderOptions, Settings, User, Video} from "../../shared/schema";
|
||||
import * as fs from "fs";
|
||||
import axios, {AxiosInstance} from "axios";
|
||||
|
||||
const ffmpeg = require('fluent-ffmpeg')
|
||||
|
||||
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 settings: Settings;
|
||||
private api: AxiosInstance;
|
||||
private s3: AxiosInstance;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
input: string,
|
||||
options: EncoderOptions,
|
||||
listeners: EncoderListeners,
|
||||
settings: Settings
|
||||
) {
|
||||
this.id = id;
|
||||
this.input = input;
|
||||
this.output = this.input.replace(/\.mp4$/, '.flv')
|
||||
this.options = options;
|
||||
this.listeners = listeners;
|
||||
this.settings = settings;
|
||||
|
||||
this.api = axios.create({
|
||||
baseURL: settings.endpoint,
|
||||
headers: {
|
||||
Authorization: `${settings.credentials.token_type} ${settings.credentials.access_token}`
|
||||
}
|
||||
})
|
||||
|
||||
this.s3 = axios.create({});
|
||||
}
|
||||
|
||||
async encode(): Promise<void> {
|
||||
this.listeners.onStart(this.id)
|
||||
|
||||
ffmpeg(this.input)
|
||||
.outputOptions(this.getOutputOptions())
|
||||
.output(this.output)
|
||||
.on('start', () => {
|
||||
console.log('start')
|
||||
})
|
||||
.on('progress', (progress) => {
|
||||
console.log('progress', progress)
|
||||
this.listeners.onProgress(this.id, progress.percent)
|
||||
})
|
||||
.on('end', async () => {
|
||||
console.log('end')
|
||||
// @ts-ignore
|
||||
try {
|
||||
const video = await this.requestUploadUrl(this.settings.credentials.user)
|
||||
await this.upload(video)
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
this.listeners.onError(this.id, error.message)
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.log('error', error)
|
||||
this.listeners.onError(this.id, error.message)
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
private getOutputOptions() {
|
||||
return [
|
||||
'-c:v libx264',
|
||||
'-preset veryfast',
|
||||
'-crf 23',
|
||||
'-maxrate 6000k',
|
||||
'-bufsize 6000k',
|
||||
'-c:a aac',
|
||||
'-b:a 128k',
|
||||
'-ac 2',
|
||||
'-f flv',
|
||||
'-movflags +faststart',
|
||||
'-y'
|
||||
]
|
||||
}
|
||||
|
||||
private async requestUploadUrl(user: User): Promise<Video> {
|
||||
const response = await this.api.post(`channels/${user.id}/videos`, {
|
||||
title: this.options.title,
|
||||
size: fs.statSync(this.output).size,
|
||||
})
|
||||
|
||||
return response.data as Video
|
||||
}
|
||||
|
||||
private async confirmUpload(video: Video): Promise<Video> {
|
||||
const response = await this.api.post(`videos/${video.id}/confirm`, {
|
||||
encoded: true,
|
||||
})
|
||||
|
||||
console.log('confirm', response.data)
|
||||
|
||||
return response.data as Video;
|
||||
}
|
||||
|
||||
private async cancelUpload(video: Video): Promise<void> {
|
||||
await this.api.delete(`videos/${video.id}/cancel`)
|
||||
}
|
||||
|
||||
private async upload(video: Video) {
|
||||
if (!video.upload_url) {
|
||||
return
|
||||
}
|
||||
|
||||
const stream = fs.createReadStream(this.output)
|
||||
stream.on('error', (error) => {
|
||||
console.log('upload error', error)
|
||||
this.listeners.onError(this.id, error.message)
|
||||
})
|
||||
|
||||
const progress = (progress: any) => {
|
||||
const progressCompleted = progress.loaded / fs.statSync(this.output).size * 100
|
||||
this.listeners.onUploadProgress(this.id, progressCompleted)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.s3.put(video.upload_url, stream, {
|
||||
onUploadProgress: progress,
|
||||
headers: {
|
||||
'Content-Type': 'video/x-flv',
|
||||
}
|
||||
})
|
||||
|
||||
this.listeners.onUploadComplete(this.id, await this.confirmUpload(video))
|
||||
} catch (error) {
|
||||
console.log('upload error', error)
|
||||
try {
|
||||
await this.cancelUpload(video)
|
||||
} catch (error) {
|
||||
console.log('cancel error', error)
|
||||
}
|
||||
this.listeners.onError(this.id, `Upload Error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
101
electron/rerun-manager/internal-server.ts
Normal file
101
electron/rerun-manager/internal-server.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import Application from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import KoaLogger from 'koa-logger';
|
||||
import json from 'koa-json';
|
||||
import koaCors from "koa-cors";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
import axios, {AxiosInstance} from "axios";
|
||||
import {app} from "electron"
|
||||
import {Credentials, User} from "../../shared/schema";
|
||||
import {SettingsRepository} from "./settings-repository";
|
||||
import * as fs from "fs";
|
||||
import {join} from "node:path";
|
||||
|
||||
export class InternalServer {
|
||||
private readonly app: Application;
|
||||
private readonly axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create();
|
||||
this.app = new Application();
|
||||
}
|
||||
|
||||
listen(settingsRepository: SettingsRepository) {
|
||||
const router = new Router()
|
||||
|
||||
router.get("/health", (ctx) => {
|
||||
const settings = settingsRepository.getSettings();
|
||||
ctx.body = {
|
||||
status: 'ok',
|
||||
version: app.getVersion(),
|
||||
has_credentials: settings.credentials !== null,
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/oauth', (ctx) => {
|
||||
if(process.env.VITE_DEV_SERVER_URL) {
|
||||
ctx.body = fs.readFileSync(join(__dirname, '..', '..', 'public', 'callback.html'), {encoding: 'utf8'})
|
||||
} else {
|
||||
ctx.body = fs.readFileSync(join(process.env.DIST, 'callback.html'), {encoding: 'utf8'})
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/callback', async (ctx) => {
|
||||
// @ts-ignore
|
||||
const params = new URLSearchParams(ctx.request.body.hash);
|
||||
|
||||
const credentials: Credentials = {
|
||||
access_token: params.get('access_token'),
|
||||
token_type: params.get('token_type'),
|
||||
expires_in: params.get('expires_in'),
|
||||
expires_at: this.calculateExpiresAt(params.get('expires_in')),
|
||||
state: params.get('state'),
|
||||
user: await this.resolveUser(params.get('access_token'), params.get('token_type')),
|
||||
}
|
||||
|
||||
console.log('credentials', credentials);
|
||||
|
||||
settingsRepository.setCredentials(credentials);
|
||||
|
||||
await settingsRepository.save();
|
||||
|
||||
ctx.body = {
|
||||
status: 'ok',
|
||||
}
|
||||
});
|
||||
|
||||
this.app.use(json());
|
||||
this.app.use(KoaLogger());
|
||||
this.app.use(koaCors());
|
||||
this.app.use(bodyParser());
|
||||
|
||||
this.app.use(router.routes());
|
||||
|
||||
this.app.listen(8361, () => {
|
||||
console.log(`Server listening http://127.0.0.1:8361/`);
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveUser(accessToken: string, tokenType: string): Promise<User> {
|
||||
const response = await this.axios.get('https://api.rerunmanager.com/v1/channels/me', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `${tokenType} ${accessToken}`,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
config: response.data.config,
|
||||
avatar_url: response.data.avatar_url,
|
||||
}
|
||||
}
|
||||
|
||||
private calculateExpiresAt(expiresIn: string) {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + parseInt(expiresIn) * 1000);
|
||||
|
||||
return expiresAt.toISOString();
|
||||
}
|
||||
}
|
||||
124
electron/rerun-manager/settings-repository.ts
Normal file
124
electron/rerun-manager/settings-repository.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import defu from 'defu'
|
||||
import {platform} from 'node:process'
|
||||
import * as fs from 'fs'
|
||||
import {Credentials, Settings} from "../../shared/schema";
|
||||
|
||||
const defaults: Settings = {
|
||||
version: '1.0.0',
|
||||
credentials: null,
|
||||
endpoint: 'https://api.rerunmanager.com/v1/',
|
||||
}
|
||||
|
||||
export class SettingsRepository {
|
||||
private settings: Settings | null
|
||||
private readonly path: string
|
||||
private readonly listeners: Array<(settings: Settings) => void>;
|
||||
private readonly directory: string;
|
||||
|
||||
constructor() {
|
||||
this.listeners = []
|
||||
this.settings = defaults
|
||||
this.directory = this.appdata('/rerunmanager')
|
||||
|
||||
if (!fs.existsSync(this.directory)) {
|
||||
console.log('Creating directory', this.directory)
|
||||
fs.mkdirSync(this.directory)
|
||||
}
|
||||
|
||||
this.path = `${this.directory}/encoder.json`
|
||||
}
|
||||
|
||||
async restore() {
|
||||
// load settings from path
|
||||
// using node's fs module if file does not exist, create it with defaults
|
||||
// if file exists, load it into this.settings
|
||||
|
||||
try {
|
||||
const data = await fs.promises.readFile(this.path, {encoding: 'utf-8'})
|
||||
|
||||
if (data) {
|
||||
const settings = JSON.parse(data) as Settings;
|
||||
|
||||
if (settings.version !== defaults.version) {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
console.log('Settings file empty, resetting to defaults')
|
||||
this.settings = defaults
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Settings file not found, resetting to defaults', e)
|
||||
this.settings = defaults
|
||||
}
|
||||
|
||||
// check if settings.credentials.expires_at is in the past
|
||||
// if so, set settings.credentials to null
|
||||
if (this.isExpired()) {
|
||||
console.log('Credentials expired!');
|
||||
}
|
||||
|
||||
await this.save()
|
||||
|
||||
return this.settings
|
||||
}
|
||||
|
||||
async save() {
|
||||
// call all listeners with the current settings
|
||||
this.listeners.forEach(
|
||||
(listener: (settings: Settings) => void) => listener(this.settings)
|
||||
)
|
||||
|
||||
const pretty = JSON.stringify(this.settings, null, 2)
|
||||
|
||||
await fs.promises.writeFile(this.path, pretty, {encoding: 'utf-8'})
|
||||
}
|
||||
|
||||
appdata(path: string): string {
|
||||
if (process.env.APPDATA) {
|
||||
return process.env.APPDATA + path
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return process.env.HOME + '/Library/Preferences' + path
|
||||
}
|
||||
|
||||
return process.env.HOME + "/.local/share" + path
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
watch(callback: (settings: Settings) => void) {
|
||||
// add callback to list of listeners
|
||||
this.listeners.push(callback)
|
||||
}
|
||||
|
||||
setCredentials(credentials: Credentials) {
|
||||
this.settings.credentials = credentials
|
||||
}
|
||||
|
||||
commitSettings(settings: Settings) {
|
||||
this.settings = defu(settings, this.settings)
|
||||
this.save()
|
||||
}
|
||||
|
||||
async logout() {
|
||||
this.settings.credentials = null
|
||||
await this.save()
|
||||
}
|
||||
|
||||
private isExpired() {
|
||||
if (this.settings.credentials) {
|
||||
const expiresAt = new Date(this.settings.credentials.expires_at)
|
||||
const now = new Date()
|
||||
return expiresAt < now
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user