Why this project
Lately I was searching for a youtube Downloader and never found one where I can download videos with high quality (eg. 4k) without paying for a subscription. Well on research I stumbled across the ytdl
npm package.
The Task
Download a YouTube Video with the highest audio and video quality.
What we will use
- ytdl-core for streaming from Youtube
- fluent-ffmpeg for transcoding video and audio to our likings
- nanospinner for good looking progress spinners
What problems do we need to solve
I will tell you from my experience with ytdl. If you look at the documentation of ytdl-core
you can see, that you can either download with the quality setting highestaudio
or highestvideo
. But in the highestvideo we have bad audio and in the highestaudio we have bad video. We will fix this by first downloading the best video to a stream and write that to a file. In the second step we will download the best audio to a stream and then put the file and the stream through ffmpeg to combine into one file.
Writing the script
initializing npm
Create a new folder and open a terminal inside that folder. Then create a new npm package and install the dependencies:
npm init -y
npm install fluent-ffmpeg ytdl-core nanospinner
creating the script
Create an jsvascript file and call it index.js
.
Now we will add all the imports.
const fs = require("fs");
const ytdl = require("ytdl-core");
const ffmpeg = require("fluent-ffmpeg");
const { createSpinner } = require("nanospinner");
var videoId = "VideoID";
We will also create a variable for the videoID.
With that done we can take a look at how the download process will work.
- Download the
highestvideo
to a file - Download the
highestaudio
to a readable stream - Take the video from the file and the audio from the stream using
ffmpeg
and write the output to a file - remove temporary file
Step 1
To download the highestvideo
to a file we will use the default ytdl(id, options)
function. This function returns a readable Stream which we will pipe with the fs.createWriteStream(path)
function to write it to a file. As a filename we will use VideoID.mp4
.
To add a simple Terminal Output for the user to see that something is happening we first create a spinner and then start the download.
let videoSpinner = createSpinner("Downloading highest video").start();
ytdl(videoId, { quality: "highestvideo" })
.pipe(fs.createWriteStream(videoId + ".mp4"))
.on("error", () => {
videoSpinner.error();
console.log("error downloading highest video");
})
.on("finish", () => {
videoSpinner.success();
// Do something after download of video
});
See how we also added handlers for the error
and finish
event. In the errorhandler we stop the spinner and print an error message. In the onfinish function we will now implement the other streaming and compining.
Step 2
To download the highestaudio to a readable Stream we again call the ytdl(id, options)
function but with the other quality option. And this time we will not pipe the stream directly. We will create a variable for the stream so that we can use it later.
But first we will create a loading spinner for the audio download and combining part. We will also define a variable for the output fulepath. This time using mkv
as file format: videoId.mkv
.
let scSpinner = createSpinner("Downloading audio and combining").start();
const audioFileStream = ytdl(videoId, { quality: "highestaudio" });
const outputFilePath = videoId + ".mkv";
Remember this has to be in the onfinish function defined earlier!
Step 3
Now to the tricky part. Combining the audio and video together. For that we will first create a new ffmpeg command. Then we will use method-chaining to set some options and define callbacks and then as the last step we will run the command.
But first create the command.
const command = ffmpeg();
Now for the options: I will add comments on the lines to explain it but here is the explanation in short:
- Define both inputs using
.input(string|readable)
- Define audioCodec and videoCodec
- Tell the inputFormat of the streams
.inputFormat('mp4')
- Define the output Filepath
.output(string)
- define callback functions
- run the command
command
// Step 1
.input(id + ".mp4")
.input(audioFileStream)
// Step 2
.audioCodec("copy")
.videoCodec("copy")
// Step 3
.inputFormat("mp4")
// Step 4
.output(outputFilePath)
// Step 5
.on("end", () => {
scSpinner.success();
// finish handling
})
.on("error", (err) => {
scSpinner.error();
// error handling
})
// Step 6
.run();
Step 4
As the last step we will remove the .mp4
file we downloaded the video to. We will do that in the onEnd function of the ffmpeg command. For that we will just create a Loading spinner and use the fs.rmSync(path)
function.
let dlSpinner = createSpinner("Delete temporary created file").start();
fs.rmSync(videoId + ".mp4");
dlSpinner.success();
Run the downloader
To run the downloader you just need to replace the placeholder for the videoId with a real id and run the script using node .
Code can be downloaded here.
Top comments (0)