Saya lagi finalisasi hero sebuah one-pager immersive untuk situs klien. Di belakang teks ada satu elemen <video> full-bleed yang loop terus, di-zoom pelan pakai animasi supaya latarnya terasa hidup. Di macOS mulus total. Lalu saya buka di Windows lewat Brave, dan muncul kedipan hitam yang datang berulang. Chrome biasa sama saja. Bukan flicker halus, tapi kedip hitam jelas yang bikin hero-nya terasa rusak.
Yang bikin saya salah tebak di awal: karena videonya sedang dianimasikan, saya langsung curiga ini soal CSS transform atau compositing layer biasa.
Gejalanya
Kalau saya perhatikan baik-baik, hitamnya bukan random di seluruh viewport. Dia muncul persis di area video, dan sempat kelihatan sisa frame lama nyangkut di bagian bawah frame yang baru. Kedipan itu datang di dua momen: tiap kali video di-scale, digeser, atau ketutup layer lain di atasnya, dan sekali lagi tiap kali video selesai satu putaran loop dan balik ke awal.
Dua pola itu penting. Sisa frame lama di bagian bawah plus hitam yang terkurung di area video adalah tanda pembeda: ini artefak decode, bukan bug CSS. Kalau ini murni CSS, hitamnya tidak akan menyisakan potongan frame sebelumnya.
Jalan buntu
Saya buang waktu di beberapa tebakan yang salah dulu:
- Ngoprek CSS compositing:
will-change: transform,backface-visibility: hidden, ganti-gantiobject-fit. Tidak ada yang mengubah apa pun. - Curiga tween GSAP-nya yang bikin layer promosi kacau, jadi saya lambatkan durasinya. Kedipnya tetap.
- Kira file videonya corrupt atau codec-nya aneh, jadi saya export ulang dari sumber. Sama saja.
Semua buntu karena saya masih memperlakukan ini sebagai masalah CSS. Padahal tanda sisa frame di bawah tadi sudah teriak bahwa masalahnya di jalur decode dan compositing GPU, bukan di stylesheet.
Akar masalahnya
Ternyata ada dua penyebab yang benar-benar terpisah dan kebetulan menumpuk.
Pertama, DirectComposition overlay plane. Di Windows, kalau videonya di-decode oleh hardware, Chromium mempromosikan video itu ke overlay plane DirectComposition. Itu jalur khusus GPU yang hemat daya untuk video. Masalahnya, begitu video di-scale, di-translate, atau ketutup layer lain, dia drop masuk dan keluar dari jalur overlay itu. Tiap transisi masuk-keluar overlay itu menghasilkan flash hitam sepersekian detik. Karena hero saya justru terus dianimasikan (zoom pelan yang menyentuh transform video), video-nya bolak-balik keluar-masuk overlay tanpa henti, dan tiap perpindahan itu satu kedipan.
Kedua, loop seek yang mendarat di frame kotor. Video loop pendek melakukan seek balik ke frame 0 tiap iterasi. Encode default x264 memakai B-frames dan GOP yang panjang, jadi titik awal loop belum tentu keyframe bersih. Waktu playback seek balik ke awal, decoder mendarat di frame parsial dan sempat menampilkan hitam sebelum keyframe berikutnya siap. Itu penyebab kedipan yang muncul sekali tiap putaran, terpisah dari yang tadi.
Perbaikannya
Karena penyebabnya dua, perbaikannya juga dua bagian.
Bagian pertama, paksa video keluar dari overlay path. Trik intinya: kasih sedikit sentuhan yang membuat compositor tidak lagi menganggap elemen <video> sebagai kandidat overlay murni, karena sekarang dia butuh blending. filter: brightness(1.001) dan opacity: 0.999 cukup untuk itu, nilainya nyaris tak terlihat mata tapi mematikan promosi overlay. Tambahkan transform: translateZ(0) yang statis:
video.hero-bg {
/* paksa keluar overlay plane DirectComposition */
filter: brightness(1.001);
opacity: 0.999;
transform: translateZ(0); /* statis, jangan pernah dianimasikan */
}Yang sama pentingnya: jangan pernah men-tween elemen <video> secara langsung. Setiap animasi transform di video itu sendiri yang memicu perpindahan overlay. Bungkus video dengan wrapper, lalu animasikan wrapper-nya, biar elemen video-nya sendiri diam:
<div class="fl-zoom">
<video class="hero-bg" autoplay muted loop playsinline></video>
</div>// SALAH: nge-tween elemen video langsung memicu masuk-keluar overlay plane
// gsap.to(video, { scale: 1.1, duration: 20 });
// BENAR: animasikan wrapper, elemen video tetap diam
gsap.to(".fl-zoom", { scale: 1.1, duration: 20, ease: "none" });Bagian kedua, encode ulang loop-nya biar seek balik mendarat di frame bersih. Matikan B-frames dan perpendek GOP supaya keyframe lebih sering, plus pindahkan moov atom ke depan:
ffmpeg -i hero-source.mp4 \
-c:v libx264 -bf 0 -g 24 -pix_fmt yuv420p \
-movflags +faststart -an \
hero-loop.mp4-bf 0 mematikan B-frames, -g 24 memperpendek GOP jadi keyframe muncul lebih rapat, dan -movflags +faststart menaruh metadata di depan. Setelah re-encode ini, titik awal loop selalu jatuh di frame yang bisa langsung ditampilkan, jadi kedipan per putaran hilang.
Gabungan dua perbaikan itu menghilangkan seluruh kedipan. Hero-nya kembali mulus di Brave dan Chrome pada Windows, dan animasi zoom-nya tetap jalan karena sekarang yang dianimasikan adalah wrapper, bukan video.
Checklist
- Kalau hitam terkurung di area video plus ada sisa frame lama, itu artefak decode, bukan CSS. Berhenti ngoprek stylesheet.
- Chromium di Windows mempromosikan video hardware-decoded ke overlay plane DirectComposition; scale, translate, atau occlusion bikin dia kedip masuk-keluar.
- Paksa keluar overlay dengan
filter: brightness(1.001),opacity: 0.999, dantransform: translateZ(0)statis di elemen video. - Jangan pernah men-tween elemen
<video>langsung. Bungkus dengan wrapper, animasikan wrapper-nya. - Encode ulang loop dengan
-bf 0 -g 24 -movflags +faststartsupaya seek balik ke frame 0 mendarat di keyframe bersih.
