I have created a next.js video downloader and I want to download videos of the highest quality. When I trigger the download I only get a muted mp4 file (in high quality) and a muted mp3 file. Here is the api file. How could I download the video, the audio and then merge them with ffmpeg correctly?
import ytdl from "ytdl-core";
import fs from "fs";
import { Server } from "socket.io";
import ffmpeg from "fluent-ffmpeg";
export default async function handler(req, res) {
if (res.socket.server.io) {
console.log("Socket is already running");
res.end();
return;
}
console.log("Socket is initializing");
const io = new Server(res.socket.server);
res.socket.server.io = io;
io.on("connection", async (socket) => {
console.log(socket.id, "socketID");
const sendError = async (msg) => {
socket.emit("showError", msg);
};
const sendProgress = async (msg) => {
console.log(msg);
socket.emit("showProgress", msg);
};
const sendComplete = async (msg) => {
console.log(msg);
socket.emit("showComplete", msg);
};
const downloadVideo = async (url) => {
try {
const videoInfo = await ytdl.getInfo(url);
const outputPath = path.join(
process.cwd(),
"mp4s",
`${videoInfo.videoDetails.title}.mp4`
);
const audioPath = path.join(
process.cwd(),
"mp4s",
`${videoInfo.videoDetails.title}.mp3`
);
const videoFormat = ytdl.chooseFormat(videoInfo.formats, {
quality: "highestvideo",
filter: "videoonly",
});
const audioFormat = ytdl.chooseFormat(videoInfo.formats, {
quality: "highestaudio",
filter: "audioonly",
});
const videoStream = ytdl(url, { quality: videoFormat.itag });
const audioStream = ytdl(url, { quality: audioFormat.itag });
const videoOutput = fs.createWriteStream(outputPath);
const audioOutput = fs.createWriteStream(audioPath);
audioStream.pipe(audioOutput);
videoStream.pipe(videoOutput);
let downloadedBytes = 0;
let totalBytes =
videoFormat.contentLength || videoInfo.length_seconds * 1000000;
videoOutput.on("data", (chunk) => {
downloadedBytes += chunk.length;
const progress = Math.round((downloadedBytes / totalBytes) * 100);
sendProgress({ progress });
});
videoOutput.on("error", (err) => {
console.error(err);
sendError({
status: "error",
message: "An error occurred while writing the video file",
});
});
audioOutput.on("error", (err) => {
console.error(err);
sendError({
status: "error",
message: "An error occurred while writing the audio file",
});
});
videoOutput.on("finish", () => {
audioOutput.on("finish", () => {
if (fs.existsSync(outputPath) && fs.existsSync(audioPath)) {
const outputFile = path.join(
process.cwd(),
"mp4s",
`${videoInfo.videoDetails.title}-with-audio.mp4`
);
const command = ffmpeg()
.input(outputPath)
.input(audioPath)
.outputOptions("-c:v copy")
.outputOptions("-c:a aac")
.outputOptions("-b:a 192k")
.outputOptions("-strict -2")
.output(outputFile)
.on("end", () => {
fs.unlink(outputPath, () => {});
fs.unlink(audioPath, () => {});
sendComplete({
status: "success",
});
})
.on("error", (err) => {
console.error("ffmpeg error:", err.message);
sendError({
status: "error",
message:
"An error occurred while processing the audio and video files",
});
});
command.run();
} else {
console.error("Output or audio file not found");
sendError({
status: "error",
message: "Output or audio file not found",
});
}
});
});
} catch (error) {
console.error(error);
sendError({
status: "error",
message: "An error occurred while downloading the video",
});
}
};
socket.on("downloadVideo", downloadVideo);
});
res.end();
}
I am also using socket.io to show the progress in the frontend, but for some reason I don't get the progress correctly. Is it possible to do that?
This not a ytdl
issue; high bitrates are always without audio, so you must download audio and video separately. Here is a detailed how-to, for properly merge two separate streams with ffmpeg
This involves some knowledge of ffmpeg
with multiple input pipes.