patx/demo
fix iPhone hero video fallback
Commit 87d8729 · patx · 2026-08-16T02:13:42-04:00
Comments
No comments yet.
Diff
diff --git a/docs/voldservices/index.html b/docs/voldservices/index.html
index e09e6c9..a579466 100644
--- a/docs/voldservices/index.html
+++ b/docs/voldservices/index.html
@@ -407,6 +407,9 @@
}
.hero-media video,
.hero-media img.hero-fallback {
+ position: absolute;
+ inset: 0;
+ display: block;
width: 100%;
height: 100%;
object-fit: cover;
@@ -416,11 +419,18 @@
transform-origin: center 52%;
will-change: transform;
}
+ .hero-media img.hero-fallback { z-index: 0; }
+ .hero-media video {
+ z-index: 1;
+ opacity: 0;
+ transition: opacity .18s ease-out;
+ }
+ .hero-media.is-video-ready video { opacity: 1; }
.hero-media::after {
content: "";
position: absolute;
inset: 0;
- z-index: 1;
+ z-index: 2;
pointer-events: none;
background:
linear-gradient(90deg, rgba(8, 8, 8, .94) 0%, rgba(8, 8, 8, .62) 43%, rgba(8, 8, 8, .06) 74%),
@@ -1256,13 +1266,13 @@
<section class="hero" id="top">
<div class="hero-sticky" id="hero-sticky">
<div class="hero-media">
+ <img class="hero-fallback" src="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg" alt="" aria-hidden="true" />
<video
id="hero-video"
poster="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg"
muted playsinline preload="auto"
aria-hidden="true" tabindex="-1">
<source src="https://gitman.io/patx/demo/raw/docs/voldservices/hero-scrub.mp4" type="video/mp4" />
- <img class="hero-fallback" src="https://gitman.io/patx/demo/raw/docs/voldservices/img/hero-scrub-poster.jpg" alt="" />
</video>
</div>
<div class="hero-grain" aria-hidden="true"></div>
@@ -1913,8 +1923,11 @@
var heroEyebrow = heroSticky.querySelector('.hero-eyebrow');
var heroLead = heroSticky.querySelector('.hero-lead');
var heroActions = document.getElementById('hero-actions');
+ var heroMedia = heroSticky.querySelector('.hero-media');
var heroVideo = document.getElementById('hero-video');
var heroVideoReady = false;
+ var heroVideoVisible = false;
+ var heroVideoRevealPending = false;
var heroVideoProgress = 0;
function clamp01(value) {
@@ -1939,11 +1952,38 @@
if (reduceMotion || !heroVideoReady || !Number.isFinite(heroVideo.duration)) return;
var usableDuration = Math.max(heroVideo.duration - (1 / 24), 0);
- var targetTime = usableDuration * heroVideoProgress;
+ // Safari can clear the poster before it has decoded a seek at exactly
+ // zero. Start a fraction into the first frame and keep a real image
+ // beneath the video until a decoded frame has actually been painted.
+ var targetTime = Math.max(1 / 48, usableDuration * heroVideoProgress);
if (!force && (heroVideo.seeking || Math.abs(heroVideo.currentTime - targetTime) < (1 / 48))) return;
heroVideo.currentTime = targetTime;
}
+ function revealHeroVideo() {
+ if (reduceMotion || heroVideoVisible || heroVideoRevealPending || heroVideo.readyState < 2) return;
+ heroVideoRevealPending = true;
+
+ var reveal = function () {
+ heroVideoRevealPending = false;
+ if (reduceMotion || heroVideoVisible || heroVideo.readyState < 2) return;
+ heroVideoVisible = true;
+ heroMedia.classList.add('is-video-ready');
+ };
+
+ if (typeof heroVideo.requestVideoFrameCallback === 'function') {
+ heroVideo.requestVideoFrameCallback(reveal);
+ // A paused iOS video can finish seeking before a queued frame
+ // callback is delivered. By this point seeked + HAVE_CURRENT_DATA
+ // make a delayed reveal safe, while the poster covers the handoff.
+ window.setTimeout(reveal, 160);
+ } else {
+ window.requestAnimationFrame(function () {
+ window.requestAnimationFrame(reveal);
+ });
+ }
+ }
+
heroVideo.addEventListener('loadedmetadata', function () {
heroVideo.pause();
heroVideoReady = true;
@@ -1953,9 +1993,17 @@
// If the user scrolls again while a seek is being decoded, catch the
// playhead up to the most recent position as soon as that frame lands.
heroVideo.addEventListener('seeked', function () {
+ revealHeroVideo();
syncHeroVideo(heroVideoProgress, false);
});
+ heroVideo.addEventListener('error', function () {
+ heroVideoReady = false;
+ heroVideoVisible = false;
+ heroVideoRevealPending = false;
+ heroMedia.classList.remove('is-video-ready');
+ });
+
function paintHeroScroll() {
if (reduceMotion) {
heroActions.inert = false;