How to download Vimeo video using JavaScript?
Tags: scraping, javascript
Published: 2022-11-02 19:50:29

<!-- wp:paragraph -->
<p>Some time ago, I made the same article but with Python. You can find it here. Some guys asked me to make a similar post, but with JavaScript and NodeJS. So, today I'll teach you how to download a Vimeo video using JavaScript. The article is oriented toward beginners. Welcome undercut. </p>
<!-- /wp:paragraph -->

<!-- wp:more -->
<!--more-->
<!-- /wp:more -->

<!-- wp:heading -->
<h2>Attention!</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>I made this post and this code for learning purposes. I don't recommend it for commercial use. I'm not responsible for any damage caused by this code. Please, don't lawlessly use this code and avoid any illegal activities.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Setup project template</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>I have NodeJS on my MacBook if you don't, please install it first. Installing NodeJS is out of scope for this post, but if you need some help feel free to ask any questions here or on my <a href="https://twitter.com/Jakeroid">Twitter</a>. My NodeJS version is 18.0.0.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The next step is creating an empty directory and initializing npm there. I use terminal commands for that.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""># create directory
mkdir download-vimeo-video-javascript

# go inside
cd download-vimeo-video-javascript

# init npm
npm init -y</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Those commands create a project directory and initialize it with the default package.json file. By default, index.js is the main file, but I prefer for those kinds of apps to use app.js. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Also, to use ES6 imports, we need to change the package type by putting the "type": "module" option. I usually do it to be able to use "import" instead of "require".</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And will be great to have npm command to run our application. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Honestly, this doesn't matter for the current article, but I am showing you what I always do. So, let's edit package.json file. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "name": "download-vimeo-video-javascript",
  "version": "1.0.0",
  "type": "module",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1",
    "start": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>We need to create a file app.js in the same directory and start coding. Finally! </p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Basic code</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Let's make basic code for our app.js file. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">'use strict';


const main = async () => {
    
    console.log('Hello World');
}

const promise = main();

promise.then(() => {
    console.log('Program finished!');
});

promise.catch((err) => {
    console.log('Program finished with error!');
    console.log(err);
});</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Let me explain why we make those code. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The first line tells NodeJS we want to use strict mode. Strict mode is out of the scope of this article too. I use it by default because it disallows you to do some not safe things, and also, it shows an error that doesn't appear in no strict mode. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The next block is the initialization async function and putting it inside the variable name. I prefer to have the ability to use async/await constructions. You can put await only inside asynchronous functions. That's why I create the main. Inside the function, we have console.log call with hello world just for testing purposes. We will remove it later. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Below the main function initialization, I put calling of it and promise with then and catch handler. If you want to know more about promises, let me know. I could write an article about that. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's test our program. Run the following command.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">npn run start</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>My result is in the screenshot bellow.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":2638,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image aligncenter size-large"><img src="https://jakeroid.com/wp-content/uploads/2022/07/image-1024x371.png" alt="hello world nodejs example" class="wp-image-2638"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Looks like everything is working. Let's move to downloading the video now. </p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Theory</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>If you watch any video from Vimeo, your browser downloads it in the background. So, we can do the same, but we need to know the video URL. If we have a video URL, there isn't a problem with downloading it. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>I have investigated before, and I know where it is. When you visit Vimeo page, your browser sends one more request with video ID to Vimeo servers, and it gets JSON object with information about the video. There is a video URL too. </p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Getting Vimeo video ID</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Let's take some video for example. I took <a href="https://vimeo.com/712159936" target="_blank" rel="noreferrer noopener nofollow">this one</a> randomly in the previous article. It looks strange, but we don't need to care about content. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Our first goal is to take the ID of the video. Video ID is those numbers at the end of video URL.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    // input video url
    let inputVideoUrl = 'https://vimeo.com/712159936/';

    // check is there ending slash and remove it
    if (inputVideoUrl.slice(-1) === '/') {
        inputVideoUrl = inputVideoUrl.slice(0, -1);
    }

    // get video id from url
    const videoId = inputVideoUrl.split('/').pop();
    
    console.log(videoId);</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Maybe you want some additional explanation. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>At first, I checked if is there an ending slash inside the input URL. This is required because I want to split the string by "/" char and get the last element or result array (video ID). So, an ending slash could interfere with doing that. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Okay, we have a video ID. What is next?</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Getting project JSON config from Vimeo</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>In my previous article <a href="https://jakeroid.com/blog/how-to-download-vimeo-video-using-python/">how to download Video video using Python</a> I show screenshots from browser inspection. They illustrate how browser sends an additional request to Vimeo servers and get project JSON config. Let me put them here.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":2595,"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full"><img src="https://jakeroid.com/wp-content/uploads/2022/05/image-2.png" alt="request to vimeo video" class="wp-image-2595"/></figure>
