You mux a video and a narration track together, ffmpeg exits with code 0, no warnings — and the output is silent. Nothing errored. Nothing told you anything went wrong. This is one of the most common ways ffmpeg quietly ruins an automated pipeline, and it comes straight from a default that is reasonable in isolation and dangerous when you're not watching.

🎬 The 60-second version is on Instagram — watch the reel. This post is the full explanation: why the default does this, the fix, and how to verify you didn't ship silence. (It's the exact bug that once ate the narration in our own reel-rendering pipeline.)

TL;DR

  • When you give ffmpeg multiple inputs without -map, it applies automatic stream selection and keeps just one audio stream for the output.
  • Its rule for audio: the stream with the most channels wins; ties break to the first input. A stereo silent track in your video beats a mono narration.
  • The fix is to select streams explicitly: -map 0:v:0 -map 1:a:0.
  • Always verify with volumedetect — a mean around −19 dB is real audio; −91 dB means you shipped silence.

Why ffmpeg drops a stream with no error

When no -map options are present, ffmpeg does not include every input stream in the output. Instead it runs automatic stream selection: for each output file it picks at most one stream of each type — one video, one audio, one subtitle — from all the inputs combined. For audio specifically, it chooses the stream with the greatest number of channels, and if several tie, it takes the one from the earliest input.

That rule is sensible when you have a single file and just want "the audio." It becomes a trap the moment you have two inputs. Say your video file happens to carry a silent stereo housekeeping track and your second input is a mono narration. Stereo (2 channels) beats mono (1 channel), so ffmpeg keeps the silent track and silently discards your narration. Because selecting a valid stream is not an error, you get exit code 0 and a clean-looking log. The pipeline marches on and ships a silent video.

The fix: map streams explicitly

The cure is to stop relying on automatic selection and tell ffmpeg exactly which streams to include. The -map option takes input:stream_type:index and can be repeated:

bash
# ❌ Automatic selection — may keep the wrong (or silent) audio track
ffmpeg -i video.mp4 -i narration.mp3 output.mp4

# ✅ Explicit: take video stream 0 from input 0, audio stream 0 from input 1
ffmpeg -i video.mp4 -i narration.mp3 \
  -map 0:v:0 -map 1:a:0 \
  -c:v copy -c:a aac \
  output.mp4

-map 0:v:0 means "the first video stream of input 0"; -map 1:a:0 means "the first audio stream of input 1." Now the choice is yours, not the heuristic's. -c:v copy passes the video through without re-encoding, and -c:a aac encodes the narration to a widely compatible codec. The rule of thumb: the moment you have more than one input, map explicitly. Never let automatic selection decide which audio survives.

Verify you didn't ship silence

Exit code 0 is not proof of success — you just saw that. So make verification part of the pipeline rather than trusting the return code. ffmpeg's volumedetect filter measures the actual loudness of the output:

bash
ffmpeg -i output.mp4 -af volumedetect -f null -

Read the mean_volume line in the output. Real speech lands somewhere around −19 dB. If you see something like −91 dB, the track is digital silence — you shipped an empty audio stream, and now you know before your users do. In an automated pipeline, parse that number and fail the build if it falls below a sane threshold. A cheap assertion here saves you from publishing a silent video at 2 a.m.

Key Takeaways

  • ffmpeg's automatic stream selection keeps one audio stream per output — most channels wins, ties go to the first input.
  • This drops streams without any error, so exit code 0 tells you nothing about whether the right audio survived.
  • Use -map explicitly whenever you have multiple inputs: -map 0:v:0 -map 1:a:0.
  • Verify the result with -af volumedetect and gate your pipeline on the measured mean_volume, not the exit code.

This is the deep-dive companion to a @teqnersinsights reel. Follow @teqnersinsights for the 60-second fixes, and browse the full reel library for more engineering deep-dives.