mirror of
https://github.com/theoleuthardt/werkzeugkiste.git
synced 2026-06-13 09:37:53 +00:00
feat: backend now uses ffmpeg as local package on server to convert video to audio, added package to dockerfile, naming conventions
This commit is contained in:
parent
95c8ba211c
commit
f16661d1fd
7 changed files with 143 additions and 84 deletions
|
|
@ -1,70 +0,0 @@
|
|||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import * as libre from "libreoffice-convert";
|
||||
import { promisify } from "util";
|
||||
import { MultipartValue } from "@fastify/multipart";
|
||||
|
||||
const libreConvertAsync = promisify(libre.convert);
|
||||
|
||||
const mimeTypes: { [key: string]: string } = {
|
||||
mp4: "video/mp4",
|
||||
avi: "video/x-msvideo",
|
||||
mkv: "video/x-matroska",
|
||||
};
|
||||
|
||||
export async function videoConvert(app: FastifyInstance) {
|
||||
app.post(
|
||||
"/api/video-convert",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
const parts = request.parts();
|
||||
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let outputFileExt = "";
|
||||
|
||||
for await (const part of parts) {
|
||||
if (part.type === "file") {
|
||||
fileBuffer = await part.toBuffer();
|
||||
} else if (
|
||||
part.fieldname === "outputFormat" &&
|
||||
part.type === "field"
|
||||
) {
|
||||
outputFileExt = (part as MultipartValue<string>).value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileBuffer) {
|
||||
return reply.status(400).send({ error: "No file uploaded!" });
|
||||
}
|
||||
if (!outputFileExt) {
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "No output format provided!" });
|
||||
}
|
||||
if (!outputFileExt.startsWith(".")) {
|
||||
outputFileExt = "." + outputFileExt;
|
||||
}
|
||||
|
||||
const format = outputFileExt.substring(1);
|
||||
const mimeType = mimeTypes[format] || "application/octet-stream";
|
||||
|
||||
const convertedBuffer = await libreConvertAsync(
|
||||
fileBuffer,
|
||||
outputFileExt,
|
||||
undefined,
|
||||
);
|
||||
|
||||
reply
|
||||
.header("Content-Type", mimeType)
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="converted${outputFileExt}"`,
|
||||
)
|
||||
.status(200)
|
||||
.send(convertedBuffer);
|
||||
} catch (error) {
|
||||
console.error("Convert error:", error);
|
||||
reply.status(500).send({ error: "Error while converting!" });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
137
backend/src/routes/videotoaudio.route.ts
Normal file
137
backend/src/routes/videotoaudio.route.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { MultipartValue } from "@fastify/multipart";
|
||||
import { spawn } from "child_process";
|
||||
import { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
interface ConversionOptions {
|
||||
format: string;
|
||||
bitrate?: string;
|
||||
channels?: number;
|
||||
sampleRate?: number;
|
||||
}
|
||||
|
||||
let options: ConversionOptions;
|
||||
|
||||
export async function videoToAudio(app: FastifyInstance) {
|
||||
app.post(
|
||||
"/api/video-to-audio",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const tmpDir = path.join(process.cwd(), "tmp");
|
||||
const sessionId = randomUUID();
|
||||
const inputPath = path.join(tmpDir, `input-${sessionId}`);
|
||||
const outputPath = path.join(tmpDir, `output-${sessionId}`);
|
||||
|
||||
try {
|
||||
await fs.mkdir(tmpDir, { recursive: true });
|
||||
|
||||
const parts = request.parts();
|
||||
|
||||
let fileBuffer: Buffer | null = null;
|
||||
options = {
|
||||
format: "mp3",
|
||||
bitrate: "192k",
|
||||
channels: 2,
|
||||
sampleRate: 44100,
|
||||
};
|
||||
|
||||
for await (const part of parts) {
|
||||
if (part.type === "file") {
|
||||
fileBuffer = await part.toBuffer();
|
||||
} else if (part.type === "field") {
|
||||
const field = part as MultipartValue<string>;
|
||||
switch (field.fieldname) {
|
||||
case "format":
|
||||
const format = field.value.toLowerCase();
|
||||
options.format = format;
|
||||
break;
|
||||
case "bitrate":
|
||||
options.bitrate = field.value;
|
||||
break;
|
||||
case "channels":
|
||||
options.channels = parseInt(field.value, 10);
|
||||
break;
|
||||
case "sampleRate":
|
||||
options.sampleRate = parseInt(field.value, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileBuffer) {
|
||||
return reply.status(400).send({ error: "No file uploaded!" });
|
||||
}
|
||||
|
||||
await fs.writeFile(inputPath, fileBuffer);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const args = [
|
||||
"-i",
|
||||
inputPath,
|
||||
"-vn",
|
||||
"-acodec",
|
||||
options.format === "mp3" ? "libmp3lame" : options.format,
|
||||
"-ab",
|
||||
options.bitrate || "192k",
|
||||
"-ac",
|
||||
String(options.channels || 2),
|
||||
"-ar",
|
||||
String(options.sampleRate || 44100),
|
||||
outputPath + "." + options.format,
|
||||
];
|
||||
|
||||
const ffmpeg = spawn("ffmpeg", args);
|
||||
|
||||
ffmpeg.stderr.on("data", (data) => {
|
||||
console.log(`ffmpeg stderr: ${data}`);
|
||||
});
|
||||
|
||||
ffmpeg.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
ffmpeg.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const outputFile = await fs.readFile(outputPath + "." + options.format);
|
||||
|
||||
await Promise.all([
|
||||
fs.unlink(inputPath),
|
||||
fs.unlink(outputPath + "." + options.format),
|
||||
]);
|
||||
|
||||
reply
|
||||
.header("Content-Type", `audio/${options.format}`)
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="converted.${options.format}"`,
|
||||
)
|
||||
.send(outputFile);
|
||||
} catch (error) {
|
||||
try {
|
||||
await Promise.all([
|
||||
fs.unlink(inputPath).catch(() => {}),
|
||||
fs
|
||||
.unlink(outputPath + "." + (options?.format || "mp3"))
|
||||
.catch(() => {}),
|
||||
]);
|
||||
} catch (cleanupError) {
|
||||
console.error("Cleanup error:", cleanupError);
|
||||
}
|
||||
|
||||
console.error("Conversion error:", error);
|
||||
reply.status(500).send({
|
||||
error: "Error during conversion process",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue