Automate FFmpeg in Programmatic Video Workflows with Node.js

When implementing video trimming for many videos in your workflows, you'll likely want to execute FFmpeg commands programmatically using Node.js. If you need to trim multiple videos with the same parameters (for example, extracting a specific segment from each video in a folder), you can automate the process using a Node.js script.

Below is a sample Node.js script that processes all video files in a directory, trimming each one to a specific segment. This script leverages Node's file system capabilities and child processes to efficiently handle multiple video files.

#!/usr/bin/env node
  /**
   * Video Batch Trimming Tool
   * 
   * This Node.js script automates FFmpeg to process multiple video files in a directory,
   * trimming each one to the same time segment. It's useful for media workflows where
   * you need to extract the same portion from multiple videos.
   */
  
  const fs = require('fs');
  const path = require('path');
  const { exec } = require('child_process');
  const { promisify } = require('util');
  const execPromise = promisify(exec);
  
  // Check if FFmpeg is installed
  async function checkFFmpeg() {
    try {
      await execPromise('ffmpeg -version');
      return true;
    } catch (error) {
      return false;
    }
  }
  
  // Get all video files from directory
  function getVideoFiles(directory) {
    const videoExtensions = ['.mp4', '.avi', '.mov', '.mkv'];
    
    return fs.readdirSync(directory)
      .filter(file => {
        const ext = path.extname(file).toLowerCase();
        return videoExtensions.includes(ext);
      })
      .map(file => path.join(directory, file));
  }
  
  // Trim a video file
  async function trimVideo(inputFile, outputFile, startTime, endTime) {
    try {
      await execPromise(`ffmpeg -i "${inputFile}" -ss ${startTime} -to ${endTime} -c copy "${outputFile}" -hide_banner -loglevel warning`);
      return true;
    } catch (error) {
      console.error(`Error trimming: ${error.message}`);
      return false;
    }
  }
  
  // Main function
  async function main() {
    // Check arguments
    const args = process.argv.slice(2);
    if (args.length !== 4) {
      console.log('Usage: node trim_videos.js [input_directory] [output_directory] [start_time] [end_time]');
      console.log('Example: node trim_videos.js ./raw_videos ./trimmed_videos 00:00:30 00:01:45');
      process.exit(1);
    }
  
    const inputDir = args[0];
    const outputDir = args[1];
    const startTime = args[2];
    const endTime = args[3];
  
    // Check if FFmpeg is installed
    const ffmpegInstalled = await checkFFmpeg();
    if (!ffmpegInstalled) {
      console.error('Error: FFmpeg is not installed. Please install it first.');
      process.exit(1);
    }
  
    // Create output directory if it doesn't exist
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }
  
    // Get all video files
    const videoFiles = getVideoFiles(inputDir);
    const totalFiles = videoFiles.length;
    
    console.log(`Found ${totalFiles} video files to process.`);
  
    // Process each video file
    for (let i = 0; i < videoFiles.length; i++) {
      const videoFile = videoFiles[i];
      const filename = path.basename(videoFile);
      const outputFile = path.join(outputDir, `trimmed_${filename}`);
      
      console.log(`Processing file ${i + 1} of ${totalFiles}: ${filename}`);
      
      const success = await trimVideo(videoFile, outputFile, startTime, endTime);
      
      if (success) {
        console.log(`Successfully trimmed: ${filename} -> ${outputFile}`);
      } else {
        console.log(`Error trimming: ${filename}`);
      }
    }
  
    console.log(`All videos processed. Trimmed videos are in ${outputDir}`);
  }
  
  // Run the main function
  main().catch(error => {
    console.error('An unexpected error occurred:', error);
    process.exit(1);
  });

This Node.js script:

  • Checks if FFmpeg is installed on the system
  • Accepts the command-line arguments: input directory, output directory, start time, end time
  • Creates the output directory if it doesn't exist
  • Finds all video files with the same extensions (.mp4, .avi, .mov, .mkv)
  • Processes each video file, showing progress information
  • Trims each video using FFmpeg with the same parameters
  • The script uses promises to handle asynchronous operations.

How to use the script:

  1. Save it as trim_videos.js
  2. Run it with: node trim_videos.js ./raw_videos ./trimmed_videos 00:00:30 00:01:45

Let's break down this line of code in detail:

await execPromise(`ffmpeg -i "${inputFile}" -ss ${startTime} -to ${endTime} -c copy "${outputFile}" -hide_banner -loglevel warning`);

This single line executes an FFmpeg command from Node.js to trim a video file. Here's what each part does:

  1. await - This keyword pauses execution of the function until the Promise returned by execPromise() resolves or rejects. It ensures the trimming operation completes before moving on.
  2. execPromise - This is a promisified version of Node's exec function created earlier in the code using promisify(exec). It runs shell commands and returns a Promise instead of using callbacks.
  3. The backtick string `...` - This is a template literal in JavaScript that allows embedding variables and expressions using ${variable} syntax.
  4. The FFmpeg command itself:
    • ffmpeg - The base command that invokes the FFmpeg program
    • -i "${inputFile}" - Specifies the input file path (in quotes to handle paths with spaces)
    • -ss ${startTime} - Sets the start time for trimming (e.g., "00:00:30")
    • -to ${endTime} - Sets the end time for trimming (e.g., "00:01:45")
    • -c copy - Uses stream copying mode, which preserves the original video/audio codecs without re-encoding (making it very fast)
    • "${outputFile}" - Specifies the output file path (in quotes for paths with spaces)
    • -hide_banner - Suppresses the FFmpeg copyright header and build information
    • -loglevel warning - Reduces the verbosity of FFmpeg's output to only show warnings and errors

When executed, this command will extract the portion of the video between startTime and endTime and save it as a new file at outputFile without re-encoding the video, which significantly speeds up the process and maintains the original quality.

Conclusion

The script will help you streamline your video processing pipeline by handling the repetitive task of trimming multiple videos to the same duration. It maintains a consistent output format and provides progress tracking as it processes each file in your collection.