export const DocsVideo = ({ src, poster, title, autoPlay = false, loop = false, portrait = false, aspect, // e.g. "16 / 10" for app recordings; the frame is 16 / 9 otherwise }) => { const videoRef = useRef(null); const playerRef = useRef(null); const hideTimerRef = useRef(null); const progressFrameRef = useRef(null); const [enhanced, setEnhanced] = useState(false); const [playing, setPlaying] = useState(false); const [waiting, setWaiting] = useState(false); // Browsers only autoplay muted video, so an autoplaying player starts muted; the sound button unmutes it. const [muted, setMuted] = useState(autoPlay); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [playbackRate, setPlaybackRate] = useState(1); // Lazy, like ShowcaseWall's: a reduce-motion visitor's first render must not autoplay. The controls still play it. const [reduced, setReduced] = useState( () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches, ); useEffect(() => { const query = window.matchMedia("(prefers-reduced-motion: reduce)"); const onChange = (event) => setReduced(event.matches); query.addEventListener("change", onChange); return () => query.removeEventListener("change", onChange); }, []); // Turning Reduce Motion on mid-visit must stop an autoplaying loop for real: React props can't pause it or drop // its file. The play button loads it again (togglePlayback). useEffect(() => { const video = videoRef.current; if (!reduced || !autoPlay || !video) return; video.pause(); video.removeAttribute("src"); video.load(); }, [reduced, autoPlay]); const [controlsVisible, setControlsVisible] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [fullscreenSupported, setFullscreenSupported] = useState(false); const [previewing, setPreviewing] = useState(false); const [scrubbing, setScrubbing] = useState(false); const [previewTime, setPreviewTime] = useState(0); const [previewPosition, setPreviewPosition] = useState(0); const formatTime = (seconds) => { if (!Number.isFinite(seconds) || seconds < 0) return "0:00"; const minutes = Math.floor(seconds / 60); const remaining = Math.floor(seconds % 60); return `${minutes}:${String(remaining).padStart(2, "0")}`; }; const clearHideTimer = () => { if (hideTimerRef.current) { window.clearTimeout(hideTimerRef.current); hideTimerRef.current = null; } }; const revealControls = () => { setControlsVisible(true); clearHideTimer(); hideTimerRef.current = window.setTimeout(() => setControlsVisible(false), 2200); }; const togglePlayback = async () => { const video = videoRef.current; if (!video) return; if (video.paused || video.ended) { if (!video.getAttribute("src")) video.src = src; // dropped when Reduce Motion stopped the loop if (video.ended) video.currentTime = 0; setWaiting(true); try { await video.play(); } catch { setWaiting(false); setPlaying(false); } } else { video.pause(); setControlsVisible(true); } }; const toggleMute = () => { const video = videoRef.current; if (!video) return; if (video.muted && video.volume === 0) video.volume = 0.8; video.muted = !video.muted; setMuted(video.muted); }; const seek = (event) => { const video = videoRef.current; if (!video) return; const nextTime = Number(event.target.value); video.currentTime = nextTime; setCurrentTime(nextTime); }; const updateScrubPreview = (event, seekMainVideo = false) => { if (!duration) return; const rect = event.currentTarget.getBoundingClientRect(); const ratio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width)); const nextTime = ratio * duration; setPreviewing(true); setPreviewTime(nextTime); setPreviewPosition(ratio * 100); if (seekMainVideo) { const video = videoRef.current; if (video) { video.currentTime = nextTime; setCurrentTime(nextTime); } } }; const cyclePlaybackRate = () => { const video = videoRef.current; if (!video) return; const rates = [1, 1.25, 1.5, 2]; const currentIndex = rates.indexOf(video.playbackRate); const nextRate = rates[(currentIndex + 1) % rates.length]; video.playbackRate = nextRate; setPlaybackRate(nextRate); }; const toggleFullscreen = async () => { const player = playerRef.current; const video = videoRef.current; if (!player || typeof document === "undefined") return; try { if (document.fullscreenElement) { await document.exitFullscreen(); } else if (player.requestFullscreen) { await player.requestFullscreen(); } else if (video?.webkitEnterFullscreen) { video.webkitEnterFullscreen(); } } catch { // The normal inline player remains fully usable when fullscreen is blocked. } }; const handleKeyboard = (event) => { if (event.target !== event.currentTarget) return; const video = videoRef.current; if (!video) return; if (event.key === " " || event.key === "Enter") { event.preventDefault(); togglePlayback(); } else if (event.key === "ArrowLeft") { event.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 5); } else if (event.key === "ArrowRight") { event.preventDefault(); video.currentTime = Math.min(duration || video.duration || 0, video.currentTime + 5); } else if (event.key.toLowerCase() === "m") { event.preventDefault(); toggleMute(); } else if (event.key.toLowerCase() === "f") { event.preventDefault(); toggleFullscreen(); } }; useEffect(() => { setEnhanced(true); setFullscreenSupported( Boolean(playerRef.current?.requestFullscreen || videoRef.current?.webkitEnterFullscreen), ); return () => { clearHideTimer(); }; }, []); useEffect(() => { if (typeof document === "undefined") return undefined; const syncFullscreen = () => setFullscreen(document.fullscreenElement === playerRef.current); document.addEventListener("fullscreenchange", syncFullscreen); return () => document.removeEventListener("fullscreenchange", syncFullscreen); }, []); useEffect(() => { clearHideTimer(); if (!playing) return undefined; hideTimerRef.current = window.setTimeout(() => setControlsVisible(false), 2200); return clearHideTimer; }, [playing]); useEffect(() => { if (!playing) return undefined; const updateProgress = () => { const video = videoRef.current; if (video && !video.paused) setCurrentTime(video.currentTime); progressFrameRef.current = window.requestAnimationFrame(updateProgress); }; progressFrameRef.current = window.requestAnimationFrame(updateProgress); return () => { if (progressFrameRef.current) window.cancelAnimationFrame(progressFrameRef.current); progressFrameRef.current = null; }; }, [playing]); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const replaying = duration > 0 && currentTime >= duration - 0.15; return (
setControlsVisible(false)} onFocus={revealControls} onBlur={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) setControlsVisible(false); }} >
); }; export const ShowcaseWall = () => { // The same films as the homepage showcase (hyperframes.heygen.com), from the team and ours: a tile plays the 720p // card, an opened film the 1080p copy. const CDN = "https://static.heygen.ai/hyperframes-oss/homepage/showcase"; const homepageFilm = (id, v, title, note, length) => ({ id, title, note, length, tile: `${CDN}/card/${id}-v${v}.mp4`, full: `${CDN}/hq/${id}-v${v}.mp4`, poster: `${CDN}/poster/${id}-v${v}.jpg`, }); const films = [ homepageFilm("frameys-opening-night", 1, "Opening night", "Awards trailer", "50s"), homepageFilm("real-ui-newspaper-1969", 2, "Men walk on the Moon", "Real UI, rebuilt in code", "8s"), homepageFilm("manga-last-onigiri", 1, "The last onigiri", "Manga", "26s"), homepageFilm("vfx-background-removal-launch", 1, "Background removal", "VFX", "43s"), homepageFilm("cow", 1, "Cow 1.0", "Launch parody", "30s"), homepageFilm("remotion-to-hyperframes", 1, "Remotion to HyperFrames", "3D voxel", "20s"), ]; const [openId, setOpenId] = useState(null); // Lazy initializer, not a post-mount effect: with useState(false) the first // committed render emits