diff --git a/backend/server.ts b/backend/server.ts index e73109a..2a8bdf5 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 { videoToAudio } from "./src/routes/videotoaudio.route"; import { removeBG } from "./src/routes/removebg.route"; const app = Fastify({ logger: true }); @@ -26,6 +27,7 @@ app.register(regexTest); app.register(tmzConvert); app.register(generateQRCode); app.register(wordCounter); +app.register(videoToAudio); app.register(removeBG); const PORT = process.env.PORT || 4000; diff --git a/backend/src/routes/videotoaudio.route.ts b/backend/src/routes/videotoaudio.route.ts new file mode 100644 index 0000000..91a5648 --- /dev/null +++ b/backend/src/routes/videotoaudio.route.ts @@ -0,0 +1,148 @@ +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; + +const ffmpegEncoder: { [key: string]: string } = { + ".mp3": "libmp3lame", + ".wav": "pcm_s16le", + ".aac": "aac", +}; + +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: "", + 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 "outputFormat": + 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!" }); + } + + console.log("options", options); + + await fs.writeFile(inputPath, fileBuffer); + + await new Promise((resolve, reject) => { + const args = [ + "-i", + inputPath, + "-vn", + "-acodec", + ffmpegEncoder[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.stdout.on("data", (data) => { + console.log(`ffmpeg stdout: ${data}`); + }); + + ffmpeg.stderr.on("data", (data) => { + console.error(`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, } } 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..b6e283c --- /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: "Video to audio 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..9a4439d --- /dev/null +++ b/frontend/src/app/video-to-audio/page.tsx @@ -0,0 +1,178 @@ +"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 { 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 convertVideo = 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-to-audio", + { + method: "POST", + body: formData, + }, + ); + + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + return; + } + + 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 ( +
+ +
+

video-to-audio

+
+ + 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..4db1620 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 (MPEG-4)", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".avi (Audio Video Interleave)", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".mov (Apple QuickTime Movie)", + output: [".mp3", ".wav", ".aac"], + }, +];