Stream remote media to FFmpeg using cURL
Pipe an HTTP download from cURL into FFmpeg to extract thumbnails and audio without keeping a local copy of the input video. The MP4 must support sequential reading. This walkthrough creates a known test clip, serves it over HTTP, and processes that response into JPEGs and a WAV file.
Overview
Use Bash on Linux, cURL, Python 3, and an FFmpeg build with the libx264 and AAC encoders. The
commands below were tested with FFmpeg 6.1.1 and 9.0.1. Python supplies the local HTTP server and
is unnecessary when you already have a suitable remote URL.
The example creates a “faststart” MP4 with its moov metadata before the media data. FFmpeg’s
MP4 documentation explains this
layout. A pipe cannot seek backward, so an ordinary MP4 with metadata at the end can fail even
after cURL has sent the entire file. When the layout is unknown, use the download-first example
below. Adding -f mp4 identifies the format; it does not make the input seekable.
Run each block from the same parent directory in Bash. The parentheses contain shell-option changes in a subshell. Each processing block requires a new output directory and refuses a rerun into an existing one. Failed runs can leave partial files in that new directory; inspect them before using them, and choose a new directory for another attempt.
Setting up the pipeline
Prepare a clip and an HTTP origin
Create a 6.4-second clip: red for two seconds, lime for two seconds, then blue. Its audio is silent until a short 880-Hz tone in the final 0.2 seconds, making an incomplete audio extraction audible.
(
set -euo pipefail
mkdir curl-media-demo
ffmpeg -nostdin -n -hide_banner -loglevel warning -filter_threads 1 \
-f lavfi -i "color=c=red:s=160x90:r=10:d=6.4" \
-f lavfi -i "aevalsrc='if(gte(t,6.2),0.25*sin(2*PI*880*t),0)':s=48000:d=6.4" \
-vf "drawbox=c=lime:t=fill:enable='gte(t,2)',drawbox=c=blue:t=fill:enable='gte(t,4)'" \
-map 0:v:0 -map 1:a:0 -c:v libx264 -pix_fmt yuv420p -threads 1 \
-c:a aac -movflags +faststart curl-media-demo/input.mp4
)
In this terminal, serve only the fixture directory on the loopback address:
(
set -eu
test -f curl-media-demo/input.mp4
python3 -m http.server 8765 --bind 127.0.0.1 --directory curl-media-demo
)
Keep that server running while using a second terminal for the next blocks. If port 8765 is busy, choose an available port and change it in both the server command and the URLs. Stop the server with Ctrl+C when finished.
Extract thumbnails and audio from one download
In the second terminal, return to the same parent directory and run:
(
set -euo pipefail
mkdir outputs
curl -fsSL "http://127.0.0.1:8765/input.mp4" | \
ffmpeg -nostdin -n -hide_banner -loglevel warning -filter_threads 1 -i pipe:0 \
-map 0:v:0 -vf "fps=1" -c:v mjpeg -q:v 2 -threads 1 -frame_pts 1 \
outputs/thumbnail_%03d.jpg \
-map 0:a:0 -c:a pcm_s16le outputs/audio.wav
)
For this fixture, expect six 160×90 JPEGs, named thumbnail_000.jpg through thumbnail_005.jpg.
The first two are red, the next two lime, and the last two blue. Play outputs/audio.wav: it
should be silent until the tone near the end of the 6.4-second recording. AAC decoding can add a
small amount of end padding to the WAV.
Explanation
pipe:0 reads standard input. Explicit -map options select the first video and audio streams,
with one output for each. A video without audio fails the required audio mapping rather than
quietly omitting the WAV. The fresh-directory check protects the image sequence from previous
runs. FFmpeg’s -n protects MP4 and WAV filenames; an image2 sequence can replace individual
JPEGs. Keep the directory guard and run these examples sequentially.
The fps filter drops or duplicates frames to produce
one frame per second. Its default rounding gives six frames for this fixture, so the final
fraction of a second does not get a separate thumbnail. -frame_pts 1 uses output presentation
timestamps as filename numbers, in the output time base. Here that base is one second, so the
numbers represent output times 0–5 seconds. They are not original frame numbers or a guarantee
of the exact source frame chosen. See the
image2 options.
cURL’s -f makes an HTTP error such as 404 fail the transfer, while -sS hides the progress meter
and retains error messages. Bash’s pipefail propagates a failed download even if FFmpeg exits
successfully. Neither option proves that the server sent intact media.
Advanced uses
Select scene changes and record their times
To sample color changes instead of a fixed rate, run a separate download into a new directory:
(
set -euo pipefail
mkdir scenes
curl -fsSL "http://127.0.0.1:8765/input.mp4" | \
ffmpeg -nostdin -n -hide_banner -loglevel warning -filter_threads 1 -i pipe:0 \
-map 0:v:0 \
-vf "select='gt(scene,0.3)',metadata=print:key=lavfi.scene_score:file=scenes/time.txt" \
-fps_mode vfr -c:v mjpeg -q:v 2 -threads 1 scenes/scene_%03d.jpg
)
For this clip, expect scene_001.jpg to be lime and scene_002.jpg to be blue. The corresponding
pts_time entries in scenes/time.txt are 2 and 4 seconds. These are the timestamps of the
selected frames on FFmpeg’s input timeline, which starts at zero for this fixture. They are not
wall-clock capture times, and the sequential JPEG numbers are not timestamps.
The select filter compares a scene
score with the threshold. A score over 0.3 is a heuristic for a visual change, not a semantic
scene boundary. A clip may produce no selected frames. -fps_mode vfr avoids filling the gaps
between selected frames with duplicates.
Troubleshooting
If the pipe reports a partial file or cannot find usable packets, the MP4 may need seeking. Download into a fresh directory, then process the actual downloaded file. This also separates network failures from decoding failures:
(
set -euo pipefail
mkdir download
curl -fsSL --retry 3 "http://127.0.0.1:8765/input.mp4" -o download/input.mp4
ffmpeg -nostdin -n -hide_banner -loglevel warning -filter_threads 1 -i download/input.mp4 \
-map 0:v:0 -vf "fps=1" -c:v mjpeg -q:v 2 -threads 1 -frame_pts 1 \
download/thumbnail_%03d.jpg \
-map 0:a:0 -c:a pcm_s16le download/audio.wav
)
With the test URL, this produces the same thumbnails and audio as the pipe. Use cURL retries with
-o for a file download, rather than with a decoder pipe. cURL cannot reset bytes already written
to a pipe before retrying; its retry documentation
describes this distinction. The block stops before decoding if the download fails.
An HTTP 200 response and a zero exit status still do not establish complete media. FFmpeg can report decoding damage yet return zero, or recover damaged frames and samples. Read its warnings, compare the expected thumbnail count and colors, and check the audio duration and final tone. For your own media, use known duration and content expectations or a trusted publisher checksum; these examples do not perform an integrity check. Downloading first solves seeking, not corruption.
Performance considerations
The pipe avoids storing the input MP4, but the JPEGs and uncompressed WAV still take disk space. FFmpeg processes data as it arrives; network speed and decoding speed determine how quickly that happens. This is an HTTP file download, not a guarantee of real-time or live-stream behavior. Lowering the thumbnail rate reduces the number of saved images, while scene detection still examines decoded frames.
For another input container, check FFmpeg’s pipe protocol documentation and whether the demuxer can consume it sequentially. Keep the download-first path available when you cannot control the MP4 layout.
