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.