first commit

This commit is contained in:
René Preuß
2023-03-01 19:24:20 +01:00
commit 4ab5e76d85
45 changed files with 1963 additions and 0 deletions

View 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}`)
}
}
}

View 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();
}
}

View 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;
}
}