DEV Community

Cover image for Batch Video Editing With Node.JS
Alex Standiford
Alex Standiford

Posted on

Batch Video Editing With Node.JS

Over at DesignFrame, one of my clients hosts videos on their own site. In order to ensure that these videos will play correctly on all devices, I have been manually converting these videos using Cloudconvert. It's a very handy tool, but the process can be tedious when you have a lot of files to deal with, and it doesn't (at least to my knowledge) handle generating screenshots of your videos for you.

So, in-order to upload the videos to their website, my (admittedly awful) workflow looked something like this:

  1. Take each video, and use cloudconvert to create ogv, webm, and mp4 versions of each video
  2. Open the video and save a screenshot at a good place
  3. Upload each version of each video to their server
  4. Publish the video with the screenshot

This wasn't too bad, but as a programmer, doing manual, repetitive tasks makes my skin crawl, so I started looking into ways to automate this. I've been playing with creating small CLI applications with Node.js using commander lately, and decided that this would be an excellent place to start.

What's nice about starting with a CLI-based solution is that it allows me to spend most of my time focusing on the back-end instead of building out some kind of interface. If you build correctly, it should be easy to set up what you've built with an interface.

Here's what the script does:

  1. Add 3 commands accessible from my terminal's command line: run, screenshots, and videos
  2. Take all of the files in a specified directory, and convert the videos to ogv, webm, and mp4
  3. Automatically generate 6 screenshots of each video at different intervals throughout.
  4. Save the results of each video in a converted files directory, with each video title as the sub directory.

The nice thing about setting it up with Node is that, if the conversion job warrants it, you can spin up a cpu-optimized droplet on DigitalOcean, upload the files, and make the conversion quickly, and then destroy the droplet. This is way faster than doing it on your local machine, and since the droplet is usually destroyed in 1-2 hours you're going to spend very little money to get the job done. This isn't a requirement, of course; The script runs perfectly fine on a local machine - the conversion will just take longer.

Completed Project Files

You can get the completed project files here.

Project Structure

I set the project up to use 3 files.

  • index.js - The entry point for our program. This is where we configure our CLI commands
  • FileConverter.js - Handles the actual conversion of a single file.
  • MultiFileConverter.js - Gathers up videos from a directory, creates instances of FileConverter, and runs the conversion.

Setting Up Your Project

Here is the resulting package.json file that I'm using for this project:

    {
      "name": "video-converstion-script",
      "version": "1.0.0",
      "description": "Converts Videos",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "bin": {
        "asconvert": "./index.js"
      },
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@ffmpeg-installer/ffmpeg": "^1.0.15",
        "@ffprobe-installer/ffprobe": "^1.0.9",
        "commander": "^2.16.0",
        "fluent-ffmpeg": "^2.1.2",
        "junk": "^2.1.0"
      }
    }
Enter fullscreen mode Exit fullscreen mode

Here's a list of each dependency and a brief description of their role in this project

  • @ffmpeg-installer/ffmpeg - sets up the binaries needed to convert the videos and create screenshots
  • @ffprobe-installer/ffprobe - sets up the binaries needed to convert the videos and create screenshots
  • commander - Super awesome tool that allows us to build out a CLI from our Node.js application.
  • fluent-ffmpeg - Allows us to interface with ffmpeg using Node
  • junk - A nice little library that makes it easy to filter out junk files from our directory. This will keep us from trying to convert a .DS_Store file, or something like that.

Note that we also have set the bin object. This allows us to associate our CLI command asconvert with our index.js file. You can change asconvert to whatever you want, just keep in mind that you will need to use whatever you call asconvert instead of what I call it in this post.

Place JSON above into your package.json file, and run npm install. Once you do that, you'll also need to run npm link. This will connect the bin configuration to your terminal so you can run your commands directly from the command line.

Setting up our Index file

Before we can start messing with our system, we need to set up some commander commands. This will allow us to test, debug, and tinker with our javascript from the terminal. We will be adding multiple commands later, but for now, let's simply add the run command. The code below is a basic example, and should respond with "hello world!' in your terminal.

#!/usr/bin/env node

/**
 * Allows us to run this script as a cli command
 */
const program = require('commander');

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

/**
 The run command
 */
