ffmpegvideo-filtersautomation

How to Add a Progress Bar to Video with FFmpeg

·Javid Jamae·9 min read
How to Add a Progress Bar to Video with FFmpeg

Search "ffmpeg progress bar" and half the results are about watching an encode finish in your terminal. That's not what you want when you're trying to burn a retention bar into 200 Reels. You want a colored rectangle that grows across the frame as the clip plays.

Quick answer: To add an ffmpeg progress bar overlaid on a video, read the clip's duration with ffprobe, then use filter_complex to stack two color sources: a static background bar and a foreground bar that slides in from the left with overlay=x='-w+(t/DURATION)*w'. FFmpeg has no built-in duration variable in filter expressions, so the duration value must be substituted into the command before it runs.

Conventional wisdom says an animated progress bar is an editing job. Open CapCut or After Effects, keyframe a rectangle's width, render. That's fine for one video. But the bar isn't an animation. It's a solid rectangle whose x position is a function of the current timestamp, and FFmpeg evaluates that function per frame for free. Once you see it that way, the whole thing is one filter graph and it batches.

The FFmpeg progress bar recipe

Two layers: a dim background strip that shows the full track, and a bright foreground strip that slides into view. The foreground bar is as wide as the video and starts fully off-screen at x = -w, moving right until its left edge hits 0.

1. Read the duration and width

DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 in.mp4)
W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 in.mp4)
echo "$DUR $W"   # 6.024000 1080

Both values get pasted into the filter graph. If you're new to pulling metadata this way, ffprobe: how to inspect video metadata before processing covers the other fields worth reading first.

2. Run the overlay

ffmpeg -i in.mp4 -filter_complex \
"color=c=0x000000@0.45:s=${W}x14[bg];\
 color=c=0xFF2D55:s=${W}x14[fg];\
 [0:v][bg]overlay=x=0:y=H-14:shortest=1[base];\
 [base][fg]overlay=x='-w+(t/${DUR})*w':y=H-14:shortest=1[out]" \
-map "[out]" -map "0:a?" -c:v libx264 -crf 20 -preset veryfast -c:a copy out.mp4

I ran this on a 6.024-second 1080x1920 clip. At t=1s the bar is filled to x=179 (1/6.024 of 1080), and at t=5s it's filled to x=896. The math holds frame by frame, no keyframes involved.

3. What each piece does

  • color=c=0x000000@0.45 is the track. The @0.45 alpha is honored through overlay without any format=rgba step. A pixel that was 00ff00 in the source came out 008e00, exactly 55% brightness.
  • s=${W}x14 sizes both strips. The width must match the video width or the timing breaks (see pitfalls).
  • y=H-14 pins the bar to the bottom. H is the main video height, w inside the x expression is the overlay's own width.
  • t is the current timestamp in seconds. This is the only moving part.
  • shortest=1 ends the output when the video ends. color sources are infinite.
  • -map "0:a?" keeps the audio if there is any, and the ? makes it optional so silent clips don't fail.

Position, height, and color for TikTok, Reels, and Shorts

Bottom placement is the default habit, and on vertical social it's the wrong one. TikTok stacks the caption, username, and music ticker over roughly the bottom 150 px, and Instagram Reels covers even more. A 14 px bar at y=H-14 on a 1080x1920 export sits underneath all of it.

Placementy valueUse when
Bottom edge`y=H-14`16:9 YouTube, embedded players, LinkedIn
Top edge`y=0`TikTok, Reels, Shorts (clears the caption stack)
Above the caption zone`y=H-220`Vertical clips with burned-in subtitles

For height, 12 to 16 px reads clearly on a 1080-wide export without stealing frame. Below 8 px it disappears after platform re-compression. Color takes any FFmpeg color syntax: a name (red), hex (0xFF2D55), or hex with alpha (0xFF2D55@0.8). Quote the whole filter string if you use #RRGGBB, since # starts a comment in most shells.

Pairing the bar with burned-in text is common. The FFmpeg drawtext filter guide covers adding a countdown or hook line in the same pass so you only re-encode once.

Common pitfalls

You forgot to substitute the duration. Copying DURATION straight out of a Stack Overflow answer gives you this:

[Parsed_overlay_1 @ 0x...] [Eval @ 0x...] Undefined constant or missing '(' in 'DURATION)*w'
[Parsed_overlay_1 @ 0x...] Error when evaluating the expression '-w+(t/DURATION)*w' for x

There is no DURATION or duration constant in overlay expressions. The shell has to expand a real number into the string before FFmpeg ever sees it.

You dropped shortest=1. The color source never ends, so neither does your output. A 6-second source with no shortest=1 produced a 20-second file when I capped it with -t 20. Without that cap it runs until the disk fills.

The bar width doesn't match the video width. I set s=1920x14 on a 1080-wide clip. The bar looked full at 3.5 seconds of a 6.02-second video, because the fill rate scales with the bar's own width, not the frame's. Always derive the size from ffprobe, never hardcode 1920.

