feat: compression 17

This commit is contained in:
2026-09-28 10:48:47 +01:00
parent 05a5101831
commit eda0ae6b1d
2 changed files with 132 additions and 1 deletions
+129
View File
@@ -0,0 +1,129 @@
import { createWriteStream } from "node:fs";
import { spawn } from "node:child_process";
import path from "node:path";
const STDERR_TAIL_BYTES = 64 * 1024;
const tokenizeCommand = (input: string): string[] => {
const args: string[] = [];
let current = "";
let quote: "'" | '"' | null = null;
let escaping = false;
let tokenStarted = false;
for (const char of input) {
if (escaping) {
current += char;
escaping = false;
tokenStarted = true;
continue;
}
if (char === "\\" && quote !== "'") {
escaping = true;
tokenStarted = true;
continue;
}
if (quote) {
if (char === quote) {
quote = null;
} else {
current += char;
}
tokenStarted = true;
continue;
}
if (char === "'" || char === '"') {
quote = char;
tokenStarted = true;
continue;
}
if (/\s/.test(char)) {
if (tokenStarted) {
args.push(current);
current = "";
tokenStarted = false;
}
continue;
}
current += char;
tokenStarted = true;
}
if (escaping) throw new Error("FFmpeg command ends with an incomplete escape sequence");
if (quote) throw new Error("FFmpeg command contains an unterminated quote");
if (tokenStarted) args.push(current);
return args;
};
const getLogPath = (outputPath: string) => {
const parsed = path.parse(outputPath);
return path.join(parsed.dir, `${parsed.name}.log`);
};
const formatArgument = (value: string) =>
/\s/.test(value) ? JSON.stringify(value) : value;
export const runLoggedFfmpegCommand = async (
command: string,
inputPath: string,
outputPath: string
) => {
const tokens = tokenizeCommand(command);
if (tokens.length === 0) throw new Error("FFmpeg command is empty");
const resolved = tokens.map((token) => token
.split("{input}").join(inputPath)
.split("{output}").join(outputPath));
// Support both "ffmpeg -i ..." and argument-only settings such as "-i ...".
const commandIncludesExecutable = !resolved[0]?.startsWith("-");
const executable = commandIncludesExecutable ? resolved[0] : "ffmpeg";
const args = commandIncludesExecutable ? resolved.slice(1) : resolved;
const logPath = getLogPath(outputPath);
const log = createWriteStream(logPath, { flags: "w" });
log.write(`Command: ${[executable, ...args].map(formatArgument).join(" ")}\n\n`);
await new Promise<void>((resolve, reject) => {
const child = spawn(executable, args, { shell: false });
let stderrTail = "";
let settled = false;
child.stdout.on("data", (chunk: Buffer) => {
log.write(chunk);
});
child.stderr.on("data", (chunk: Buffer) => {
log.write(chunk);
stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_BYTES);
});
const finish = (error?: Error) => {
if (settled) return;
settled = true;
log.end(() => error ? reject(error) : resolve());
};
child.on("error", (error) => {
log.write(`\n\nFailed to start FFmpeg: ${error.message}\n`);
finish(error);
});
child.on("close", (code, signal) => {
if (settled) return;
log.write(
`\n\nFFmpeg exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}\n`
);
if (code === 0) {
finish();
return;
}
@@ -45,7 +45,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const originalStat = await stat(inputPath);
const command = await getFfmpegCommand(currentUser.id);
await runFfmpegCommand(command, inputPath, outputPath);
const runLoggedFfmpegCommand: typeof runFfmpegCommand =
(await import("@/helpers/ffmpeg-log.server")).runLoggedFfmpegCommand;
await runLoggedFfmpegCommand(command, inputPath, outputPath);
const outputStat = await stat(outputPath);
const outputFileName = outputPath.split("/").pop() || `${asset.id}-compressed.mp4`;