'use client'; import { ExternalLink, Play, Volume2, VolumeX } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; /** * Hero video player tuned for the stubborn-autoplay cases we hit in * the field (Chrome with prefers-reduced-motion, data saver, etc.). * * Three failure modes we defend against: * * 1. Browser allows muted autoplay — the happy path. Element loads, * starts playing, the `play` event flips `playing` to true and * hides the overlay. * * 2. Browser blocks autoplay but allows playback on user gesture — * the play overlay sits over the poster, the user clicks, we * call `.load()` first to reset the resource-selection state * (some Chrome builds park the element at networkState=2/ * readyState=0 forever if the original autoplay was blocked * before the source ever fetched) and then `.play()` with the * user gesture in scope. Plays. * * 3. Browser refuses to play even after the gesture — extension- * sandboxed contexts, hardware-decoder failures, etc. We catch * the promise rejection, surface a small "open video in a new * tab" link so the visitor isn't completely stuck. * * Source order: MP4 only. We previously offered WebM (VP9) first as a * size win, but Chrome will pick WebM if listed first, and if that * decode fails it does NOT fall back to MP4 — it just sits in an * unloaded state. Single MP4 source (Main profile / yuv420p / TV- * range / faststart) plays everywhere and the file is 2.6 MB which is * close enough to the WebM that the extra HTTP source-juggling isn't * worth the failure surface. */ export function HeroVideo() { const videoRef = useRef(null); const [muted, setMuted] = useState(true); const [playing, setPlaying] = useState(false); const [playFailed, setPlayFailed] = useState(false); useEffect(() => { const v = videoRef.current; if (!v) return; const onPlay = () => { setPlaying(true); setPlayFailed(false); }; const onPause = () => setPlaying(false); v.addEventListener('play', onPlay); v.addEventListener('pause', onPause); // Best-effort autoplay attempt — silently fail on browsers that // block it; the overlay is the user's escape hatch. v.play().catch(() => undefined); return () => { v.removeEventListener('play', onPlay); v.removeEventListener('pause', onPause); }; }, []); const togglePlay = useCallback(async () => { const v = videoRef.current; if (!v) return; if (v.paused) { setPlayFailed(false); try { // .load() resets the media element's resource selection — required // when an earlier autoplay attempt was blocked before the source // ever fetched. Without it, .play() on a stuck element no-ops. v.load(); await v.play(); } catch { setPlayFailed(true); } } else { v.pause(); } }, []); const toggleMute = useCallback((e: React.MouseEvent) => { e.stopPropagation(); const v = videoRef.current; if (!v) return; const next = !v.muted; v.muted = next; setMuted(next); if (!next && v.paused) { v.play().catch(() => undefined); } }, []); return ( <> {/* PLAY overlay — visible while paused. Full-frame so clicking anywhere starts playback (YouTube / Vimeo pattern). */} {!playing && ( )} {/* Fallback escape hatch — surfaces only if .play() rejects even after a user gesture (extension sandbox, hardware-decoder failures, etc.). One-line link to the raw MP4 so the visitor isn't trapped staring at a poster forever. */} {playFailed && !playing && ( your browser blocked playback — open the video directly )} {/* Mute toggle — always visible, top of z-stack. */} ); }