zsh eats the optional audio map. On macOS, -map 0:a? unquoted returns no matches found: 0:a? because zsh treats ? as a glob. Quote it: -map "0:a?".

The drawbox one-liner needs a recent FFmpeg. You'll see drawbox=...:eval=frame suggested as a shorter alternative. The eval option landed in FFmpeg 5.0. On FFmpeg 3.3.3 it fails with Option 'eval' not found, and without eval=frame the width expression is computed once at init, so you get a static bar instead of a moving one. The two-overlay recipe above works on every build I've tested going back to 3.x.

That other ffmpeg progress bar: terminal encode progress

Different problem, different tools. ffpb is a Python wrapper (pip install ffpb) that swaps FFmpeg's scrolling stats line for a tqdm progress bar in your terminal. For scripts, ffmpeg -progress pipe:1 -nostats emits machine-readable key=value lines including out_time_ms and speed, which you can parse to drive your own progress UI.

Neither one draws anything into the video. If your goal is a bar viewers can see, you need the overlay filter. If your goal is knowing when a batch job finishes, use -progress or a webhook.

Doing this on 200 clips: CLI vs one API call

The filter graph is cheap to write once and annoying to operate at volume. A 60-second 1080x1920 clip took 9.96 seconds wall clock on a 6-core machine at -crf 20 -preset veryfast. Two hundred clips run serially is about 33 minutes of a machine you can't use for anything else, plus the ffprobe call, the temp files, and the retry logic when one input is corrupt.

Local FFmpegFFmpeg Micro
SetupInstall FFmpeg, match versions across machinesNone, it's an HTTP call
Duration lookupSeparate `ffprobe` shell-out per fileHandled in the job
200 clips~33 min serial on one boxParallel jobs, no servers to run
Failure handlingYour own retry wrapperJob status plus webhook
From n8n / Make / ZapierRequires a self-hosted node with a binaryNative HTTP node

The API version is a job submission and a result:

curl -X POST https://api.ffmpeg-micro.com/v1/jobs \
  -H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "https://cdn.example.com/clips/reel-042.mp4",
    "filter_complex": "color=c=0x000000@0.45:s=1080x14[bg];color=c=0xFF2D55:s=1080x14[fg];[0:v][bg]overlay=x=0:y=0:shortest=1[base];[base][fg]overlay=x='\''-w+(t/DUR)*w'\'':y=0:shortest=1[out]",
    "output": { "format": "mp4" }
  }'

Submit the job, poll for status or take a webhook, download the output. Check the docs for the exact parameter names and the filter options available on your plan. In n8n, that's an HTTP Request node to submit and a Wait node on the webhook, which sidesteps the n8n timeout problem that bites people who try to run video work inline.

When not to use this

Skip the FFmpeg approach if you need a rounded bar with a gradient fill, a moving thumb, or per-section chapter segments. FFmpeg draws rectangles well and rounded corners badly. Build the bar as a transparent PNG sequence or an APNG in your design tool and overlay that instead.

Skip it entirely if the bar needs to reflect real playback position rather than clip time. A player-controlled scrubber belongs in your video player's UI, not burned into the pixels. Burned-in bars only make sense on autoplay feeds where there's no scrubber, which is exactly why they work on TikTok and Reels.

FAQ

How do I add a progress bar to a video with FFmpeg?

Read the duration with ffprobe, then run a filter_complex with two color sources overlaid on the video: a static background strip and a foreground strip positioned at x='-w+(t/DURATION)*w' so it slides in as the timestamp advances. Substitute the real duration number into the expression before running.

Does FFmpeg have a built-in duration variable for filters?

No. Filter expressions expose t (current timestamp in seconds), n (frame number), W/H (main input size), and w/h (overlay size), but not total duration. That's why every working progress bar recipe shells out to ffprobe first.

Why is my FFmpeg progress bar not moving?

Almost always because the width expression was evaluated once at filter init instead of per frame. With drawbox you need eval=frame and FFmpeg 5.0 or newer. With the overlay recipe the x expression is evaluated per frame by default, so a frozen bar there usually means the duration substituted into the expression was much larger than the actual clip length.

Can I add a progress bar in n8n or Make without installing FFmpeg?

Yes. Send the clip URL and the filter string to a hosted FFmpeg API from an HTTP Request node and take the result on a webhook. No binary in your workflow container, no version drift between your laptop and your server. The cost comparison of running FFmpeg yourself breaks down where the crossover point lands.

What color and height should the bar be?

12 to 16 px tall on a 1080-wide export, in a saturated color that survives platform re-compression. Put it at y=0 for TikTok, Reels, and Shorts so it clears the caption and button overlays, and at y=H-14 for YouTube and embedded players.

You can paste that filter string into the playground and watch the bar render on your own clip before writing any code. When you're ready to run it across a batch, sign up free and point the job at your clip URLs.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free