program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{
 console.log('hello world!');
//We will put our actual command here.
 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

Once you add this, you should be able to run asconvert run from your terminal and you should get "hello world!" back. Superkewl!

Set Up the MultiFileConverter Class

Now that we've got some simple command line things set up, let's start working on the good stuff.

Create a new file called MultiFileConverter.js and add the following code.

/**
 * Parses file names
 */
const path = require('path');

/**
 * converts files from a directory
 */
class MultiFileConverter{
 constructor(args = {}){
 //Set the argument object
 const defaults = {
 directory: false,
 formats: false
 };
 this.args = Object.assign(args, defaults);

 //Construct from the args object
 this.formats = this.args.formats;
 this.directory = this.args.directory === false ? `${path.dirname(require.main.filename)}/files-to-convert/` : this.args.directory;
 }
}

module.exports = MultiFileConverter;
Enter fullscreen mode Exit fullscreen mode

This basic setup will allow us to pass an object of arguments to our constructor, which will merge with default arguments and build everything we'll need to complete the conversions.

Connect The Converter to the CLI

Once you do this, we need to set up our CLI command to use this object. Go back to your index.js file and create an instance of this class, like so.

#!/usr/bin/env node
/**
 * Allows us to run this script as a cli command
 */
const program = require('commander');

const MultiFileConverter = require('./lib/MultiFileConverter');

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

/**
 The run command
 */
program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{
 const converter = new MultiFileConverter();
 console.log(converter);
 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

If you run the command now, the converter object should be displayed in the terminal.

I personally organize my js files inside a lib directory. You can put your files wherever you want, just make sure your include paths are correct.

Get the List of FileConverter objects

The primary purpose of the MultiFileConverter class is to batch-convert files in the directory. In order to do that, we are going to loop through the files in the directory and construct an array of FileConverter objects from each file. We'll let the FileConverter object handle the actual conversion and other file-specific things.

I like to delay processes that have the potential to be time-consuming until I absolutely need them. That way I can construct the class without going through the time-consuming bits every time. To do this, I often create a getter method, like this:

/**
 * Constructs the files object
 * @returns {*}
 */
getFiles(){
 if(this.files) return this.files;
 this.files = [];
 const files = fs.readdirSync(this.directory, {});
 //Loop through and construct the files from the specified directory
 files.filter(junk.not).forEach((file) =>{
 this.files.push(new FileConverter(this.directory + file, false, this.formats));
 });

 return this.files;
}
Enter fullscreen mode Exit fullscreen mode

You'll notice the first line checks to see if the class already has a files array set. If it does, it simply returns that array. Otherwise, it goes through and builds this array. This allows us to use getFiles() throughout the class without re-building the array every time.

A lot is happening in this method. Let's break it down.

  1. Check to see if the files array exists. If it does, it returns the value
  2. Reads the specified directory and returns an array of files
  3. Filters out junk files, and then loops through the filtered array.
  4. Inside the loop, we push a new instance of FileConverter and pass the arguments into the the files array.
  5. Return the files in the object

Update your MultiFileConverter class to include a couple of required libraries, and add the getFiles() class. You should end up with something like this:

/**
 * Node File system
 */
const fs = require('fs');

/**
 * Parses file names
 */
const path = require('path');

/**
 * Allows us to filter out junk files in our results
 */
const junk = require('junk');

/**
 * Handles the actual file conversion of individual files
 * @type {FileConverter}
 */
const FileConverter = require('./FileConverter');

/**
 * converts files from a directory
 */
class MultiFileConverter{
 constructor(args = {}){
 //Set the argument object
 const defaults = {
 directory: false,
 formats: false
 };
 this.args = Object.assign(args, defaults);

 //Construct from the args object
 this.formats = this.args.formats;
 this.directory = this.args.directory === false ? `${path.dirname(require.main.filename)}/files-to-convert/` : this.args.directory;
 }

 /**
 * Constructs the files object
 * @returns {*}
 */
 getFiles(){
 if(this.files) return this.files;
 this.files = [];
 const files = fs.readdirSync(this.directory, {});
 //Loop through and construct the files from the specified directory
 files.filter(junk.not).forEach((file) =>{
 this.files.push(new FileConverter(this.directory + file, false, this.formats));
 });

 return this.files;
 }
}

module.exports = MultiFileConverter;
Enter fullscreen mode Exit fullscreen mode

Set Up the FileConverter Class

Now that we are looping through our files, it's time to build a basic instance of the FileConverter class so our files array builds properly.

 /**
 * Parses file names
 */
const path = require('path');

/**
 * Node File system
 */
const fs = require('fs');

/**
 * Handles the actual file conversion
 */
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
const ffprobePath = require('@ffprobe-installer/ffprobe').path;
const ffmpeg = require('fluent-ffmpeg');
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
ffmpeg.setFfprobePath(ffprobePath);

/**
 * Converts files and takes screenshots
 */
class FileConverter{

 constructor(inputPath, outputPath = false, formats = false){
 this.formats = formats === false ? ['ogv', 'webm', 'mp4'] : formats.split(',');
 this.file = path.basename(inputPath);
 this.format = path.extname(this.file);
 this.fileName = path.parse(this.file).name;
 this.conversion = ffmpeg(inputPath);
 this.outputPath = outputPath === false ? `${path.dirname(require.main.filename)}/converted-files/${this.fileName}` : `${outputPath}/${this.fileName}`;
 }
}

module.exports = FileConverter;
Enter fullscreen mode Exit fullscreen mode

You'll notice that we are constructing some useful data related to the file and its impending conversion, but we don't actually do the conversion step yet. This simply sets the file up. We'll add the actual conversion in a separate method.

Test It Out

We now have all 3 of our files all set up and connected. We haven't started the actual conversion process yet, but if we make a change to our command action we can check to make sure everything is working as-expected.

If you haven't yet, now would be a good time to create 2 directories in the root of your project. converted-files and files-to-convert. Add a few video files in your files-to-convert directory.

Modify your commander action in your index.js file so that it logs the result of the getFiles() method. If all went well, you should get a big ol' array of objects.

#!/usr/bin/env node
/**
 * Allows us to run this script as a cli command
 */
const program = require('commander');

const MultiFileConverter = require('./lib/MultiFileConverter');

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

/**
 The run command
 */
program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{
 const converter = new MultiFileConverter();
 console.log(converter.getFiles());
 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

Convert Videos

Whew. All this effort and we haven't even started converting videos yet. Let's change that.

Add a new method, called getVideos() to your MultiFileConverter.js file.

/**
 * Loops through and converts files
 */
getVideos(){
 return this.getFiles().forEach(file => file.convert());
}
Enter fullscreen mode Exit fullscreen mode

This iddy biddy method simply loops through our files array and runs the convert method on each FileConverter object. Of course, we have to actually create the convert method on the FileConverter object for this to work, so let's do that now.

Add a new method, called convert() to your FileConverter.js file.

/**
 * Converts the file into the specified formats
 */
convert(){
 fs.mkdir(this.outputPath,() =>{

 //Loop through file formats
 this.formats.forEach((fileFormat) =>{
 //Check to see if the current file format matches the given file's format
 if(`.${fileFormat}` !== this.format){
 //Start the conversion
 this.conversion.output(`${this.outputPath}/${this.fileName}.${fileFormat}`)
 .on('end', () => console.log(`${this.file} has been converted to a ${fileFormat}`))
 .on('start', () =>{
 console.log(`${this.fileName}.${fileFormat} conversion started`);
 })
 }

 //If the file format matches the file's format, skip it and let us know.
 else{
 console.log(`Skipping ${this.fileName} conversion to ${fileFormat} as this file is already in the ${fileFormat} format.`);
 }
 });

 this.conversion.run();
 });
}
Enter fullscreen mode Exit fullscreen mode

Here's the real meat and potatoes of the build. A lot is happening here, so let's break it down.

  1. Creates a directory named after the original video we're converting. This will hold all files generated for this video.
  2. Loops through each file format specified for this conversion.
  3. In the loop, we check to see if the current file format matches the format of the video we're converting. If they match, the converter skips that conversion and moves on to the next format. This keeps us from needlessly converting an .mp4 to another .mp4.
  4. If the formats are different, we queue up the converter using the specified format.
  5. Once we've looped through all of the formats we're converting to, we run the actual converter.

Test It Out

We have now set up the actual converter. Let's see if it works as expected.

Modify your commander action in your index.js file to use the getVideos() method, like so.

#!/usr/bin/env node
/**
 * Allows us to run this script as a cli command
 */
const program = require('commander');

const MultiFileConverter = require('./lib/MultiFileConverter');

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

/**
 The run command
 */
program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{

 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

You should see a message for each video, stating that the conversion started for each format. It will also let you know if it skipped one of the conversions, and why. This will take a long time to convert, and since we're just testing, cancel the command (CTRL+C on a Mac) after about 20 seconds. Check your converted-files directory and see if the video conversion started to run.

Generate Screenshots

Sweet! Now that we have videos converting, let's get generate some screenshots while we're at it. The process of adding screenshots is very similar.

Add a new method, called getScreenshots() to your MultiFileConverter.js file.

/**
 * Loops through and generates screenshots
 */
getScreenshots(){
 return this.getFiles().forEach(file => file.getScreenshots());
}
Enter fullscreen mode Exit fullscreen mode

This works just like getVideos(), only it runs getScreenshots method on each FileConverter object instead. Again, we need to create the convert method on the FileConverter object for this to work.

Add a new method, called getScreenshots() to your FileConverter.js file.

/**
 * Creates 6 screenshots taken throughout the video
 */
getScreenshots(){
 this.conversion
 .on('filenames', filenames => console.log(`\n ${this.fileName} Will generate 6 screenshots, ${filenames.join('\n ')}`))
 .on('end', () =>{
 console.log(`\n Screenshots for ${this.fileName} complete.\n`)
 })
 .screenshots({
 count: 6,
 timestamps: [2, 5, '20%', '40%', '60%', '80%'],
 folder: this.outputPath,
 filename: `${this.fileName}-%s.png`
 })

}
Enter fullscreen mode Exit fullscreen mode

This method is a bit simpler than getVideos(). We simply chain the screenshots() method (included in our ffmpeg library) and pass some arguments. Our arguments instruct ffmpeg to create 6 screenshots at 2 seconds, 5 seconds, and at 20%, 40%, 60%, and 80% of the video. Each file is saved inside the same directory as our converted videos are saved.

Test It Out

Let's make sure that we can generate screenshots.

Modify your commander action in your index.js file to use the getScreenshots() method, like so.

#!/usr/bin/env node
/**
 * Allows us to run this script as a cli command
 */
const program = require('commander');

const MultiFileConverter = require('./lib/MultiFileConverter');

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

/**
 The run command
 */
program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{
const converter = new MultiFileConverter();
return converter.getScreenshots();
 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

You should see a message for each video, listing off the screenshots that will be created. This will take a some time to convert, and since we're just testing, cancel the command (CTRL+C on a Mac) after about 20 seconds. Check your converted-files directory and see if the screenshots started to generate.

Generate Everything

Now that we have a way to generate screenshots and convert our videos, we need to make one more method in our MultiFileConverter.js file. This method will run both the convert() method and the getScreenshots() method.

We are creating a third method to do both of these because it allows us to loop through the files once, instead of twice, and as such is more efficient than running getVideos() and then getScreenshots() separately.

Add this method to your MultiFileConverter class.

/**
 * Runs the complete converter, converting files and getting screenshots
 */
runConverter(){
 return this.getFiles().forEach((file) =>{
 file.convert();
 file.getScreenshots();
 });
Enter fullscreen mode Exit fullscreen mode

Create Commands

Now that we have everything needed, let's create our 3 commands we talked about earlier - asconvert videos, asconvert screenshots, and asconvert run

/**
 * Sets up the command to run from the cli
 */
program
 .version('0.1.0')
 .description('Convert Video Files From a Directory');

program
 .command('run')
 .description('Converts the files in the files-to-convert directory of this project')
 .action(() =>{
 const converter = new MultiFileConverter();
 return converter.runConverter();
 });

/**
 * Sets up the command to run from the cli
 */
program
 .command('screenshots')
 .description('Gets a screenshot of each video')
 .action(() =>{
 const converter = new MultiFileConverter();
 return converter.getScreenshots();
 });

/**
 * Sets up the command to run from the cli
 */
program
 .command('videos')
 .description('Gets conversions of each video')
 .action(() =>{
 const converter = new MultiFileConverter();
 return converter.getVideos();
 });

program.parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

You can now run any of those 3 commands, and convert videos, create screenshots, or do both at the same time.

Closing Remarks

There are a couple of things that could improve this tool.

  1. I'm sure someone who knows Docker better than I could put it in some kind of container to make this EZPZ to set up/tear down on a server
  2. The directory that houses the videos is a part of the project. With further configuration you could set this up so that the videos are pulled directly from Google Drive, or something like that. I didn't have a need for that, but it would be pretty slick.

All in all, it was a fun little build, and I'm sure it will save me some time in the future.

If you are using this, I'd love to hear about how it worked for you, and why you needed it. Cheers!

Discussion (29)

Collapse
estelletaylor profile image
Estelletaylor

Thanks for sharing, very nice. I often use kinemaster pro at Techbigs.com for video editing. You can refer to here

Collapse
pubgnames profile image
techhube

For those of you who’re interested in the art of photography and would love to experience amazing footages on your mobile devices, PicsArt Photo Editor will offer you the complete photography experiences with amazing photos and videos for you to enjoy and play with. Explore the exciting and interesting visual options in PicsArt, along with many other unique features that you can’t find anywhere else, to create and enjoy awesome visual experiences with your edits.
techhube.com/picsart-mod-apk/
picsart mod apk

The app offers both video and photo editor tools that were completely built into your devices. Therefore, allowing you to combine your act of taking photos or capturing videos with the complete editing options. Here, you can feel free to have fun with the awesome visual experiences with each of your edits and enjoy unique feels with the amazing visual customizations.

Collapse
saltsma43544801 profile image
saltsman

Most of the people like to use kinemaster app for editing. Now this app premium version is available. Just download kinemaster pro mod apk on your android smartphone.

Collapse
hamzarana76 profile image
Hamza Rana

I can use apkvest.com site for downloading mod site

Collapse
waylonsims profile image
WaylonSims

Thank you for sharing. You can experience Rokkr, an application to watch TV and dramas that you will enjoy.

Collapse
saltsma43544801 profile image
saltsman

Nice article. Video editing is my dream. I am a video designing student. But I studied online. Free chegg answers are very helpful for me on those times. Thanks for that.

Collapse
kirito39 profile image
kirito

thanks Alex for sharing this with us. everything is so detailed and is easy to get. before it, i used to use Kinemaster mod to edit video which supports batch editing. but it also helps me a lot. thanks again. i look forward to more of your posts.

Collapse
cpqlinux profile image
CPQ Linux

Well Nod.js seems to be the easiest one but not sure how many professional features available on that?
Here are some of the video editors available on Linux: Lives Video Editor, Lightworks video editor, OpenShot video editor, Blender video editor, and here is a list of some best video editing software available in Linux.

Collapse
greysed profile image
Grey-Sed

For video editing, I use SolveigMM HTML5 Video Editor (official version solveigmm.com/en/products/html5-cl...) - an advanced video editor that allows you to manage any content. With it, you can cut and merge videos, add transitions, text, graphics, video overlay, voiceover. SolveigMM HTML5 Video Editor is the only online editor with smart rendering.

Collapse
idkwhattoname profile image
IDKWHATTONAME • Edited on

Well, it's for sure that you always learn something from coding and in this article everything is so clear. I'll definitely share it to my other friends.

Well, I also created a website which is Onlinebattegrounds to may check it out and give me some reviews

Collapse
oliviamusk profile image
oliviamusk

Lucky Patcher iOS app is a very popular app to hack games, remove app permissions, modify games, etc. There is a free application for iOS lucky patcher that can modify games and apps very easily. Through this, you can use the paid feature for free and enjoy the premium feature of iOS for free. You can use this application for your Android device as well as iPad and iPhone. Lucky Patcher iOS

Collapse
apklance profile image
Apk Lance

When i use kine master and do import some video. Sometimes i cannot import video it show that" you can't import this media. Kindly refresh it and import again."
If anyone know how to solve this problem kindly help me.
Thank you....

Collapse
lonessam00 profile image
Lones Sam

I prefer inshot without watermark to edit all my videos and photos on my smartphone. In fact, I will recommend all of you to use this app. This is the best video editor of all the time.

Collapse
maureenglover profile image
maureenGlover • Edited on

Alight Motion Pro APK is a great editing app. have you downloaded it yet. you can download it via Alight Motion Pro Techgara

Collapse
sethfields profile image
sethfields • Edited on

I like your post. kinemaster mod apk terbaru is the leading video and image editing application. How often do you use this app?

Collapse
saltsma43544801 profile image
saltsman

Now you can easily download Pokemon Go Mod APK on your android smartphone.

Collapse
techloo profile image
pinoy1tv

It's the same JavaScript that runs in your web browser. To that end, there already exists at least one video editing tool written modapk for node.js

Collapse
jacksonjhon21 profile image
jacksonjhon21

Want to know about DD Free Dish Channel List?

Collapse
tenorjamal profile image
tenorjamal

OK! You want to download kinemaster? I'm right.

Collapse
teckcloudz profile image
TeckCloudz

Thank You for this Article I really found this very informative. Thank You! For More Click Here

Collapse
androidappbd_ profile image
Androidappbd • Edited on

Download kinmaster from HERE = androidappbd.com/kinemaster-pro-fu...

Collapse
saltsma43544801 profile image
saltsman

You can easily download and install spotify premium apk on your android smartphone. Just try this application on your smartphone.

Collapse
danish68484748 profile image
Danish

I see there are also a lot of similar apps like . I am currently using teatv and i got this app from modapkpure.in

Collapse
ronwis0122 profile image
ronwis0122

Yes, it worked for me indeed. Thank you very much, man!

Collapse
shoaibmahmoud9 profile image
Shoaib Mahmoud

Important to know
KingAnBru Pubg id

Collapse
idkwhattoname profile image
IDKWHATTONAME

Bro I also have a similar website here at online battlegrounds that you can visit