This commit is contained in:
waveringana 2023-11-18 12:53:15 -05:00
parent 807e50388a
commit 832189a346
10 changed files with 518 additions and 299 deletions

View file

@ -1,39 +1,21 @@
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
env: {
browser: true,
es2021: true,
node: true,
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
overrides: [],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {
indent: ["error", 2, { SwitchCase: 1 }],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

View file

@ -1,13 +1,13 @@
import { extension, videoExtensions, imageExtensions } from "./lib";
import ffmpeg, { FfprobeData, ffprobe } from 'fluent-ffmpeg';
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
import ffprobeInstaller from '@ffprobe-installer/ffprobe';
import which from 'which';
import ffmpeg, { FfprobeData, ffprobe } from "fluent-ffmpeg";
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
import ffprobeInstaller from "@ffprobe-installer/ffprobe";
import which from "which";
/**
* Enum to represent different types of video encoding methods.
*
*
* @enum {string}
* @property {string} CPU - Uses the libx264 codec for CPU-based encoding
* @property {string} NVIDIA - Uses the h264_nvenc codec for NVIDIA GPU-based encoding
@ -16,11 +16,11 @@ import which from 'which';
* @property {string} APPLE - Uses the h264_videotoolbox codec for Apple GPU/MediaEngine-based encoding
*/
export enum EncodingType {
CPU = 'libx264',
NVIDIA = 'h264_nvenc',
AMD = 'h264_amf',
INTEL = 'h264_qsv',
APPLE = 'h264_videotoolbox'
CPU = "libx264",
NVIDIA = "h264_nvenc",
AMD = "h264_amf",
INTEL = "h264_qsv",
APPLE = "h264_videotoolbox",
}
/**
@ -31,7 +31,7 @@ export let currentEncoding: EncodingType = EncodingType.CPU;
/**
* Sets the current encoding type.
*
*
* @param {EncodingType} type - The encoding type to set.
*/
export const setEncodingType = (type: EncodingType) => {
@ -40,16 +40,20 @@ export const setEncodingType = (type: EncodingType) => {
/**
* Returns the path to an executable by checking environment variables, the system path, or a default installer.
*
*
* @param {string} envVar - The environment variable to check for the executable's path.
* @param {string} executable - The name of the executable to search for in the system path.
* @param {Object} installer - An object containing the default installer path.
* @param {string} installer.path - The default path to use if the executable is not found in the environment or system path.
*
*
* @returns {string} - The path to the executable.
* @throws Will throw an error if the executable is not found and the installer path is not available.
*/
const getExecutablePath = (envVar: string, executable: string, installer: { path: string }) => {
const getExecutablePath = (
envVar: string,
executable: string,
installer: { path: string },
) => {
if (process.env[envVar]) {
return process.env[envVar];
}
@ -61,8 +65,16 @@ const getExecutablePath = (envVar: string, executable: string, installer: { path
}
};
const ffmpegPath = getExecutablePath('EB_FFMPEG_PATH', 'ffmpeg', ffmpegInstaller);
const ffprobePath = getExecutablePath('EB_FFPROBE_PATH', 'ffprobe', ffprobeInstaller);
const ffmpegPath = getExecutablePath(
"EB_FFMPEG_PATH",
"ffmpeg",
ffmpegInstaller,
);
const ffprobePath = getExecutablePath(
"EB_FFPROBE_PATH",
"ffprobe",
ffprobeInstaller,
);
console.log(`Using ffmpeg from path: ${ffmpegPath}`);
console.log(`Using ffprobe from path: ${ffprobePath}`);
@ -74,10 +86,16 @@ const checkEnvForEncoder = () => {
const envEncoder = process.env.EB_ENCODER?.toUpperCase();
if (envEncoder && Object.keys(EncodingType).includes(envEncoder)) {
setEncodingType(EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType);
console.log(`Setting encoding type to ${envEncoder} based on environment variable.`);
setEncodingType(
EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType,
);
console.log(
`Setting encoding type to ${envEncoder} based on environment variable.`,
);
} else if (envEncoder) {
console.warn(`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`);
console.warn(
`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`,
);
}
};
@ -85,12 +103,12 @@ checkEnvForEncoder();
/**
* Downscale a video using ffmpeg with various encoding options.
*
*
* @param {string} path - The input video file path.
* @param {string} filename - The name of the file.
* @param {string} extension - The file extension of the file
* @returns {Promise<void>} - A promise that resolves when the downscaling is complete, and rejects on error.
*
*
* @example
* ffmpegDownscale('input.mp4').then(() => {
* console.log('Downscaling complete.');
@ -98,13 +116,21 @@ checkEnvForEncoder();
* console.log(`Error: ${error}`);
* });
*/
export const ffmpegDownscale = (path: string, filename: string, extension: string) => {
export const ffmpegDownscale = (
path: string,
filename: string,
extension: string,
) => {
const startTime = Date.now();
const outputOptions = [
'-vf', 'scale=-2:720',
'-c:v', currentEncoding,
'-c:a', 'copy',
"-pix_fmt", "yuv420p",
"-vf",
"scale=-2:720",
"-c:v",
currentEncoding,
"-c:a",
"copy",
"-pix_fmt",
"yuv420p",
];
return new Promise<void>((resolve, reject) => {
@ -112,23 +138,27 @@ export const ffmpegDownscale = (path: string, filename: string, extension: strin
.input(path)
.outputOptions(outputOptions)
.output(`uploads/720p-${filename}${extension}`)
.on('end', () => {
console.log(`720p copy complete using ${currentEncoding}, took ${Date.now() - startTime}ms to complete`);
.on("end", () => {
console.log(
`720p copy complete using ${currentEncoding}, took ${
Date.now() - startTime
}ms to complete`,
);
resolve();
})
.on('error', (e) => reject(new Error(e)))
.on("error", (e) => reject(new Error(e)))
.run();
});
}
};
/**
* Convert a video to a gif or vice versa using ffmpeg with various encoding options.
*
*
* @param {string} path - The input video file path.
* @param {string} filename - The name of the file.
* @param {string} extension - The file extension of the file
* @returns {Promise<void>} - A promise that resolves when the conversion is complete, and rejects on error.
*
*
* @example
* ffmpegConvert('input.mp4').then(() => {
* console.log('Conversion complete.');
@ -136,22 +166,31 @@ export const ffmpegDownscale = (path: string, filename: string, extension: strin
* console.log(`Error: ${error}`);
* });
*/
export const ffmpegConvert = (path: string, filename: string, extension: string) => {
export const ffmpegConvert = (
path: string,
filename: string,
extension: string,
) => {
const startTime = Date.now();
const outputOptions = [
'-vf', 'scale=-2:720',
'-c:v', currentEncoding,
'-c:a', 'copy',
"-movflags", "+faststart",
"-pix_fmt", "yuv420p",
]
"-vf",
"scale=-2:720",
"-c:v",
currentEncoding,
"-c:a",
"copy",
"-movflags",
"+faststart",
"-pix_fmt",
"yuv420p",
];
let outputFormat: string;
if (videoExtensions.includes(extension)) {
outputFormat = '.gif';
} else if (extension == '.gif') {
outputFormat = '.mp4';
outputFormat = ".gif";
} else if (extension == ".gif") {
outputFormat = ".mp4";
} else {
return new Promise<void>((resolve, reject) => {
reject(`Submitted file is neither a video nor a gif: ${path}`);
@ -162,24 +201,30 @@ export const ffmpegConvert = (path: string, filename: string, extension: string)
ffmpeg()
.input(path)
.outputOptions(outputOptions)
.output(`uploads/`)
.output("uploads/")
.outputFormat(outputFormat)
.output(`uploads/${filename}${outputFormat}`)
.on("end", function() {
console.log(`Conversion complete, took ${Date.now() - startTime} to complete`);
.on("end", function () {
console.log(
`Conversion complete, took ${Date.now() - startTime} to complete`,
);
console.log(`uploads/${filename}${outputFormat}`);
resolve();
})
.on("error", (e) => reject(e))
.run();
});
}
};
export const ffProbe = (path: string, filename: string, extension: string) => {
export const ffProbe = async (
path: string,
filename: string,
extension: string,
) => {
return new Promise<FfprobeData>((resolve, reject) => {
ffprobe(path, (err, data) => {
if (err) reject (err);
if (err) reject(err);
resolve(data);
});
});
}
};

View file

@ -2,25 +2,45 @@
declare global {
namespace Express {
interface User {
id? : number | string,
username: string,
hashed_password?: any,
salt?: any
id?: number | string;
username: string;
hashed_password?: any;
salt?: any;
}
}
}
/**Splits a file name into its name and then its extension */
export function extension(str: string){
export function extension(str: string) {
const file = str.split("/").pop();
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
return [
file.substr(0, file.lastIndexOf(".")),
file.substr(file.lastIndexOf("."), file.length).toLowerCase(),
];
}
/**Type for user data */
export interface User {
id? : number | string,
username: string,
hashed_password?: any,
salt?: any
id?: number | string;
username: string;
hashed_password?: any;
salt?: any;
}
export const videoExtensions = [".mp4", ".mov", ".avi", ".flv", ".mkv", ".wmv", ".webm"];
export const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".tiff", ".webp"];
export const videoExtensions = [
".mp4",
".mov",
".avi",
".flv",
".mkv",
".wmv",
".webm",
];
export const imageExtensions = [
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".tiff",
".webp",
];

View file

@ -1,11 +1,12 @@
import type {RequestHandler as Middleware, NextFunction} from "express";
import type { RequestHandler as Middleware, NextFunction } from "express";
import fs from "fs";
import process from "process";
import {extension, videoExtensions, imageExtensions} from "./lib";
import {insertToDB} from "./db";
import {ffmpegDownscale} from "./ffmpeg";
import { extension, videoExtensions, imageExtensions } from "./lib";
import { insertToDB } from "./db";
import { ffmpegDownscale, ffProbe } from "./ffmpeg";
import { ffprobe } from "fluent-ffmpeg";
export const checkAuth: Middleware = (req, res, next) => {
if (!req.user) {
@ -16,45 +17,70 @@ export const checkAuth: Middleware = (req, res, next) => {
/**Checks shareX auth key */
export const checkSharexAuth: Middleware = (req, res, next) => {
const auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
const auth =
process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
let key = null;
if (req.headers["key"]) {
key = req.headers["key"];
} else {
return res.status(400).send("{success: false, message: \"No key provided\", fix: \"Provide a key\"}");
return res
.status(400)
.send(
"{success: false, message: 'No key provided', fix: 'Provide a key'}",
);
}
if (auth != key) {
return res.status(401).send("{success: false, message: '\"'Invalid key\", fix: \"Provide a valid key\"}");
return res
.status(401)
.send(
"{success: false, message: 'Invalid key', fix: 'Provide a valid key'}",
);
}
const shortKey = key.substr(0, 3) + "...";
const shortKey = key.substr(0, 3) + "...";
console.log(`Authenicated user with key: ${shortKey}`);
next();
};
/**Creates oembed json file for embed metadata */
export const createEmbedData: Middleware = (req, res, next) => {
export const createEmbedData: Middleware = async (req, res, next) => {
const files = req.files as Express.Multer.File[];
for (const file in files) {
const nameAndExtension = extension(files[file].originalname);
const ffProbeData = await ffProbe(
`uploads/${files[file].originalname}`,
nameAndExtension[0],
nameAndExtension[1],
);
const width = ffProbeData.streams[0].width;
const height = ffProbeData.streams[0].height;
const oembed = {
type: "video",
version: "1.0",
provider_name: "embedder",
provider_url: "https://github.com/WaveringAna/embedder",
cache_age: 86400,
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${nameAndExtension[0]}${nameAndExtension[1]}'></iframe>`,
width: 640,
height: 360
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${
nameAndExtension[0]
}${nameAndExtension[1]}'></iframe>`,
width: width,
height: height,
};
fs.writeFile(`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`, JSON.stringify(oembed), function (err) {
if (err) return next(err);
console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`);
});
fs.writeFile(
`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`,
JSON.stringify(oembed),
function (err) {
if (err) return next(err);
console.log(
`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`,
);
},
);
}
next();
};
@ -67,7 +93,10 @@ export const convertTo720p: Middleware = (req, res, next) => {
const nameAndExtension = extension(files[file].originalname);
//Skip if not a video
if (!videoExtensions.includes(nameAndExtension[1]) && nameAndExtension[1] !== ".gif") {
if (
!videoExtensions.includes(nameAndExtension[1]) &&
nameAndExtension[1] !== ".gif"
) {
console.log(`${files[file].originalname} is not a video file`);
console.log(nameAndExtension[1]);
continue;
@ -75,11 +104,17 @@ export const convertTo720p: Middleware = (req, res, next) => {
console.log(`Creating 720p for ${files[file].originalname}`);
ffmpegDownscale(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`, nameAndExtension[0], nameAndExtension[1]).then(() => {
//Nothing for now, can fire event flag that it is done to front end when react conversion is done
}).catch((error) => {
console.log(`Error: ${error}`);
});
ffmpegDownscale(
`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`,
nameAndExtension[0],
nameAndExtension[1],
)
.then(() => {
//Nothing for now, can fire event flag that it is done to front end when react conversion is done
})
.catch((error) => {
console.log(`Error: ${error}`);
});
}
next();
@ -91,17 +126,18 @@ export const handleUpload: Middleware = (req, res, next) => {
console.log("No files were uploaded");
return res.status(400).send("No files were uploaded.");
}
const files = (req.files) ? req.files as Express.Multer.File[] : req.file; //Check if a single file was uploaded or multiple
const username = (req.user) ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex
const expireDate: Date = (req.body.expire) ? new Date(Date.now() + (req.body.expire * 24 * 60 * 60 * 1000)) : null;
const files = req.files ? (req.files as Express.Multer.File[]) : req.file; //Check if a single file was uploaded or multiple
const username = req.user ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex
const expireDate: Date = req.body.expire
? new Date(Date.now() + req.body.expire * 24 * 60 * 60 * 1000)
: null;
if (files instanceof Array) {
for (const file in files) {
insertToDB(files[file].filename, expireDate, username);
}
} else
insertToDB(files.filename, expireDate, username);
} else insertToDB(files.filename, expireDate, username);
next();
};

View file

@ -1,49 +1,70 @@
import {Request} from "express";
import multer, {FileFilterCallback} from "multer";
import { Request } from "express";
import multer, { FileFilterCallback } from "multer";
import {db, MediaRow} from "./db";
import {extension} from "./lib";
import { db, MediaRow } from "./db";
import { extension } from "./lib";
export type DestinationCallback = (error: Error | null, destination: string) => void
export type FileNameCallback = (error: Error | null, filename: string) => void
export type DestinationCallback = (
error: Error | null,
destination: string,
) => void;
export type FileNameCallback = (error: Error | null, filename: string) => void;
export const fileStorage = multer.diskStorage({
destination: (
request: Request,
file: Express.Multer.File,
callback: DestinationCallback
callback: DestinationCallback,
): void => {
callback(null, __dirname + "/../../uploads");
},
filename: (
request: Request,
file: Express.Multer.File,
callback: FileNameCallback
callback: FileNameCallback,
): void => {
const nameAndExtension = extension(file.originalname);
console.log(`Uploading ${file}`);
db.all("SELECT * FROM media WHERE path = ?", [nameAndExtension[0] + nameAndExtension[1]], (err: Error, exists: []) => {
if (err) {
console.log(err);
callback(err, null);
}
if (exists.length != 0) {
const suffix = new Date().getTime() / 1000;
db.all(
"SELECT * FROM media WHERE path = ?",
[nameAndExtension[0] + nameAndExtension[1]],
(err: Error, exists: []) => {
if (err) {
console.log(err);
callback(err, null);
}
if (exists.length != 0) {
const suffix = new Date().getTime() / 1000;
if (request.body.title == "" || request.body.title == null || request.body.title == undefined) {
callback(null, nameAndExtension[0] + "-" + suffix + nameAndExtension[1]);
if (
request.body.title == "" ||
request.body.title == null ||
request.body.title == undefined
) {
callback(
null,
nameAndExtension[0] + "-" + suffix + nameAndExtension[1],
);
} else {
callback(
null,
request.body.title + "-" + suffix + nameAndExtension[1],
);
}
} else {
callback(null, request.body.title + "-" + suffix + nameAndExtension[1]);
if (
request.body.title == "" ||
request.body.title == null ||
request.body.title == undefined
) {
callback(null, nameAndExtension[0] + nameAndExtension[1]);
} else {
callback(null, request.body.title + nameAndExtension[1]);
}
}
} else {
if (request.body.title == "" || request.body.title == null || request.body.title == undefined) {
callback(null, nameAndExtension[0] + nameAndExtension[1]);
} else {
callback(null, request.body.title + nameAndExtension[1]);
}
}
});
}
},
);
},
});
export const allowedMimeTypes = [
@ -56,17 +77,18 @@ export const allowedMimeTypes = [
"video/mov",
"video/webm",
"audio/mpeg",
"audio/ogg"
"audio/ogg",
];
export const fileFilter = (
request: Request,
file: Express.Multer.File,
callback: FileFilterCallback
callback: FileFilterCallback,
): void => {
if (allowedMimeTypes.includes(file.mimetype)) {
callback(null, true);
} else {
callback(null, false);
}
};
};

View file

@ -1,64 +1,80 @@
import crypto from "crypto";
import express from "express";
import passport from "passport";
import {Strategy as LocalStrategy} from "passport-local";
import { Strategy as LocalStrategy } from "passport-local";
import {User} from "../lib/lib";
import {db, UserRow} from "../lib/db";
import { User } from "../lib/lib";
import { db, UserRow } from "../lib/db";
const router = express.Router();
passport.use(new LocalStrategy(function verify(username, password, cb) {
db.get("SELECT * FROM users WHERE username = ?", [username], function(err: Error, row: UserRow) {
if (err) {
return cb(err);
}
if (!row) {
return cb(null, false, {
message: "Incorrect username or password."
});
}
passport.use(
new LocalStrategy(function verify(username, password, cb) {
db.get(
"SELECT * FROM users WHERE username = ?",
[username],
function (err: Error, row: UserRow) {
if (err) {
return cb(err);
}
if (!row) {
return cb(null, false, {
message: "Incorrect username or password.",
});
}
crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function(err, hashedPassword) {
if (err) {
return cb(err);
}
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return cb(null, false, {
message: "Incorrect username or password."
});
}
return cb(null, row);
});
});
}));
crypto.pbkdf2(
password,
row.salt,
310000,
32,
"sha256",
function (err, hashedPassword) {
if (err) {
return cb(err);
}
if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) {
return cb(null, false, {
message: "Incorrect username or password.",
});
}
return cb(null, row);
},
);
},
);
}),
);
passport.serializeUser(function(user:User, cb) {
process.nextTick(function() {
passport.serializeUser(function (user: User, cb) {
process.nextTick(function () {
cb(null, {
id: user.id,
username: user.username
username: user.username,
});
});
});
passport.deserializeUser(function(user:User, cb) {
process.nextTick(function() {
passport.deserializeUser(function (user: User, cb) {
process.nextTick(function () {
return cb(null, user);
});
});
router.get("/login", function(req, res) {
router.get("/login", function (req, res) {
res.render("login");
});
router.post("/login/password", passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login"
}));
router.post(
"/login/password",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
}),
);
router.post("/logout", function(req, res, next) {
req.logout(function(err) {
router.post("/logout", function (req, res, next) {
req.logout(function (err) {
if (err) {
return next(err);
}

View file

@ -1,33 +1,47 @@
import type {RequestHandler as Middleware, Request, Response, NextFunction} from "express";
import type {
RequestHandler as Middleware,
Request,
Response,
NextFunction,
} from "express";
import multer from "multer";
import express from "express";
import imageProbe from "probe-image-size";
import {ffProbe} from "../lib/ffmpeg";
import { ffProbe } from "../lib/ffmpeg";
import fs from "fs";
import {extension, videoExtensions} from "../lib/lib";
import {db, MediaRow, getPath, deleteId} from "../lib/db";
import {fileStorage} from "../lib/multer";
import {checkAuth, checkSharexAuth, convertTo720p, createEmbedData, handleUpload} from "../lib/middleware";
import { extension, videoExtensions } from "../lib/lib";
import { db, MediaRow, getPath, deleteId } from "../lib/db";
import { fileStorage } from "../lib/multer";
import {
checkAuth,
checkSharexAuth,
convertTo720p,
createEmbedData,
handleUpload,
} from "../lib/middleware";
const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable?
/**Middleware to grab media from media database */
const fetchMedia: Middleware = (req, res, next) => {
const admin: boolean = req.user.username == "admin" ? true : false;
/**Check if the user is an admin, if so, show all posts from all users */
const query: string = admin == true ? "SELECT * FROM media" : `SELECT * FROM media WHERE username = '${req.user.username}'`;
const query: string =
admin == true
? "SELECT * FROM media"
: `SELECT * FROM media WHERE username = '${req.user.username}'`;
db.all(query, (err:Error, rows: []) => {
db.all(query, (err: Error, rows: []) => {
if (err) return next(err);
const files = rows.map((row: MediaRow)=> {
const files = rows.map((row: MediaRow) => {
return {
id: row.id,
path: row.path,
expire: row.expire,
username: row.username,
url: "/" + row.id
url: "/" + row.id,
};
});
res.locals.files = files.reverse(); //reverse so newest files appear first
@ -38,63 +52,118 @@ const fetchMedia: Middleware = (req, res, next) => {
const router = express.Router();
router.get("/", (req: Request, res: Response, next: NextFunction) => {
if (!req.user)
return res.render("home");
next();
}, fetchMedia, (req: Request, res: Response) => {
res.locals.filter = null;
res.render("index", { user: req.user });
});
router.get(
"/",
(req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.render("home");
next();
},
fetchMedia,
(req: Request, res: Response) => {
res.locals.filter = null;
res.render("index", { user: req.user });
},
);
router.get("/gifv/:file", async (req: Request, res: Response, next: NextFunction) => {
const url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`;
let width; let height;
router.get(
"/gifv/:file",
async (req: Request, res: Response, next: NextFunction) => {
const url = `${req.protocol}://${req.get("host")}/uploads/${
req.params.file
}`;
let width;
let height;
const nameAndExtension = extension(`uploads/${req.params.file}`);
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".mov" || nameAndExtension[1] == ".webm" || nameAndExtension[1] == ".gif") {
let imageData = ffProbe(`uploads/${req.params.file}`, nameAndExtension[0], nameAndExtension[1]);
const nameAndExtension = extension(`uploads/${req.params.file}`);
if (
nameAndExtension[1] == ".mp4" ||
nameAndExtension[1] == ".mov" ||
nameAndExtension[1] == ".webm" ||
nameAndExtension[1] == ".gif"
) {
const imageData = ffProbe(
`uploads/${req.params.file}`,
nameAndExtension[0],
nameAndExtension[1],
);
width = (await imageData).streams[0].width;
height = (await imageData).streams[0].height;
width = (await imageData).streams[0].width;
height = (await imageData).streams[0].height;
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: width, height: height });
} else {
const imageData = await imageProbe(fs.createReadStream(`uploads/${req.params.file}`));
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: imageData.width, height: imageData.height });
}
});
return res.render("gifv", {
url: url,
host: `${req.protocol}://${req.get("host")}`,
width: width,
height: height,
});
} else {
const imageData = await imageProbe(
fs.createReadStream(`uploads/${req.params.file}`),
);
return res.render("gifv", {
url: url,
host: `${req.protocol}://${req.get("host")}`,
width: imageData.width,
height: imageData.height,
});
}
},
);
router.post("/", [checkAuth, upload.array("fileupload"), convertTo720p, createEmbedData, handleUpload], (req: Request, res: Response) => {
res.redirect("/");
});
router.post(
"/",
[
checkAuth,
upload.array("fileupload"),
convertTo720p,
createEmbedData,
handleUpload,
],
(req: Request, res: Response) => {
res.redirect("/");
},
);
router.post("/sharex", [checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => {
return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`);
});
router.post(
"/sharex",
[checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload],
(req: Request, res: Response) => {
return res.send(
`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`,
);
},
);
router.post("/:id(\\d+)/delete", [checkAuth], async (req: Request, res: Response) => {
const path: any = await getPath(req.params.id);
router.post(
"/:id(\\d+)/delete",
[checkAuth],
async (req: Request, res: Response) => {
const path: any = await getPath(req.params.id);
const nameAndExtension = extension(path.path);
const nameAndExtension = extension(path.path);
const filesToDelete = [path.path, "oembed-" + path.path + ".json"];
const filesToDelete = [path.path, "oembed-" + path.path + ".json"];
if (videoExtensions.includes(nameAndExtension[1]) || nameAndExtension[1] == ".gif") {
filesToDelete.push("720p-" + path.path);
}
if (
videoExtensions.includes(nameAndExtension[1]) ||
nameAndExtension[1] == ".gif"
) {
filesToDelete.push("720p-" + path.path);
}
filesToDelete.forEach(path => {
fs.unlink(path, async (err) => {
console.log(`Deleting ${path}`);
if (err && err.errno == -4058) {
filesToDelete.forEach((path) => {
fs.unlink(path, async (err) => {
console.log(`Deleting ${path}`);
if (err && err.errno == -4058) {
await deleteId("media", req.params.id);
}
await deleteId("media", req.params.id);
}
await deleteId("media", req.params.id);
});
});
});
return res.redirect("/");
});
return res.redirect("/");
},
);
export default router;
export default router;

View file

@ -2,6 +2,7 @@ FROM node:16-alpine AS BUILD_IMAGE
RUN apk add curl
WORKDIR /
COPY package*.json ./
COPY tsconfig.json ./
COPY /app ./app
@ -11,6 +12,7 @@ RUN npm prune --production
FROM node:16-alpine
WORKDIR /
COPY --from=BUILD_IMAGE /node_modules ./node_modules
COPY --from=BUILD_IMAGE /dist ./dist
COPY package*.json ./

View file

@ -1,9 +1,13 @@
import ffmpeg from 'fluent-ffmpeg';
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
import ffprobeInstaller from '@ffprobe-installer/ffprobe';
import which from 'which';
import ffmpeg from "fluent-ffmpeg";
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
import ffprobeInstaller from "@ffprobe-installer/ffprobe";
import which from "which";
const getExecutablePath = (envVar: string, executable: string, installer: { path: string }) => {
const getExecutablePath = (
envVar: string,
executable: string,
installer: { path: string },
) => {
if (process.env[envVar]) {
return process.env[envVar];
}
@ -15,8 +19,16 @@ const getExecutablePath = (envVar: string, executable: string, installer: { path
}
};
const ffmpegPath = getExecutablePath('EB_FFMPEG_PATH', 'ffmpeg', ffmpegInstaller);
const ffprobePath = getExecutablePath('EB_FFPROBE_PATH', 'ffprobe', ffprobeInstaller);
const ffmpegPath = getExecutablePath(
"EB_FFMPEG_PATH",
"ffmpeg",
ffmpegInstaller,
);
const ffprobePath = getExecutablePath(
"EB_FFPROBE_PATH",
"ffprobe",
ffprobeInstaller,
);
console.log(`Using ffmpeg from path: ${ffmpegPath}`);
console.log(`Using ffprobe from path: ${ffprobePath}`);
@ -25,65 +37,76 @@ ffmpeg.setFfmpegPath(ffmpegPath!);
ffmpeg.setFfprobePath(ffprobePath!);
export enum EncodingType {
CPU = 'libx264',
NVIDIA = 'h264_nvenc',
AMD = 'h264_amf',
INTEL = 'h264_qsv',
APPLE = 'h264_videotoolbox',
CPU = "libx264",
NVIDIA = "h264_nvenc",
AMD = "h264_amf",
INTEL = "h264_qsv",
APPLE = "h264_videotoolbox",
}
export const generateTestVideo = async (encodingType: EncodingType): Promise<void> => {
export const generateTestVideo = async (
encodingType: EncodingType,
): Promise<void> => {
console.log(`Generating test video using ${encodingType}...`);
const startTime = Date.now();
let totalFrames = 0;
const outputOptions = [
'-vf', 'scale=-2:720',
'-vcodec', encodingType,
'-c:a', 'copy',
'-b:v', '5000k',
'-pix_fmt', 'yuv420p',
"-vf",
"scale=-2:720",
"-vcodec",
encodingType,
"-c:a",
"copy",
"-b:v",
"5000k",
"-pix_fmt",
"yuv420p",
];
// Adjust output options based on encoder for maximum quality
switch(encodingType) {
switch (encodingType) {
case EncodingType.CPU:
//outputOptions.push('-crf', '0');
break;
case EncodingType.NVIDIA:
outputOptions.push('-rc', 'cqp', '-qp', '0');
outputOptions.push("-rc", "cqp", "-qp", "0");
break;
case EncodingType.AMD:
outputOptions.push('-qp_i', '0', '-qp_p', '0', '-qp_b', '0');
outputOptions.push("-qp_i", "0", "-qp_p", "0", "-qp_b", "0");
break;
case EncodingType.INTEL:
outputOptions.push('-global_quality', '1'); // Intel QSV specific setting for high quality
outputOptions.push("-global_quality", "1"); // Intel QSV specific setting for high quality
break;
case EncodingType.APPLE:
outputOptions.push('-global_quality', '1');
outputOptions.push("-global_quality", "1");
break;
}
return new Promise<void>((resolve, reject) => {
ffmpeg()
.input('unknown_replay_2023.10.29-22.57-00.00.38.103-00.01.00.016.mp4')
.inputFormat('mp4')
.input("unknown_replay_2023.10.29-22.57-00.00.38.103-00.01.00.016.mp4")
.inputFormat("mp4")
.outputOptions(outputOptions)
.output(`720p-test-${encodingType}.mp4`)
.on('progress', (progress) => {
.on("progress", (progress) => {
totalFrames = progress.frames;
})
.on('end', () => {
const elapsedTime = (Date.now() - startTime) / 1000; // Convert to seconds
.on("end", () => {
const elapsedTime = (Date.now() - startTime) / 1000; // Convert to seconds
const avgFps = totalFrames / elapsedTime;
console.log(`720p copy complete using ${encodingType}, took ${Date.now() - startTime}ms to complete`);
console.log(
`720p copy complete using ${encodingType}, took ${
Date.now() - startTime
}ms to complete`,
);
console.log(`Average FPS for the entire process: ${avgFps.toFixed(2)}`);
resolve();
})
.on('error', (e) => reject(new Error(e)))
.on("error", (e) => reject(new Error(e)))
.run();
});
};

View file

@ -1,13 +1,13 @@
import readline from 'readline';
import readline from "readline";
import { generateTestVideo, EncodingType } from "./ffmpeg";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
input: process.stdin,
output: process.stdout,
});
const questionAsync = (query: string) => {
return new Promise<string>(resolve => {
return new Promise<string>((resolve) => {
rl.question(query, resolve);
});
};
@ -16,27 +16,31 @@ const main = async () => {
console.log("Testing software encoder: ");
await generateTestVideo(EncodingType.CPU).catch(console.error);
const answer = await questionAsync('Would you like to test other hardware encoders? (yes/no): ');
const answer = await questionAsync(
"Would you like to test other hardware encoders? (yes/no): ",
);
if (answer.toLowerCase() === 'yes') {
const encoder = await questionAsync('Which hardware encoder would you like to test? (INTEL/NVIDIA/AMD/APPLE): ');
if (answer.toLowerCase() === "yes") {
const encoder = await questionAsync(
"Which hardware encoder would you like to test? (INTEL/NVIDIA/AMD/APPLE): ",
);
let selectedEncoder: EncodingType;
switch (encoder.toUpperCase()) {
case 'INTEL':
case "INTEL":
selectedEncoder = EncodingType.INTEL;
break;
case 'NVIDIA':
case "NVIDIA":
selectedEncoder = EncodingType.NVIDIA;
break;
case 'AMD':
case "AMD":
selectedEncoder = EncodingType.AMD;
break;
case 'APPLE':
case "APPLE":
selectedEncoder = EncodingType.APPLE;
break;
default:
console.log('Invalid choice. Exiting.');
console.log("Invalid choice. Exiting.");
rl.close();
return;
}
@ -46,11 +50,11 @@ const main = async () => {
} else {
console.log("Exiting.");
}
rl.close();
};
main().catch(err => {
main().catch((err) => {
console.error("An error occurred:", err);
rl.close();
});