Skip to main content

fluent-ffmpeg vs @ffmpeg/ffmpeg vs node-video-lib: Video Processing in Node.js (2026)

·PkgPulse Team

TL;DR

fluent-ffmpeg is the most-used FFmpeg wrapper for Node.js — a clean chainable API over the FFmpeg binary, handles transcoding, thumbnail extraction, streaming, and any format FFmpeg supports. @ffmpeg/ffmpeg is a WebAssembly build of FFmpeg that runs in browsers AND Node.js — no binary dependency, but slower and limited. node-video-lib is a lightweight MP4/FLV parser that reads metadata and packets without FFmpeg — fast and dependency-free for read-only tasks. For server-side video processing: fluent-ffmpeg. For browser-side video processing: @ffmpeg/ffmpeg. For reading video metadata: node-video-lib.

Key Takeaways

  • fluent-ffmpeg: ~400K weekly downloads — FFmpeg wrapper, full transcoding, streaming, thumbnails
  • @ffmpeg/ffmpeg: ~300K weekly downloads — WASM build, browser + Node.js, no binary dep, slower
  • node-video-lib: ~30K weekly downloads — MP4/FLV metadata reader, no FFmpeg dependency
  • fluent-ffmpeg requires FFmpeg to be installed (brew install ffmpeg / apt install ffmpeg)
  • ffmpeg-static package bundles FFmpeg binary for easy deployment
  • For cloud functions: ffmpeg-static bundles the binary or use AWS Lambda layers

fluent-ffmpeg

fluent-ffmpeg — chainable FFmpeg wrapper:

Setup

# Install FFmpeg system-wide:
brew install ffmpeg  # macOS
apt install ffmpeg   # Ubuntu

# Or bundle the binary with your app:
npm install fluent-ffmpeg ffmpeg-static

Basic transcoding

import ffmpeg from "fluent-ffmpeg"
import ffmpegPath from "ffmpeg-static"

// Set FFmpeg binary path (when using ffmpeg-static):
ffmpeg.setFfmpegPath(ffmpegPath!)

// Transcode MP4 → WebM:
await new Promise<void>((resolve, reject) => {
  ffmpeg("input.mp4")
    .output("output.webm")
    .videoCodec("libvpx-vp9")    // VP9 codec
    .audioCodec("libopus")        // Opus audio
    .videoBitrate(1000)           // 1000 kbps video
    .audioBitrate(128)            // 128 kbps audio
    .size("1280x720")             // Scale to 720p
    .on("end", resolve)
    .on("error", reject)
    .on("progress", (p) => console.log(`Progress: ${p.percent?.toFixed(1)}%`))
    .run()
})

// Promise-based wrapper:
function transcode(input: string, output: string): Promise<void> {
  return new Promise((resolve, reject) => {
    ffmpeg(input)
      .output(output)
      .on("end", resolve)
      .on("error", reject)
      .run()
  })
}

Thumbnail extraction

import ffmpeg from "fluent-ffmpeg"

// Extract a single thumbnail at a specific time:
function extractThumbnail(videoPath: string, outputPath: string, time = "00:00:05"): Promise<void> {
  return new Promise((resolve, reject) => {
    ffmpeg(videoPath)
      .screenshots({
        timestamps: [time],       // Time position
        filename: "thumbnail.jpg",
        folder: outputPath,
        size: "640x360",          // Output size
      })
      .on("end", resolve)
      .on("error", reject)
  })
}

// Extract multiple thumbnails (grid/sprite):
function extractSprite(videoPath: string, outputDir: string): Promise<void> {
  return new Promise((resolve, reject) => {
    ffmpeg(videoPath)
      .screenshots({
        count: 10,           // 10 evenly-spaced thumbnails
        folder: outputDir,
        size: "320x180",
        filename: "thumb-%i.jpg",  // thumb-1.jpg, thumb-2.jpg, etc.
      })
      .on("end", resolve)
      .on("error", reject)
  })
}

HLS streaming output

import ffmpeg from "fluent-ffmpeg"
import path from "path"

// Generate HLS segments for video streaming:
function createHLS(inputPath: string, outputDir: string): Promise<void> {
  return new Promise((resolve, reject) => {
    ffmpeg(inputPath)
      .addOption("-hls_time", "10")           // 10-second segments
      .addOption("-hls_playlist_type", "vod") // VOD (not live)
      .addOption("-hls_segment_filename", path.join(outputDir, "segment%03d.ts"))
      .output(path.join(outputDir, "playlist.m3u8"))
      .on("end", resolve)
      .on("error", reject)
      .run()
  })
}
// Generates: playlist.m3u8 + segment001.ts, segment002.ts, etc.
// Serve with any HTTP server — browsers can play via <video src="playlist.m3u8">

Probe video metadata

import ffmpeg from "fluent-ffmpeg"

