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:
- ✅ QuickTime: fine
- ✅ IINA: fine
- ✅ Opening it locally in the browser: also fine
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:
- Delete and re-upload
- Create a new bucket
- Switch domains
- Clear the cache
- Download it back and replay
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:
- CRF
- Preset
- Profile
- Level
- GOP
- Keyframe
- B Frame
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:
- Discontinuous PTS
- DTS jumps
- Variable frame rate (VFR)
- A small number of corrupted frames
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:
- ✅ QuickTime: fine
- ✅ Safari: fine
- ✅ Chrome: fine
- ✅ Upload to R2: fine
- ✅ Website playback: fine
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:
- Timeline anomalies
- Sample Table anomalies
- GOPs that can’t be recovered
- Timestamp jumps
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.
A Recommended Set of Parameters for Web Video
If your video ultimately needs to be deployed to:
- A website
- Cloudflare R2
- S3
- A CDN
- Safari
- Chrome
- Edge
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:
- Re-encoding
- Uploading to object storage
- Online playback in the browser
do those deeply hidden problems truly surface.
If you also run into:
- Consistent glitching at a fixed time after FFmpeg compression
- Playback freezing after uploading to Cloudflare R2
- Safari stopping playback halfway
- Anomalies during online browser playback
I recommend checking these first:
- Video timestamps (PTS / DTS)
- Whether it’s VFR (Variable Frame Rate)
- Whether the timeline needs to be regenerated