feat: compression 17
This commit is contained in:
@@ -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 originalStat = await stat(inputPath);
|
||||||
const command = await getFfmpegCommand(currentUser.id);
|
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 outputStat = await stat(outputPath);
|
||||||
const outputFileName = outputPath.split("/").pop() || `${asset.id}-compressed.mp4`;
|
const outputFileName = outputPath.split("/").pop() || `${asset.id}-compressed.mp4`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user