function getVideoInfo(videoPath: string): Promise<ffmpeg.FfprobeData> {
  return new Promise((resolve, reject) => {
    ffmpeg.ffprobe(videoPath, (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}

const info = await getVideoInfo("video.mp4")
// {
//   streams: [
//     { codec_type: "video", width: 1920, height: 1080, r_frame_rate: "30/1", duration: "120.5" },
//     { codec_type: "audio", codec_name: "aac", sample_rate: "44100", channels: 2 },
//   ],
//   format: { duration: "120.5", size: "52428800", bit_rate: "3477333" },
// }

const videoStream = info.streams.find((s) => s.codec_type === "video")
console.log(`${videoStream?.width}x${videoStream?.height} @ ${videoStream?.r_frame_rate}`)

@ffmpeg/ffmpeg (WASM)

@ffmpeg/ffmpeg — FFmpeg in WebAssembly:

Browser usage

// Browser — no server needed for client-side video processing:
import { FFmpeg } from "@ffmpeg/ffmpeg"
import { fetchFile, toBlobURL } from "@ffmpeg/util"

const ffmpeg = new FFmpeg()

// Load WASM (must be called before use):
await ffmpeg.load({
  coreURL: await toBlobURL("/ffmpeg-core.js", "text/javascript"),
  wasmURL: await toBlobURL("/ffmpeg-core.wasm", "application/wasm"),
})

// Convert video (in browser, no upload needed):
ffmpeg.on("log", ({ message }) => console.log(message))
ffmpeg.on("progress", ({ progress }) => console.log(`${(progress * 100).toFixed(1)}%`))

// Write input file to WASM virtual filesystem:
await ffmpeg.writeFile("input.mp4", await fetchFile(videoFile))  // File from <input type="file">

// Run FFmpeg command:
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libvpx-vp9", "output.webm"])

// Read output:
const data = await ffmpeg.readFile("output.webm")
const url = URL.createObjectURL(new Blob([data], { type: "video/webm" }))
// Use url as video src

Node.js usage

// @ffmpeg/ffmpeg also works in Node.js (but fluent-ffmpeg is usually better):
import { FFmpeg } from "@ffmpeg/ffmpeg"
import { fetchFile } from "@ffmpeg/util"
import { readFile, writeFile } from "fs/promises"

const ffmpeg = new FFmpeg()
await ffmpeg.load()

const inputData = await readFile("input.mp4")
await ffmpeg.writeFile("input.mp4", inputData)
await ffmpeg.exec(["-i", "input.mp4", "-ss", "00:00:05", "-frames:v", "1", "thumb.jpg"])

const thumbData = await ffmpeg.readFile("thumb.jpg")
await writeFile("thumbnail.jpg", thumbData)

WASM trade-offs

// Performance comparison for a 1-minute 720p video conversion:
// fluent-ffmpeg (native):     ~8 seconds
// @ffmpeg/ffmpeg (WASM):      ~45-120 seconds (5-15x slower)

// When to accept the WASM slowdown:
// ✅ Browser-side — no server upload needed
// ✅ No binary installation required
// ✅ Serverless functions where binary isn't available
// ❌ Real-time video processing
// ❌ High-volume batch processing

node-video-lib

node-video-lib — lightweight MP4/FLV reader:

Read video metadata (without FFmpeg)

import * as VideoLib from "node-video-lib"
import fs from "fs"

// Read MP4 metadata — no FFmpeg required, very fast:
const fd = fs.openSync("video.mp4", "r")
const movie = VideoLib.MovieParser.parse(fd)

console.log("Duration:", movie.relativeDuration())  // In seconds
console.log("Resolution:", `${movie.videoTrack()?.width}x${movie.videoTrack()?.height}`)
console.log("FPS:", movie.videoTrack()?.frameRate)

// List video tracks:
const videoTrack = movie.videoTrack()
const audioTrack = movie.audioTrack()

console.log("Video codec:", videoTrack?.extraData)
console.log("Audio channels:", audioTrack?.channels)
console.log("Sample rate:", audioTrack?.sampleRate)
console.log("Total samples:", movie.videoTrack()?.samples.length)

fs.closeSync(fd)

Extract raw video packets (without re-encoding)

import * as VideoLib from "node-video-lib"
import fs from "fs"

// Create an HLS-compatible MPEG-TS segment from MP4 frames:
// This is FAST because it's just repackaging, not re-encoding

const fd = fs.openSync("source.mp4", "r")
const movie = VideoLib.MovieParser.parse(fd)

const fragment = VideoLib.FragmentReader.readFragment(fd, movie, 0, 10)
// fragment: 0-10 second slice as a Fragment object

// Write as MPEG-TS segment (for HLS):
const output = fs.createWriteStream("segment.ts")
const encoder = new VideoLib.HLSPacketizer(fragment)
encoder.pipe(output)

fs.closeSync(fd)

Feature Comparison

Featurefluent-ffmpeg@ffmpeg/ffmpegnode-video-lib
Transcoding✅ All formats✅ (slow WASM)
Thumbnail
HLS output✅ (repackage)
Browser support
Native binary req.✅ FFmpeg
Performance⚡ Native speed5-15x slower⚡ Fastest
Metadata reading✅ (ffprobe)
TypeScript✅ @types
MP4 support
FLV support
Bundle size~10KB (+binary)~30MB (WASM)~80KB

When to Use Each

Choose fluent-ffmpeg if:

  • Server-side video transcoding, conversion, streaming
  • Production video pipeline (batch processing, thumbnails, HLS)
  • You can install FFmpeg on the server or bundle via ffmpeg-static
  • Maximum format support and codec options

Choose @ffmpeg/ffmpeg if:

  • Client-side video processing in the browser (no upload required)
  • Serverless environments where FFmpeg binary isn't available
  • Small-scale one-off conversions where speed isn't critical

Choose node-video-lib if:

  • Only need to read metadata or repackage MP4/FLV without re-encoding
  • You want zero binary dependencies and maximum read speed
  • Reading video files for index building, content analysis

Methodology

Download data from npm registry (weekly average, February 2026). Performance benchmarks are approximate for 720p 1-minute video conversion. Feature comparison based on fluent-ffmpeg v2.x, @ffmpeg/ffmpeg v0.12.x, and node-video-lib v3.x.

Compare media processing packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.