D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

Animator's Video Only Plays in Safari? It's HEVC, and Dropping It to 30fps Creates a Second Problem

· · 6 min read
Animator's Video Only Plays in Safari? It's HEVC, and Dropping It to 30fps Creates a Second Problem

Safari is usually the browser that refuses a media file while everyone else takes it without complaining. This time it went the other way. Out of the five browsers I tried, Safari was the only one that would play the video at all.

The file arrived from the animator on 3 May 2026, for a short product loop. One MP4, 1080p, 60fps, 7.6MB, still carrying an audio track. In Safari it played. In Chrome, Firefox, Brave, and Edge it was broken.

Read the delivery before wiring it up

The command I run every time a video asset comes in:

ffprobe -v error \
  -show_entries stream=codec_name,codec_type,width,height,r_frame_rate,duration,bit_rate \
  -show_entries format=duration,size,bit_rate \
  -of default=noprint_wrappers=0 source.mp4

I am only after a few lines of that output: the stream codec name, the frame rate, and whether there is an audio stream at all. On this file it came back HEVC at 1080p 60fps, plus an audio track that this asset was never going to use. The rule I have kept since then is plain enough. Inspect deliveries with ffprobe before wiring them in, not after somebody files a report.

Why Safari was the only one

HEVC, or H.265, has no web support outside Safari. That already accounts for the entire symptom, and there was nothing further to debug on the page side.

The part worth internalising is that this was not one animator slipping up. Animator output is commonly HEVC/H.265, because that is what falls out of the compositing and grading tools they normally work in. A delivery like this is the default state of the world, not an exception worth querying.

The size did not raise a flag either. The threshold I work to for video that ships alongside the theme is that anything up to 10MB is acceptable, and 7.6MB sits comfortably under it. Not a single size signal was wrong about this delivery. The codec was.

My first transcode, and the person who caught it

I ran the first transcode at 30fps to save size. My reasoning at the time went no deeper than smaller is better, and half the frame rate means half the frames to store.

I was not the one who caught the mistake. A user reported that the loop was "patah-patah", motion judder visible on the rotating product loop. Dropping 60fps to 30fps on rotating or motion-heavy loops does produce visible judder. The animator picked 60fps for a reason.

I re-encoded at 60fps, and it came out smooth.

Since then the -r 60 flag in my command carries its own note: preserve the 60fps source, do not drop to 30fps, because the judder shows up especially on rotating product loops and users notice. The lesson is to never down-sample frame rate on rotating or motion-heavy loops.

The most annoying part of the mistake is that the saving I was chasing through frame rate was never needed. Stripping audio plus a modern codec already shrinks the file enough, with no cost to motion quality.

Two sources, still 60fps, audio stripped

The actual fix was not one file but two sources at once, both at 60fps and both without audio.

ffmpeg -y -i source.mp4 \
  -c:v libx264 -preset slower -crf 18 \
  -r 60 -an -pix_fmt yuv420p -movflags +faststart \
  loop-h264.mp4
ffmpeg -y -i source.mp4 \
  -c:v libvpx-vp9 -crf 30 -b:v 0 \
  -row-mt 1 -tile-columns 4 -frame-parallel 1 \
  -r 60 -an -pix_fmt yuv420p \
  -deadline good -cpu-used 2 \
  loop-vp9.webm

Flag by flag, why the numbers are what they are:

The results were 2.5MB for H.264 and 2.5MB for VP9. The original went into an asset archive folder rather than being overwritten.

Going from 7.6MB to 2.5MB is a drop of 5.1MB, or 5.1 divided by 7.6, about 67 percent. What remains is 2.5 divided by 7.6, roughly 33 percent of the delivery. The two files do occupy 5.0MB in the repo, but each browser downloads only one of them, so what a visitor pays for stays 2.5MB. All of that without touching the frame rate at all.

One caveat so the numbers do not get used blind. The rule of thumb I keep says VP9 WebM runs about 30 percent smaller than H.264 at the same quality, yet on this clip both landed on the same figure. A rule of thumb like that is useful for estimating before an encode, and what I actually decide on is the size that comes out of the file.

Source order decides who downloads what

<video autoplay muted loop playsinline preload="auto"
       poster="loop-poster.webp">
  <source src="loop.webm" type="video/webm">
  <source src="loop.mp4" type="video/mp4">
  <img src="loop-fallback.webp">
</video>

Order matters here. WebM goes first because it is the smaller file and modern browsers take the first source they can play. MP4 sits behind it as the fallback for Safari and older browsers. The <img> is the final fallback if both video sources fail. In the template, the video element is also marked aria-hidden="true".

On preload, I ruled out preload="metadata" for this. It only fetches metadata, so the video buffers as it plays and stutters exactly when autoplay starts. preload="auto" buffers fully and plays smoothly, and for small decorative loops in the 2 to 5MB range the network cost is negligible. That 2.5MB file falls right inside the range.

When the file should stop shipping with the theme

The size ceiling I work to has three tiers. Up to 10MB is acceptable as a theme asset. Between 10 and 20MB is still fine but makes the push slow. Above 20MB the file no longer ships as a theme asset at all. It gets uploaded through the admin panel instead, and the public URL is referenced from a URL-type theme setting.

What I took away