From 324156fd036b397c585c5e39616eb6e400e0cc90 Mon Sep 17 00:00:00 2001 From: Domenik Date: Sat, 22 Feb 2025 14:25:05 +0100 Subject: [PATCH 1/5] feat: video-to-audio converter implemented without logic --- backend/package-lock.json | 9 +- backend/package.json | 3 +- backend/server.ts | 2 + backend/src/routes/videoconvert.route.ts | 70 ++++++++ frontend/src/app/video-to-audio/layout.tsx | 20 +++ frontend/src/app/video-to-audio/page.tsx | 186 +++++++++++++++++++++ frontend/src/constants/index.ts | 30 ++++ 7 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 backend/src/routes/videoconvert.route.ts create mode 100644 frontend/src/app/video-to-audio/layout.tsx create mode 100644 frontend/src/app/video-to-audio/page.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index b6705e9..7b41736 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,7 +17,8 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "video-to-audio": "^1.0.1" }, "devDependencies": { "@types/luxon": "^3.4.2", @@ -1317,6 +1318,12 @@ "node": ">= 0.8" } }, + "node_modules/video-to-audio": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/video-to-audio/-/video-to-audio-1.0.1.tgz", + "integrity": "sha512-6+h79x2kSO60PYTT2cXcgXIsqw/3X8a5qshsZanxH0cxbGUVxVllwQdNOWr/8ywDNcgGgecSmk8Q6TJ79gtFjQ==", + "license": "MIT" + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index ab5c3e8..fb60513 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,7 +20,8 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "video-to-audio": "^1.0.1" }, "devDependencies": { "@types/luxon": "^3.4.2", diff --git a/backend/server.ts b/backend/server.ts index 7593215..bffc6aa 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -8,6 +8,7 @@ import { regexTest } from "./src/routes/regextest.route"; import { tmzConvert } from "./src/routes/tmzconvert.route"; import { generateQRCode } from "./src/routes/generateqrcode.route"; import { wordCounter } from "./src/routes/wordcounter.route"; +import { videoConvert } from "./src/routes/videoconvert.route"; const app = Fastify({ logger: true }); @@ -25,6 +26,7 @@ app.register(regexTest); app.register(tmzConvert); app.register(generateQRCode); app.register(wordCounter); +app.register(videoConvert); const PORT = process.env.PORT || 4000; app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => { diff --git a/backend/src/routes/videoconvert.route.ts b/backend/src/routes/videoconvert.route.ts new file mode 100644 index 0000000..5daca9b --- /dev/null +++ b/backend/src/routes/videoconvert.route.ts @@ -0,0 +1,70 @@ +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).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!" }); + } + }, + ); +} diff --git a/frontend/src/app/video-to-audio/layout.tsx b/frontend/src/app/video-to-audio/layout.tsx new file mode 100644 index 0000000..76ac270 --- /dev/null +++ b/frontend/src/app/video-to-audio/layout.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import type { Metadata } from "next"; +import { toolLinks } from "@/constants"; + +export const metadata: Metadata = { + title: toolLinks[8].title, + description: "MP4 to MP3 converter!", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/video-to-audio/page.tsx b/frontend/src/app/video-to-audio/page.tsx new file mode 100644 index 0000000..e8de4f1 --- /dev/null +++ b/frontend/src/app/video-to-audio/page.tsx @@ -0,0 +1,186 @@ +"use client"; +import React, { useState } from "react"; +import Navbar from "../../components/Navbar"; +import Footer from "../../components/Footer"; +import Button from "../../components/Button"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import Dropdown from "@/components/Dropdown"; +import { + FileFormatsTable, + outputFileFormats, + videoAudioFormats, + videoAudioFormatsTable, +} from "@/constants"; + +export default function DocConverter() { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const [tableOpen, setTableOpen] = useState(false); + const [filteredOptions, setFilteredOptions] = useState([]); + const [selectedOutputFormat, setSelectedOutputFormat] = useState( + "", + ); + + const handleFileChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + if (event.target.files && event.target.files.length > 0) { + const selectedFile = event.target.files[0]; + const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase(); + + const isSupported = videoAudioFormats.some((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + + if (!isSupported) { + alert("File format not supported!"); + event.target.value = ""; + return; + } + + setFile(selectedFile); + setSelectedOutputFormat(""); + + const matchedFormat = videoAudioFormats.find((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + setFilteredOptions(matchedFormat ? matchedFormat.output : []); + } + }; + + const convertDoc = async () => { + if (!file) { + alert("No file selected"); + return; + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("outputFormat", selectedOutputFormat); + + setLoading(true); + + try { + const response = await fetch( + process.env.backend_url + "/api/video-convert", + { + method: "POST", + body: formData, + }, + ); + + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const filename = file.name.split(".")[0]; + + const a = document.createElement("a"); + a.href = url; + a.download = `${filename}${selectedOutputFormat}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + setTimeout(() => { + URL.revokeObjectURL(url); + }, 5000); + } catch (error) { + console.error("Error while converting:", error); + alert("Error while converting!"); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

doc-converter

+
+ + 0 ? filteredOptions : [""]} + onClick={(event) => { + const selectedFormat = + event.currentTarget.textContent?.trim() || ""; + setSelectedOutputFormat(selectedFormat); + }} + /> +
+
+
+
+
setTableOpen(!tableOpen)} + > + + + + + + + +
+ 📥 Input Format + +

📤 Output Format

+
+ {tableOpen ? ( + + ) : ( + + )} +
+
+
+ +
+ + + {videoAudioFormatsTable.map((format) => ( + + + + + ))} + +
+ {format.input} + + {format.output.join(", ")} +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 13db40f..792dbce 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -154,3 +154,33 @@ export const FileFormatsTable = [ output: [".pdf", ".ppt", ".pptx"], }, ]; + +export const videoAudioFormats = [ + { + input: ".mp4", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".avi", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".mov", + output: [".mp3", ".wav", ".aac"], + }, +]; + +export const videoAudioFormatsTable = [ + { + input: ".mp4", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".avi", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".mov", + output: [".mp3", ".wav", ".aac"], + }, +]; From 903709c3344da6113f8b796bf4317148127b72e8 Mon Sep 17 00:00:00 2001 From: Domenik Date: Sat, 22 Feb 2025 14:25:47 +0100 Subject: [PATCH 2/5] fix: removed wrong imports --- frontend/src/app/video-to-audio/page.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/app/video-to-audio/page.tsx b/frontend/src/app/video-to-audio/page.tsx index e8de4f1..b92fd8b 100644 --- a/frontend/src/app/video-to-audio/page.tsx +++ b/frontend/src/app/video-to-audio/page.tsx @@ -5,12 +5,7 @@ import Footer from "../../components/Footer"; import Button from "../../components/Button"; import { ChevronDown, ChevronUp } from "lucide-react"; import Dropdown from "@/components/Dropdown"; -import { - FileFormatsTable, - outputFileFormats, - videoAudioFormats, - videoAudioFormatsTable, -} from "@/constants"; +import { videoAudioFormats, videoAudioFormatsTable } from "@/constants"; export default function DocConverter() { const [file, setFile] = useState(null); From 95c8ba211cb3c0c7d9f35f75fd7fb1e42adf276d Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 22 Feb 2025 15:23:47 +0100 Subject: [PATCH 3/5] feat: renaming some constants, correct table constant in frontend --- frontend/src/app/video-to-audio/layout.tsx | 2 +- frontend/src/app/video-to-audio/page.tsx | 18 ++++++++---------- frontend/src/constants/index.ts | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/video-to-audio/layout.tsx b/frontend/src/app/video-to-audio/layout.tsx index 76ac270..b6e283c 100644 --- a/frontend/src/app/video-to-audio/layout.tsx +++ b/frontend/src/app/video-to-audio/layout.tsx @@ -4,7 +4,7 @@ import { toolLinks } from "@/constants"; export const metadata: Metadata = { title: toolLinks[8].title, - description: "MP4 to MP3 converter!", + description: "Video to audio converter!", }; export default function RootLayout({ diff --git a/frontend/src/app/video-to-audio/page.tsx b/frontend/src/app/video-to-audio/page.tsx index b92fd8b..cfedd34 100644 --- a/frontend/src/app/video-to-audio/page.tsx +++ b/frontend/src/app/video-to-audio/page.tsx @@ -43,7 +43,7 @@ export default function DocConverter() { } }; - const convertDoc = async () => { + const convertVideo = async () => { if (!file) { alert("No file selected"); return; @@ -57,7 +57,7 @@ export default function DocConverter() { try { const response = await fetch( - process.env.backend_url + "/api/video-convert", + process.env.backend_url + "/api/video-to-audio", { method: "POST", body: formData, @@ -66,6 +66,7 @@ export default function DocConverter() { if (!response.ok) { console.error(`Error: ${response.statusText}`); + return; } const blob = await response.blob(); @@ -94,7 +95,7 @@ export default function DocConverter() {
-

doc-converter

+

video-to-audio

@@ -125,7 +127,7 @@ export default function DocConverter() { "convert" ) } - onClick={convertDoc} + onClick={convertVideo} />
@@ -162,12 +164,8 @@ export default function DocConverter() { {videoAudioFormatsTable.map((format) => ( - - {format.input} - - - {format.output.join(", ")} - + {format.input} + {format.output.join(", ")} ))} diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 792dbce..4db1620 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -172,15 +172,15 @@ export const videoAudioFormats = [ export const videoAudioFormatsTable = [ { - input: ".mp4", + input: ".mp4 (MPEG-4)", output: [".mp3", ".wav", ".aac"], }, { - input: ".avi", + input: ".avi (Audio Video Interleave)", output: [".mp3", ".wav", ".aac"], }, { - input: ".mov", + input: ".mov (Apple QuickTime Movie)", output: [".mp3", ".wav", ".aac"], }, ]; From f16661d1fd9a54fd8d275cbdf19b150838335e59 Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 22 Feb 2025 15:25:08 +0100 Subject: [PATCH 4/5] feat: backend now uses ffmpeg as local package on server to convert video to audio, added package to dockerfile, naming conventions --- backend/Dockerfile | 2 +- backend/package-lock.json | 9 +- backend/package.json | 3 +- backend/server.ts | 4 +- backend/src/routes/videoconvert.route.ts | 70 ------------ backend/src/routes/videotoaudio.route.ts | 137 +++++++++++++++++++++++ backend/tsconfig.json | 2 +- 7 files changed, 143 insertions(+), 84 deletions(-) delete mode 100644 backend/src/routes/videoconvert.route.ts create mode 100644 backend/src/routes/videotoaudio.route.ts diff --git a/backend/Dockerfile b/backend/Dockerfile index 6583c94..49c0c65 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM node:18-alpine AS base # Install system dependencies -RUN apk add --no-cache libc6-compat libreoffice ttf-liberation +RUN apk add --no-cache libc6-compat libreoffice ttf-liberation ffmpeg WORKDIR /app diff --git a/backend/package-lock.json b/backend/package-lock.json index 7b41736..b6705e9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,8 +17,7 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4", - "video-to-audio": "^1.0.1" + "qrcode": "^1.5.4" }, "devDependencies": { "@types/luxon": "^3.4.2", @@ -1318,12 +1317,6 @@ "node": ">= 0.8" } }, - "node_modules/video-to-audio": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/video-to-audio/-/video-to-audio-1.0.1.tgz", - "integrity": "sha512-6+h79x2kSO60PYTT2cXcgXIsqw/3X8a5qshsZanxH0cxbGUVxVllwQdNOWr/8ywDNcgGgecSmk8Q6TJ79gtFjQ==", - "license": "MIT" - }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index fb60513..ab5c3e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,8 +20,7 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4", - "video-to-audio": "^1.0.1" + "qrcode": "^1.5.4" }, "devDependencies": { "@types/luxon": "^3.4.2", diff --git a/backend/server.ts b/backend/server.ts index bffc6aa..6051ea6 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -8,7 +8,7 @@ import { regexTest } from "./src/routes/regextest.route"; import { tmzConvert } from "./src/routes/tmzconvert.route"; import { generateQRCode } from "./src/routes/generateqrcode.route"; import { wordCounter } from "./src/routes/wordcounter.route"; -import { videoConvert } from "./src/routes/videoconvert.route"; +import { videoToAudio } from "./src/routes/videotoaudio.route"; const app = Fastify({ logger: true }); @@ -26,7 +26,7 @@ app.register(regexTest); app.register(tmzConvert); app.register(generateQRCode); app.register(wordCounter); -app.register(videoConvert); +app.register(videoToAudio); const PORT = process.env.PORT || 4000; app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => { diff --git a/backend/src/routes/videoconvert.route.ts b/backend/src/routes/videoconvert.route.ts deleted file mode 100644 index 5daca9b..0000000 --- a/backend/src/routes/videoconvert.route.ts +++ /dev/null @@ -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).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!" }); - } - }, - ); -} diff --git a/backend/src/routes/videotoaudio.route.ts b/backend/src/routes/videotoaudio.route.ts new file mode 100644 index 0000000..31ffe0a --- /dev/null +++ b/backend/src/routes/videotoaudio.route.ts @@ -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; + 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((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", + }); + } + }, + ); +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index b0de5ac..c6f6778 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -6,6 +6,6 @@ "rootDir": ".", "strict": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, } } From 21a68e87e74445807cecf5da87f74cd8a1253f20 Mon Sep 17 00:00:00 2001 From: theoleuthardt Date: Sat, 22 Feb 2025 23:27:33 +0100 Subject: [PATCH 5/5] fix: ffmpeg now uses right encoder for every supported input format --- backend/src/routes/videotoaudio.route.ts | 21 ++++++++++++++++----- frontend/src/app/video-to-audio/page.tsx | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/videotoaudio.route.ts b/backend/src/routes/videotoaudio.route.ts index 31ffe0a..91a5648 100644 --- a/backend/src/routes/videotoaudio.route.ts +++ b/backend/src/routes/videotoaudio.route.ts @@ -11,9 +11,14 @@ interface ConversionOptions { channels?: number; sampleRate?: number; } - let options: ConversionOptions; +const ffmpegEncoder: { [key: string]: string } = { + ".mp3": "libmp3lame", + ".wav": "pcm_s16le", + ".aac": "aac", +}; + export async function videoToAudio(app: FastifyInstance) { app.post( "/api/video-to-audio", @@ -30,7 +35,7 @@ export async function videoToAudio(app: FastifyInstance) { let fileBuffer: Buffer | null = null; options = { - format: "mp3", + format: "", bitrate: "192k", channels: 2, sampleRate: 44100, @@ -42,7 +47,7 @@ export async function videoToAudio(app: FastifyInstance) { } else if (part.type === "field") { const field = part as MultipartValue; switch (field.fieldname) { - case "format": + case "outputFormat": const format = field.value.toLowerCase(); options.format = format; break; @@ -63,6 +68,8 @@ export async function videoToAudio(app: FastifyInstance) { return reply.status(400).send({ error: "No file uploaded!" }); } + console.log("options", options); + await fs.writeFile(inputPath, fileBuffer); await new Promise((resolve, reject) => { @@ -71,7 +78,7 @@ export async function videoToAudio(app: FastifyInstance) { inputPath, "-vn", "-acodec", - options.format === "mp3" ? "libmp3lame" : options.format, + ffmpegEncoder[options.format], "-ab", options.bitrate || "192k", "-ac", @@ -83,8 +90,12 @@ export async function videoToAudio(app: FastifyInstance) { const ffmpeg = spawn("ffmpeg", args); + ffmpeg.stdout.on("data", (data) => { + console.log(`ffmpeg stdout: ${data}`); + }); + ffmpeg.stderr.on("data", (data) => { - console.log(`ffmpeg stderr: ${data}`); + console.error(`ffmpeg stderr: ${data}`); }); ffmpeg.on("close", (code) => { diff --git a/frontend/src/app/video-to-audio/page.tsx b/frontend/src/app/video-to-audio/page.tsx index cfedd34..9a4439d 100644 --- a/frontend/src/app/video-to-audio/page.tsx +++ b/frontend/src/app/video-to-audio/page.tsx @@ -114,7 +114,6 @@ export default function DocConverter() { const selectedFormat = event.currentTarget.textContent?.trim() || ""; setSelectedOutputFormat(selectedFormat); - console.log(selectedOutputFormat); }} />