'use client' /** * `useVideoWarmup` — single-source-of-truth hook for warming the * network path to a public entity's main video so click→first-frame * lands in sub-second on Fast 4G. * * Behavior: * * 1. **Preconnect on every render** (`ReactDOM.preconnect`) — buys * the TCP / TLS handshakes to the video-bearing origins. React * 19 de-dupes identical preconnects, so this is safe to call * on every render. * * 2. **Preload the video bytes** (``) * ONLY when: * - the consumer's container scrolls within `nearMargin` of * the viewport (gated via the lib's IO singleton hook), AND * - `navigator.connection?.saveData !== true`, AND * - the URL is on the Supabase storage origin (Mux HLS warms * via its own manifest fetch when MuxPlayer mounts; YouTube * has its own origin pool, no preload benefit). * * Origin configuration: * - Mux origins (`stream.mux.com` / `image.mux.com`) are public * Mux CDN hostnames and stable across the Mux API contract — * hardcoded here. * - Supabase storage origin varies per-deployment (different * project per env). Threaded via the `supabaseStorageOrigin` * argument so the lib stays env-agnostic; hub callers pass * `getSupabaseStorageOrigin()` from their env config, or read * it from `ChatRuntime.endpoints.supabaseStorageOrigin`. * * Lifted from hub `hooks/use-video-warmup.ts`. The Mux constants * and the IO-gated preload semantics are byte-equivalent. */ import { useEffect } from 'react' import ReactDOM from 'react-dom' import { useNearViewport } from '../../hooks/use-near-viewport' import { useChatRuntime } from '../../contexts/chat-runtime-context' // Re-export from the server-safe `mux-origins.ts` module so the // constants are NOT bound to this `'use client'` file. See the // JSDoc in `mux-origins.ts` for the bug history. Backward-compat: // existing imports that read `MUX_STREAM_ORIGIN` from // `@flamingo-stack/openframe-frontend-core/components/features` // continue to resolve through this re-export. export { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins' import { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins' /** * Save-Data detection — the ONE source of truth for "is this a metered * connection". Consumed by the preload gate below and by `video.tsx`'s * default `preload` policy. SSR-safe (returns false on the server). */ export function saveDataEnabled(): boolean { if (typeof navigator === 'undefined') return false type Connection = { saveData?: boolean } const conn = (navigator as Navigator & { connection?: Connection }).connection return conn?.saveData === true } /** * Preconnect-only variant — fires the three video-bearing origin * preconnects (Supabase Storage + Mux stream + Mux image) without * setting up the IntersectionObserver subscription or the preload * `` injection. * * Use this when the consumer can't attach a `ref` to the video * container (e.g. release detail page, which delegates the player * render to a sibling component). Calling the full `useVideoWarmup` * from there would subscribe to a never-mounted ref and ship dead * preload machinery in the bundle. * * For consumers that own the video container, use `useVideoWarmup` * (which composes this hook + the IO-gated preload step). * * Reads `supabaseStorageOrigin` from `ChatRuntime.endpoints` by * default — callers in hosts that mount `HubRuntimeProvider` (or * any equivalent provider that wires the field) get the origin * automatically. The explicit `supabaseStorageOrigin` argument * overrides the runtime value when set. * * The hub's `app/layout.tsx` also emits STATIC `` * tags for the same Mux origins (document-parse-time warmup; React * dedupes against these runtime calls via the matching * crossOrigin="anonymous"). A change to the origin list here must be * mirrored there. */ export function useVideoOriginPreconnect({ supabaseStorageOrigin, }: { supabaseStorageOrigin?: string } = {}): void { const runtime = useChatRuntime() const resolvedOrigin = supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin try { ReactDOM.preconnect(MUX_STREAM_ORIGIN, { crossOrigin: 'anonymous' }) ReactDOM.preconnect(MUX_IMAGE_ORIGIN, { crossOrigin: 'anonymous' }) if (resolvedOrigin) { ReactDOM.preconnect(resolvedOrigin, { crossOrigin: 'anonymous' }) } } catch (err) { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.warn('[useVideoOriginPreconnect] preconnect failed:', err) } } } interface UseVideoWarmupOptions { /** * Effective video URL the page renders. Pass null/undefined when * there's no video yet (the hook still preconnects). Only URLs on * `supabaseStorageOrigin` are preloaded — Mux HLS and YouTube are * no-ops on the preload side. */ videoUrl?: string | null /** * Supabase storage origin (e.g. `https://xyz.supabase.co`). When * omitted, falls back to `ChatRuntime.endpoints.supabaseStorageOrigin` * — hosts that mount `HubRuntimeProvider` (or any equivalent * provider) get the origin automatically. When neither is set, the * preload step is skipped (preconnect to Mux still fires). */ supabaseStorageOrigin?: string /** * IO root margin gate for the preload step. Default `'1000px'` — * about one viewport's worth of lookahead on desktop. */ nearMargin?: string } export interface UseVideoWarmupResult { ref: (node: T | null) => void isNear: boolean } export function useVideoWarmup({ videoUrl, supabaseStorageOrigin, nearMargin = '1000px', }: UseVideoWarmupOptions = {}): UseVideoWarmupResult { // Resolve origin once — runtime fallback so callers in hosts that // mount `HubRuntimeProvider` don't need to thread it themselves. const runtime = useChatRuntime() const resolvedOrigin = supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin // Preconnect on every render — React 19 dedupes. Delegates to the // shared preconnect-only variant so the origin list is a single // source of truth. useVideoOriginPreconnect({ supabaseStorageOrigin: resolvedOrigin }) const { ref, isNear } = useNearViewport(nearMargin) useEffect(() => { if (!isNear || !videoUrl || !resolvedOrigin) return // Save-Data gate — metered connections skip preload. if (saveDataEnabled()) return // Origin gate: only preload Supabase-hosted MP4s. Mux HLS warms // via the manifest fetch when MuxPlayer mounts; YouTube has no // preload benefit. let videoOrigin: string try { videoOrigin = new URL(videoUrl, 'http://placeholder.local').origin } catch { return } if (videoOrigin !== resolvedOrigin) return const link = document.createElement('link') link.rel = 'preload' link.as = 'video' link.href = videoUrl link.crossOrigin = 'anonymous' // `fetchPriority='low'` matches the plan — the hint should not // steal network from the LCP image; the click→first-frame win is // in milliseconds, not the first paint. if ('fetchPriority' in link) { ;(link as HTMLLinkElement & { fetchPriority?: string }).fetchPriority = 'low' } document.head.appendChild(link) return () => { link.remove() } }, [isNear, videoUrl, resolvedOrigin]) return { ref, isNear } }