Last updated: June 12, 2025

<span aria-hidden="true" id="stream-video-thumbnails-with-curl-and-ffmpeg-pipes"></span>

# Stream video thumbnails with cURL and FFmpeg pipes

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_5eQDcFwLGbzg3PfkSEW2a2j2x5pY)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

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.

<span aria-hidden="true" id="understand-http-range-requests-with-curl"></span>

## 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:

```bash
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:

```bash
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.

<span aria-hidden="true" id="pipe-partial-video-data-into-ffmpeg"></span>

## 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:

```bash
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.

<span aria-hidden="true" id="calculate-an-appropriate-byte-range"></span>

## 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:

```bash
# 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:

```bash
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

```

<span aria-hidden="true" id="automate-the-workflow-with-a-bash-helper"></span>

## 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:

```bash
#!/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"

```

<span aria-hidden="true" id="handle-different-container-formats"></span>

## Handle different container formats

Not every format seeks the same way. Adjust the buffer sizes below to avoid partial-file errors.

<span aria-hidden="true" id="mp4"></span>

### MP4

The moov atom often lives at the start of modern MP4s—great news for ranges:

```bash
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

```

<span aria-hidden="true" id="webm"></span>

### WebM

WebM headers are lighter, but keyframes can be spaced widely, so a larger slice helps:

```bash
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

```

<span aria-hidden="true" id="mkv"></span>

### MKV

Matroska is flexible but not optimised for byte-range seeking. Expect to pull down more data:

```bash
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

```

<span aria-hidden="true" id="performance-best-practices"></span>

## 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:

```bash
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

```

<span aria-hidden="true" id="real-world-use-cases"></span>

## Real-world use cases

<span aria-hidden="true" id="video-preview-grids"></span>

### Video preview grids

```bash
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

```

<span aria-hidden="true" id="on-demand-thumbnails-for-streaming-platforms"></span>

### 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:

```bash
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

```

<span aria-hidden="true" id="parallel-batch-processing"></span>

### Parallel batch processing

GNU Parallel can farm out many URLs at once:

```bash
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

```

<span aria-hidden="true" id="troubleshoot-common-issues"></span>

## 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:

```bash
curl -fsSL "$VIDEO_URL" | \
  ffmpeg -hide_banner -loglevel error -f mp4 -i pipe:0 -frames:v 1 -c:v mjpeg -f image2 thumb.jpg

```

<span aria-hidden="true" id="wrap-up"></span>

## 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](/docs/robots/video-thumbs.md). It handles format detection, retries, and scaling, so you can focus on building your product.

[#devtips](/devtips.md)#curl#ffmpeg#video-processing#bandwidth-optimization#video-encoding-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
