ffmpegvideo-encodingtroubleshooting

Fix "Output File Is Empty, Nothing Was Encoded" in FFmpeg

·Javid Jamae·10 min read
Fix "Output File Is Empty, Nothing Was Encoded" in FFmpeg

Your job ran, the exit code was 0, and the file the next step downloaded is a few hundred bytes of MP4 header with no frames. Buried in stderr is a warning-level line: Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used). Nothing about the run looked like a failure to your pipeline, which is why it reached production.

Quick answer: An ffmpeg output file empty result means FFmpeg opened the muxer and wrote a container header, but zero packets reached it, so it prints Output file is empty, nothing was encoded as a warning and still exits 0. Find the input-selection option that matched nothing (-ss past the end of the file, a -t or -frames value that computed to zero, a select expression that never evaluated true, an empty concat list), then add the global flag -abort_on empty_output so the run is fatal instead of silent. To skip that assertion work in every pipeline, send the job to the FFmpeg Micro API: one call, no encoder to host, and a render that produces nothing returns a failed job, not a 0-byte download.

Why FFmpeg treats an empty output as a warning

FFmpeg decides the run is empty at the very end, when it tallies bytes written across video, audio, subtitle, and data streams. If that total is zero it prints the "nothing was encoded" line and returns the same status as a clean run. The design assumes a person is watching the terminal. A worker in a queue is not.

The fix is a global option, so it goes before your inputs:

ffmpeg -hide_banner -abort_on empty_output \
  -ss 00:12:30 -i input.mp4 -t 15 -c copy clip.mp4
# ... Empty output
# echo $? -> 1

A stricter sibling, -abort_on empty_output_stream, fires when any single output stream got no packets. Use it when a missing audio track is as bad as a missing video track, which covers most social-media renders. Check your build with ffmpeg -h full | grep -A 4 abort_on. Same problem as FFmpeg error messages, not exit codes, tell you what to retry: the exit status carries no signal.

`-ss` past the end of the input

Seeking past the last frame is the most common cause of an empty render in an automated clipper. FFmpeg seeks, hits end of file, decodes nothing, and closes the muxer. There's no "seek beyond duration" error, because seeking past the end isn't illegal.

The scenario is constant: a repurposing workflow reads a timestamp range from a transcript, asks for 00:12:30 to 00:12:45, and the source is nine minutes long because the upload was truncated. The clip list was right for the video someone meant to send.

Two habits kill this class:

  1. Read the real duration with ffprobe before you build the command, and drop any clip that starts past it.
  2. Express the window as -ss <start> -t <duration> rather than -ss <start> -to <end>. A -to smaller than -ss is a zero-length window, and the two interact differently depending on where each sits relative to -i.

Copy-mode trims add a second trap: with -c copy the cut lands on keyframes, so a short window between them can come out empty or unplayable. FFmpeg cuts on keyframes covers when to re-encode.

`-t` and `-frames` computed from metadata that wasn't there

A duration of zero produces an empty file, and zero is what you get when ffprobe returns nothing and your shell math treats the blank as a number. Plenty of real files have no container duration: WebM and Matroska from a streaming encoder, fragmented MP4, and anything still being written report N/A for format=duration.

ffprobe -v error -show_entries format=duration -of csv=p=0 recording.webm
# N/A

Feed that into dur=$(...) and then -t $((dur - 5)) and you either crash on the arithmetic or, in an awk or JavaScript path, sail through with -t 0. FFmpeg accepts -t 0 and -frames:v 0 and writes an empty file. Same for a frame count from a bogus duration times fps.

When the header has no duration, count packets instead:

ffprobe -v error -select_streams v:0 -count_packets \
  -show_entries stream=nb_read_packets -of csv=p=0 recording.webm

It reads the whole file, so it costs time on large inputs, but it returns a real number.

Inputs that matched nothing: globs, numbering, and concat lists

An image sequence that matches no files fails loudly: ffmpeg -pattern_type glob -i 'frames/*.png' on an empty directory returns Could find no file with path 'frames/*.png' and exits non-zero. A printf width mismatch (frame_%04d.png against frame_1.png) does the same.

The silent version is the concat demuxer. An empty or all-comments list file is a valid input, so FFmpeg opens it, finds zero entries, writes the header, and reports an empty output:

# list.txt written by a loop that iterated over zero items
ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4

If an n8n loop, a Make iterator, or a for loop in GitHub Actions built that list from an empty API response, you get a valid command over an empty set. Assert wc -l < list.txt first, and treat "zero clips" as a workflow branch, not an FFmpeg input.

Filters that never pass a frame

A filtergraph can drop every frame and still be correct. select is the usual culprit because its expression is data-dependent: -vf "select='gt(scene,0.4)'" returns nothing on a static screencast or a slow talking-head clip, where no two consecutive frames differ by 40%. Same with select='between(t,120,130)' on a 90-second video.

That's the filter telling you the content has no scene changes at your threshold. Use a lower threshold (0.2 to 0.3 for talking-head footage) or a fallback that grabs frames at a fixed interval when the scene pass returns zero. Extracting frames by scene change instead of 1 FPS covers picking the threshold.

