Skip to main content

Guide

fluent-ffmpeg vs @ffmpeg/ffmpeg vs node-video-lib 2026

Compare fluent-ffmpeg, @ffmpeg/ffmpeg (WebAssembly), and node-video-lib for video processing in Node.js. Transcoding, thumbnail generation, streaming, and WASM.

·PkgPulse Team·
0

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

Deploying fluent-ffmpeg in Cloud Environments

The biggest practical challenge with fluent-ffmpeg is ensuring the FFmpeg binary is available in cloud environments where you can't install system packages directly. The ffmpeg-static npm package solves this elegantly: it ships precompiled FFmpeg binaries for macOS, Linux, and Windows as part of the package itself (~70MB), and exposes the binary path as its default export. Setting ffmpeg.setFfmpegPath(ffmpegPath) before any encoding calls is all that's required. This approach works in Docker containers, AWS Lambda (within the 250MB unzipped layer limit), Google Cloud Run, and Fly.io.

AWS Lambda presents specific constraints worth understanding. The unzipped deployment package limit is 250MB, and ffmpeg-static adds ~70MB. If you're using the @ffmpeg-installer/ffmpeg alternative (which uses a smaller, platform-specific binary), you can get this down to ~30MB. Lambda's /tmp directory (512MB by default, up to 10GB with configuration) is the only writable filesystem, so both input and output files must be written there. For processing videos larger than available /tmp space, streaming directly to S3 via Node.js streams and fluent-ffmpeg's pipe output API is the correct pattern.

Serverless GPU-accelerated transcoding via services like AWS MediaConvert or Cloudflare Stream is often a better choice than running fluent-ffmpeg in a Lambda for production video pipelines. fluent-ffmpeg excels when you need low-latency one-off processing (thumbnail generation for user-uploaded content, format checking, metadata extraction) rather than high-volume batch transcoding. For background job queues processing user video uploads, a dedicated EC2 or ECS task with fluent-ffmpeg and hardware-accelerated codecs (NVENC via -c:v h264_nvenc) is more cost-effective at scale.

@ffmpeg/ffmpeg WASM: Browser Use Cases and Limitations

The @ffmpeg/ffmpeg WASM build enables genuinely novel browser-side video processing workflows. The most compelling use case is client-side video conversion before upload: instead of uploading a 4GB ProRes file from a videographer's laptop and transcoding it on a server, you can transcode it to H.264/MP4 in the browser, reducing upload bandwidth by 90% and eliminating server transcoding cost entirely. The 5-15x slower-than-native speed is usually acceptable for this use case since it's happening on the user's hardware, not your server.

Memory limits are the critical constraint for browser WASM processing. The WASM module allocates its own virtual memory space (default 256MB, configurable up to the browser's per-tab memory limit), and both the input and output files must fit within this space simultaneously during processing. A 1GB video file will exceed this limit and require chunked processing — splitting the video into segments before transcoding, then concatenating the output. This adds significant complexity and is generally impractical for very large files in the browser.

Cross-Origin-Embedder-Policy (COEP) and Cross-Origin-Opener-Policy (COOP) headers are required for SharedArrayBuffer, which @ffmpeg/ffmpeg v0.12+ relies on for multi-threading. If your application can't set these headers (common for sites with third-party embeds or content from different origins), you must fall back to the single-threaded build, which is roughly 3x slower again. Vercel and Netlify support setting these headers via configuration files; some CDNs and legacy hosting providers don't.

node-video-lib: Zero-Dependency Metadata for Specialized Workflows

node-video-lib's narrow focus on MP4 and FLV parsing without requiring FFmpeg makes it valuable in specific architectures where introducing a binary dependency is impractical. Content moderation pipelines that need to screen video files for duration, resolution, and frame rate before accepting them for storage can use node-video-lib for instant metadata extraction (microseconds, not seconds) without queuing a full FFmpeg probe job. At the scale where thousands of videos are uploaded per minute, the difference between FFmpeg-based metadata extraction and node-video-lib's pure-JavaScript parser is significant.

The fragment reader and MPEG-TS packager in node-video-lib handle a specific HLS optimization: if your source videos are already H.264/AAC encoded (the most common codec combination), you can generate HLS segments by repackaging the existing encoded frames into MPEG-TS containers without re-encoding. This "transmuxing" operation is effectively a container format change — it preserves the original codec data bit-for-bit. The result is HLS output generated in near-real-time with no quality loss and no CPU-intensive encoding, ideal for live or on-demand streaming scenarios where the source is already H.264.

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 →

In 2026, fluent-ffmpeg remains the standard for server-side video processing in Node.js when FFmpeg is available on the host, while ffmpeg-wasm opens browser-side video editing workflows without any server infrastructure, at the cost of slower performance and a larger bundle.

Progress Reporting and User Feedback

For video processing tasks that run for more than a few seconds, user-facing progress reporting is essential. fluent-ffmpeg exposes an on('progress', handler) event that fires periodically with the current timemark and percent completion — calculated by comparing elapsed frame count against the total frame count from the input file's metadata. This is the most accurate progress signal available without a separate probe step, but it requires that FFmpeg can determine the total duration upfront (it can for most container formats but not for some streaming inputs). @ffmpeg/ffmpeg exposes a setProgress callback that fires with a ratio from 0 to 1 as the WASM operation proceeds, which is useful for browser UI feedback. node-video-lib does not support progress events since its operations are synchronous and typically complete in milliseconds for metadata reads and near-real-time for transmuxing. For UI applications using fluent-ffmpeg on a server, streaming progress back to the client via Server-Sent Events or WebSockets gives users feedback during multi-minute transcodes, which significantly improves perceived quality for file upload workflows.

See also: pm2 vs node:cluster vs tsx watch and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.