diff --git a/components/hero-video.tsx b/components/hero-video.tsx index 15e43f3..9e4acfd 100644 --- a/components/hero-video.tsx +++ b/components/hero-video.tsx @@ -4,7 +4,9 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import Image from "next/image"; -const POSTER_SRC = "/photos/herograin.jpg"; +// Issue #135: the brewery still replaces the grain hero photo so the static +// experience looks intentional instead of duplicating the hero above it. +const POSTER_SRC = "/photos/video-still.jpg"; const WEBM_SRC = "/videos/ddbwebvid.webm"; const MP4_SRC = "/videos/ddbwebvid.mp4"; @@ -25,7 +27,7 @@ function useMediaQuery(query: string): boolean { export function HeroVideo() { const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); const isSmallScreen = useMediaQuery("(max-width: 768px)"); - const [canPlay, setCanPlay] = useState(false); + const [playing, setPlaying] = useState(false); const [inView, setInView] = useState(false); const sectionRef = useRef(null); const videoRef = useRef(null); @@ -53,12 +55,26 @@ export function HeroVideo() { if (!video || !inView || reducedMotion || isSmallScreen) return; // Load metadata first so the browser can decide codec, then play. + // play() itself initiates the fetch — no explicit load() needed. video.preload = "metadata"; - void video.play().catch(() => { - // Autoplay may be blocked by browser policy; the poster remains visible. + void video.play().catch((err: unknown) => { + // Autoplay may be refused by browser policy (e.g. low-power mode); + // the still stays visible either way. Dev-only diagnostic keeps + // expected rejections (NotAllowedError) distinguishable from real + // decode/network failures without sending noise to monitoring. + if (process.env.NODE_ENV !== "production") { + console.info( + "[HeroVideo] play() rejected — static still remains.", + err instanceof DOMException ? err.name : err, + video.error?.code + ); + } }); }, [inView, reducedMotion, isSmallScreen]); + // Deliberate: small screens get only the still — a multi-megabyte + // decorative autoplay video is not worth the data/battery cost on + // phones, and the approved still is a complete design, not a fallback. const showStaticPoster = reducedMotion || isSmallScreen; return ( @@ -83,17 +99,33 @@ export function HeroVideo() { )} diff --git a/docs/operations/performance.md b/docs/operations/performance.md index 32e603f..2ce912b 100644 --- a/docs/operations/performance.md +++ b/docs/operations/performance.md @@ -100,11 +100,13 @@ re-creates the contention it solves. ## Findings reviewed and intentionally left alone -- **Hero video files** (`ddbwebvid.mp4` 13.3MB, `ddbwebvid.webm` 4.2MB): - large, but the video only loads when the section scrolls into view on - desktop without reduced-motion — it never competes with initial load. - A lower-bitrate re-encode is a content/quality decision for the owner, - not an engineering default. +- **Hero video files** (`ddbwebvid.mp4` 2.1MB, `ddbwebvid.webm` 2.2MB): + re-encoded in #135 (H.264 High CRF 27, VP9 CRF 33, both ~0.8Mbps, no + audio, MP4 faststart) — down from 13.6MB/4.3MB at equal SSIM ≥0.98. + The video only loads when the section scrolls into view on desktop + without reduced-motion — it never competes with initial load. The + static poster is `photos/video-still.jpg` (approved brewery still, + distinct from the `herograin.jpg` hero image above it). - **`/beers/[slug]` is now static** (Issue #104): `generateStaticParams` enumerates build-time slugs with `dynamicParams = false`, reading via `getDocsFromServer` so a backend failure throws and fails the build diff --git a/public/photos/video-still.jpg b/public/photos/video-still.jpg new file mode 100644 index 0000000..d3a69d1 Binary files /dev/null and b/public/photos/video-still.jpg differ diff --git a/public/photos/video-still.png b/public/photos/video-still.png new file mode 100644 index 0000000..c840a2a Binary files /dev/null and b/public/photos/video-still.png differ diff --git a/public/videos/ddbwebvid.mp4 b/public/videos/ddbwebvid.mp4 index e7a244b..f133579 100644 Binary files a/public/videos/ddbwebvid.mp4 and b/public/videos/ddbwebvid.mp4 differ diff --git a/public/videos/ddbwebvid.webm b/public/videos/ddbwebvid.webm index bf8f1b8..de11b88 100644 Binary files a/public/videos/ddbwebvid.webm and b/public/videos/ddbwebvid.webm differ diff --git a/scripts/hero-video-check.mjs b/scripts/hero-video-check.mjs index e45ee37..7d1f644 100644 --- a/scripts/hero-video-check.mjs +++ b/scripts/hero-video-check.mjs @@ -13,10 +13,11 @@ async function check(contextOptions, label) { await page.waitForTimeout(800); const videoCount = await page.locator("video").count(); - const posterCount = await page.locator('img[src*="herograin"]').count(); + const stillCount = await page.locator('img[src*="video-still"]').count(); - // Count only the brewery section image/poster (the hero has one too). - console.log(`${label}: video elements=${videoCount}, herograin images=${posterCount}`); + // The brewery section poster is the video still (Issue #135); the grain + // hero image belongs to the hero section above it. + console.log(`${label}: video elements=${videoCount}, video-still images=${stillCount}`); await context.close(); } diff --git a/smoke-tests/hero-video.spec.ts b/smoke-tests/hero-video.spec.ts new file mode 100644 index 0000000..a0f00fd --- /dev/null +++ b/smoke-tests/hero-video.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from "./fixtures"; + +// Brewery video section (Issue #135). The decorative