Single-image output makes this worse. Writing one PNG to a fixed filename needs -update 1, or the image2 muxer wants a numbered pattern and dies at frame 2. But -update 1 creates the file as soon as the output opens, so a select that never fires leaves a genuine 0-byte PNG that any file-exists check passes.

The output went somewhere the container threw away

Writing to a directory that doesn't exist fails loudly: No such file or directory, exit 1, and FFmpeg won't create parent directories. What turns it silent is the layer above.

On AWS Lambda, Google Cloud Run, or any ephemeral container, /tmp belongs to one instance, so if the encode and upload steps land in different containers the download finds nothing and the orchestrator reports a missing file, not an encoder error. Same in n8n when a Docker-hosted FFmpeg writes inside the container and a later node reads from the host. Passing URLs instead of local paths removes the class, the argument in Google Drive video automation fails in n8n.

The guard: assert the output, don't trust the exit code

Never mark a render complete on exit code alone, and don't trust a file-size test either. An "empty" MP4 usually isn't 0 bytes: FFmpeg writes the ftyp and moov boxes when it opens the muxer, so test -s out.mp4 passes on a valid container with no samples. Only ffprobe sees inside.

#!/usr/bin/env bash
set -euo pipefail
out="clip.mp4"

ffmpeg -hide_banner -loglevel error -abort_on empty_output \
  -ss 90 -i input.mp4 -t 15 -c:v libx264 -c:a aac -y "$out"

codec=$(ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name -of csv=p=0 "$out")
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$out")

[ -n "$codec" ] || { echo "no video stream in $out"; exit 65; }
awk -v d="$dur" 'BEGIN { exit !(d+0 > 0.5) }' \
  || { echo "duration $dur is not a real clip"; exit 65; }

Three checks: FFmpeg aborts on an empty mux, ffprobe confirms a video stream, and awk confirms a duration above half a second (bash can't compare floats, so [ "$dur" -gt 0 ] fails on 15.033). Exit 65 is arbitrary; the point is a status your worker can route on.

Both paths have a price:

StepRunning FFmpeg yourselfOne API call
Empty render`-abort_on empty_output` plus your own ffprobe assertionJob returns failed with an error
Bad timestampsProbe duration first, clamp the clip listRejected with a typed error, not a 0-byte file
Output storageYour `/tmp`, your bucket, your lifecycleOutput URL you download
Long jobsYou babysit the timeoutPoll or take a webhook

That's the honest trade. Keep FFmpeg in-house and budget for the assertion script above in every worker. If the failure should instead surface as a failed job, FFmpeg Micro runs the same operations as one REST call with no servers to run, and the playground takes the exact command shape before you wire it into n8n, Make, or Zapier.

Pitfalls that produce an empty file with no error

Four habits cause most silent empty renders in production pipelines:

  • Testing for a 0-byte file. An empty MP4 has a header; test with ffprobe.
  • Setting -loglevel quiet or -nostats in a worker, which discards the only warning you get.
  • Assuming ffprobe duration exists. WebM, fragmented MP4, and in-progress recordings return N/A.
  • Retrying an empty render. The input didn't change, so it produces the same empty file and burns quota.

When an empty output is the right answer

Not every empty render is a bug. Scene detection over a static slideshow, silence removal over a silent track, and an empty clip list are correct results that need a workflow branch, not a retry. The real failure is a pipeline that marks the job done and ships the file downstream.

FAQ

Why does FFmpeg exit 0 when the output file is empty?

FFmpeg logs Output file is empty, nothing was encoded at warning level, not error level, so the process returns status 0. It checks the encoded byte total only at the end of the run, after the muxer has written a valid container header. -abort_on empty_output makes it fatal.

How do I make FFmpeg fail loudly when nothing was encoded?

Pass the global option -abort_on empty_output before your inputs, and -abort_on empty_output_stream if one missing stream (audio, usually) should fail the job too. Add an ffprobe check for a video stream and a duration above zero, since the flag catches an empty mux but not a one-frame render.

Why is my empty MP4 a few hundred bytes instead of 0 bytes?

An empty MP4 contains the ftyp and moov boxes FFmpeg writes when it opens the output, with no media samples, so it's small but not zero-length, and any size-based check passes it. Use ffprobe -select_streams v:0 -show_entries stream=codec_name and treat empty output as a failed render.

Does "nothing was encoded" mean my input file is corrupt?

The "nothing was encoded" warning almost never means a corrupt input. Every frame was filtered out or seeked past, usually by -ss, -t, -frames, or a select expression. Broken inputs produce a different message, covered in FFmpeg "Invalid data found" isn't a corrupt video.

Can I detect an empty output before the file reaches my users?

Run ffprobe -v error -show_entries format=duration -show_entries stream=codec_type -of json on every render, in the worker that produced the file, and reject anything without a video stream or with a duration near zero.

If you'd rather stop maintaining that guard in every worker, run the transcode, trim, caption, or frame-extraction step through the FFmpeg Micro API and let the job status tell you what happened. The free tier is enough to compare it on a real file: sign up free.

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