After Effects is a $55/month tax on every video project. You pay whether you use it once a quarter or every day. And if you need to regenerate a video with updated copy, new footage, or a different colour grade, you're back in the compositor's chair tweaking timelines. But what if you could define a video template once — concat logic, text overlays, audio levels, effects — and then feed it different inputs (footage, captions, audio tracks) to spit out fully rendered outputs? That's what Aidxn does with FFmpeg orchestration. Node.js pipeline, input JSON, rendered MP4 in 90 seconds. No UI. No human in the loop. Just repeatability.
FFmpeg is 20 years old, runs on every OS, and is probably already on your machine. But most developers think of it as a CLI tool for converting codecs. They don't realise you can automate entire video workflows — concat with dissolves, burn animated text, sync audio, colour-grade, extract stills — all from JavaScript. This is how production studios at scale handle batch video generation. Aiden's been doing it for 6 months to ship client deliverables (animated case studies, product demos, kinetic typography sequences) in hours instead of weeks. And the payoff is ridiculous: $0/month, 100% reproducible, and you can regenerate entire video libraries with a code change.
What FFmpeg Pipelines Actually Give You
FFmpeg is a command-line video encoder. You can call it from Node.js using the `fluent-ffmpeg` npm package. Instead of building UI, you write a JavaScript template that composes FFmpeg commands. Feed it JSON input (video files, text, audio, timings). Out comes a finished video.
This isn't "batch processing a folder of videos". This is programmatic video composition. Your code becomes the compositor. You define where clips play, when text overlays appear, how audio mixes, what the final aspect ratio is. Change one parameter in your JSON, run the script again, get a new video with the updated parameter. No timeline scrubbing. No human decisions. Just deterministic video generation.
Why this matters: repeatability at scale. Aidxn ships 20+ animated case studies a year for clients. Each one is a 60–90 second MP4 with voiceover, animated text callouts, background music, and a logo stamp. If every video required hand-compositing in After Effects, that's 20–30 hours of timeline tweaking. Instead, Aiden's pipeline automates 90% of it. You define the template (text positions, timing, audio mixing rules). Change client name + voiceover file + brand colours in one JSON. The pipeline renders a finished MP4 in 2 minutes. That's the difference between a $4k/month After Effects habit and $0/month in tooling.
Setup: Node.js + fluent-ffmpeg
Install the orchestrator library:
npm install fluent-ffmpeg ffmpeg-static
Test that FFmpeg is available:
const ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
ffmpeg.setFfmpegPath(ffmpegStatic);
// Test with -version
ffmpeg.getAvailableCodecs((err, codecs) => {
if (err) console.error('FFmpeg error:', err);
else console.log('FFmpeg is ready. Codecs:', Object.keys(codecs).length);
});
That's it. FFmpeg is now callable from your Node.js app. Now you just need to define video composition templates.
Pattern 1: Concat + Crossfade (The Bread-and-Butter Move)
Most video workflows start here: take multiple clips (intro, main content, outro) and concatenate them with smooth transitions.
const ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
ffmpeg.setFfmpegPath(ffmpegStatic);
async function concatWithCrossfade(clips, outputPath, fadeDuration = 1.0) {
// clips = [ { file: 'intro.mp4', duration: 5 }, { file: 'main.mp4', duration: 30 }, ... ]
// Crossfade = 1 second overlap between clips
return new Promise((resolve, reject) => {
const filter = buildCrossfadeFilter(clips, fadeDuration);
let cmd = ffmpeg();
clips.forEach(clip => {
cmd = cmd.input(clip.file);
});
cmd
.complexFilter(filter)
.output(outputPath)
.on('end', () => {
console.log(`✓ Concat complete: ${outputPath}`);
resolve();
})
.on('error', reject)
.run();
});
}
function buildCrossfadeFilter(clips, fadeDuration) {
// FFmpeg filter_complex syntax for crossfade
// [0]...[1]...[2] are video inputs
// xfade=transition=fade:duration=1:offset=
Now you've got a 53-second video (5 + 45 + 3) with 1-second fade transitions between each clip. No After Effects. Just FFmpeg compositing the clips with a crossfade filter. Scale this to 100 videos and the time savings are stupid.
Pattern 2: Drawtext Overlays (Kinetic Typography)
Burn animated text directly onto video without a separate text layer. FFmpeg's `drawtext` filter lets you position, time, and animate captions:
const ffmpeg = require('fluent-ffmpeg');
async function addTextOverlays(inputVideo, outputVideo, textConfig) {
// textConfig = array of { text, start, duration, x, y, fontSize, color }
return new Promise((resolve, reject) => {
const drawfilters = textConfig
.map((text, i) => buildDrawtextFilter(text, i))
.join(',');
ffmpeg(inputVideo)
.complexFilter(drawfilters)
.output(outputVideo)
.on('end', () => {
console.log(`✓ Text overlay complete: ${outputVideo}`);
resolve();
})
.on('error', reject)
.run();
});
}
function buildDrawtextFilter(textObj, index) {
const { text, start, duration, x, y, fontSize = 40, color = 'white' } = textObj;
// FFmpeg drawtext syntax
// enable=between(t,start,end): text only shows during that time window
// fontfile, fontsize, fontcolor, shadowx/shadowy for readability
const filter = `drawtext=
fontfile=/System/Library/Fonts/Inter-Bold.otf:
text='${text}':
fontsize=${fontSize}:
fontcolor=${color}:
shadowx=2:shadowy=2:shadowcolor=black@0.8:
x=${x}:
y=${y}:
enable='between(t,${start},${start + duration})'`;
return filter.replace(/\n\s+/g, '');
}
// Usage: Kinetic typography on a 60-second video
const textOverlays = [
{ text: 'Hail Damage Detected', start: 0, duration: 3, x: 'w*0.1', y: 'h*0.2', fontSize: 48, color: 'red' },
{ text: 'Processing claim...', start: 3.5, duration: 2, x: 'w*0.1', y: 'h*0.3', fontSize: 36, color: 'white' },
{ text: 'Approved in 24h', start: 5.5, duration: 3, x: 'w*0.1', y: 'h*0.4', fontSize: 52, color: 'green' }
];
addTextOverlays('input.mp4', 'output-with-text.mp4', textOverlays);
Now you've burned kinetic typography directly into the video. Text appears at frame 0, stays for 3 seconds, disappears. Next text fades in at 3.5s. All timed, positioned, and styled via JSON. Change the text once in your config, regenerate the video instantly. No timeline scrubbing.
Pattern 3: Audio Normalisation (The Invisible Magic)
Raw voiceovers are rarely consistent. Some clips are loud, some quiet. FFmpeg's `loudnorm` filter analyzes the audio and normalizes it to broadcast standard (-23 LUFS for YouTube):
async function normaliseAudio(inputVideo, outputVideo, targetLUFS = -16) {
// LUFS = Loudness Units relative to Full Scale
// -16 LUFS = typical streaming standard (YouTube, Netflix)
// -23 LUFS = broadcast standard
return new Promise((resolve, reject) => {
ffmpeg(inputVideo)
.audioFilter(`loudnorm=I=${targetLUFS}:TP=-1.5:LRA=11`)
.output(outputVideo)
.on('end', () => {
console.log(`✓ Audio normalised to ${targetLUFS} LUFS: ${outputVideo}`);
resolve();
})
.on('error', reject)
.run();
});
}
// Usage
normaliseAudio('raw-voiceover.mp4', 'normalized.mp4', -16);
One filter. Consistent audio across all clips. No more jumping volume. Viewers don't have to touch their speaker knob between scenes.
Pattern 4: Thumbnail Extraction (Web Previews)
After rendering video, extract 5 keyframe thumbnails for web galleries, social previews, or YouTube custom thumbnails:
async function extractThumbnails(videoFile, outputDir, count = 5) {
// Extract evenly-spaced thumbnails from a video
return new Promise((resolve, reject) => {
ffmpeg(videoFile)
.screenshots({
count: count,
folder: outputDir,
filename: 'thumb-%03d.png',
size: '1280x720'
})
.on('end', () => {
console.log(`✓ ${count} thumbnails extracted to ${outputDir}`);
resolve();
})
.on('error', reject);
});
}
// Usage
extractThumbnails('case-study.mp4', './thumbnails', 5);
Now you've got 5 evenly-spaced PNG frames from the video at 1280×720. Use the middle one for the YouTube thumbnail. Use all 5 for a carousel preview. All automated.
Full Orchestrator Template (Put It Together)
Here's a production-ready orchestrator that handles the entire pipeline: concat → text overlay → audio normalise → thumbnail extract:
const ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
const fs = require('fs');
const path = require('path');
ffmpeg.setFfmpegPath(ffmpegStatic);
async function renderCaseStudy(config) {
// config = { name, clips, textOverlays, voiceover, outputDir, ... }
try {
const tempDir = path.join(config.outputDir, 'temp');
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
console.log(`\n🎬 Rendering: ${config.name}`);
// Step 1: Concat clips
const concatOutput = path.join(tempDir, '01-concat.mp4');
await concatWithCrossfade(config.clips, concatOutput, 1.0);
// Step 2: Add text overlays
const textOutput = path.join(tempDir, '02-text.mp4');
await addTextOverlays(concatOutput, textOutput, config.textOverlays);
// Step 3: Mix with voiceover
const audioOutput = path.join(tempDir, '03-with-audio.mp4');
await mixAudio(textOutput, config.voiceover, audioOutput);
// Step 4: Normalise audio
const normOutput = path.join(tempDir, '04-normalized.mp4');
await normaliseAudio(audioOutput, normOutput, -16);
// Step 5: Extract thumbnails
const thumbDir = path.join(config.outputDir, 'thumbs');
if (!fs.existsSync(thumbDir)) fs.mkdirSync(thumbDir, { recursive: true });
await extractThumbnails(normOutput, thumbDir, 5);
// Step 6: Move final output
const finalOutput = path.join(config.outputDir, `${config.name}.mp4`);
fs.renameSync(normOutput, finalOutput);
// Cleanup temp files
fs.rmSync(tempDir, { recursive: true });
console.log(`✅ Complete: ${finalOutput}`);
console.log(`Thumbnails: ${thumbDir}`);
return { video: finalOutput, thumbnails: thumbDir };
} catch (err) {
console.error('❌ Render failed:', err.message);
throw err;
}
}
// Usage
const caseStudyConfig = {
name: 'rebuild-relief-hail-damage',
clips: [
{ file: 'footage/intro.mp4', duration: 5 },
{ file: 'footage/drone-damage.mp4', duration: 20 },
{ file: 'footage/before-after.mp4', duration: 25 },
{ file: 'footage/outro.mp4', duration: 3 }
],
textOverlays: [
{ text: '2024 Hail Storm Response', start: 0, duration: 5, x: 'w*0.1', y: 'h*0.85', fontSize: 48 },
{ text: '47 Claims Processed in 72h', start: 8, duration: 6, x: 'w*0.1', y: 'h*0.75', fontSize: 42 }
],
voiceover: 'audio/voiceover.mp3',
outputDir: './outputs'
};
renderCaseStudy(caseStudyConfig);
Run once, get a finished case study video with text, audio, normalized levels, and web-ready thumbnails. Change the voiceover file, run again, get a new video in 2 minutes. Scale to 50 case studies and you've just saved 40+ hours of compositor time.
Six FAQs
Can I use FFmpeg on a server?
Yes. Install FFmpeg on your server, run orchestrator scripts via cron or API endpoints. Generate videos on-demand or batch. Netlify Edge Functions can't run FFmpeg (too heavy), but a Node.js backend, AWS Lambda (with custom layer), or DigitalOcean droplet handles it fine.
What codecs should I use?
H.264 (`libx264`) for compatibility and file size. VP9 (`libvpx-vp9`) if you need smaller files. ProRes 422 (`prores`) if you need archive-quality masters. For web, H.264 at 1080p@25fps with 5000kbps bitrate is the sweet spot: fast encoding, broad device support, reasonable file size.
Can I add effects like blur, color grade, or distortion?
Yes. FFmpeg has 300+ filters. `boxblur`, `hue`, `colorspace`, `scale`, `rotate`, `overlay`, `fade`, `zoom`, etc. Chain them in your filter graph: `video=hue=h=30:boxblur=10`. Anything After Effects does, FFmpeg can do — it's just text instead of a UI.
How long does encoding take?
Depends on resolution, codec, and bitrate. A 60-second 1080p H.264 render typically takes 30–90 seconds on modern hardware. VP9 is 3–5× slower. ProRes is 2–3× slower. Start with H.264, benchmark your typical pipeline, then optimize if needed.
Can I use custom fonts?
Yes. In drawtext, use `fontfile=/path/to/font.ttf`. On Linux, fonts live in `/usr/share/fonts`. On macOS, `/Library/Fonts`. On Windows, `C:\Windows\Fonts`. Specify the full path or FFmpeg won't find it. (Pro tip: Use absolute paths in your orchestrator so scripts are portable.)
What if I need real-time video compositing?
FFmpeg is batch-oriented. For real-time (streaming VJ rigs, live overlays), use WebGL or GPU-accelerated tools. But for pre-recorded content, automated compositing, or batch rendering, FFmpeg is unbeatable at scale and cost.
The Bottom Line
After Effects costs $55/month because it's powerful, but that power is locked behind a UI. FFmpeg is free, and its real power is that you can automate it. You define a video template once. Write a Node.js orchestrator. Feed it JSON config. Get a finished video. Change one line of JSON, get a new video. Do that 50 times a year and you've saved your studio weeks of timeline-scrubbing and hundreds of dollars in subscription fees.
Aiden uses this pattern for every branded video deliverable: animated case studies, product demos, kinetic typography sequences, dashboard recordings. One template. Infinite outputs. The upfront time investment (building the orchestrator) pays off the moment you regenerate a second video with it. For the visual side of video — cinematography, shot composition, storytelling — you still need humans and cameras. But for the compositing, timing, effects, and automation layer, FFmpeg handles all of it. To explore how this fits into a larger motion design workflow, check how Velocity X scales creative operations.