Skip to content

Debugging Video Corruption After FFmpeg Compression

Published: at 12:00 AM

A strange corruption bug caused by video compression.

I’ve recently been building my new product, OrbitSpace.

To create the website, I needed to record a product demo video to use as a showcase on the homepage. So the whole flow was actually quite simple:

Screen recording → FFmpeg compression → Upload to Cloudflare R2 → Play online on the website

I assumed this would be the easiest step in the entire release process. Instead, I stumbled into a very subtle pitfall, and along the way learned quite a bit about MP4 timelines, FFmpeg, and browser video decoding.

This article documents the whole debugging process, in the hope that anyone who runs into a similar problem in the future can avoid some detours.

How It Started

This product demo video was recorded directly with screen recording software, then exported as: origin.mp4. To make sure everything was fine, I first checked it locally:

The video had no issues at all from start to finish, so I started preparing to compress it, using a very common FFmpeg command:

ffmpeg -i origin.mp4 \
  -c:v libx264 \
  -crf 24 \
  -preset medium \
  -c:a aac \
  -b:a 128k \
  -movflags +faststart \
  showcase.mp4

The entire compression process ran without any errors, until I played the resulting video.

A Bizarre Symptom

Since the video ultimately needed to go on the website, I uploaded the compressed video to Cloudflare R2. As a result, it couldn’t play in the browser at all: after playing to the 6-second mark, it just froze. The whole flow looked like this:

origin.mp4


FFmpeg compression


showcase.mp4
      └── Browser (R2): freezes right at 6 seconds

First Reaction: Did Cloudflare R2 Break the Video?

Because the problem surfaced after uploading, my first reaction was:

Could it be Cloudflare R2?

So I started investigating in all sorts of ways:

The result: the downloaded video still glitched at the 6-second mark. In other words, R2 was storing a video that already had a problem. The real issue was actually happening before the upload.

Second Suspect: FFmpeg

Since it wasn’t R2, I naturally started suspecting FFmpeg, so I began frantically trying parameters, including:

I tried just about everything, and there was no improvement at all. It was still:

Freezing at 6 seconds, glitching on local playback

That was when I began to realize something: if it always breaks at the exact same spot, it’s most likely not an encoding-quality issue.

The Real Breakthrough: Suspecting the Timeline

Later, while researching, I came across a line that suddenly made me realize I might have been heading in the wrong direction.

A player being able to play a video normally does not mean the video fully conforms to the spec.

Many players automatically apply error correction for the user, for example:

Many players silently fix these, so the user never notices. But when FFmpeg re-encodes, it continues to use this timing information. As a result, the originally hidden problem got amplified by re-encoding.

The Final Solution

I later tried reconstructing the entire video timeline, rather than simply re-encoding.

The command is as follows:

ffmpeg \
  -fflags +genpts \
  -i origin.mp4 \
  -map 0:v:0 \
  -map 0:a? \
  -vf "fps=30,format=yuv420p" \
  -c:v libx264 \
  -preset medium \
  -crf 24 \
  -profile:v high \
  -level:v 4.1 \
  -fps_mode cfr \
  -c:a aac \
  -b:a 128k \
  -af "aresample=async=1:first_pts=0" \
  -movflags +faststart \
  showcase-fixed.mp4

After regenerating:

The problem disappeared entirely. And the truly critical parts were actually the following:

-fflags +genpts

Regenerate the video timestamps (PTS). If the timing information exported by the screen recording software is faulty, this step recalculates it.

-vf "fps=30"

Convert the video to a fixed frame rate. Many screen recording tools export VFR (Variable Frame Rate); once unified, the entire timeline gets reconstructed.

-fps_mode cfr

Explicitly tell FFmpeg that the output must be constant frame rate (CFR). This step and the previous one usually need to be used together.

-af "aresample=async=1:first_pts=0"

Re-sync the audio timeline. Although the problem appears to be with the picture, when a browser plays a video, the audio and video timelines actually advance together.

Once re-synced, many strange issues disappear along with it.

format=yuv420p

Ensures browser and hardware decoding compatibility. It’s not the culprit behind the 6-second glitch, but almost all web video includes this option.

Why Does the Problem Surface More Easily After Uploading?

The first thing to be clear about is that Cloudflare R2 does not modify video content. Object storage stores the raw bytes. What actually causes the difference is how the browser plays the video. The browser doesn’t download the entire video at once; instead:

Play

HTTP Range Request

Download a small segment

Continue decoding

Request the next segment

If the video has:

The browser’s hardware decoder may simply stop decoding, which manifests as: playing to the 6-second mark, the video freezes completely. QuickTime’s error tolerance is clearly stronger, so it can keep playing.

Post-Mortem

Looking back now, the problem was there the whole time. It’s just that in the original video file, the player was hiding it for me.

The real flow should be understood like this:

Screen recording software


origin.mp4

├── QuickTime: fine
├── IINA: fine
└── Actually has timeline anomalies (player auto error-corrects)


FFmpeg inherits the timing info by default


showcase.mp4

├── Local: glitches at 6 seconds
└── Browser: freezes completely at 6 seconds

So the real problem was: the video exported by the screen recording software contained timing information that players can automatically error-correct, but which re-encoding and browser hardware decoding will not fix for you.

And that’s exactly why it didn’t truly surface until after compression / uploading to a CDN.

If your video ultimately needs to be deployed to:

I now directly use the following set of parameters:

ffmpeg \
  -fflags +genpts \
  -i input.mp4 \
  -vf "fps=30,format=yuv420p" \
  -c:v libx264 \
  -preset medium \
  -crf 24 \
  -profile:v high \
  -level:v 4.1 \
  -fps_mode cfr \
  -c:a aac \
  -b:a 128k \
  -af "aresample=async=1:first_pts=0" \
  -movflags +faststart \
  output.mp4

There are a few more parameters than the default, but this avoids quite a lot of subtle problems caused by timelines, variable frame rates, and player compatibility.

Final Thoughts

When dealing with videos generated by screen recording software, players silently handle a lot of problems for us in order to preserve the user experience.

Only when a video goes through the full pipeline of:

do those deeply hidden problems truly surface.

If you also run into:

I recommend checking these first: