Stream video thumbnails with cURL and FFmpeg pipes

Extracting thumbnails from remote videos usually means downloading the whole file—hardly ideal when those files weigh hundreds of megabytes or more. Instead, you can ask the server for only the bytes you actually need, pipe them straight into FFmpeg, and grab the frame you’re after, all without hitting your bandwidth budget.
Understand HTTP range requests with cURL
An HTTP range request lets a client fetch a byte slice of a resource rather than the full object.
Most modern web servers support this with the Accept-Ranges: bytes
response header.
First, make sure the origin supports ranges:
curl -fsSL -I -H "Range: bytes=0-0" https://example.com/video.mp4 \
| grep -i "^accept-ranges" || echo "Server does not advertise range support"
If you see Accept-Ranges: bytes
, you’re good to go. To fetch, say, the first 1 MiB, you can use
cURL’s --range
(-r
) flag instead of manually setting the header:
curl -fsSL -r 0-1048575 https://example.com/video.mp4 -o head.mp4
The -r
flag is easier to read, and cURL will fall back gracefully if the server ignores the
header.
Pipe partial video data into FFmpeg
FFmpeg can read from standard input (pipe:0
). Because a stream has no filename, you often need to
specify the container with -f
so FFmpeg doesn’t have to guess:
curl -fsSL https://example.com/video.mp4 | \
ffmpeg -hide_banner -loglevel error \
-f mp4 -i pipe:0 \
-ss 00:00:10 -frames:v 1 -f image2 thumbnail.jpg
That single command downloads the file, seeks to 10 seconds, and outputs a JPEG—without ever touching the disk.
Calculate an appropriate byte range
How many bytes you need depends on bitrate, timestamp, and a small buffer for container metadata and the keyframe nearest your seek position. A quick back-of-the-envelope formula is:
bytes ≈ seconds × bitrate(B/s) + buffer
With bitrate in bytes per second (1 Mb/s ≈ 125,000 B/s) and a 1 MiB buffer:
# Thumbnail at 10 s from a 5 mb/s h.264 MP4 stream
BITRATE_BPS=$((5 * 125000)) # 625,000 B/s
SECONDS=10
BUFFER=$((1 * 1024 * 1024)) # 1,048,576 B
BYTES_NEEDED=$((SECONDS * BITRATE_BPS + BUFFER))
# Bytes_needed = 7,298,576
You can now request just that slice:
curl -fsSL -r 0-${BYTES_NEEDED} https://example.com/video.mp4 | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 -ss 00:00:10 -frames:v 1 -f image2 thumb.jpg
Automate the workflow with a bash helper
Below is a 25-line script that calculates the byte range and retries on transient network errors. It assumes an average bitrate you pass in; if you don’t know the bitrate, err on the high side:
#!/usr/bin/env bash
set -euo pipefail
VIDEO_URL=${1:-}
TIMESTAMP=${2:-00:00:05} # HH:MM:SS[.ms]
BITRATE_MBPS=${3:-5} # average megabits-per-second
OUT=${4:-thumbnail.jpg}
[[ -z "$VIDEO_URL" ]] && {
echo "Usage: $0 <url> [timestamp] [bitrate_mbps] [out]" >&2
exit 1
}
# Convert timestamp → seconds
IFS=: read -r H M S <<< "$TIMESTAMP"
SECONDS=$((10#$H * 3600 + 10#$M * 60 + ${S%.*}))
BYTES=$(((SECONDS * BITRATE_MBPS * 125000) + 1048576)) # +1 MiB buffer
RANGE_HEADER="--range 0-$BYTES"
# Check range support; fall back to full download if missing
if ! curl -fsSL -I -H "Range: bytes=0-0" "$VIDEO_URL" | grep -qi "^accept-ranges: bytes"; then
RANGE_HEADER="" # server ignores ranges
fi
echo "Fetching $VIDEO_URL ($RANGE_HEADER)"
curl -fsSL --retry 3 $RANGE_HEADER "$VIDEO_URL" | \
ffmpeg -hide_banner -loglevel error \
-f mp4 -i pipe:0 -ss "$TIMESTAMP" -frames:v 1 -q:v 2 -f image2 "$OUT"
echo "Thumbnail saved to $OUT"
Handle different container formats
Not every format seeks the same way. Adjust the buffer sizes below to avoid partial-file errors.
MP4
The moov atom often lives at the start of modern MP4s—great news for ranges:
curl -fsSL -r 0-5242879 https://example.com/video.mp4 | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 -ss 00:00:05 -frames:v 1 -f image2 thumb.jpg
WebM
WebM headers are lighter, but keyframes can be spaced widely, so a larger slice helps:
curl -fsSL -r 0-10485759 https://example.com/video.webm | \
ffmpeg -hide_banner -loglevel error -f webm -i pipe:0 -ss 00:00:05 -frames:v 1 -f image2 thumb.jpg
MKV
Matroska is flexible but not optimised for byte-range seeking. Expect to pull down more data:
curl -fsSL -r 0-15728639 https://example.com/video.mkv | \
ffmpeg -hide_banner -loglevel error -f matroska -i pipe:0 -ss 00:00:05 -frames:v 1 -f image2 thumb.jpg
Performance best practices
• For clips under roughly 100 MB, downloading the whole file can be quicker than a series of ranged requests.
• Use a 1 MiB buffer for SD, 2 MiB for HD, and 5 MiB for 4 K to catch metadata and the nearest keyframe.
• When you need multiple thumbnails from one file, avoid repeated downloads. FFmpeg’s select
filter lets you pull several frames in one pass:
curl -fsSL -r 0-15728639 https://example.com/video.mp4 | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 \
-vf "select=eq(n\,150)+eq(n\,300)+eq(n\,450)" -vsync vfr -f image2 thumb_%02d.jpg
Real-world use cases
Video preview grids
curl -fsSL -r 0-20971519 https://example.com/video.mp4 | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 \
-vf "select='not(mod(n,300))',scale=160:90,tile=4x3" \
-frames:v 1 -f image2 preview.jpg
On-demand thumbnails for streaming platforms
HLS and DASH segments are small, static files; you can pipe the most recent segment into FFmpeg and process it just like any other video data:
curl -fsSL https://example.com/live/segment-123.ts | \
ffmpeg -hide_banner -loglevel error -f mpegts -i pipe:0 -frames:v 1 -f image2 live_thumb.jpg
Parallel batch processing
GNU Parallel can farm out many URLs at once:
parallel -j 4 'curl -fsSL -r 0-6291455 {1} | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 -ss 00:00:10 -frames:v 1 -f image2 {1/.}_thumb.jpg' ::: \
https://cdn.example.com/a.mp4 \
https://cdn.example.com/b.mp4 \
https://cdn.example.com/c.mp4
Troubleshoot common issues
• Corrupted images → Increase the byte range or confirm you used the correct -f
container
flag.
• Slow downloads → Pull from a CDN close to you and keep ranges small. Add --compressed
to
cURL so the HTTP layer can gzip-compress headers.
• FFmpeg can’t detect the format → Force the demuxer and output format:
curl -fsSL "$VIDEO_URL" | \
ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 -frames:v 1 -c:v mjpeg -f image2 thumb.jpg
Wrap-up
Byte-range requests plus FFmpeg pipes give you fast, disk-free thumbnail extraction that scales from one-off scripts to large media pipelines.
If you’d prefer an off-the-shelf service, check out Transloadit’s /video/thumbs Robot. It handles format detection, retries, and scaling, so you can focus on building your product.