fluent-ffmpeg vs @ffmpeg/ffmpeg vs node-video-lib: Video Processing in Node.js (2026)
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-staticpackage bundles FFmpeg binary for easy deployment- For cloud functions:
ffmpeg-staticbundles 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
| Feature | fluent-ffmpeg | @ffmpeg/ffmpeg | node-video-lib |
|---|---|---|---|
| Transcoding | ✅ All formats | ✅ (slow WASM) | ❌ |
| Thumbnail | ✅ | ✅ | ❌ |
| HLS output | ✅ | ✅ | ✅ (repackage) |
| Browser support | ❌ | ✅ | ❌ |
| Native binary req. | ✅ FFmpeg | ❌ | ❌ |
| Performance | ⚡ Native speed | 5-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.