<!-- /wp:image -->

<!-- wp:image {"id":2594,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://jakeroid.com/wp-content/uploads/2022/05/image-1-1024x551.png" alt="request result to vimeo video" class="wp-image-2594"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>As you can see, a browser sends the request to some Vimeo URL and get the JSON configuration of the video. You can't see this on screenshots, but for a request, the browser uses host player.vimeo.com.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p> Time to make the same. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>First, let's import https module. For that just add import on the top of the file import https from 'https'; I like to use async/await contraction, so let's use promises to get our JSON with https module. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    // video json config url
    const videoJsonConfigUrl = `https://player.vimeo.com/video/${videoId}/config`;

    // get video json config
    const videoConfig = await new Promise((resolve, reject) => {

        https.get(videoJsonConfigUrl, (res) => {

            let result = '';

            res.on('data', data => {
                result += data;
            });

            res.on('error', err => {
                reject(err);
            });

            res.on('end', () => {
                resolve(JSON.parse(result));
            });

        });
    });

    console.log(videoConfig);</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>Here I put await and create new promise. The main code for getting video is inside the promise. I put data in the result string chunk by chunk and resolve it when the end event is emitted. If some error happens, then my code call rejects.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>I put the result on the screenshot.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":2640,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://jakeroid.com/wp-content/uploads/2022/07/image-1-1024x479.png" alt="getting video url from Vimeo" class="wp-image-2640"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>What is the next step?</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Download Vimeo video using JavaScript</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Each video has different video quality. Each quality item has its own URL. There is a field videoConfig.request.files.progressive. It is an array of objects. Each object represents a video quality item. And, of course, each object has its own URL.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":2641,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://jakeroid.com/wp-content/uploads/2022/07/image-2-1024x793.png" alt="how to download vimeo video using javascript - quality items" class="wp-image-2641"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Let's ignore quality choosing for now and download the first available one. At first, we need to import module fs. Add this line to the top of file <code data-enlighter-language="generic" class="EnlighterJSRAW">import fs from 'fs';</code></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now we can implement downloading and finally get that video from Vimeo. </p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    // video quality items
    const videoQualityItems = videoConfig.request.files.progressive;

    // select file url
    const targetItem = videoQualityItems[0];
    const targetVideoFileUlr = targetItem.url;
    const localPath = `./${videoId}-${targetItem.quality}.mp4`;

    // download it
    await new Promise((resolve, reject) => {

            https.get(targetVideoFileUlr, (res) => {

                const file = fs.createWriteStream(localPath);

                res.pipe(file);

                res.on('error', err => {
                    reject(err);
                });

                res.on('end', () => {
                    resolve();
                });

            });
    });</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>I used almost the same way to download video, except adding pipe from the input https stream to the output file write stream. This allows us to download a huge file without taking care of memory.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Looks like everything works as excepted. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":2643,"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full"><img src="https://jakeroid.com/wp-content/uploads/2022/07/image-3.png" alt="result of video downloading" class="wp-image-2643"/></figure>
<!-- /wp:image -->

<!-- wp:heading -->
<h2>Choosing the quality of video</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Now, let's say we want to choose the quality of the video. For example, let's take the best one. I use reduce method of the array for that.</p>
<!-- /wp:paragraph -->

<!-- wp:enlighter/codeblock -->
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    // selecting the best video quality based on height and width
    // you could also add FPS multiplication to get the best quality based on FPS too
    const targetItem = videoQualityItems.reduce((prev, curr) => {
        return prev.width * prev.height > curr.width * curr.height ? prev : curr;
    });</pre>
<!-- /wp:enlighter/codeblock -->

<!-- wp:paragraph -->
<p>The logic here is very simple. Best quality = bigger amount of pixels. You could also FPS multiplication to get the best quality based on FPS too.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Conclusions</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>In general downloading video from Vimeo using JavaScript is pretty simple. I hope my article was useful for you. If you have any questions, suggestions, or criticism, please ask them below in the comments or write me directly on <a href="https://twitter.com/Jakeroid">Twitter</a>. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>I put all code in my <a href="https://github.com/Jakeroid/download-vimeo-video-js">GitHub</a> repository in case you need to check the full version. </p>
<!-- /wp:paragraph -->