MPEG-TS Concat Can Eat Your Audio: A ffmpeg Pitfall

I had 47 video sections rendered individually, each a segment of the final product with transitions between them. Each one was perfect — correct aspect ratio, burned-in subtitles, clean audio. All I needed to do was stitch them together into a single compilation video.

The ffmpeg concat demuxer should handle this in one command. I had a list file, I ran the command, and the output was 12 minutes of video with audio that disappeared for 30 seconds in the middle and a profile mismatch warning that VLC ignored but YouTube's uploader did not.

Here is what went wrong, why the obvious fixes did not work, and the approach that finally produced clean concatenations.

Building planning-first AI systems, one pipeline at a time. Get weekly deep dives into local AI, Rust, and agent orchestration — no fluff, no hype.

The Setup

The pipeline produces individual MPEG-TS files — one per section. Each file is a self-contained video segment: H.264 video, AAC audio, 1080×1920 pixels, 30 fps. The segments are generated sequentially by the same encoder with the same settings, so they should be compatible.

The concatenation command:

ffmpeg -f concat -safe 0 -i segments.txt -c copy output.mp4

This uses the concat demuxer with stream copy (no re-encoding). It is fast. It is the recommended approach in the ffmpeg wiki. And it failed silently.

Problem 1: Missing Key Frames

The concat demuxer with -c copy does not re-encode. It copies raw packets from input to output. This works when every segment starts with a key frame (I-frame). If a segment starts with a P-frame or B-frame, the decoder has no reference frame and produces garbage until the next key frame arrives.

The encoder I was using produced key frames at regular intervals (every 2 seconds), but there was no guarantee that the first frame of each segment was a key frame. When a segment happened to start mid-GOP (group of pictures), the concatenation produced a visible glitch: a few frames of blocky artifacts at every segment boundary.

The concat demuxer assumes your segments are designed for concatenation. If they are arbitrary cuts from a longer stream, they are not.

The fix is to ensure every segment starts with a key frame. If you control the encoder, force key frames at the start of each segment. If you do not control the encoder, you must re-encode during concatenation:

ffmpeg -f concat -safe 0 -i segments.txt -c:v libx264 -c:a aac output.mp4

This is slower — re-encoding 12 minutes of 1080p video takes about 90 seconds instead of 2 seconds — but it produces clean output regardless of key frame placement.

Problem 2: Truncated Audio

This was the one that took the longest to diagnose. The concatenated video had gaps in the audio — specifically, the last 1-2 seconds of audio from some segments were missing. Not silent. Missing. The audio track jumped forward, and the video stayed on the last frame while the audio skipped ahead.

The cause was a mismatch between video and audio stream lengths within individual segments. Each segment's video stream was exactly the right length, but the audio stream was occasionally shorter by 20-50 milliseconds. With -c copy, the concat demuxer uses the longest stream's timing, so the shorter audio stream just ended early.

This is an AAC encoder quirk. AAC frames are 1024 samples long (about 23ms at 44100 Hz). If the source audio is not an exact multiple of 1024 samples, the encoder pads the last frame or drops it. Different segments end at different sample positions, so the padding is inconsistent.

The fix for stream copy is to regenerate presentation timestamps:

ffmpeg -f concat -safe 0 -i segments.txt \
  -c copy -fflags +genpts output.mp4

But the more reliable fix is re-encoding. When you re-encode, ffmpeg handles the sample alignment at the container level, and the output audio is continuous.

If audio matters, re-encode. Stream copy concatenation is an optimization that trades reliability for speed. For a YouTube upload, spending 90 seconds instead of 2 seconds to get clean audio is not a trade-off worth making.

Problem 3: Profile and Level Mismatches

The third problem was the most insidious because it did not cause visible errors. It caused rejections.

Each MPEG-TS segment carried H.264 profile/level metadata in its header. The segments were all encoded with the same settings, but the metadata was slightly different: some segments reported [email protected], others reported High@L4. This happens when the encoder adjusts the level based on the actual bitrate and resolution of each segment, even when the requested profile is the same.

With -c copy, the concat demuxer uses the metadata from the first segment. The output file reports [email protected]. But some segments contain streams that are technically High@L4. Players and platforms that check consistency will flag this.

YouTube's uploader was the one that caught it. Not with an error — with a silent quality downgrade. The uploaded video was processed at a lower resolution than the source file. It took three uploads to notice the pattern and trace it back to the metadata mismatch.

Metadata consistency matters even when the actual stream is compatible. A platform that checks the header will trust the header over the actual data.

The fix is either re-encoding with explicit profile/level flags:

ffmpeg -f concat -safe 0 -i segments.txt \
  -c:v libx264 -profile:v high -level 4.2 \
  -c:a aac output.mp4

Or, if you must use stream copy, normalize the metadata with -bsf:v (bitstream filter) to rewrite headers consistently across all segments before concatenating.

When Stream Copy Is Fine

Stream copy concatenation (-c copy) is fine when all of the following are true:

  • Every segment starts with a key frame
  • Audio and video stream lengths match exactly within each segment
  • All segments share the same codec, profile, level, and metadata
  • You generated the segments yourself with a single encoder pass

Stream copy is not fine when any of those conditions are not met. And the hard part is that ffmpeg will not warn you. It will produce output that looks fine in ffprobe, plays fine in VLC, and fails in ways you discover later — sometimes days later, after a platform has already processed and degraded the upload.

The Principle

Concatenation is a join operation, not a merge operation. It assumes the inputs are compatible. When they are not, it produces output that is technically valid but subtly broken — the kind of broken that passes local testing and fails in production.

The ffmpeg concat demuxer is the right tool when you control the entire pipeline and can guarantee compatibility. When you are working with segments from different sources, different encoding passes, or segments that were cut rather than generated, re-encode. The time cost is negligible for any video short enough to upload to YouTube, and the reliability gain eliminates an entire class of bugs that are extremely hard to diagnose.

I now re-encode by default for all concatenation work. The 2-second stream copy is tempting, but the 90-second re-encode never produces missing audio, never has key frame issues, and never triggers a metadata mismatch. It is boring, and that is exactly what you want from a build step.