@hadoku/watchparty-ui 0.45.27 → 0.45.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Room-CQy_wCnX.js","sources":["../../src/lib/chatMedia/upload.ts","../../../../node_modules/.pnpm/@wolffm+cdn-client@2.0.3_react@19.2.3/node_modules/@wolffm/cdn-client/dist/react.js","../../src/lib/chatMedia/previewPlacement.ts","../../src/lib/chatMedia/react/MemePicker.tsx","../../../../packages/rooms-protocol/dist/game-bundle.js","../../src/lib/openMediaDrawer.ts","../../src/components/ErrorBoundary.tsx","../../src/lib/useIsFullscreen.ts","../../src/lib/mediaDrawerAffordance.ts","../../src/lib/avatarPictures.ts","../../src/lib/cosmetics.tsx","../../src/components/ChatAvatar.tsx","../../src/lib/chatMedia/imageClickIntent.ts","../../src/lib/portalTarget.ts","../../src/components/ChatImageLightbox.tsx","../../src/components/ChatImageAttachments.tsx","../../src/lib/chatOwnership.ts","../../src/components/ChatMessage.tsx","../../src/components/ChatLog.tsx","../../src/lib/slashCommands.ts","../../src/components/SlashCommandPicker.tsx","../../src/components/ChatAttachmentStrip.tsx","../../src/hooks/useChatImageAttachments.ts","../../src/components/ChatInput.tsx","../../src/components/Roster.tsx","../../src/components/RosterButton.tsx","../../src/lib/leaderAffordance.ts","../../src/hooks/useStoreSelectors.ts","../../src/components/GameSettingsSection.tsx","../../src/components/ChatPanel.tsx","../../src/roomChrome/hostTheme.ts","../../src/lib/gameBundleHost.ts","../../src/components/GameBundleSlot.tsx","../../src/components/DrawerShell.tsx","../../src/components/FloatingChat.tsx","../../src/components/bufferHealth.ts","../../src/lib/notificationSound.ts","../../src/lib/readyToggleCue.ts","../../src/components/ClientStatusOverlay.tsx","../../src/components/TierlistModal.tsx","../../src/components/FullscreenPortal.tsx","../../src/components/controls/Icons.tsx","../../src/components/controls/SelectMediaButton.tsx","../../src/hooks/useChatToggle.ts","../../src/components/controls/ChatToggleButton.tsx","../../src/components/RoomCornerControls.tsx","../../src/hooks/useAvatarChanges.ts","../../src/lib/audioUnlock.ts","../../src/lib/roomKeyProbe.ts","../../src/lib/controls.ts","../../src/components/controls/PlayPauseButton.tsx","../../src/components/controls/UnloadButton.tsx","../../src/components/controls/seekState.ts","../../src/lib/seekBarValue.ts","../../src/components/controls/SeekBar.tsx","../../src/components/controls/VolumeSlider.tsx","../../src/components/controls/AudioTrackSelector.tsx","../../src/components/controls/SubtitleSelector.tsx","../../src/components/controls/SubtitleStyleSelector.tsx","../../src/components/controls/FullscreenButton.tsx","../../src/components/controls/ControlBar.tsx","../../src/lib/backgroundPlay.ts","../../src/components/MediaTitle.tsx","../../src/lib/bufferUtils.ts","../../src/components/SubtitleOverlay.tsx","../../src/lib/perfCollector.ts","../../src/lib/PlaylistFragmentCache.ts","../../src/lib/evictionStrategy.ts","../../src/lib/fragmentManager.ts","../../src/lib/subtitlePolicy.ts","../../src/lib/autoReload.ts","../../src/lib/autoplay.ts","../../src/lib/seekCoordinator.ts","../../src/lib/healthReporter.ts","../../src/hooks/useControlsVisibility.ts","../../src/lib/loadedIndex.ts","../../src/hooks/useIndexJson.ts","../../src/hooks/useMediaTracks.ts","../../src/lib/silentStall.ts","../../src/hooks/useStallRecovery.ts","../../src/hooks/usePrefetch.ts","../../src/hooks/usePlaybackSync.ts","../../src/hooks/usePlaylistPreBuffer.ts","../../src/hooks/useBackgroundPlay.ts","../../src/components/MediaPlayer.tsx","../../src/lib/mediaPlayerBundle.tsx","../../src/lib/defaultActivity.ts","../../src/routes/Room.tsx"],"sourcesContent":["/**\n * The composer's attachment model, on top of `@wolffm/cdn-client`.\n *\n * The package owns TRANSPORT: `upload()`, `measureImage()`,\n * `filesFromClipboard()`/`filesFromDrop()`, and the size/type constants\n * mirrored from the worker. What stays here is what is watchparty's:\n *\n * - the pending-attachment model, which is a UNION because a meme pick is\n * already on the CDN and a pasted file is not;\n * - the user-facing wording for each failure;\n * - the guard on the key the CDN hands back.\n *\n * Everything is a pure function or one `await`, deliberately: the apps/ui\n * vitest project runs in the `node` environment and collects only `.ts`, so a\n * rule that lives in a component cannot be tested at all.\n */\nimport {\n CdnError,\n MAX_BYTES,\n filesFromClipboard,\n filesFromDrop,\n kindOfType,\n measureImage as measureImageBytes,\n type CdnErrorReason,\n} from '@wolffm/cdn-client';\nimport { isChatImageKey } from '@watchparty/shared';\nimport { getCdnClient } from './config';\n\n/**\n * An attachment sitting on the composer, not yet sent.\n *\n * Two kinds, and the distinction is load-bearing on three separate paths:\n *\n * - `file` — pasted or dropped. Needs uploading on submit, and `previewUrl`\n * is an object URL that MUST be revoked when it goes away.\n * - `remote` — picked from the meme library. Already content-addressed on the\n * CDN, so submitting it costs no network at all, and `previewUrl` is an\n * ordinary CDN URL that must NOT be revoked (harmless, but it would mean\n * the revoke path had stopped tracking what it owns).\n *\n * A discriminated union rather than optional fields, so neither path can\n * silently read the other's shape — `file` being `undefined` at the one point\n * that uploads is exactly the bug this prevents.\n */\ninterface PendingBase {\n /** Stable id for React keys and for removing one specific attachment. */\n id: string;\n /** What the composer renders right now — object URL or CDN URL by kind. */\n previewUrl: string;\n /** Intrinsic pixels, or 0 when unknown (renderers fall back to natural flow). */\n w: number;\n h: number;\n}\n\nexport interface PendingFileImage extends PendingBase {\n kind: 'file';\n file: File;\n}\n\nexport interface PendingRemoteImage extends PendingBase {\n kind: 'remote';\n /** Already on the CDN — this goes straight onto the message. */\n key: string;\n /** Library title, carried through to the rendered `alt`. */\n title?: string;\n}\n\nexport type PendingImage = PendingFileImage | PendingRemoteImage;\n\n/** True when this attachment owns an object URL that has to be revoked. */\nexport function ownsObjectUrl(p: PendingImage): boolean {\n return p.kind === 'file';\n}\n\n/**\n * A finished attachment, ready to go on the message.\n *\n * Structurally the wire `ChatImage`, kept as its own type because the package\n * emits keys and dimensions, not watchparty's protocol.\n */\nexport interface ChatMediaAttachment {\n key: string;\n w: number;\n h: number;\n title?: string;\n}\n\n/** Raised when the CDN accepted an upload but returned something unusable. */\nexport class ChatImageUploadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ChatImageUploadError';\n }\n}\n\n/**\n * User-facing wording for a `CdnError`.\n *\n * Branching on `reason` rather than a status is what the package's error type\n * is for. Two of these must stay distinct: `unauthenticated` is fixable by the\n * user and `forbidden` is not, so collapsing them shows a sign-in prompt that\n * changes nothing.\n */\nexport function uploadErrorMessage(reason: CdnErrorReason): string {\n switch (reason) {\n case 'unauthenticated': return 'Sign in to paste images';\n case 'forbidden': return \"Your account can't upload images\";\n case 'too-large': return 'Image too large (max 10 MB)';\n case 'unsupported-type': return \"That image type isn't supported\";\n case 'invalid': return \"That image couldn't be read\";\n case 'network': return 'Check your connection and try again';\n case 'unavailable': return 'Image upload is having trouble — try again';\n default: return 'Image upload failed';\n }\n}\n\n/** Turn any thrown value into something worth showing a user. */\nexport function messageForUploadFailure(err: unknown): string {\n if (err instanceof CdnError) return uploadErrorMessage(err.reason);\n if (err instanceof ChatImageUploadError) return err.message;\n return 'Image upload failed';\n}\n\n/**\n * Reject a file at PASTE time, before it ever reaches the composer.\n *\n * `client.upload()` pre-flights the same limits, but it does so at SEND — and\n * telling someone their 12 MB screenshot is too large only after they have\n * typed a message and hit Enter is the experience this avoids. The constants\n * come from the package, which parity-tests them against the worker's own\n * values, so this cannot drift into promising an upload the server refuses.\n *\n * A file with NO type passes: some clipboard and drag sources supply nothing,\n * and refusing those would drop valid images the server would have accepted.\n * The server sniffs the bytes and is the actual gate either way.\n *\n * IMAGE-only, deliberately: cdn-client 2.x also accepts audio, but chat renders\n * every upload in an <img>. 2.0 removed ACCEPTED_TYPES/MAX_UPLOAD_BYTES rather\n * than widen them for exactly this reason; `kindOfType` also resolves the\n * browser's alternate spellings of a type.\n */\nexport function preflightMessage(file: { size: number; type: string; name?: string }): string | null {\n const type = file.type.toLowerCase().split(';')[0].trim();\n if (type && kindOfType(type) !== 'image') {\n return uploadErrorMessage('unsupported-type');\n }\n if (file.size > MAX_BYTES.image) return uploadErrorMessage('too-large');\n if (file.size === 0) return uploadErrorMessage('invalid');\n return null;\n}\n\n/**\n * Pull image files out of a paste or a drop.\n *\n * Both delegate to the package, which reads `clipboardData.files` first and\n * falls back to walking `items` — not equivalent, since some browsers populate\n * only `items` for an image copied out of another page.\n */\nexport function imageFilesFromClipboard(dt: DataTransfer | null | undefined): File[] {\n return filesFromClipboard(dt);\n}\n\nexport function imageFilesFromDrop(dt: DataTransfer | null | undefined): File[] {\n return filesFromDrop(dt);\n}\n\n/**\n * Measure a file's intrinsic dimensions.\n *\n * The package returns `null` when the environment has no decoder; the wire\n * schema spells \"unknown\" as **0**, and the renderer falls back to natural\n * flow on it. Translating here keeps that single spelling everywhere.\n */\nexport async function measureImage(file: Blob): Promise<{ w: number; h: number }> {\n const dims = await measureImageBytes(file);\n return { w: dims?.width ?? 0, h: dims?.height ?? 0 };\n}\n\n/**\n * Upload one file and return the key to put on the message.\n *\n * The key is re-validated even though the CDN produced it. A malformed one\n * would be persisted into chat history permanently, and `urlFor` will happily\n * compose a URL for anything — so this is the last point at which a bad key\n * can be refused rather than stored.\n */\nexport async function uploadChatImage(\n file: File,\n opts: { signal?: AbortSignal } = {},\n): Promise<{ key: string; deduplicated: boolean }> {\n const result = await getCdnClient().upload(file, { signal: opts.signal });\n if (!isChatImageKey(result.key)) {\n throw new ChatImageUploadError('Image upload returned an unusable key');\n }\n return { key: result.key, deduplicated: result.deduplicated === true };\n}\n\n/**\n * Turn every pending attachment into a wire attachment, preserving order.\n * Throws on the first failure.\n *\n * A `remote` attachment (a meme pick) is passed straight through: its bytes\n * are already on the CDN under that key, so re-uploading them would spend\n * bandwidth to arrive at the identical content-addressed key. That is what\n * makes picking a meme feel instant while pasting a screenshot does not.\n */\nexport async function uploadAll(\n pending: readonly PendingImage[],\n opts: { signal?: AbortSignal } = {},\n): Promise<ChatMediaAttachment[]> {\n const out: ChatMediaAttachment[] = [];\n for (const p of pending) {\n if (p.kind === 'remote') {\n out.push({ key: p.key, w: p.w, h: p.h, ...(p.title ? { title: p.title } : {}) });\n continue;\n }\n const { key } = await uploadChatImage(p.file, opts);\n out.push({ key, w: p.w, h: p.h });\n }\n return out;\n}\n\n/** Log helper so callers keep one shape for upload failures. */\nexport function uploadErrorFields(err: unknown): Record<string, unknown> {\n return {\n error: err instanceof Error ? err.message : String(err),\n name: err instanceof Error ? err.name : typeof err,\n reason: err instanceof CdnError ? err.reason : undefined,\n status: err instanceof CdnError ? err.status : undefined,\n };\n}\n","import {\n CdnError,\n SEARCH_DEBOUNCE_MS,\n createCdnClient,\n measureImage\n} from \"./chunk-KAVZB4QP.js\";\n\n// src/react.ts\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nfunction useCdnClient(options) {\n const { cdnBase, uploadPath, fetch: fetchImpl } = options;\n return useMemo(\n () => createCdnClient({ cdnBase, uploadPath, fetch: fetchImpl }),\n [cdnBase, uploadPath, fetchImpl]\n );\n}\nfunction useClientRef(client) {\n const ref = useRef(client);\n useEffect(() => {\n ref.current = client;\n });\n return [ref, `${client.cdnBase}|${client.uploadPath}`];\n}\nfunction useCdnUpload(client) {\n const [uploads, setUploads] = useState([]);\n const [clientRef, clientKey] = useClientRef(client);\n const objectUrls = useRef([]);\n useEffect(() => {\n return () => {\n for (const url of objectUrls.current) URL.revokeObjectURL(url);\n objectUrls.current = [];\n };\n }, []);\n const upload = useCallback(\n async (files) => {\n if (files.length === 0) return [];\n const entries = await Promise.all(\n files.map(async (file) => {\n const previewUrl = URL.createObjectURL(file);\n objectUrls.current.push(previewUrl);\n const dims = await measureImage(file);\n return {\n file,\n previewUrl,\n status: \"uploading\",\n width: dims?.width ?? null,\n height: dims?.height ?? null\n };\n })\n );\n setUploads((prev) => [...prev, ...entries]);\n const settled = await Promise.all(\n entries.map(async (entry) => {\n try {\n const result = await clientRef.current.upload(entry.file);\n setUploads(\n (prev) => prev.map(\n (u) => u.previewUrl === entry.previewUrl ? { ...u, status: \"done\", result } : u\n )\n );\n return result;\n } catch (err) {\n const error = err instanceof CdnError ? err : new CdnError(\"unknown\", String(err));\n setUploads(\n (prev) => prev.map(\n (u) => u.previewUrl === entry.previewUrl ? { ...u, status: \"error\", error } : u\n )\n );\n return null;\n }\n })\n );\n return settled.filter((r) => r !== null);\n },\n [clientRef, clientKey]\n );\n const clear = useCallback(() => {\n for (const url of objectUrls.current) URL.revokeObjectURL(url);\n objectUrls.current = [];\n setUploads([]);\n }, []);\n return { uploads, upload, clear };\n}\nfunction useCatalogueSearch(client, basePath, query, options = {}) {\n const { limit, debounceMs = SEARCH_DEBOUNCE_MS } = options;\n const [clientRef, clientKey] = useClientRef(client);\n const [items, setItems] = useState([]);\n const [cursor, setCursor] = useState(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n const [page, setPage] = useState(0);\n const abortRef = useRef(null);\n useEffect(() => {\n setPage(0);\n setCursor(null);\n }, [query, basePath]);\n useEffect(() => {\n let cancelled = false;\n const isFirstPage = page === 0;\n const run = async () => {\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n setLoading(true);\n setError(null);\n try {\n const result = await clientRef.current.catalogue(basePath).search({\n query,\n limit,\n cursor: isFirstPage ? null : cursor,\n signal: controller.signal\n });\n if (cancelled) return;\n setItems((prev) => isFirstPage ? result.results : [...prev, ...result.results]);\n setCursor(result.next);\n } catch (err) {\n if (cancelled) return;\n if (err instanceof Error && err.name === \"AbortError\") return;\n setError(err instanceof CdnError ? err : new CdnError(\"unknown\", String(err)));\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n const timer = setTimeout(\n // A block body, not `() => void run()`: `no-void` is configured with\n // allowAsStatement, so the discard is fine as a statement and an\n // error as an expression body.\n () => {\n void run();\n },\n // Only debounce typing. Paging is a deliberate click and should feel\n // immediate.\n isFirstPage ? debounceMs : 0\n );\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [clientRef, clientKey, basePath, query, limit, debounceMs, page]);\n const loadMore = useCallback(() => {\n if (loading || !cursor) return;\n setPage((p) => p + 1);\n }, [loading, cursor]);\n return { items, loading, error, hasMore: cursor !== null, loadMore };\n}\nexport {\n useCatalogueSearch,\n useCdnClient,\n useCdnUpload\n};\n","/**\n * Where the meme picker's hover preview goes.\n *\n * Ported from dataplatform's `/tube/` comment picker\n * (`src/components/MediaPicker.tsx: placePreview`), which solved this first.\n * Keeping the geometry identical is deliberate — the two pickers read the same\n * catalogue and should not feel like different products — so if you change the\n * rules here, change them there too.\n *\n * The preview sits BESIDE the panel rather than over it, so the grid stays\n * readable and you can keep scanning while one item is expanded. Left is\n * preferred because the panel is right-anchored above the composer, so that is\n * where the room usually is; it flips right when it will not fit, and is\n * clamped into the viewport either way so a tile near an edge still gets a\n * whole preview rather than a sliced one.\n */\n\n/** Side of the enlarged preview box, in px. */\nexport const PREVIEW_PX = 240;\n\n/** Breathing room from the panel and the viewport edges. */\nexport const PREVIEW_GAP = 8;\n\n/**\n * Vertical slack for the caption row beneath the image.\n *\n * The box HUGS its image (`height: auto`), so its real height varies with the\n * meme's aspect. Placement clamps against the TALLEST box it could be, which\n * means a shorter one is simply clamped more conservatively — never pushed\n * off-screen. Getting this backwards (clamping against the actual height)\n * would let a tall image overflow the bottom edge.\n */\nexport const PREVIEW_CHROME_PX = 28;\n\ninterface Rect {\n left: number;\n top: number;\n right: number;\n width: number;\n height: number;\n}\n\ninterface Viewport {\n width: number;\n height: number;\n}\n\n/** Max height the image itself may paint at (mirrors the CSS `max-height`). */\nexport const PREVIEW_MAX_IMG_PX = 240;\n\nexport function placePreview(\n tile: Rect,\n panel: Rect,\n viewport: Viewport\n): { left: number; top: number } {\n let left = panel.left - PREVIEW_PX - PREVIEW_GAP;\n // No room on the left — put it on the panel's other side.\n if (left < PREVIEW_GAP) left = panel.right + PREVIEW_GAP;\n // Clamp AFTER the flip: on a narrow viewport neither side fits cleanly, and\n // a preview half off-screen is worse than one overlapping the panel.\n left = Math.min(left, viewport.width - PREVIEW_PX - PREVIEW_GAP);\n left = Math.max(PREVIEW_GAP, left);\n\n // Vertically centred on the tile, so the eye does not have to travel.\n const boxHeight = PREVIEW_MAX_IMG_PX + PREVIEW_CHROME_PX;\n let top = tile.top + tile.height / 2 - boxHeight / 2;\n top = Math.min(top, viewport.height - boxHeight - PREVIEW_GAP);\n top = Math.max(PREVIEW_GAP, top);\n\n return { left, top };\n}\n","import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useCatalogueSearch } from '@wolffm/cdn-client/react';\nimport type { CatalogueItem } from '@wolffm/cdn-client';\nimport { getCdnClient, resolveChatImageUrl } from '../config';\nimport {\n PREVIEW_PX,\n PREVIEW_MAX_IMG_PX,\n placePreview,\n} from '../previewPlacement';\n\n/**\n * The shared meme library, as a picker.\n *\n * This is the half that deliberately did NOT move into `@wolffm/cdn-client`:\n * the package ships headless hooks and no markup, because watchparty's chat\n * picker and a `/tube/` comment box want the same transport and nothing like\n * the same layout. `useCatalogueSearch` owns everything that is hard and\n * invisible when wrong — the 250ms debounce, cursor paging, and aborting a\n * superseded request so a slow \"ca\" cannot land after a fast \"cat\" — and this\n * file owns the grid.\n *\n * Styling is theme tokens (`bg-bg-card`, `text-text-secondary`) from\n * `@wolffm/themes`, which are ecosystem-wide rather than watchparty-local.\n */\n\n/**\n * Facts about the corpus that change how you write against it:\n *\n * **There are no GIFs.** dataplatform sniffed it by magic bytes: 690 JPEG, 189\n * PNG, 105 WebP, 266 MP4, and ZERO GIFs — short video was transcoded to animated\n * WebP. Call them **memes**, never GIFs. There is also no third-party provider\n * and no attribution owed; the library is ours.\n *\n * **Do NOT branch on `contentType`.** Animated and still items are mixed with no\n * flag, and the encoder is moving WebP -> animated AVIF. Treat any `image/*` as\n * renderable — there is no `<video>` path to build here.\n *\n * (255 long clips are simply not pickable — videos over 6s stayed on\n * dataplatform's tunnel. Nothing to work around.)\n */\n\n/** The catalogue this picker reads. */\nexport const MEME_CATALOGUE_PATH = '/dataplatform/api/memes';\n\nexport interface MemePickerProps {\n open: boolean;\n onClose: () => void;\n /** Called with the picked item. The host decides what that means. */\n onPick: (item: CatalogueItem) => void;\n /**\n * Fired when the catalogue answers 5xx. Hosts should hide the meme\n * affordance for the session — pasting an image has to keep working when the\n * library does not, so the two features never share a failure.\n */\n onUnavailable?: () => void;\n}\n\n/**\n * Hover-intent delay. Sweeping the pointer across a row of nine memes on the\n * way to the tenth should not strobe nine panels at you.\n */\nconst HOVER_INTENT_MS = 120;\n\ninterface Preview {\n item: CatalogueItem;\n left: number;\n top: number;\n}\n\n/**\n * The open panel. Mounted ONLY while the picker is open — see `MemePicker`.\n */\nfunction MemePickerPanel({ onClose, onPick, onUnavailable }: Omit<MemePickerProps, 'open'>) {\n const [query, setQuery] = useState('');\n const panelRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const client = getCdnClient();\n\n // Trimmed so that adding a trailing space does not look like a NEW query to\n // the hook and refire an identical request. It is not a correctness fix:\n // dataplatform trims server-side (verified — 'warhammer', 'warhammer ' and\n // ' warhammer' return byte-identical result sets), and cdn-client trims as\n // of 1.0.2. This only avoids the redundant round trip.\n const { items, loading, error, hasMore, loadMore } = useCatalogueSearch(\n client,\n MEME_CATALOGUE_PATH,\n query.trim(),\n );\n\n // `@wolffm/cdn-client` >= 1.0.2 shares ONE `CdnError` across its entry\n // points, so a reason classified by the core survives the hook. In 1.0.0 it\n // did not — both entries inlined their own class, `instanceof` failed inside\n // the hooks, and every failure arrived as `unknown`. That is why the version\n // floor matters; do not relax it below 1.0.2 (1.0.1 still has the bug).\n const reason = error?.reason ?? null;\n\n /**\n * Dedup by key before rendering.\n *\n * Keyset pagination plus a content-addressed catalogue (one image filed\n * under several categories collapses to a single key) makes a repeat across\n * pages possible, and `useCatalogueSearch` appends pages verbatim. Two rows\n * with the same key is a duplicate React key — a rendering bug, not a\n * cosmetic one — so the grid dedups rather than trusting the feed.\n */\n const rows = useMemo(() => {\n const seen = new Set<string>();\n return items.filter((item) => {\n if (seen.has(item.key)) return false;\n seen.add(item.key);\n return true;\n });\n }, [items]);\n\n // ── hover-to-expand ────────────────────────────────────────────────────\n // Tiles are square and cropped with `object-fit: cover`, which is the wrong\n // shape for deciding WHICH meme this is — a wide one is showing you its\n // middle third. Resting on one expands it, uncropped.\n //\n // A `position: fixed` box, NOT a `transform: scale()` on the tile. That is\n // forced rather than stylistic: the grid scrolls (`overflow-y-auto`), which\n // clips a scaled child, so every tile in the first and last row would be\n // sliced by the container it lives in. Fixed positioning escapes the clip.\n // Same reasoning and same geometry as dataplatform's `/tube/` picker.\n const [preview, setPreview] = useState<Preview | null>(null);\n const hoverTimer = useRef<number | null>(null);\n\n const cancelHover = useCallback(() => {\n if (hoverTimer.current !== null) {\n window.clearTimeout(hoverTimer.current);\n hoverTimer.current = null;\n }\n }, []);\n\n const hidePreview = useCallback(() => {\n cancelHover();\n setPreview(null);\n }, [cancelHover]);\n\n // Only where hovering is a real gesture. A touch screen synthesises a hover\n // on tap, so without this the preview flashes over the grid on the way to\n // selecting something.\n const canHover =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(hover: hover)').matches;\n\n const showPreview = useCallback(\n (item: CatalogueItem, tile: HTMLElement, immediate = false) => {\n const panel = panelRef.current;\n if (!panel) return;\n const place = () =>\n setPreview({\n item,\n ...placePreview(tile.getBoundingClientRect(), panel.getBoundingClientRect(), {\n width: window.innerWidth,\n height: window.innerHeight,\n }),\n });\n cancelHover();\n if (immediate) place();\n else hoverTimer.current = window.setTimeout(place, HOVER_INTENT_MS);\n },\n [cancelHover],\n );\n\n // A pending timer must not fire into an unmounted component.\n useEffect(() => () => cancelHover(), [cancelHover]);\n\n useEffect(() => {\n if (reason === 'unavailable') onUnavailable?.();\n }, [reason, onUnavailable]);\n\n useEffect(() => { inputRef.current?.focus(); }, []);\n\n // Escape closes, and a click outside closes. This component only exists\n // while open, so there is nothing to gate.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };\n const onDown = (e: MouseEvent) => {\n if (panelRef.current && !panelRef.current.contains(e.target as Node)) onClose();\n };\n document.addEventListener('keydown', onKey);\n document.addEventListener('mousedown', onDown);\n return () => {\n document.removeEventListener('keydown', onKey);\n document.removeEventListener('mousedown', onDown);\n };\n }, [onClose]);\n\n const pick = useCallback((item: CatalogueItem) => {\n onPick(item);\n onClose();\n }, [onPick, onClose]);\n\n const isEmpty = !loading && !error && rows.length === 0;\n\n return (\n <div\n ref={panelRef}\n role=\"dialog\"\n aria-label=\"Meme library\"\n data-testid=\"meme-picker\"\n className=\"absolute bottom-full left-0 right-0 mb-2 z-30 flex max-h-80 flex-col rounded border border-border bg-bg-card shadow-lg\"\n >\n <div className=\"border-b border-border-light p-2\">\n <input\n ref={inputRef}\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search memes...\"\n aria-label=\"Search memes\"\n data-testid=\"meme-search\"\n className=\"w-full rounded border border-border-light bg-bg-alt px-2 py-1 text-sm text-text placeholder:text-text-secondary focus:outline-none focus:border-primary\"\n />\n </div>\n\n <div\n className=\"flex-1 overflow-y-auto p-2\"\n /* onScroll must sit on the element that ACTUALLY scrolls — scroll does\n not bubble from it to the grid inside. Scrolling moves the tile the\n box was measured against, so the preview would hang beside nothing;\n dropping it is cheaper and more honest than recomputing per frame. */\n onScroll={hidePreview}\n /* Leaving the panel entirely (not just one tile) also drops it. */\n onMouseLeave={hidePreview}\n >\n {error && (\n <p\n className=\"m-2 p-2 text-center text-sm bg-danger-bg text-on-danger-bg rounded\"\n role=\"alert\"\n /* The reason drives both the copy and the host's decision to hide\n the affordance, so it is exposed for assertions — an outage and\n a permission failure look identical in the rendered text. */\n data-reason={reason}\n >\n {reason === 'unauthenticated'\n ? 'Sign in to browse memes'\n : reason === 'forbidden'\n ? \"Your account can't browse the meme library\"\n : 'Meme library is unavailable'}\n </p>\n )}\n\n {isEmpty && (\n <p className=\"p-4 text-center text-sm text-text-secondary\">\n {query.trim() ? `No memes match \"${query.trim()}\"` : 'The meme library is empty'}\n </p>\n )}\n\n {rows.length > 0 && (\n <div className=\"grid grid-cols-3 gap-1.5 sm:grid-cols-4\">\n {rows.map((item) => {\n // Compose from the KEY, never interpolate it. `null` means the\n // key failed the content-addressed shape check, so the row is\n // dropped rather than reaching an <img src>.\n const src = resolveChatImageUrl(item.key);\n if (!src) return null;\n return (\n <button\n key={item.key}\n type=\"button\"\n onClick={() => pick(item)}\n title={item.title}\n data-testid=\"meme-result\"\n onMouseEnter={(e) => {\n if (canHover) showPreview(item, e.currentTarget);\n }}\n onMouseLeave={hidePreview}\n /* Keyboard users get it too, and without the intent delay —\n arrowing onto a tile is already deliberate, so there is no\n sweep to protect against. */\n onFocus={(e) => showPreview(item, e.currentTarget, true)}\n onBlur={hidePreview}\n className=\"group relative aspect-square overflow-hidden rounded border border-border-light bg-bg-alt focus:outline-none focus:border-primary hover:border-primary\"\n >\n <img\n src={src}\n /* The title is frequently the ONLY text describing the\n image, and a meme-heavy chat log is unusable with a\n screen reader without it. */\n alt={item.title || 'Meme'}\n loading=\"lazy\"\n decoding=\"async\"\n className=\"h-full w-full object-cover\"\n />\n </button>\n );\n })}\n </div>\n )}\n\n {loading && (\n <p className=\"p-2 text-center text-xs text-text-secondary\">Loading…</p>\n )}\n\n {hasMore && !loading && (\n <button\n type=\"button\"\n onClick={loadMore}\n data-testid=\"meme-load-more\"\n className=\"mt-2 w-full rounded border border-border-light py-1 text-xs text-text-secondary hover:text-text hover:border-primary\"\n >\n Load more\n </button>\n )}\n </div>\n\n {preview && (\n <div\n data-testid=\"meme-preview\"\n className=\"fixed z-50 flex flex-col gap-1 rounded border border-border-light bg-bg-card p-1.5 shadow-2xl\"\n style={{\n left: preview.left,\n top: preview.top,\n width: PREVIEW_PX,\n /* The placement maths reserves exactly PREVIEW_PX of width.\n Without border-box the padding and border sit OUTSIDE that\n number and the box laps over the panel it is meant to sit\n beside. */\n boxSizing: 'border-box',\n /* Never eats the click it is describing. */\n pointerEvents: 'none',\n }}\n /* Decoration for a control that is already labelled: the tile it\n belongs to carries the same title as its `alt` and `title`, so a\n screen reader announcing this again would only echo. */\n aria-hidden=\"true\"\n >\n <img\n src={resolveChatImageUrl(preview.item.key) ?? undefined}\n alt=\"\"\n /* The box HUGS the image rather than being a fixed square. A wide\n meme in a tall box is mostly empty card, which reads as a\n loading state. */\n className=\"block w-full rounded bg-bg object-contain\"\n style={{ height: 'auto', maxHeight: PREVIEW_MAX_IMG_PX }}\n />\n {preview.item.title && (\n /* One line. A long title must not change the box height, because\n the box height is what the placement maths clamped against. */\n <span className=\"overflow-hidden text-ellipsis whitespace-nowrap text-center text-[10px] text-text-secondary\">\n {preview.item.title}\n </span>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Mount gate — the panel does not exist until the picker is opened.\n *\n * **This split is load-bearing, not organisation.** `useCatalogueSearch` fires\n * its first search on MOUNT (deliberately: an empty query returns the\n * catalogue's default set, so a picker opens populated). Hooks cannot be\n * called conditionally, so an `if (!open) return null` inside the panel sits\n * AFTER the hook and does not prevent it — the search still runs.\n *\n * With the panel rendered unconditionally, every page load fetched the meme\n * library for every viewer, whether or not they ever opened the picker: a\n * friend-gated request per load, and an error banner already showing the\n * moment someone first clicked. Keeping the mount itself behind `open` is what\n * makes \"opens populated\" cost nothing until it is opened.\n */\nexport default function MemePicker({ open, ...rest }: MemePickerProps) {\n if (!open) return null;\n return <MemePickerPanel {...rest} />;\n}\n","/**\n * The game-bundle contract: how a room host mounts a game it knows nothing\n * about.\n *\n * A host (watchparty's room) holds a list of `GameEntry` rows as DATA. For a\n * `module` entry it dynamically imports `url`, expects the module to export\n * `mount` (a `GameBundleModule`), and hands it an element plus a context. The\n * bundle renders into that element — inside its own shadow root, so neither\n * side's styles reach the other — and talks to its own server; the host only\n * relays who is looking, what theme they are in, and whether they lead.\n *\n * Adding a game is therefore a data edit on the host, never host code.\n */\n/**\n * Whether `value` is a module that satisfies `GameBundleModule`. A bundle is\n * loaded from a URL at runtime, so its shape is a claim the host checks rather\n * than a type it can trust.\n */\nexport function isGameBundleModule(value) {\n return typeof value === 'object' && value !== null\n && typeof value.mount === 'function';\n}\n/** Whether `value` is what a bundle's `mount` must resolve to. */\nexport function isMountedGameBundle(value) {\n if (typeof value !== 'object' || value === null)\n return false;\n const v = value;\n return typeof v.unmount === 'function'\n && typeof v.update === 'function'\n && (v.mountSettings === undefined || typeof v.mountSettings === 'function');\n}\n","import { debug } from './debug';\n\n/** Event Room.tsx listens for to open the MediaDrawer. */\nexport const OPEN_MEDIA_DRAWER_EVENT = 'watchparty:open-media-drawer';\n\n/**\n * Open the media drawer.\n *\n * Single helper so every affordance dispatches the identical event name —\n * there is more than one entry point now (chat toolbar + control bar) and a\n * typo in either would fail silently, since nothing errors on an unheard\n * CustomEvent.\n */\nexport function openMediaDrawer(source: string): void {\n debug('navigation', `Select Media clicked (${source})`);\n window.dispatchEvent(new CustomEvent(OPEN_MEDIA_DRAWER_EVENT));\n}\n","import { Component, type ReactNode, type ErrorInfo } from 'react';\nimport { error as logError } from '../lib/debug';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n}\n\ninterface State {\n hasError: boolean;\n}\n\nexport class ErrorBoundary extends Component<Props, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(err: Error, info: ErrorInfo) {\n logError('app', '[ErrorBoundary] ' + err.message, { stack: err.stack, componentStack: info.componentStack });\n }\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback ?? (\n <div className=\"flex flex-col items-center justify-center h-full gap-4 text-text-secondary\">\n <p className=\"text-lg\">Something went wrong.</p>\n <button\n className=\"px-4 py-2 rounded bg-bg-card text-text hover:bg-bg-alt\"\n onClick={() => this.setState({ hasError: false })}\n >\n Try Again\n </button>\n </div>\n );\n }\n return this.props.children;\n }\n}\n","import { useEffect, useState } from 'react';\n\n/**\n * True while any element is fullscreen.\n *\n * Fullscreen is requested on the MediaPlayer root div, and only the fullscreen\n * element and its descendants paint — so anything rendered as a SIBLING of it\n * (ChatPanel, in Room.tsx) is invisible for the duration, even though it is\n * still in the DOM. Components that own an affordance which must stay reachable\n * need to know that; see FullscreenPortal for the other half of this problem.\n */\nexport function useIsFullscreen(): boolean {\n const [isFullscreen, setIsFullscreen] = useState(\n () => typeof document !== 'undefined' && !!document.fullscreenElement,\n );\n useEffect(() => {\n const sync = () => setIsFullscreen(!!document.fullscreenElement);\n sync();\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n }, []);\n return isFullscreen;\n}\n","/**\n * Which \"Select Media\" button is on screen.\n *\n * There are three sites, and the rule matters enough to live outside all of\n * them:\n *\n * - chat toolbar (ChatPanel) — the primary one. ChatPanel is hidden when\n * `chatCollapsed && mediaId`, and it renders as a SIBLING of the fullscreen\n * element (Room.tsx) so it stops painting in fullscreen.\n * - control bar (ControlBar) — the fallback. ControlBar lives INSIDE\n * MediaPlayer, so it only exists once media is loaded, but it is visible in\n * both of the states above — unless a game is up.\n * - room corner (RoomCornerControls) — the fallback's fallback. A running game\n * (the built-in iframe game or a `kind: 'module'` bundle) covers the main\n * area at z-30, ControlBar included, while ControlBar is still in the DOM\n * beneath it. With chat collapsed over loaded media the leader then had no\n * reachable button at all, and no way to stop the game (party 2026-10-11:\n * \"can't leave cardtable\"). Fullscreen is NOT this case: the game overlay\n * is a sibling of the fullscreen element, so in fullscreen the player and\n * its ControlBar are what paints.\n *\n * The invariant is EXACTLY ONE for a leader in every reachable state:\n *\n * - zero would strand the leader with no way to change media. That is the\n * regression media-reload.spec.ts:111 guards, and it came back through a\n * second route: the original fix moved the button out of a `!mediaInfo`\n * gate and into a panel that itself disappears.\n * - two is a Playwright strict-mode violation on\n * `button[aria-label=\"Select Media\"]`, and ready-check-fixes.spec.ts drives\n * fullscreen while locating exactly that.\n *\n * Non-leaders get none — they cannot load media at all.\n */\nexport interface MediaDrawerState {\n isLeader: boolean;\n /** A media is loaded (snapshot.mediaId is non-null). */\n hasMedia: boolean;\n chatCollapsed: boolean;\n isFullscreen: boolean;\n /** A game covers the main area (`useActiveGame()` is non-null). */\n gameOverlay: boolean;\n}\n\nexport type MediaDrawerAffordance = 'chat-toolbar' | 'control-bar' | 'room-corner' | 'none';\n\n/**\n * Whether ChatPanel is off screen. ChatPanel's own early return and the\n * chat toggle (`chatToggleSite`) both read this, so the panel can never be hidden\n * with no way back, nor shown beside a button offering to show it.\n */\nexport function isChatPanelHidden(\n s: Pick<MediaDrawerState, 'hasMedia' | 'chatCollapsed' | 'isFullscreen'>,\n): boolean {\n // Without media the panel always shows: it is then the only carrier of\n // Select Media, since ControlBar does not exist.\n return s.hasMedia && (s.chatCollapsed || s.isFullscreen);\n}\n\n/** Where the single reachable \"Select Media\" button is, if any. */\nexport function selectMediaAffordance(s: MediaDrawerState): MediaDrawerAffordance {\n if (!s.isLeader) return 'none';\n if (!isChatPanelHidden(s)) return 'chat-toolbar';\n // The panel only hides with media, so ControlBar exists — but a game covers\n // it everywhere except fullscreen, where the overlay does not paint.\n if (s.gameOverlay && !s.isFullscreen) return 'room-corner';\n return 'control-bar';\n}\n\ntype ChatToggleSite = 'control-bar' | 'room-corner' | 'none';\n\n/**\n * Where the one Hide/Show chat toggle is. In the control bar, which fades with\n * the rest of the player controls (operator, 2026-10-31: the always-visible\n * corner button was in the way). Without media the panel cannot collapse, so\n * there is no toggle. A game covers the main area at z-30 and the control bar\n * with it, so while one is up outside fullscreen the toggle moves to the room\n * corner: otherwise a viewer who hid chat during a game could not get it back.\n * In fullscreen the game does not paint, so the control bar is the site again.\n */\nexport function chatToggleSite(\n s: Pick<MediaDrawerState, 'hasMedia' | 'isFullscreen' | 'gameOverlay'>,\n): ChatToggleSite {\n if (!s.hasMedia) return 'none';\n if (s.gameOverlay && !s.isFullscreen) return 'room-corner';\n return 'control-bar';\n}\n","import type { RosterUser } from './store';\n\n/**\n * Which picture a chat line shows. A message carries its sender's character\n * as of sending (`sprite`), but not their picture: that is looked up from the\n * roster by participant handle, so a line shows the sender's CURRENT picture\n * while they are here and falls back to the character once they have left.\n */\nexport function pictureForAuthor(roster: Record<string, RosterUser>, author: string | undefined): string | undefined {\n if (!author) return undefined;\n for (const u of Object.values(roster)) {\n if (u.author === author && u.avatarUrl) return u.avatarUrl;\n }\n return undefined;\n}\n","import React, { createContext, useContext, useEffect, useMemo, type CSSProperties, type RefObject } from 'react';\nimport { ensureCosmeticsStyles, type AvatarCosmetics } from '@wolffm/prefs-client/cosmetics';\nimport { useAppStore, type RosterUser } from './store';\n\n/**\n * Avatar cosmetics in the room: what each person wears (a name effect,\n * gradient and font, an avatar overlay, a message reveal), drawn with\n * `@wolffm/prefs-client/cosmetics` so it looks the way the settings dialog\n * previews it.\n *\n * - What someone wears arrives in presence (`RosterUser.cosmetics`, cleaned at\n * the socket boundary). A chat line reads it from the roster by author, like\n * its picture: the sender's current look while they are here, nothing once\n * they have left.\n * - Colours derive from the person's own colour, passed as `--hc-base`.\n * - Loops pause while a row is off screen (`useIdleWhenHidden`), and a message\n * reveal plays only for a message that arrived live, once.\n * - The stylesheet: the app injects it into its own <head>. RoomChrome sits\n * on someone else's page and must leave that <head> as it found it, so its\n * static, scoped `room-chrome.css` carries the same rules, appended at build\n * (vite.room-chrome.config.ts), and the chrome provides\n * `InjectCosmeticsStyles` false.\n */\n\n/** Whether this tree injects the cosmetics stylesheet. RoomChrome provides false: its CSS ships the rules. */\nexport const InjectCosmeticsStyles = createContext(true);\n\n/** Inject the stylesheet once (idempotent). Fonts and images load from hadoku.me/cosmetics. */\nexport function useCosmeticsStyles(): void {\n const inject = useContext(InjectCosmeticsStyles);\n useEffect(() => {\n if (inject) ensureCosmeticsStyles();\n }, [inject]);\n}\n\n/** The roster entry's cosmetics as a stable key: '' when the author wears nothing or has left. */\nfunction wornKeyFor(roster: Record<string, RosterUser>, author: string | undefined): string {\n if (!author) return '';\n for (const u of Object.values(roster)) {\n if (u.author === author && u.cosmetics) return JSON.stringify(u.cosmetics);\n }\n return '';\n}\n\n/**\n * What the author of a line wears. Selected as a string so a presence update\n * that changes nothing about them does not re-render their lines.\n */\nexport function useAuthorCosmetics(author: string | undefined): AvatarCosmetics | undefined {\n const key = useAppStore(s => wornKeyFor(s.roster, author));\n return useMemo(() => (key ? (JSON.parse(key) as AvatarCosmetics) : undefined), [key]);\n}\n\n/** `--hc-base` for a person's colour (any CSS colour). */\nexport function baseColour(colour: string | undefined): CSSProperties {\n return colour ? ({ '--hc-base': colour } as CSSProperties) : {};\n}\n\n/**\n * Trusted HTML from the cosmetics renderer, which escapes every string it is\n * given. Memoised on the string: React re-sets innerHTML on every render even\n * when the string is the same, which would replay a finished message reveal\n * and replace nodes under the pointer.\n */\nexport const CosmeticHtml = React.memo(\n function CosmeticHtml({ html, as = 'span', className, style }: {\n html: string;\n as?: 'span' | 'p';\n className?: string;\n style?: CSSProperties;\n }) {\n return React.createElement(as, { className, style, dangerouslySetInnerHTML: { __html: html } });\n },\n (a, b) => a.html === b.html && a.as === b.as && a.className === b.className &&\n JSON.stringify(a.style) === JSON.stringify(b.style),\n);\n\n/**\n * Pause every cosmetics loop under `ref` while it is off screen: the chat log\n * keeps hundreds of lines, and a name effect on each would keep animating.\n */\nexport function useIdleWhenHidden(ref: RefObject<HTMLElement | null>, enabled: boolean): void {\n useEffect(() => {\n const el = ref.current;\n if (!el || !enabled || typeof IntersectionObserver === 'undefined') return;\n const io = new IntersectionObserver(([entry]) => {\n el.classList.toggle('hc-idle', !entry?.isIntersecting);\n }, { rootMargin: '64px' });\n io.observe(el);\n return () => {\n io.disconnect();\n el.classList.remove('hc-idle');\n };\n }, [ref, enabled]);\n}\n","import type { CSSProperties } from 'react';\nimport { decorHtml, type AvatarCosmetics } from '@wolffm/prefs-client/cosmetics';\nimport AvatarImage from './AvatarImage';\nimport { CosmeticHtml } from '../lib/cosmetics';\n\n/**\n * A chat line's avatar: the picture (or character portrait) with the sender's\n * overlay drawn on its own box, so an overlay can reach past the round picture\n * without being clipped. Shared by ChatMessage and FloatingChat.\n */\nexport default function ChatAvatar({ sprite, url, alt, worn }: {\n sprite: string | null;\n url: string | undefined;\n alt?: string;\n worn: AvatarCosmetics | undefined;\n}) {\n return (\n <span className=\"hc-av\" style={{ '--hc-sz': '32px' } as CSSProperties}>\n <span className=\"hc-pic\">\n <AvatarImage\n sprite={sprite}\n url={url}\n alt={alt}\n className=\"w-8 h-8 rounded-full object-cover flex-shrink-0\"\n fallback={<span className=\"inline-block w-8 h-8 bg-bg-alt/50 rounded-full flex-shrink-0\" />}\n />\n </span>\n {worn?.overlay && <CosmeticHtml html={decorHtml(worn)} style={{ display: 'contents' }} />}\n </span>\n );\n}\n","/**\n * What a click on a chat image means.\n *\n * The attachment stays a real `<a href>`, and that is the whole trick. Middle\n * click, ctrl/cmd+click, shift+click and the right-click context menu are\n * browser behaviours on anchors that no handler has to reimplement — and\n * reimplementing them is how you end up with a \"new tab\" that ignores the\n * user's modifier conventions. We only intercept the ONE gesture we want to\n * repurpose, the plain left click, and let the anchor keep the rest.\n *\n * Concretely, on an `<a>`:\n *\n * | gesture | fires | result |\n * | -------------------- | ------------ | ----------------------------- |\n * | left click | `click` | intercepted -> lightbox |\n * | middle click | `auxclick` | never reaches us -> new tab |\n * | ctrl / cmd + click | `click` | NOT intercepted -> new tab |\n * | shift + click | `click` | NOT intercepted -> new window |\n * | right click | `contextmenu`| never reaches us -> menu |\n *\n * Middle click is the case the user asked for explicitly, and it is the one\n * that needs no code at all — a `click` handler simply never sees it. The\n * modifier cases are here because intercepting them would silently break\n * habits people already have.\n */\n\n/** The parts of a mouse event this decision reads. */\ninterface ClickIntent {\n /** 0 = primary/left. `auxclick` (middle) never reaches a `click` handler. */\n button: number;\n ctrlKey: boolean;\n metaKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n}\n\n/**\n * True when the click should open the in-page lightbox (and the anchor's\n * default navigation must be prevented).\n *\n * False means \"let the browser do its normal thing with this anchor\" — which\n * covers every modifier gesture above, so the fallback is always the\n * previously-shipped behaviour rather than nothing.\n */\nexport function shouldOpenLightbox(e: ClickIntent): boolean {\n if (e.button !== 0) return false;\n // Any modifier is a deliberate \"not the normal action\" signal.\n if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return false;\n return true;\n}\n","import { createContext, useContext } from 'react';\n\n/**\n * Where a portal-rendered overlay (the chat image lightbox) mounts when there\n * is no fullscreen element to go into.\n *\n * `null` means `document.body` — right for the app, which owns the page. The\n * embeddable `RoomChrome` provides its own root instead: its stylesheet is\n * scoped under `.wp-room-chrome`, so an overlay appended to the host's <body>\n * would render unstyled, and it would be a node in the host's document that\n * the host never asked for.\n */\nexport const PortalTargetContext = createContext<HTMLElement | null>(null);\n\nexport function usePortalTarget(): HTMLElement | null {\n return useContext(PortalTargetContext);\n}\n","import React, { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\nimport { usePortalTarget } from '../lib/portalTarget';\n\n/**\n * Full-view for a chat image, Discord-style: the image pops out above the rest\n * of the site on a plain left click, and everything else about the anchor\n * (middle click, ctrl+click, right-click menu) keeps working — see\n * `lib/chatMedia/imageClickIntent.ts`.\n *\n * **Portals into the fullscreen element when there is one.** Fullscreen is\n * requested on the MediaPlayer root (`Room.tsx`), and only that element and its\n * descendants paint — so a lightbox appended to `document.body` while the video\n * is fullscreen renders to nothing at all. Chat is reachable in fullscreen via\n * `FloatingChat`, so a meme clicked there has to open somewhere visible. This\n * is the same problem `FullscreenPortal` solves for the overlays, but the\n * target is resolved per-open rather than by wrapping, because the lightbox is\n * mounted from a leaf component rather than a sibling of the player.\n *\n * Otherwise it goes to the `PortalTargetContext` target (the room chrome's own\n * root when embedded), falling back to `document.body`.\n *\n * `z-[10000]` is the top of this app's ladder: the point of the feature is to\n * sit above everything.\n */\n\ninterface ChatImageLightboxProps {\n src: string;\n /** The library title when the image has one; used as the caption and alt. */\n title?: string;\n onClose: () => void;\n}\n\nexport default function ChatImageLightbox({ src, title, onClose }: ChatImageLightboxProps) {\n // Escape closes, and the listener is on `document` so it works no matter\n // where focus went — a click on the backdrop does not focus anything.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n // Stop the key reaching anything behind it. Escape is also fullscreen's\n // own exit gesture, so without this, closing the lightbox in fullscreen\n // drops the viewer out of fullscreen at the same time.\n e.stopPropagation();\n onClose();\n }\n };\n document.addEventListener('keydown', onKey, true);\n return () => document.removeEventListener('keydown', onKey, true);\n }, [onClose]);\n\n // The video keeps playing behind the lightbox, deliberately — this is a\n // watch party, and pausing the room because one person looked at a meme\n // would be a synchronized-playback event for everybody.\n const scoped = usePortalTarget();\n const target =\n typeof document === 'undefined' ? null : (document.fullscreenElement ?? scoped ?? document.body);\n if (!target) return null;\n\n return createPortal(\n <div\n className=\"fixed inset-0 z-[10000] flex flex-col items-center justify-center bg-black/85 p-6\"\n onClick={onClose}\n data-testid=\"chat-image-lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title || 'Image'}\n >\n <img\n src={src}\n alt={title ?? ''}\n // Clicking the image itself must NOT close: people click to zoom in on\n // detail, and a click-through-to-close makes the image feel like a\n // trap door.\n onClick={(e) => e.stopPropagation()}\n className=\"max-h-full max-w-full rounded object-contain shadow-2xl\"\n data-testid=\"chat-image-lightbox-img\"\n />\n {title && (\n <p className=\"mt-3 max-w-full truncate text-center text-sm text-white/80\">{title}</p>\n )}\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close image\"\n className=\"absolute right-4 top-4 rounded px-3 py-1 text-2xl leading-none text-white/70 hover:text-white focus:outline-none focus:ring-1 focus:ring-white/50\"\n >\n &times;\n </button>\n </div>,\n target,\n );\n}\n","import React, { useState } from 'react';\nimport { type TChatImage } from '@watchparty/shared';\nimport { resolveChatImageUrl } from '../lib/chatMedia';\nimport { shouldOpenLightbox } from '../lib/chatMedia/imageClickIntent';\nimport ChatImageLightbox from './ChatImageLightbox';\n\n/**\n * Renders a chat message's image attachments.\n *\n * ONE component for both chat surfaces (`ChatMessage` in the sidebar and\n * `FloatingMessage` in the fullscreen overlay). Those two already re-implement\n * the avatar/name/text row separately, and that duplication is exactly how a\n * fix lands in one and not the other — attachments do not repeat it.\n *\n * Two rules live here and nowhere else:\n *\n * 1. **Compose the src from the KEY.** The message carries a content-addressed\n * key; `chatImageUrl` re-validates its shape and returns `null` for\n * anything that does not match, so a hostile key renders nothing instead of\n * reaching an `<img src>`. Never interpolate `image.key` directly.\n *\n * 2. **Reserve the layout box before the bytes arrive.** The chat log sits\n * beside a synchronized video player, where a reflow on every image load is\n * very visible. `aspect-ratio` from the sender-measured `w`/`h` holds the\n * space from first paint. A sender that failed to measure sends 0×0; we\n * then fall back to natural flow rather than a collapsed box.\n */\n\n/** Cap the on-screen size so one tall paste can't push the room out of view. */\nconst MAX_H_PX = 240;\nconst MAX_W_PX = 320;\n\nfunction Attachment({ image }: { image: TChatImage }) {\n const [expanded, setExpanded] = useState(false);\n // The CDN base comes from the chat-media layer's own configuration rather\n // than the app's config context, so this component carries no watchparty\n // dependency and moves to the shared package unchanged.\n const src = resolveChatImageUrl(image.key);\n if (!src) return null;\n\n const hasDims = image.w > 0 && image.h > 0;\n // Fit inside the cap while preserving aspect, so the reserved box is the\n // size the image will actually paint at — reserving the intrinsic size\n // would leave a gap that collapses on load, which is the reflow again.\n const scale = hasDims ? Math.min(1, MAX_W_PX / image.w, MAX_H_PX / image.h) : 1;\n\n return (\n <>\n {/* Still a real anchor, deliberately. Middle click, ctrl/cmd+click and the\n right-click menu are then browser behaviours nothing has to\n reimplement — only the plain left click is intercepted, and the href\n remains the fallback for every gesture we do not claim. */}\n <a\n href={src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"block max-w-full cursor-zoom-in\"\n data-testid=\"chat-image\"\n onClick={(e) => {\n if (!shouldOpenLightbox(e)) return;\n e.preventDefault();\n setExpanded(true);\n }}\n >\n <img\n src={src}\n /* A meme carries the library's title, and it is frequently the ONLY\n text describing the image — a meme-heavy log is unusable with a\n screen reader without it. A pasted screenshot has no title, and an\n invented one would be worse than none, so it stays decorative. */\n alt={image.title ?? ''}\n loading=\"lazy\"\n decoding=\"async\"\n width={hasDims ? Math.round(image.w * scale) : undefined}\n height={hasDims ? Math.round(image.h * scale) : undefined}\n className=\"rounded border border-border-light object-contain bg-bg-alt/30\"\n style={\n hasDims\n ? { aspectRatio: `${image.w} / ${image.h}`, maxWidth: '100%', maxHeight: MAX_H_PX }\n : { maxWidth: '100%', maxHeight: MAX_H_PX }\n }\n />\n </a>\n {expanded && (\n <ChatImageLightbox\n src={src}\n title={image.title}\n onClose={() => setExpanded(false)}\n />\n )}\n </>\n );\n}\n\nexport default function ChatImageAttachments({ images }: { images?: TChatImage[] }) {\n if (!images?.length) return null;\n return (\n <div className=\"flex flex-wrap gap-1 mt-1\" data-testid=\"chat-images\">\n {/* Index in the key: the same image pasted twice is a legitimate\n message and would otherwise collide on a key-only React key. */}\n {images.map((im, i) => <Attachment key={`${im.key}-${i}`} image={im} />)}\n </div>\n );\n}\n","/**\n * Is this chat message mine?\n *\n * By AUTHOR (the sender's participant handle) when both sides have one, by\n * clientId only as a fallback. `clientId` is the socket id: it is reassigned on\n * every reconnect, so comparing on it alone made a viewer's own history render\n * as someone else's after any reload. The handle is derived from the person's\n * account (or device) and survives that.\n *\n * The fallback covers a server or stored history that predates `author`. It is\n * deliberately NOT applied when the message has an author and we know ours:\n * a mismatch there is a real \"not mine\", not missing data.\n */\nexport function isOwnMessage(\n message: { clientId: string; author?: string },\n me: { clientId: string | null; author?: string },\n): boolean {\n if (message.author && me.author) return message.author === me.author;\n return !!me.clientId && message.clientId === me.clientId;\n}\n","import React, { useRef, useState } from 'react';\nimport { messageHtml, nameHtml } from '@wolffm/prefs-client/cosmetics';\nimport type { ChatMessage as ChatMessageType} from '../lib/store';\nimport { useAppStore } from '../lib/store';\nimport { usePersonColor } from '../lib/pictureColor';\nimport { pictureForAuthor } from '../lib/avatarPictures';\nimport ChatAvatar from './ChatAvatar';\nimport ChatImageAttachments from './ChatImageAttachments';\nimport { isOwnMessage } from '../lib/chatOwnership';\nimport { baseColour, CosmeticHtml, useAuthorCosmetics, useIdleWhenHidden } from '../lib/cosmetics';\n\ninterface Props {\n message: ChatMessageType;\n}\n\nfunction formatTime(ts: number): string {\n const d = new Date(ts);\n return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n}\n\nconst ChatMessage = React.memo(function ChatMessage({ message }: Props) {\n const myClientId = useAppStore(s => s.me.clientId);\n const myAuthor = useAppStore(s => (s.me.clientId ? s.roster[s.me.clientId]?.author : undefined));\n const isOwn = isOwnMessage(message, { clientId: myClientId, author: myAuthor });\n const pictureUrl = useAppStore(s => pictureForAuthor(s.roster, message.author));\n const nameColor = usePersonColor(message.sprite, pictureUrl);\n const worn = useAuthorCosmetics(message.isSystem ? undefined : message.author);\n const row = useRef<HTMLDivElement>(null);\n useIdleWhenHidden(row, !!worn);\n // The reveal is built once, when the line first renders: a live line plays\n // its sender's reveal one time, and later presence updates never replay it.\n const [textHtml] = useState(() =>\n message.live && message.text ? messageHtml(message.text, worn) : null,\n );\n\n // A recap is CONTENT, not chrome. Every other system message is a one-line\n // status (\"Ada joined\", \"Now playing: X\") and the minimal styling below is\n // right for those — but a ~450-character recap paragraph rendered at 11px,\n // muted, italic and centred reads as noise and gets skipped over, which is\n // the whole point of having asked for it.\n if (message.isSystem && message.systemType === 'recap') {\n return (\n <div className=\"px-2 py-1.5\">\n <div className=\"border-l-2 border-border-light pl-2.5 py-0.5\">\n <div className=\"text-[10px] uppercase tracking-wide text-text-muted mb-0.5\">\n Recap\n <span className=\"ml-1.5 normal-case tracking-normal opacity-60\">\n {formatTime(message.ts)}\n </span>\n </div>\n <p className=\"text-[13px] text-text-secondary leading-relaxed whitespace-pre-wrap break-words\">\n {message.text}\n </p>\n </div>\n </div>\n );\n }\n\n // System messages render differently - minimal\n if (message.isSystem) {\n return (\n <div className=\"px-2 py-0.5 text-[11px] text-text-muted italic text-center leading-tight\">\n {message.text}\n <span className=\"ml-1 text-[10px] opacity-50\">{formatTime(message.ts)}</span>\n </div>\n );\n }\n\n return (\n <div ref={row} style={baseColour(nameColor)} className={`flex items-start gap-2 px-2 py-1 hover:bg-bg-card/20 ${isOwn ? 'bg-bg-card/30' : ''}`}>\n <ChatAvatar sprite={message.sprite} url={pictureUrl} alt={message.name} worn={worn} />\n\n {/* Message content - inline layout like Discord */}\n <div className=\"flex-1 min-w-0 -mt-0.5\">\n {worn ? (\n <CosmeticHtml html={nameHtml(message.name, worn)} className=\"text-[13px]\" style={{ color: nameColor }} />\n ) : (\n <span className=\"font-medium text-[13px]\" style={{ color: nameColor }}>\n {message.name}\n </span>\n )}\n <span className=\"text-[10px] text-text-muted opacity-60 ml-1.5\">\n {formatTime(message.ts)}\n </span>\n {/* An image-only message has empty text; don't render an empty <p>. */}\n {message.text && (textHtml ? (\n <CosmeticHtml as=\"p\" html={textHtml} className=\"text-[13px] text-text break-words leading-snug m-0\" />\n ) : (\n <p className=\"text-[13px] text-text break-words leading-snug m-0\">{message.text}</p>\n ))}\n <ChatImageAttachments images={message.images} />\n </div>\n </div>\n );\n});\n\nexport default ChatMessage;\n","import React, { useRef, useEffect, useCallback } from 'react';\nimport { useAppStore } from '../lib/store';\nimport ChatMessage from './ChatMessage';\nimport { useCosmeticsStyles } from '../lib/cosmetics';\n\n/**\n * The scrolling message list: date breaks, stick-to-bottom unless the reader\n * scrolled up, and the empty state.\n *\n * ONE implementation for both chat columns — the app's `ChatPanel` and the\n * embeddable `RoomChrome`. The render path already has a second copy\n * (`FloatingChat`) and CLAUDE.md records what that costs; a third would be\n * the same drift with one more place for it.\n */\nexport default function ChatLog() {\n useCosmeticsStyles();\n const chat = useAppStore(s => s.chat);\n const containerRef = useRef<HTMLDivElement>(null);\n const userScrolledUpRef = useRef(false);\n\n const handleScroll = useCallback(() => {\n const container = containerRef.current;\n if (!container) return;\n const { scrollHeight, scrollTop, clientHeight } = container;\n const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n userScrolledUpRef.current = distanceFromBottom > 40;\n }, []);\n\n // Scroll the LIST, never `scrollIntoView`: that scrolls every scrollable\n // ancestor too, and inside a host page (`RoomChrome`) each new message would\n // yank the host's own scroll position to the chat.\n useEffect(() => {\n const container = containerRef.current;\n if (container && !userScrolledUpRef.current) {\n container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });\n }\n }, [chat.length]);\n\n return (\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className=\"flex-1 overflow-y-auto overflow-x-hidden chat-messages\"\n >\n {chat.length === 0 ? (\n <div className=\"p-4 text-xs text-text-muted text-center\">\n No messages yet. Be the first to say something!\n </div>\n ) : (\n <>\n {chat.map((msg, i) => {\n const prev = i > 0 ? chat[i - 1] : null;\n const showDateBreak = prev && (msg.ts - prev.ts) > 24 * 60 * 60 * 1000;\n return (\n <React.Fragment key={msg.id}>\n {showDateBreak && (\n <div className=\"flex items-center gap-2 px-4 py-2 my-1\">\n <div className=\"flex-1 h-px bg-border\" />\n <span className=\"text-[10px] text-text-muted whitespace-nowrap\">\n {new Date(msg.ts).toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })}\n </span>\n <div className=\"flex-1 h-px bg-border\" />\n </div>\n )}\n <ChatMessage message={msg} />\n </React.Fragment>\n );\n })}\n </>\n )}\n </div>\n );\n}\n","/**\n * Slash-command registry and input rules for the chat box.\n *\n * Everything here is pure so the behaviour can be tested without a DOM:\n * `apps/ui/vitest.config.ts` runs in a `node` environment and only picks up\n * `.ts` tests, so a rule that lives inside the React component is a rule that\n * never gets tested. ChatInput/SlashCommandPicker consume these functions and\n * own nothing but rendering and focus.\n */\n\n/** How a command's result reaches people. */\ntype SlashScope =\n /** Result is rendered only for the person who ran it. */\n | 'local'\n /** Result is broadcast to everyone in the room. */\n | 'room';\n\nexport interface SlashCommand {\n /** Command word WITHOUT the leading slash, lowercase. */\n name: string;\n /** One-line help text shown in the picker. */\n description: string;\n /** Shown in the picker as a hint, e.g. '/recap'. */\n usage: string;\n scope: SlashScope;\n /**\n * Command is only meaningful while media is loaded. The picker still lists\n * it (so it is discoverable), but greys it out and refuses to run it — a\n * silently-missing command is worse than a disabled one that explains itself.\n */\n requiresMedia?: boolean;\n}\n\nexport const SLASH_COMMANDS: SlashCommand[] = [\n {\n name: 'recap',\n description: 'Summarise the previous episode of what is loaded',\n usage: '/recap',\n scope: 'room',\n requiresMedia: true,\n },\n {\n name: 'tierlist',\n description: 'Open the tierlist',\n usage: '/tierlist',\n scope: 'local',\n },\n {\n name: 'help',\n description: 'List the available commands',\n usage: '/help',\n scope: 'local',\n },\n];\n\n/** Marker character. Kept as a constant so tests and UI cannot disagree. */\nconst SLASH = '/';\n\n/**\n * The token the picker should filter on, or null when the picker must stay\n * closed.\n *\n * Open only while the caret is still inside the COMMAND WORD:\n * '' -> null (empty input)\n * 'hello' -> null (ordinary message)\n * 'a /recap' -> null (slash must be the first character; otherwise every\n * URL — \"https://x\" — and every \"and/or\" would pop\n * the picker mid-sentence)\n * '/' -> '' (show the whole list)\n * '/rec' -> 'rec'\n * '/recap ' -> null (a space closes the word: the user is typing\n * arguments now, and a picker covering the input at\n * that point is pure obstruction)\n *\n * Leading whitespace is NOT trimmed first — ' /recap' is a message that\n * happens to contain a slash, and treating it as a command would make the\n * behaviour depend on invisible characters.\n */\nexport function getPickerQuery(text: string): string | null {\n if (!text.startsWith(SLASH)) return null;\n const rest = text.slice(SLASH.length);\n if (/\\s/.test(rest)) return null;\n return rest.toLowerCase();\n}\n\n/**\n * Commands matching `query`, best first.\n *\n * Prefix matches outrank substring matches so typing 'he' puts '/help' at the\n * top rather than whatever merely contains 'he'. Ties break alphabetically so\n * the order is stable and does not depend on registry order.\n */\nexport function filterCommands(\n query: string,\n commands: SlashCommand[] = SLASH_COMMANDS,\n): SlashCommand[] {\n const q = query.toLowerCase();\n if (!q) return [...commands].sort((a, b) => a.name.localeCompare(b.name));\n const scored: Array<{ cmd: SlashCommand; rank: number }> = [];\n for (const cmd of commands) {\n const name = cmd.name.toLowerCase();\n if (name.startsWith(q)) scored.push({ cmd, rank: 0 });\n else if (name.includes(q)) scored.push({ cmd, rank: 1 });\n }\n return scored\n .sort((a, b) => a.rank - b.rank || a.cmd.name.localeCompare(b.cmd.name))\n .map((s) => s.cmd);\n}\n\n/**\n * Input text after completing to `cmd` (Tab, click, or Enter on a highlight).\n *\n * The trailing space matters twice over: it lets the user type arguments\n * immediately, and it makes `getPickerQuery` return null, which is what\n * actually dismisses the picker. Without it the picker stays open over a\n * completed command and Enter would re-complete instead of submitting.\n */\nexport function applyCompletion(cmd: SlashCommand): string {\n return `${SLASH}${cmd.name} `;\n}\n\n/** Wrap an index within [0, len). Returns 0 for an empty list. */\nexport function wrapIndex(index: number, len: number): number {\n if (len <= 0) return 0;\n return ((index % len) + len) % len;\n}\n\ntype Submission =\n | { kind: 'command'; command: SlashCommand; args: string }\n | { kind: 'chat'; text: string };\n\n/**\n * Decide what pressing Enter means.\n *\n * An UNKNOWN slash word is deliberately sent as an ordinary message rather\n * than rejected. Refusing it would need a whole error-surface of its own, and\n * a typo'd '/recp' landing in chat is self-explanatory and harmless — whereas\n * swallowing input silently is the one outcome a chat box must never have.\n */\nexport function resolveSubmission(\n text: string,\n commands: SlashCommand[] = SLASH_COMMANDS,\n): Submission | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n if (!trimmed.startsWith(SLASH)) return { kind: 'chat', text: trimmed };\n\n const body = trimmed.slice(SLASH.length);\n const spaceAt = body.search(/\\s/);\n const word = (spaceAt === -1 ? body : body.slice(0, spaceAt)).toLowerCase();\n const args = spaceAt === -1 ? '' : body.slice(spaceAt).trim();\n\n const command = commands.find((c) => c.name.toLowerCase() === word);\n if (!command) return { kind: 'chat', text: trimmed };\n return { kind: 'command', command, args };\n}\n\n/**\n * Why a command cannot run right now, or null when it can.\n * Returned as a message so the caller can show it verbatim.\n */\nexport function commandBlockedReason(\n cmd: SlashCommand,\n ctx: { hasMedia: boolean },\n): string | null {\n if (cmd.requiresMedia && !ctx.hasMedia) {\n return `${SLASH}${cmd.name} needs an episode loaded.`;\n }\n return null;\n}\n","import type { SlashCommand } from '../lib/slashCommands';\n\ninterface Props {\n commands: SlashCommand[];\n activeIndex: number;\n /** Commands needing media are listed but disabled while nothing is loaded. */\n hasMedia: boolean;\n onPick: (cmd: SlashCommand) => void;\n /** Keeps the highlight in sync when the pointer moves over a row. */\n onHover: (index: number) => void;\n listboxId: string;\n}\n\n/**\n * The command list that pops up while the user is typing a slash command.\n *\n * Rendering only — every rule about what to show and in what order lives in\n * `lib/slashCommands.ts` so it can be tested without a DOM.\n *\n * Positioned `bottom-full` (above the input) because the chat box sits at the\n * bottom of the sidebar; a downward menu would open off-screen.\n */\nexport default function SlashCommandPicker({\n commands, activeIndex, hasMedia, onPick, onHover, listboxId,\n}: Props) {\n if (commands.length === 0) return null;\n\n return (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label=\"Slash commands\"\n // `list-none m-0 p-0` is load-bearing, not tidiness: styles.css imports\n // Tailwind v4 UTILITIES ONLY, with no preflight/base, so the browser's\n // UA stylesheet still applies `padding-inline-start: 40px` and\n // `margin-block: 1em` to every <ul>. That indented all the rows by 40px.\n // The bullets were invisible (a `display: flex` list item drops its\n // marker box), so it read as a mysterious gap rather than a stray list.\n // `py-1`/`mb-1` follow the resets and win — longhand beats shorthand in\n // Tailwind's generated order.\n className=\"absolute bottom-full left-0 right-0 z-30 max-h-56 overflow-y-auto\n bg-bg-card border border-border-light rounded shadow-lg\n list-none m-0 p-0 py-1 mb-1\"\n >\n {commands.map((cmd, i) => {\n const blocked = Boolean(cmd.requiresMedia) && !hasMedia;\n const active = i === activeIndex;\n return (\n <li\n key={cmd.name}\n id={`${listboxId}-opt-${cmd.name}`}\n role=\"option\"\n aria-selected={active}\n aria-disabled={blocked}\n // onMouseDown, not onClick: the input's blur would otherwise fire\n // first and close the picker before the click ever lands.\n onMouseDown={(e) => { e.preventDefault(); onPick(cmd); }}\n onMouseEnter={() => onHover(i)}\n className={`px-3 py-1.5 cursor-pointer flex items-baseline gap-2 ${\n active ? 'bg-bg-alt' : ''\n } ${blocked ? 'opacity-50' : ''}`}\n >\n <span className=\"text-sm font-semibold text-text shrink-0\">\n {cmd.usage}\n </span>\n <span className=\"text-xs text-text-secondary truncate\">\n {blocked ? 'needs an episode loaded' : cmd.description}\n </span>\n </li>\n );\n })}\n </ul>\n );\n}\n","import React from 'react';\nimport type { PendingImage } from '../lib/chatMedia';\n\n/**\n * The row of not-yet-sent attachments above a chat input.\n *\n * Renders from the local object URL, so the image is visible the instant it is\n * pasted — the upload only happens on submit. Shared by both chat inputs for\n * the same reason `ChatImageAttachments` is: two copies drift.\n */\nexport default function ChatAttachmentStrip({\n pending,\n uploading,\n onRemove,\n}: {\n pending: readonly PendingImage[];\n uploading: boolean;\n onRemove: (id: string) => void;\n}) {\n if (!pending.length) return null;\n return (\n <div className=\"flex flex-wrap gap-1.5 pb-1.5\" data-testid=\"chat-attachment-strip\">\n {pending.map((p) => (\n <div key={p.id} className=\"relative\">\n <img\n src={p.previewUrl}\n alt=\"\"\n className={`h-14 w-14 rounded border border-border-light object-cover ${\n uploading ? 'opacity-50' : ''\n }`}\n />\n <button\n type=\"button\"\n onClick={() => onRemove(p.id)}\n disabled={uploading}\n aria-label=\"Remove attachment\"\n className=\"absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-bg-card border border-border-light text-[10px] text-text-secondary hover:bg-danger-bg hover:text-on-danger-bg disabled:opacity-50\"\n >\n ×\n </button>\n </div>\n ))}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ClipboardEvent as ReactClipboardEvent, DragEvent as ReactDragEvent } from 'react';\nimport { DEFAULT_CHAT_MAX_IMAGES, type TChatImage } from '@watchparty/shared';\nimport {\n getCdnClient,\n imageFilesFromClipboard,\n imageFilesFromDrop,\n measureImage,\n messageForUploadFailure,\n ownsObjectUrl,\n preflightMessage,\n resolveChatImageUrl,\n uploadAll,\n uploadErrorFields,\n type CatalogueItem,\n type PendingImage,\n} from '../lib/chatMedia';\nimport { MEME_CATALOGUE_PATH } from '../lib/chatMedia/react/MemePicker';\nimport { useAppStore } from '../lib/store';\nimport { warn } from '../lib/debug';\n\n/**\n * Paste/drop image attachments for a chat input.\n *\n * There are TWO chat inputs — the sidebar `ChatInput` and the inline one in\n * `ControlBar` (shown when the sidebar is collapsed and floating chat is on).\n * They already keep separate text/cooldown state, and attaching this behaviour\n * to only one of them is how the two silently diverge. Both consume this hook.\n *\n * Uploads happen on SUBMIT, not on paste. Pasting is cheap and often\n * reconsidered; uploading on paste would push bytes at a CDN with no rate\n * limiting for images the user then deletes. The local preview is an object\n * URL, so the user sees the image immediately either way.\n *\n * Meme picks land in the SAME pending list (`addMeme`). They are already on\n * the CDN, so they cost no upload on submit — but routing them through one\n * list is what makes the cap, the remove button, the disabled-while-uploading\n * state and the send path work identically for both without a second copy of\n * any of it.\n */\nexport function useChatImageAttachments() {\n const [pending, setPending] = useState<PendingImage[]>([]);\n const [uploading, setUploading] = useState(false);\n const pushToast = useAppStore((s) => s.pushToast);\n // Object URLs are revoked on removal and on unmount; without this every\n // pasted image leaks its blob for the lifetime of the tab.\n const liveUrls = useRef(new Set<string>());\n\n useEffect(() => () => {\n for (const url of liveUrls.current) URL.revokeObjectURL(url);\n liveUrls.current.clear();\n }, []);\n\n /**\n * Drop an attachment's preview.\n *\n * Guarded on `ownsObjectUrl` because a meme's `previewUrl` is an ordinary\n * CDN URL, not a blob. Revoking it is a no-op in every browser, but doing so\n * would mean this function had stopped tracking what it actually owns — and\n * the day something here starts depending on that set being accurate is the\n * day it breaks quietly.\n */\n const releasePreview = useCallback((p: PendingImage) => {\n if (!ownsObjectUrl(p)) return;\n URL.revokeObjectURL(p.previewUrl);\n liveUrls.current.delete(p.previewUrl);\n }, []);\n\n const addFiles = useCallback(async (files: File[]) => {\n if (!files.length) return;\n // Read the count from state at apply time, not from the closure, so two\n // fast pastes cannot both see the old length and blow past the cap.\n const accepted: PendingImage[] = [];\n for (const file of files) {\n const rejection = preflightMessage(file);\n if (rejection) {\n pushToast('error', rejection);\n continue;\n }\n const { w, h } = await measureImage(file);\n const previewUrl = URL.createObjectURL(file);\n liveUrls.current.add(previewUrl);\n accepted.push({\n kind: 'file',\n id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n file, previewUrl, w, h,\n });\n }\n if (!accepted.length) return;\n setPending((prev) => {\n const room = DEFAULT_CHAT_MAX_IMAGES - prev.length;\n if (room <= 0) {\n pushToast('info', `Up to ${DEFAULT_CHAT_MAX_IMAGES} images per message`);\n for (const p of accepted) releasePreview(p);\n return prev;\n }\n if (accepted.length > room) {\n pushToast('info', `Up to ${DEFAULT_CHAT_MAX_IMAGES} images per message`);\n for (const p of accepted.slice(room)) releasePreview(p);\n }\n return [...prev, ...accepted.slice(0, room)];\n });\n }, [pushToast, releasePreview]);\n\n /**\n * Attach a meme from the shared library.\n *\n * The pick is recorded HERE rather than on send, deliberately: that call is\n * what makes the library's default view popularity-ordered, but it must\n * never be able to delay or fail a message. Keeping it at pick time keeps it\n * out of the send path entirely, instead of relying on a caller remembering\n * not to await it.\n */\n const addMeme = useCallback((item: CatalogueItem) => {\n const previewUrl = resolveChatImageUrl(item.key);\n // `null` means the key failed the content-addressed shape check. A row the\n // renderer would refuse to display must not become an attachment either.\n if (!previewUrl) return;\n setPending((prev) => {\n if (prev.length >= DEFAULT_CHAT_MAX_IMAGES) {\n pushToast('info', `Up to ${DEFAULT_CHAT_MAX_IMAGES} images per message`);\n return prev;\n }\n return [...prev, {\n kind: 'remote',\n id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n key: item.key,\n previewUrl,\n // The catalogue spells \"unknown\" as null; the wire schema spells it\n // as 0, and the renderer falls back to natural flow on that.\n w: item.width ?? 0,\n h: item.height ?? 0,\n title: item.title || undefined,\n }];\n });\n // Fire-and-forget by construction: `recordUse` returns void and swallows\n // everything, so it cannot be awaited and cannot be why a send fails.\n getCdnClient().catalogue(MEME_CATALOGUE_PATH).recordUse(item.key);\n }, [pushToast]);\n\n const remove = useCallback((id: string) => {\n setPending((prev) => {\n const hit = prev.find((p) => p.id === id);\n if (hit) releasePreview(hit);\n return prev.filter((p) => p.id !== id);\n });\n }, [releasePreview]);\n\n const clear = useCallback(() => {\n setPending((prev) => {\n for (const p of prev) releasePreview(p);\n return [];\n });\n }, [releasePreview]);\n\n /** Paste and drop are the same path — both carry a `DataTransfer`. */\n const onPaste = useCallback((e: ReactClipboardEvent) => {\n const files = imageFilesFromClipboard(e.clipboardData);\n if (!files.length) return; // ordinary text paste — leave it alone\n e.preventDefault();\n void addFiles(files);\n }, [addFiles]);\n\n const onDrop = useCallback((e: ReactDragEvent) => {\n const files = imageFilesFromDrop(e.dataTransfer);\n if (!files.length) return;\n e.preventDefault();\n void addFiles(files);\n }, [addFiles]);\n\n const onDragOver = useCallback((e: ReactDragEvent) => {\n if (e.dataTransfer?.types?.includes('Files')) e.preventDefault();\n }, []);\n\n /**\n * Upload everything and hand back the wire attachments.\n *\n * Returns `null` when an upload failed — the caller must then NOT send the\n * message and must NOT clear the input, so the user keeps their text and can\n * retry rather than losing it to a 401.\n */\n const uploadPending = useCallback(async (): Promise<TChatImage[] | null> => {\n if (!pending.length) return [];\n setUploading(true);\n try {\n return await uploadAll(pending);\n } catch (err: unknown) {\n warn('chat', 'Image upload failed', uploadErrorFields(err));\n // `CdnError.message` is developer-facing and not fit to show verbatim —\n // the reason is what maps to wording a user can act on.\n pushToast('error', messageForUploadFailure(err));\n return null;\n } finally {\n setUploading(false);\n }\n }, [pending, pushToast]);\n\n return {\n pending, uploading, addFiles, addMeme, remove, clear, uploadPending,\n onPaste, onDrop, onDragOver,\n };\n}\n","import React, { useState, useRef, useCallback, useEffect, useMemo, useId } from 'react';\nimport { DEFAULT_CHAT_MAX_LENGTH, DEFAULT_CHAT_COOLDOWN_MS } from '@watchparty/shared';\nimport { sendChat, emitRecapRequest } from '../lib/socket';\nimport { useAppStore } from '../lib/store';\nimport { warmTierlistPosters } from '../lib/tierlistApi';\nimport {\n SLASH_COMMANDS,\n getPickerQuery,\n filterCommands,\n applyCompletion,\n wrapIndex,\n resolveSubmission,\n commandBlockedReason,\n type SlashCommand,\n} from '../lib/slashCommands';\nimport { Icon } from '@wolffm/themes';\nimport SlashCommandPicker from './SlashCommandPicker';\nimport MemePicker from '../lib/chatMedia/react/MemePicker';\nimport ChatAttachmentStrip from './ChatAttachmentStrip';\nimport { useChatImageAttachments } from '../hooks/useChatImageAttachments';\n\nconst MAX_LENGTH = DEFAULT_CHAT_MAX_LENGTH;\nconst COOLDOWN_MS = DEFAULT_CHAT_COOLDOWN_MS;\n\ninterface ChatInputProps {\n /** Slash commands this input offers. Every command is a watchparty-product\n * action (`/recap` reads an episode's prebuilt recap, `/tierlist` opens\n * watchparty's global tierlist), so the embeddable\n * room chrome passes none and a leading `/` is just text. */\n commands?: SlashCommand[];\n}\n\nexport default function ChatInput({ commands = SLASH_COMMANDS }: ChatInputProps) {\n const [text, setText] = useState('');\n const [cooldown, setCooldown] = useState(false);\n // Selection is stored WITH the query it belongs to, so a narrowing query\n // resets the highlight by derivation instead of via an effect that would\n // setState during render and cascade (react-hooks/set-state-in-effect).\n const [selection, setSelection] = useState<{ q: string | null; i: number }>({ q: null, i: 0 });\n // Escape closes the picker without clearing the input. Sticky until the\n // command word changes again, so Escape is not undone by the next keystroke.\n const [dismissed, setDismissed] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const listboxId = useId();\n const [memeOpen, setMemeOpen] = useState(false);\n // Sticky for the session. The meme library is a separate service from the\n // CDN, so it going down must cost the meme button and nothing else — paste\n // and drop keep working through the exact same composer.\n const [memeUnavailable, setMemeUnavailable] = useState(false);\n\n const attach = useChatImageAttachments();\n\n const mediaId = useAppStore((s) => s.snapshot.mediaId);\n const pushToast = useAppStore((s) => s.pushToast);\n const setTierlistOpen = useAppStore((s) => s.setTierlistOpen);\n const hasMedia = Boolean(mediaId);\n\n const query = getPickerQuery(text);\n const matches = useMemo(\n () => (query === null ? [] : filterCommands(query, commands)),\n [query, commands],\n );\n const pickerOpen = query !== null && !dismissed && matches.length > 0;\n const activeIndex = selection.q === query ? wrapIndex(selection.i, matches.length) : 0;\n const setActiveIndex = useCallback(\n (i: number) => setSelection({ q: query, i }),\n [query],\n );\n\n const runCommand = useCallback((cmd: SlashCommand, args: string) => {\n const blocked = commandBlockedReason(cmd, { hasMedia });\n if (blocked) {\n pushToast('info', blocked);\n return;\n }\n // Execution is intentionally a single seam: commands are dispatched by\n // name here rather than each carrying a closure, so the registry stays a\n // plain data table that tests can import without pulling in the socket.\n switch (cmd.name) {\n case 'help':\n pushToast('info', commands.map((c) => c.usage).join(' '));\n break;\n case 'recap':\n // Fire-and-forget: the server resolves the previous episode, reads the\n // prebuilt recap and broadcasts it to the room as a system message.\n // Anything that goes wrong arrives as a socket error with a recap/*\n // code, which the existing error toast already surfaces — so there is\n // deliberately no local \"loading\" or success state to get out of sync.\n emitRecapRequest();\n break;\n case 'tierlist':\n warmTierlistPosters();\n setTierlistOpen(true);\n break;\n default:\n pushToast('info', `${cmd.usage} is not implemented yet.`);\n }\n void args;\n }, [hasMedia, pushToast, commands, setTierlistOpen]);\n\n const complete = useCallback((cmd: SlashCommand) => {\n setText(applyCompletion(cmd));\n setDismissed(false);\n inputRef.current?.focus();\n }, []);\n\n const handleSubmit = useCallback(async (e: React.FormEvent) => {\n e.preventDefault();\n if (cooldown || attach.uploading) return;\n\n const resolved = resolveSubmission(text, commands);\n // `resolveSubmission` returns null for empty text — which is a legitimate\n // message when images are attached, and the common case for a paste.\n if (!resolved && !attach.pending.length) return;\n if (resolved?.kind === 'command' && attach.pending.length) {\n pushToast('info', 'Send images on their own, not with a command.');\n return;\n }\n\n if (resolved?.kind === 'command') {\n runCommand(resolved.command, resolved.args);\n setText('');\n // No cooldown for commands: the chat rate limit exists to stop message\n // spam, and a local command sends nothing to the room.\n if (!document.hidden && document.hasFocus()) inputRef.current?.focus();\n return;\n }\n\n // Upload attachments BEFORE clearing the input: a failed upload must\n // leave the user's text and images exactly where they were, so a 401 or a\n // flaky network costs a retry rather than the message.\n const images = await attach.uploadPending();\n if (images === null) return;\n\n sendChat(resolved?.text ?? '', images);\n setText('');\n attach.clear();\n setCooldown(true);\n setTimeout(() => setCooldown(false), COOLDOWN_MS);\n\n // Re-focus input only when watchparty actually has focus. If the user\n // hit Enter then alt-tabbed before the setTimeout fired, a late focus()\n // could yank attention back (the focus-trap guard pattern).\n if (!document.hidden && document.hasFocus()) {\n inputRef.current?.focus();\n }\n }, [text, cooldown, runCommand, attach, pushToast, commands]);\n\n const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Escape' && pickerOpen) {\n e.preventDefault();\n setDismissed(true);\n return;\n }\n if (!pickerOpen) return;\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setActiveIndex(wrapIndex(activeIndex + 1, matches.length));\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setActiveIndex(wrapIndex(activeIndex - 1, matches.length));\n } else if (e.key === 'Tab') {\n // Tab completes rather than moving focus — the picker is the focus\n // context while it is open.\n e.preventDefault();\n const cmd = matches[activeIndex];\n if (cmd) complete(cmd);\n } else if (e.key === 'Enter') {\n // Enter picks the highlight instead of submitting a half-typed word.\n // The form's onSubmit still runs on the NEXT Enter, once the picker has\n // closed (applyCompletion's trailing space is what closes it).\n e.preventDefault();\n const cmd = matches[activeIndex];\n if (cmd) complete(cmd);\n }\n }, [pickerOpen, matches, activeIndex, complete, setActiveIndex]);\n\n // Pop focus to the chat input from anywhere on the page via a custom event.\n // The Bomberman bundle fires this when the user presses Enter while its\n // canvas had keyboard focus (hadoku-bomberman `FOCUS_CHAT_EVENT`).\n useEffect(() => {\n const onFocusRequest = () => {\n if (document.hidden || !document.hasFocus()) return;\n inputRef.current?.focus();\n inputRef.current?.scrollIntoView({ block: 'nearest' });\n };\n window.addEventListener('watchparty:focus-chat-input', onFocusRequest);\n return () => window.removeEventListener('watchparty:focus-chat-input', onFocusRequest);\n }, []);\n\n const remaining = MAX_LENGTH - text.length;\n // The counter only renders near the limit; reserving its gutter always\n // stopped the text ~48px short of the field's right edge.\n const showCounter = remaining <= 100;\n const activeCmd = pickerOpen ? matches[activeIndex] : undefined;\n\n return (\n <form\n onSubmit={(e) => { void handleSubmit(e); }}\n onPaste={attach.onPaste}\n onDrop={attach.onDrop}\n onDragOver={attach.onDragOver}\n className=\"p-2 border-t border-border\"\n >\n <ChatAttachmentStrip pending={attach.pending} uploading={attach.uploading} onRemove={attach.remove} />\n <div className=\"relative\">\n {pickerOpen && (\n <SlashCommandPicker\n commands={matches}\n activeIndex={activeIndex}\n hasMedia={hasMedia}\n onPick={complete}\n onHover={setActiveIndex}\n listboxId={listboxId}\n />\n )}\n <MemePicker\n open={memeOpen}\n onClose={() => setMemeOpen(false)}\n onPick={attach.addMeme}\n onUnavailable={() => { setMemeUnavailable(true); setMemeOpen(false); }}\n />\n <div className=\"flex items-center gap-1.5\">\n <div className=\"relative flex-1\">\n <input\n ref={inputRef}\n type=\"text\"\n value={text}\n onChange={(e) => { setText(e.target.value); setDismissed(false); }}\n onKeyDown={handleKeyDown}\n maxLength={MAX_LENGTH}\n placeholder=\"Send a message\"\n role=\"combobox\"\n aria-expanded={pickerOpen}\n aria-controls={pickerOpen ? listboxId : undefined}\n aria-activedescendant={activeCmd ? `${listboxId}-opt-${activeCmd.name}` : undefined}\n aria-autocomplete=\"list\"\n className={`w-full bg-bg-card border border-border-light rounded px-3 py-1.5 text-sm focus:outline-none focus:border-text-muted ${showCounter ? 'pr-12' : ''}`}\n />\n {/* Character counter */}\n <span className={`absolute right-2 top-1/2 -translate-y-1/2 text-[10px] ${\n remaining <= 50\n ? remaining <= 0\n ? 'bg-danger-bg text-on-danger-bg px-1 rounded'\n : 'bg-primary-bg text-on-primary-bg px-1 rounded'\n : 'text-text-muted'\n }`}>\n {showCounter && remaining}\n </span>\n </div>\n {!memeUnavailable && (\n <button\n type=\"button\"\n onClick={() => setMemeOpen((v) => !v)}\n aria-label=\"Add a meme\"\n aria-expanded={memeOpen}\n title=\"Add a meme\"\n data-testid=\"meme-button\"\n className={`shrink-0 rounded border border-border-light px-2 py-1.5 text-sm hover:border-primary ${\n memeOpen ? 'text-text border-primary' : 'text-text-secondary'\n }`}\n >\n <Icon name=\"image\" />\n </button>\n )}\n </div>\n </div>\n </form>\n );\n}\n","import { useAppStore, type RosterUser } from '../lib/store';\nimport { usePersonColor } from '../lib/pictureColor';\nimport AvatarImage from './AvatarImage';\n\n/**\n * Simple \"who's online\" panel. Ready-check moved out to ClientStatusOverlay\n * (rendered inside the video container) so it survives chat-closed and\n * fullscreen modes — see apps/ui/src/components/ClientStatusOverlay.tsx.\n */\nexport default function Roster() {\n const roster = useAppStore(s => s.roster);\n const users = Object.values(roster);\n\n if (!users.length) {\n return <div className=\"text-xs text-text-muted\">No users online</div>;\n }\n\n return (\n <div className=\"flex flex-col gap-1\">\n {users.map(u => <RosterRow key={u.clientId} u={u} />)}\n </div>\n );\n}\n\nfunction RosterRow({ u }: { u: RosterUser }) {\n const color = usePersonColor(u.sprite, u.avatarUrl);\n return (\n <div className=\"flex items-center gap-2 py-0.5\">\n <AvatarImage\n sprite={u.sprite}\n url={u.avatarUrl}\n alt={u.name || 'Anon'}\n className=\"w-6 h-6 rounded-full object-cover flex-shrink-0\"\n fallback={<span className=\"w-6 h-6 bg-bg-alt rounded-full flex-shrink-0\" />}\n />\n <span className=\"text-xs font-medium truncate\" style={{ color }}>\n {u.name || 'Anon'}\n </span>\n {u.isLeader && (\n <span className=\"ml-auto px-1.5 py-0.5 rounded text-[9px] font-medium bg-success/20 text-success\">\n Leader\n </span>\n )}\n </div>\n );\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { debug } from '../lib/debug';\nimport Roster from './Roster';\n\n/**\n * The \"who's here\" toolbar button: an icon with the head count, opening the\n * roster as a popover. Shared by the app's `ChatPanel` and the embeddable\n * `RoomChrome`.\n */\nexport default function RosterButton() {\n const userCount = useAppStore(s => Object.keys(s.roster).length);\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!open) return;\n function handleClickOutside(e: MouseEvent) {\n if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);\n }\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [open]);\n\n return (\n <div className=\"relative\" ref={ref}>\n <button\n onClick={() => {\n debug('chat', 'User list toggled', { open: !open, userCount });\n setOpen(!open);\n }}\n className=\"p-1.5 rounded text-text-secondary hover:text-white hover:bg-bg-alt transition-colors flex items-center gap-1 cursor-pointer\"\n title=\"Users\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z\" />\n </svg>\n <span className=\"text-xs\">{userCount}</span>\n </button>\n\n {/* Anchored right-0, NOT left-0. The chat column is a 320px `w-80` aside\n pinned to the right edge, and Room.tsx wraps the layout in\n `overflow-hidden` — so anything extending past the column's right\n edge is clipped away and simply never appears. A left-anchored card\n opened from this button (~168px into the toolbar for a leader, who\n also gets the Select Media and Games buttons) would end at 348-388px\n and be invisible, not merely cut off. Opening leftward can overhang\n the centre instead, which is fine: the aside is z-10 over it, so the\n card paints on top and stays inside the clipping wrapper. */}\n {open && (\n <div className=\"absolute top-full right-0 mt-1 bg-bg-card rounded-lg shadow-xl p-3 max-h-64 overflow-y-auto z-50 w-max min-w-[180px] max-w-[220px]\">\n <div className=\"text-[10px] uppercase tracking-wide text-text-muted mb-2\">\n {userCount} online\n </div>\n <Roster />\n </div>\n )}\n </div>\n );\n}\n","/**\n * Should this client be shown the presenter-only affordances (Select Media,\n * Games) — including BEFORE the socket has told us who the leader is?\n *\n * The problem this solves is a timing one. `isLeader` is\n * `snapshot.leaderId === me.clientId`, and both halves arrive only once the\n * socket join completes — which Room.tsx deliberately holds behind an\n * identity-prefs wait capped at 1500ms, so the first presence broadcast\n * carries the right name and sprite. That wait is load-bearing and must not\n * be removed. The visible cost was that the two leader buttons appeared a\n * beat after the rest of the toolbar and shoved it sideways.\n *\n * The room KEY already knows the answer, and knows it over plain HTTP with no\n * dependency on that wait: `GET /api/rooms/validate/:roomKey` returns the\n * key's `type`. So we render provisionally from the key type and let the\n * socket supersede it — the same provisional-then-pinned shape the fragment\n * delivery flag uses.\n *\n * The one case where the provisional answer is WRONG is a friend opening a\n * leader URL for a seat someone else already holds: ws/join.ts downgrades\n * them to a viewer (`effectiveKeyType`), so `leaderId` comes back as another\n * client's id and the buttons disappear. That is rare (the lobby hides the\n * Leader shortcut for occupied rooms) and it is self-correcting, which is why\n * it is a better trade than either making every viewer flash the buttons or\n * leaving the pop-in in place.\n *\n * Note what \"the socket has spoken\" means here: a NON-NULL `leaderId`. Null is\n * a legitimate steady state (nobody is leading), so treating it as \"known\"\n * would collapse the provisional answer the moment the store initialised and\n * put the pop-in straight back.\n *\n * This governs DISPLAY only. Anything that gates an ACTION keeps using\n * `useIsLeader` (socket truth) — and the server is the real authority either\n * way: control.ts rejects play/pause/seek/load/home from a non-leader, so a\n * provisional button that turns out to be wrong costs a rejected control, not\n * a room someone else can drive.\n */\n\nexport type RoomKeyType = 'leader' | 'viewer';\n\ninterface LeaderAffordanceInput {\n /** snapshot.leaderId — null until the socket says otherwise. */\n leaderId: string | null;\n /** me.clientId — null before connect. */\n clientId: string | null;\n /** Key type from the validate probe; null while it is in flight or failed. */\n keyType: RoomKeyType | null;\n}\n\nexport function resolveLeaderAffordance({ leaderId, clientId, keyType }: LeaderAffordanceInput): boolean {\n // Socket truth, once there is any. Supersedes the key type in both\n // directions: it promotes a viewer who was handed leadership mid-session,\n // and it demotes a leader-key holder who lost the takeover race.\n if (leaderId !== null) return leaderId === clientId;\n // No leader known yet — fall back to what the key entitles us to.\n return keyType === 'leader';\n}\n\n/**\n * A leader key's SHAPE, read synchronously off the URL — no network at all.\n *\n * The probe above still costs a round trip (~80-110ms measured against\n * production), and the rest of the toolbar paints with zero. That gap is\n * exactly the pop-in this whole mechanism exists to remove, so the answer has\n * to be available in the FIRST render, before any fetch can land.\n *\n * A leader key is the room id plus a 16-hex suffix; a viewer key is the bare\n * room id. Both shapes are real and they differ in case:\n *\n * viewer cedar-ridge\n * leader cedar-ridge-8C31A5EA7E4D402F (curated rooms, UPPERCASE)\n * leader cedar-ridge-8c31a5ea7e4d402f (rooms.ts:136-138, randomBytes → lowercase)\n *\n * so the match is case-insensitive. Verified against the live registry — do\n * not \"tidy\" this to lowercase-only, which would silently stop matching every\n * curated room, i.e. every room anyone actually uses.\n *\n * This is a HEURISTIC and is treated as one: both the probe and the socket\n * supersede it. Its failure modes are asymmetric on purpose — a leader key\n * that stops matching just restores the old late-but-correct behaviour, while\n * a false positive needs a viewer key ending in 16 hex digits, which no room\n * slug does. If the server ever changes the key format, this returns null and\n * nothing breaks.\n */\nconst LEADER_KEY_SUFFIX_RE = /-[0-9a-fA-F]{16}$/;\n\nexport function guessRoomKeyType(roomKey: string | null | undefined): RoomKeyType | null {\n if (!roomKey) return null;\n return LEADER_KEY_SUFFIX_RE.test(roomKey) ? 'leader' : null;\n}\n\n/** The validate probe's answer, tagged with the key it was asked about. */\nexport interface ProbedRoomKeyType {\n roomKey: string;\n /** null = the server does not know the key (404). */\n type: RoomKeyType | null;\n}\n\nconst ROOM_PATH_RE = /\\/room\\/([^/]+)\\/?$/;\n\n/**\n * The room key in a page path (`/room/<key>`, under any basename), or null.\n *\n * The guess is DERIVED from the URL while rendering, never written into the\n * store first. Room.tsx used to seed it from a useLayoutEffect, which lost a\n * frame on a slow machine: zustand reads the store through\n * useSyncExternalStore, which subscribes in a PASSIVE effect, so a store write\n * from a layout effect in the same commit reaches a component that mounted in\n * that commit only when its passive effects run, and a busy browser paints\n * before then. The toolbar painted one frame with Home and without Select\n * Media (10-29 nightly; 4 in 15 runs locally at 6x CPU throttle, always the\n * first frame, with the store already saying 'leader'). Reading the URL is\n * synchronous in every React root, including the media player bundle's.\n */\nexport function roomKeyFromPath(pathname: string | null | undefined): string | null {\n const m = pathname ? ROOM_PATH_RE.exec(pathname) : null;\n if (!m) return null;\n try {\n return decodeURIComponent(m[1]);\n } catch {\n return m[1];\n }\n}\n\n/**\n * What the key entitles us to before the socket speaks: the probe's answer\n * for THIS key once it has one (in both directions, including \"unknown key\"),\n * else the shape guess. A probe answer for a different key — the room before a\n * navigation — is ignored rather than trusted.\n */\nexport function effectiveKeyType(\n roomKey: string | null,\n probed: ProbedRoomKeyType | null,\n): RoomKeyType | null {\n if (roomKey && probed?.roomKey === roomKey) return probed.type;\n return guessRoomKeyType(roomKey);\n}\n","import { useEffect, useState } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { effectiveKeyType, resolveLeaderAffordance, roomKeyFromPath } from '../lib/leaderAffordance';\nimport { useGameCatalog } from '../lib/gameCatalog';\nimport type { GameEntry } from '@wolffm/rooms-protocol';\n\n/**\n * Hook to determine if the current user is the room leader\n */\nexport function useIsLeader(): boolean {\n return useAppStore(s => !!s.snapshot.leaderId && s.snapshot.leaderId === s.me.clientId);\n}\n\n/**\n * The game covering the room's main area, or null. An `activeGameId` missing\n * from the catalog mounts nothing (Room.tsx), so it is not a game here either.\n *\n * Room.tsx mounts the overlay from this, and the `selectMediaAffordance` call\n * sites read it as `gameOverlay` — one answer, so the rule's idea of \"a game\n * covers ControlBar\" cannot drift from what is actually on screen.\n */\nexport function useActiveGame(): GameEntry | null {\n const activeGameId = useAppStore(s => s.activeGameId);\n const games = useGameCatalog();\n return activeGameId ? games.find(g => g.id === activeGameId) ?? null : null;\n}\n\n/**\n * Whether to SHOW the presenter-only affordances (Select Media, Games).\n *\n * Differs from `useIsLeader` only in the window before the socket join\n * resolves leadership, where it answers from the room key type instead of\n * waiting — see lib/leaderAffordance.ts for the whole rationale and the one\n * case it gets wrong.\n *\n * Use this for DISPLAY. Anything gating an ACTION keeps `useIsLeader`.\n *\n * All three `selectMediaAffordance` call sites (ChatPanel, ControlBar and\n * RoomCornerControls) must be fed the SAME value: that rule guarantees exactly\n * one Select Media button exists in every reachable state, and feeding the\n * sites different leader-ness would let it render zero or two — the second of\n * which is a Playwright strict-mode violation. See lib/mediaDrawerAffordance.ts.\n *\n * The key comes from the URL at render time, not from the store, so the very\n * first render already has it — see `roomKeyFromPath` for the lost frame a\n * store write cost.\n */\nexport function useLeaderAffordance(): boolean {\n return useAppStore(s => resolveLeaderAffordance({\n leaderId: s.snapshot.leaderId,\n clientId: s.me.clientId,\n keyType: effectiveKeyType(roomKeyFromPath(globalThis.location?.pathname), s.roomKeyType),\n }));\n}\n\n/**\n * Hook to get current readiness state\n */\nexport function useReadiness() {\n return useAppStore(s => s.readiness);\n}\n\n/**\n * Hook to get current snapshot state\n */\nexport function useSnapshot() {\n return useAppStore(s => s.snapshot);\n}\n\n// Leader-only escape hatch from a stuck all-or-nothing readiness gate. One\n// slow or reconnecting viewer can otherwise pin the leader's playback\n// controls on \"Waiting for sync...\" indefinitely. After this grace period\n// the leader may start anyway; late viewers buffer and catch up via the\n// normal join/sync path.\nconst READINESS_OVERRIDE_MS = 30_000;\n\n/**\n * Subscribe to a leader-side override that flips true `READINESS_OVERRIDE_MS`\n * after a media load if the readiness gate hasn't cleared. Reset every time\n * `readiness.mediaId` changes so each new media load starts a fresh timer;\n * also resets when the gate naturally clears so the override is forgotten\n * before the next stuck cycle.\n *\n * Used by every leader-gated control (PlayPauseButton, SeekBar). Centralised\n * so the controls can't drift — PlayPauseButton having an override but\n * SeekBar not was Bug 8 from the 2026-05-20 retrospective.\n */\nexport function useReadinessOverride(): boolean {\n const readiness = useReadiness();\n const [overrideReady, setOverrideReady] = useState(false);\n const [gate, setGate] = useState({ mediaId: readiness.mediaId, ready: readiness.ready });\n if (gate.mediaId !== readiness.mediaId || gate.ready !== readiness.ready) {\n setGate({ mediaId: readiness.mediaId, ready: readiness.ready });\n setOverrideReady(false);\n }\n useEffect(() => {\n if (readiness.ready) return;\n const t = setTimeout(() => setOverrideReady(true), READINESS_OVERRIDE_MS);\n return () => clearTimeout(t);\n }, [readiness.mediaId, readiness.ready]);\n return overrideReady;\n}\n","import { useEffect, useRef } from 'react';\n\n/**\n * The open game's own preferences, inside the settings popout's \"App\n * preferences\" section. The popout renders its children only while it is\n * open, so mounting here is opening and unmounting is closing; the game draws\n * into the element (`MountedGameBundle.mountSettings`). ChatPanel renders this\n * only while a game offers settings — the popout draws a section for any\n * child, so an empty one would leave a blank row.\n */\nexport default function GameSettingsSection({ draw }: { draw: (el: HTMLElement) => (() => void) | null }) {\n const ref = useRef<HTMLDivElement>(null);\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n return draw(el) ?? undefined;\n }, [draw]);\n return <div ref={ref} data-testid=\"game-settings\" />;\n}\n","import React from 'react';\nimport { Link } from 'react-router-dom';\nimport { openMediaDrawer } from '../lib/openMediaDrawer';\nimport { useIsFullscreen } from '../lib/useIsFullscreen';\nimport { isChatPanelHidden, selectMediaAffordance } from '../lib/mediaDrawerAffordance';\nimport { useAppStore } from '../lib/store';\nimport ChatLog from './ChatLog';\nimport ChatInput from './ChatInput';\nimport RosterButton from './RosterButton';\nimport { lazyWithRetry } from '../lib/lazyWithRetry';\nconst AdminPanel = lazyWithRetry(() => import('./AdminPanel'));\nconst PlaylistPanel = lazyWithRetry(() => import('./PlaylistPanel'));\nimport { ConnectedSettings, HadokuThemePicker } from '@wolffm/task-ui-components';\nimport '@wolffm/task-ui-components/theme-picker.css';\n// The settings popout's styles; ConnectedSettings does not load them itself.\nimport '@wolffm/task-ui-components/app-header.css';\nimport { debug } from '../lib/debug';\nimport { useActiveGame, useIsLeader, useLeaderAffordance } from '../hooks/useStoreSelectors';\nimport { leaveRoom, sendIdentityUpdate } from '../lib/socket';\nimport GameSettingsSection from './GameSettingsSection';\nexport default function ChatPanel() {\n const chatCollapsed = useAppStore(s => s.chatCollapsed);\n const isFullscreen = useIsFullscreen();\n const sidebarTab = useAppStore(s => s.sidebarTab);\n const setSidebarTab = useAppStore(s => s.setSidebarTab);\n const clientCount = useAppStore(s => s.healthSnapshot?.clients.length ?? 0);\n const playlistLoaded = useAppStore(s => !!s.playlist);\n const isLeader = useIsLeader();\n // Presenter-only CHROME renders off the affordance, which answers from the\n // room key while the socket join is still in flight, so these two buttons\n // arrive with the rest of the toolbar instead of shoving it sideways a beat\n // later. The tab bar below deliberately stays on `isLeader`: the Health tab\n // has nothing to show before the socket is up anyway.\n const leaderAffordance = useLeaderAffordance();\n const gameSettings = useAppStore(s => s.gameSettings);\n\n // Hidden when collapsed over loaded media (RoomCornerControls' chat toggle\n // hides and shows it), and while fullscreen: fullscreen paints only the fullscreen\n // element and its descendants, and this panel is a SIBLING of it (Room.tsx),\n // so it is invisible no matter what it renders. Unmount rather than leave a\n // dead subtree on screen-readers and, more concretely, a second\n // aria-label=\"Select Media\" in the DOM: another site supplies that button in\n // exactly these states, and two matches is a Playwright strict-mode\n // violation (ready-check-fixes.spec.ts drives fullscreen and locates it).\n // Chat itself stays available in fullscreen through FloatingChat, which IS\n // portaled in. Without media the panel always shows — it is then the only\n // carrier of Select Media. The rule is shared: lib/mediaDrawerAffordance.ts.\n const mediaId = useAppStore(s => s.snapshot.mediaId);\n const gameOverlay = useActiveGame() !== null;\n if (isChatPanelHidden({ hasMedia: !!mediaId, chatCollapsed, isFullscreen })) {\n return null;\n }\n\n // Compute effective tab: fall back to 'chat' for invalid tab states\n const activeTab = (() => {\n if (sidebarTab === 'health' && !isLeader) return 'chat';\n if (sidebarTab === 'playlist' && !playlistLoaded) return 'chat';\n return sidebarTab;\n })();\n\n return (\n <aside className=\"w-80 self-stretch border-l border-border flex flex-col bg-bg relative z-10\">\n {/* Compact toolbar */}\n <div className=\"chat-toolbar relative flex items-center gap-1 px-2 py-1.5 border-b border-border\">\n {/* Home button */}\n <Link\n to=\"/\"\n className=\"p-1.5 rounded text-text-secondary hover:text-white hover:bg-bg-alt transition-colors cursor-pointer\"\n title=\"Home\"\n // hadoku_site's shell turns every title= into a themed tooltip and\n // strips the attribute (installTitleTooltips, 2026-10-23), which left\n // this icon-only link with no accessible name in production.\n aria-label=\"Home\"\n onClick={() => { debug('navigation', 'Home button clicked (chat toolbar)'); leaveRoom(); }}\n >\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6\" />\n </svg>\n </Link>\n\n {/* Select Media button — leader only. Dispatches an event that Room.tsx\n listens for to open the MediaDrawer. Uses a CustomEvent instead of\n lifting drawer state into the store because the drawer is a purely\n UI-local concern and doesn't need to be shared across components.\n\n Gated on the SHARED rule, not on `isLeader` alone: ControlBar or\n RoomCornerControls renders the same button whenever this panel is\n hidden, and exactly one must exist (see lib/mediaDrawerAffordance.ts). */}\n {selectMediaAffordance({ isLeader: leaderAffordance, hasMedia: !!mediaId, chatCollapsed, isFullscreen, gameOverlay }) === 'chat-toolbar' && (\n <button\n onClick={() => openMediaDrawer('chat toolbar')}\n className=\"p-1.5 rounded text-text-secondary hover:text-white hover:bg-bg-alt transition-colors cursor-pointer\"\n title=\"Select Media\"\n aria-label=\"Select Media\"\n >\n {/* Film/media icon */}\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 4h16a1 1 0 011 1v14a1 1 0 01-1 1H4a1 1 0 01-1-1V5a1 1 0 011-1z\" />\n </svg>\n </button>\n )}\n\n {/* Theme picker — the platform's, propless by design (see\n HadokuThemePicker). It persists the theme itself through the\n shared theme prefs, so the old savePref('theme') mirror and the\n hand-managed open state are gone with it. */}\n <HadokuThemePicker />\n\n {/* The fleet's settings popout: the person's tier, registry name,\n content level and key, then the open game's own preferences. A\n rename re-reads the registry server-side (ws/presence.ts treats a\n name in identity_update as \"re-read whoami\"), so the roster shows\n it without a rejoin. */}\n <ConnectedSettings onNameChange={(renamed) => { if (renamed && useAppStore.getState().me.clientId) sendIdentityUpdate(renamed); }}>\n {gameSettings ? <GameSettingsSection draw={gameSettings} /> : null}\n </ConnectedSettings>\n\n <RosterButton />\n </div>\n\n {/* Tab bar — visible to everyone when playlist is loaded, or always for leaders */}\n {(isLeader || playlistLoaded) && (\n <div className=\"flex border-b border-border shrink-0\">\n <button\n onClick={() => setSidebarTab('chat')}\n className={`flex-1 text-xs py-1.5 transition-colors cursor-pointer ${\n activeTab === 'chat'\n ? 'text-text border-b-2 border-primary'\n : 'text-text-muted hover:text-text-secondary'\n }`}\n >\n Chat\n </button>\n {playlistLoaded && (\n <button\n onClick={() => setSidebarTab('playlist')}\n className={`flex-1 text-xs py-1.5 transition-colors cursor-pointer ${\n activeTab === 'playlist'\n ? 'text-text border-b-2 border-primary'\n : 'text-text-muted hover:text-text-secondary'\n }`}\n >\n Playlist\n </button>\n )}\n {isLeader && (\n <button\n onClick={() => setSidebarTab('health')}\n className={`flex-1 text-xs py-1.5 transition-colors cursor-pointer flex items-center justify-center gap-1 ${\n activeTab === 'health'\n ? 'text-text border-b-2 border-primary'\n : 'text-text-muted hover:text-text-secondary'\n }`}\n >\n Health\n <span className=\"text-[10px] text-text-muted\">{clientCount}</span>\n </button>\n )}\n </div>\n )}\n\n {/* Tab content */}\n {activeTab === 'health' ? (\n <React.Suspense fallback={<div className=\"p-4 text-xs text-text-muted\">Loading...</div>}>\n <AdminPanel />\n </React.Suspense>\n ) : activeTab === 'playlist' ? (\n <React.Suspense fallback={<div className=\"p-4 text-xs text-text-muted\">Loading...</div>}>\n <PlaylistPanel />\n </React.Suspense>\n ) : (\n <>\n <ChatLog />\n\n {/* Input */}\n <ChatInput />\n </>\n )}\n </aside>\n );\n}\n","import { THEMES, type Theme } from '@wolffm/themes';\n\nconst isTheme = (t: string | null | undefined): t is Theme =>\n typeof t === 'string' && (THEMES as readonly string[]).includes(t);\n\n/**\n * Which `@wolffm/themes` theme the chrome paints, highest precedence first:\n *\n * 1. the host's explicit `theme` prop;\n * 2. the AMBIENT theme — what the host's own `<HadokuThemeRoot>` provides. This\n * is what keeps chrome and host agreeing when the host is not dark;\n * 3. the viewer's saved hadoku theme (our own store), a fallback for a host\n * with no provider;\n * 4. `dark`.\n *\n * An ambient or saved value outside `THEMES` falls through rather than being\n * painted: `data-theme=\"<unknown>\"` matches no stylesheet and renders unthemed.\n * (`HadokuThemeValue.theme` is typed `string`, so the type does not rule it out.)\n */\nexport function resolveChromeTheme(\n explicit: Theme | undefined,\n ambient: string | null | undefined,\n saved: string | null | undefined,\n): Theme {\n if (explicit) return explicit;\n if (isTheme(ambient)) return ambient;\n if (isTheme(saved)) return saved;\n return 'dark';\n}\n","import { isSameOriginPath } from '@watchparty/shared';\nimport {\n isGameBundleModule, isMountedGameBundle,\n type GameBundleContext, type GameEntry, type MountedGameBundle,\n} from '@wolffm/rooms-protocol';\n\n/**\n * Mounting a `kind: 'module'` game — the lifecycle half of `GameBundleSlot`,\n * kept out of the component so its races are testable without a DOM.\n *\n * The bundle is code this app has never seen, loaded at runtime, so every step\n * that can fail reports through `onError` instead of throwing into React: a\n * failed import, a module that does not export `mount`, a `mount` that throws\n * or rejects or resolves to the wrong shape, an `update` that throws.\n */\n\n/**\n * The absolute URL to import for a catalog `url`, or null when it would leave\n * the page's origin. The catalog was validated already (server and browser);\n * this re-checks against the REAL origin at the moment of import, because a\n * dynamic `import()` resolves against the importing module's URL, not the\n * page's — and the app bundle may be served from somewhere else.\n */\nexport function resolveGameBundleUrl(url: string, pageOrigin: string): string | null {\n if (!isSameOriginPath(url)) return null;\n const resolved = new URL(url, pageOrigin);\n return resolved.origin === pageOrigin ? resolved.href : null;\n}\n\n/**\n * What `GameBundleSlot` mounts: a catalog game, or the room's built-in\n * default activity (`lib/defaultActivity.ts`). Both are a `GameBundleModule`;\n * they differ only in where the module comes from. `load` is null when a\n * catalog url would leave the page's origin.\n */\nexport interface BundleActivity {\n id: string;\n name: string;\n load: (() => Promise<unknown>) | null;\n}\n\n/** A `kind: 'module'` catalog entry as an activity: imported by URL. */\nexport function moduleActivity(entry: Extract<GameEntry, { kind: 'module' }>, pageOrigin: string): BundleActivity {\n const href = resolveGameBundleUrl(entry.url, pageOrigin);\n return { id: entry.id, name: entry.name, load: href ? () => import(/* @vite-ignore */ href) : null };\n}\n\nexport interface GameBundleHandle {\n /** Pass the bundle a new context. Held until mount resolves if it has not yet. */\n update(ctx: GameBundleContext): void;\n /** Unmount (now, or as soon as an in-flight mount resolves). Idempotent. */\n dispose(): void;\n /**\n * Draw the bundle's settings into `el` (`MountedGameBundle.mountSettings`).\n * Returns the cleanup, or null when the bundle has no settings, is not\n * mounted, or threw. A throw is reported through `onSettingsError` and never\n * takes the game down: a broken preferences panel is not a broken game.\n */\n mountSettings(el: HTMLElement): (() => void) | null;\n}\n\ninterface MountGameBundleOptions<El> {\n /** Resolves to the bundle's module namespace. */\n load: () => Promise<unknown>;\n el: El;\n ctx: GameBundleContext;\n onError: (message: string, cause?: unknown) => void;\n /** Whether the mounted bundle offers settings: true once a bundle with\n * `mountSettings` is mounted, false again when it goes away. */\n onSettingsAvailable?: (available: boolean) => void;\n onSettingsError?: (message: string, cause?: unknown) => void;\n}\n\nconst describe = (e: unknown) => (e instanceof Error ? e.message : String(e));\n\n/**\n * Load and mount a bundle into `el`. Returns at once; the mount is async.\n *\n * Generic over the element so tests can pass a plain object — the host never\n * touches `el` itself, the bundle does.\n */\nexport function mountGameBundle<El = HTMLElement>(opts: MountGameBundleOptions<El>): GameBundleHandle {\n let latest = opts.ctx;\n let mounted: MountedGameBundle | null = null;\n let disposed = false;\n let failed = false;\n // Settings panels currently drawn, so they are removed BEFORE the bundle\n // unmounts rather than whenever the popout next re-renders.\n // One entry per open, even if a bundle hands back the same function twice.\n const openSettings = new Set<{ cleanup: () => void }>();\n\n const settingsError = (message: string, cause?: unknown) => opts.onSettingsError?.(message, cause);\n\n const closeSettings = (open: { cleanup: () => void }) => {\n if (!openSettings.delete(open)) return;\n try {\n open.cleanup();\n } catch (e) {\n settingsError(`the game's settings failed to close: ${describe(e)}`, e);\n }\n };\n\n /** The bundle is going away: its settings first, then say so. */\n const releaseSettings = (bundle: MountedGameBundle) => {\n for (const open of [...openSettings]) closeSettings(open);\n if (bundle.mountSettings) opts.onSettingsAvailable?.(false);\n };\n\n const teardown = (bundle: MountedGameBundle) => {\n releaseSettings(bundle);\n try {\n bundle.unmount();\n } catch (e) {\n // Reported, not rethrown: the slot is going away either way, and an\n // exception here would escape a React cleanup. Never over a first error.\n if (!failed) opts.onError(`unmount threw: ${describe(e)}`, e);\n }\n };\n\n /** Report the first failure and take a half-working bundle down with it. */\n const fail = (message: string, cause?: unknown) => {\n if (failed || disposed) return;\n failed = true;\n const bundle = mounted;\n mounted = null;\n if (bundle) teardown(bundle);\n opts.onError(message, cause);\n };\n\n const update = (ctx: GameBundleContext) => {\n latest = ctx;\n if (!mounted || disposed) return;\n try {\n mounted.update(ctx);\n } catch (e) {\n fail(`the game failed to update: ${describe(e)}`, e);\n }\n };\n\n void (async () => {\n let mod: unknown;\n try {\n mod = await opts.load();\n } catch (e) {\n fail(`could not load the game: ${describe(e)}`, e);\n return;\n }\n if (disposed) return;\n if (!isGameBundleModule(mod)) {\n fail('the game module does not export mount()');\n return;\n }\n\n const mountedCtx = latest;\n let result: unknown;\n try {\n // `as` is the narrowing the guard above proved; El is HTMLElement in the app.\n result = await mod.mount(opts.el as unknown as HTMLElement, mountedCtx);\n } catch (e) {\n fail(`the game failed to start: ${describe(e)}`, e);\n return;\n }\n if (!isMountedGameBundle(result)) {\n fail('the game\\'s mount() did not return { unmount, update }');\n return;\n }\n if (disposed) {\n teardown(result);\n return;\n }\n mounted = result;\n if (result.mountSettings) opts.onSettingsAvailable?.(true);\n // A context change that landed while mount was in flight.\n if (latest !== mountedCtx) update(latest);\n })();\n\n return {\n update,\n dispose() {\n if (disposed) return;\n disposed = true;\n if (mounted) teardown(mounted);\n mounted = null;\n },\n mountSettings(el: HTMLElement) {\n const bundle = mounted;\n if (!bundle?.mountSettings || disposed) return null;\n let raw: unknown;\n try {\n raw = bundle.mountSettings(el);\n } catch (e) {\n settingsError(`the game's settings failed to open: ${describe(e)}`, e);\n return null;\n }\n if (typeof raw !== 'function') {\n settingsError('the game\\'s mountSettings() did not return a cleanup function');\n return null;\n }\n const open = { cleanup: raw as () => void };\n openSettings.add(open);\n return () => closeSettings(open);\n },\n };\n}\n","import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';\nimport { useHadokuThemeOptional } from '@wolffm/themes';\nimport type { GameBundleContext } from '@wolffm/rooms-protocol';\nimport { useAppStore } from '../lib/store';\nimport { emitGameSelect } from '../lib/socket';\nimport { getUserId, getUserType } from '../lib/auth';\nimport { loadPreferences } from '../lib/preferences';\nimport { resolveChromeTheme } from '../roomChrome/hostTheme';\nimport { mountGameBundle, type BundleActivity, type GameBundleHandle } from '../lib/gameBundleHost';\nimport { debug, warn } from '../lib/debug';\n\n/**\n * One activity in the room's main area, mounted through `GameBundleModule`.\n *\n * Two layers use it. `base` holds the room's default activity (the media\n * player, `lib/defaultActivity.ts`) for the life of the room; `overlay` holds\n * a `kind: 'module'` game the leader started, on top of it. Watchparty knows\n * nothing about either: it loads the module, hands it an empty host element\n * and a `GameBundleContext`, and passes later context changes to `update()`.\n * The bundle owns everything inside the element. This component is the whole\n * of what a new game costs watchparty — adding one is a `GAMES` config edit.\n *\n * `close()` for the leader is the Stop game path (`game_select null`), so it\n * closes for the room; for anyone else it hides the game for them only, until\n * the leader picks a game again. The base layer has nothing beneath it to\n * close back to, so there it does nothing. Room keys the overlay by game id,\n * so a new pick is a fresh mount with a fresh error and dismissal state.\n */\nexport default function GameBundleSlot({ activity, roomKey, layer }: {\n activity: BundleActivity;\n roomKey: string;\n layer: 'base' | 'overlay';\n}) {\n const hostRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<GameBundleHandle | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [dismissed, setDismissed] = useState(false);\n\n const leaderId = useAppStore((s) => s.snapshot.leaderId);\n const myClientId = useAppStore((s) => s.me.clientId);\n const isLeader = !!leaderId && leaderId === myClientId;\n const name = useAppStore((s) => s.name);\n const ambientTheme = useHadokuThemeOptional()?.theme;\n const theme = resolveChromeTheme(undefined, ambientTheme, loadPreferences().theme);\n\n const ctx = useMemo<GameBundleContext>(() => {\n return {\n roomKey,\n identity: {\n // The registry userId — what RoomChrome's `userId` prop is.\n userId: getUserId(),\n userType: getUserType(),\n name: name || null,\n },\n theme,\n isLeader,\n close: () => {\n debug('game', 'bundle close', { gameId: activity.id, isLeader, layer });\n if (layer === 'base') return;\n if (isLeader) emitGameSelect(null);\n else setDismissed(true);\n },\n };\n }, [roomKey, name, theme, isLeader, activity.id, layer]);\n\n // Mount reads the context once; every later change goes through update().\n const readCtx = useEffectEvent(() => ctx);\n const reportError = useEffectEvent((message: string, cause: unknown) => {\n warn('game', 'bundle failed', { gameId: activity.id, message, cause: String(cause) });\n setError(message);\n });\n const reportSettingsError = useEffectEvent((message: string, cause: unknown) => {\n warn('game', 'bundle settings failed', { gameId: activity.id, message, cause: String(cause) });\n });\n\n useEffect(() => {\n const el = hostRef.current;\n const load = activity.load;\n if (!el || !load || dismissed) return;\n debug('game', 'bundle mount', { gameId: activity.id });\n const { setGameSettings } = useAppStore.getState();\n // Only the overlay's game gets the popout's section. The base layer is\n // the media player, which has no settings of its own.\n const drawSettings = (target: HTMLElement) => handle.mountSettings(target);\n const handle = mountGameBundle({\n load,\n el,\n ctx: readCtx(),\n onError: reportError,\n onSettingsAvailable: layer === 'overlay'\n ? (available) => {\n const current = useAppStore.getState().gameSettings;\n if (available) setGameSettings(drawSettings);\n else if (current === drawSettings) setGameSettings(null);\n }\n : undefined,\n onSettingsError: reportSettingsError,\n });\n handleRef.current = handle;\n return () => {\n handle.dispose();\n handleRef.current = null;\n };\n }, [activity.id, activity.load, dismissed, layer]);\n\n useEffect(() => {\n handleRef.current?.update(ctx);\n }, [ctx]);\n\n if (dismissed) return null;\n\n // Validated twice before it gets here, so this is a guard, not a path.\n const shown = activity.load ? error : 'This game is not served from this site.';\n\n return (\n <div\n className={layer === 'overlay' ? 'absolute inset-0 z-30 bg-bg' : 'absolute inset-0'}\n data-testid=\"game-bundle-slot\"\n data-game-id={activity.id}\n >\n <div ref={hostRef} className=\"absolute inset-0\" />\n {shown && (\n <div role=\"alert\" className=\"absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-center bg-bg-card\">\n <div className=\"text-text font-medium\">{activity.name} could not start</div>\n <div className=\"text-text-secondary text-sm\">{shown}</div>\n {layer === 'overlay' && (\n <button type=\"button\" className=\"drawer-btn\" onClick={() => ctx.close()}>\n Close\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport type { ReactNode } from 'react';\n\n/**\n * The chrome every right-hand drawer shares: backdrop, panel, header row,\n * Escape-to-close and backdrop-to-close.\n *\n * MediaDrawer and the old GameDrawer (now MediaDrawer's \"Play\" tab, see\n * GamePicker.tsx) had hand-copied versions of all of it, and they had already\n * drifted — GameDrawer closed on a backdrop click and MediaDrawer\n * did not (its Escape handler even carried a comment about a backdrop handler\n * \"below\" that was never written). Both also carried\n * `bg-bg-card hover:bg-bg-card` on their buttons: a hover state identical to\n * the base, i.e. no hover at all. The fix for that is `--color-bg-hover`,\n * which the theme has exported all along.\n *\n * A new drawer belongs here rather than in a third copy.\n */\n\n/**\n * Shared class for drawer header actions.\n *\n * A REAL class, not Tailwind utilities: `styles.css` carries an unlayered\n * `button { background: none; color: inherit }` reset, and unlayered CSS beats\n * anything in a @layer, so every background, colour and border utility on a\n * <button> is silently discarded. These buttons used to be `bg-bg-card hover:bg-bg-card`\n * and rendered as bare text — padding and radius survive the reset, so the\n * geometry looked deliberate and only the surface was missing. See the\n * `.drawer-btn` block in styles.css.\n */\nexport const DRAWER_BTN = 'drawer-btn';\n\ninterface DrawerShellProps {\n open: boolean;\n onClose: () => void;\n /** Header content, rendered after the shared Close button. */\n header?: ReactNode;\n /** Optional footer strip. Omitted entirely when not supplied — a drawer\n * should not carry an empty bar just to have one. */\n footer?: ReactNode;\n children: ReactNode;\n}\n\nexport default function DrawerShell({ open, onClose, header, footer, children }: DrawerShellProps) {\n useEffect(() => {\n if (!open) return;\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') onClose();\n }\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open, onClose]);\n\n if (!open) return null;\n\n return (\n <div className=\"fixed inset-0 bg-black/40 flex justify-end z-40\" onClick={onClose}>\n <div\n className=\"w-[440px] max-w-full h-full bg-bg border-l border-border flex flex-col\"\n onClick={e => e.stopPropagation()}\n >\n <div className=\"p-3 border-b border-border flex items-center gap-2 text-sm\">\n <button onClick={onClose} className={DRAWER_BTN}>Close</button>\n {header}\n </div>\n {children}\n {footer}\n </div>\n </div>\n );\n}\n\n/**\n * Suspense fallback for the lazily-loaded drawers.\n *\n * Both drawers are `lazyWithRetry` chunks behind `fallback={null}`, so opening\n * one before its chunk had landed painted NOTHING — no backdrop, no panel, no\n * hint the click registered. On a cold load that is a click that appears to do\n * nothing at all. This renders the same chrome with a loading line, so the\n * drawer always opens instantly and fills in.\n *\n * Room.tsx must import this STATICALLY — a fallback that lives inside the\n * chunk it is covering can never render.\n */\nexport function DrawerLoading({ open, onClose }: { open: boolean; onClose: () => void }) {\n return (\n <DrawerShell open={open} onClose={onClose}>\n <div className=\"px-4 py-3 text-xs text-text-muted\">Loading…</div>\n </DrawerShell>\n );\n}\n","import { useMemo, useEffect, useState } from 'react';\nimport { messageHtml, nameHtml } from '@wolffm/prefs-client/cosmetics';\nimport type { ChatMessage } from '../lib/store';\nimport { useAppStore } from '../lib/store';\nimport { usePersonColor } from '../lib/pictureColor';\nimport { pictureForAuthor } from '../lib/avatarPictures';\nimport ChatAvatar from './ChatAvatar';\nimport { baseColour, CosmeticHtml, useAuthorCosmetics, useCosmeticsStyles } from '../lib/cosmetics';\nimport type { SubtitleStyle } from './SubtitleOverlay';\nimport ChatImageAttachments from './ChatImageAttachments';\n\nconst MAX_VISIBLE = 12;\nconst LIFETIME_MS = 9000;\nconst FADE_DURATION_MS = 1500;\n\n// CC style classes matching SubtitleOverlay (without text color)\nconst CC_STYLE_CLASSES: Record<SubtitleStyle, string> = {\n classic: '[text-shadow:_-1px_-1px_0_#000,_1px_-1px_0_#000,_-1px_1px_0_#000,_1px_1px_0_#000,_0_-1px_0_#000,_0_1px_0_#000,_-1px_0_0_#000,_1px_0_0_#000]',\n cinema: 'drop-shadow-[0_2px_4px_rgba(0,0,0,0.9)]',\n minimal: 'drop-shadow-[0_2px_4px_rgba(0,0,0,0.9)] drop-shadow-[0_0_8px_rgba(0,0,0,0.7)]',\n contrast: 'bg-black/70 px-2 py-0.5 rounded',\n};\n\ninterface FloatingMessageProps {\n message: ChatMessage;\n age: number;\n ccStyle: SubtitleStyle;\n}\n\nfunction FloatingMessage({ message, age, ccStyle }: FloatingMessageProps) {\n // Calculate opacity based on age\n const fadeStart = LIFETIME_MS - FADE_DURATION_MS;\n let opacity = 1;\n if (age > fadeStart) {\n opacity = Math.max(0, 1 - (age - fadeStart) / FADE_DURATION_MS);\n }\n\n const ccClass = CC_STYLE_CLASSES[ccStyle];\n const pictureUrl = useAppStore(s => pictureForAuthor(s.roster, message.author));\n const nameColor = usePersonColor(message.sprite, pictureUrl);\n const worn = useAuthorCosmetics(message.isSystem ? undefined : message.author);\n // Built once: this re-renders every tick to fade, and must not replay the reveal.\n const [textHtml] = useState(() =>\n message.live && message.text ? messageHtml(message.text, worn) : null,\n );\n\n return (\n <div\n className={`flex items-start gap-2 py-1 ${ccClass}`}\n style={{ opacity, transition: 'opacity 0.15s ease-out', ...baseColour(nameColor) }}\n >\n <ChatAvatar sprite={message.sprite} url={pictureUrl} worn={worn} />\n <div className=\"min-w-0 flex-1\" style={{ WebkitTextStroke: '0.5px black', paintOrder: 'stroke fill' }}>\n {worn ? (\n <CosmeticHtml html={nameHtml(message.name, worn)} className=\"text-sm mr-2\" style={{ color: nameColor }} />\n ) : (\n <span className=\"font-medium text-sm mr-2\" style={{ color: nameColor }}>\n {message.name}\n </span>\n )}\n {textHtml ? (\n <CosmeticHtml html={textHtml} className=\"text-sm text-white break-words\" />\n ) : (\n <span className=\"text-sm text-white break-words\">{message.text}</span>\n )}\n <ChatImageAttachments images={message.images} />\n </div>\n </div>\n );\n}\n\nexport default function FloatingChat() {\n useCosmeticsStyles();\n const chat = useAppStore(s => s.chat);\n const chatCollapsed = useAppStore(s => s.chatCollapsed);\n const floatingChatEnabled = useAppStore(s => s.floatingChatEnabled);\n const subtitleStyle = useAppStore(s => s.mediaPrefs.subtitleStyle) as SubtitleStyle;\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [now, setNow] = useState(() => Date.now());\n\n // Check fullscreen state\n useEffect(() => {\n const checkFullscreen = () => {\n setIsFullscreen(!!document.fullscreenElement);\n };\n\n document.addEventListener('fullscreenchange', checkFullscreen);\n checkFullscreen();\n\n return () => document.removeEventListener('fullscreenchange', checkFullscreen);\n }, []);\n\n // Update `now` periodically for fade animation (~5fps), but only when there are messages to show\n const hasChat = chat.length > 0;\n useEffect(() => {\n const id = setInterval(() => {\n if (hasChat) setNow(Date.now());\n }, 200);\n return () => clearInterval(id);\n }, [hasChat]);\n\n // Only show when enabled and (chat is collapsed or in fullscreen)\n const shouldShow = floatingChatEnabled && (chatCollapsed || isFullscreen);\n\n // Filter to recent non-system messages\n const recentMessages = useMemo(() => {\n const cutoff = now - LIFETIME_MS;\n return chat\n .filter(m => !m.isSystem && m.ts > cutoff)\n .slice(-MAX_VISIBLE);\n }, [chat, now]);\n\n if (!shouldShow || recentMessages.length === 0) {\n return null;\n }\n\n return (\n <div\n className=\"absolute right-4 w-80 z-20 pointer-events-none flex flex-col\"\n style={{ bottom: '35%' }}\n >\n {recentMessages.map(msg => (\n <FloatingMessage\n key={msg.id}\n message={msg}\n age={now - msg.ts}\n ccStyle={subtitleStyle}\n />\n ))}\n </div>\n );\n}\n","import { CRITICAL_BUFFER_MS, LOW_BUFFER_MS, type TClientHealth } from '@watchparty/shared';\n\ntype BufferHealthLevel = 'idle' | 'buffering' | 'critical' | 'warning' | 'healthy';\n\ntype HealthInput = Pick<\n TClientHealth,\n 'playhead_ms' | 'is_buffering' | 'buffered_ahead_ms' | 'duration_ms'\n> & {\n /** HTMLMediaElement.readyState. Optional so the viewer ring — whose\n * BufferClient payload defaults it to -1 — and older clients both work. */\n video_ready_state?: number;\n};\n\n/** HAVE_CURRENT_DATA. At or below this the element has the current frame and\n * nothing it can advance to. */\nconst READY_STATE_CANNOT_ADVANCE = 2;\n\n/**\n * Pure classifier for a client's buffer health, used by the leader's health\n * panel. Order matters: idle → buffering (real stall) → near-end exemption →\n * buffered-ahead thresholds.\n *\n * Near end-of-stream there is legitimately little content ahead of the playhead,\n * so `buffered_ahead_ms` naturally falls below CRITICAL/LOW in the final seconds\n * of every episode. Flagging that as \"Critical\" is a false alarm (reported from\n * the 2026-06-23 ember-cove party). When the playhead is within LOW_BUFFER_MS of\n * the media end we suppress the buffered-ahead thresholds — but a genuine stall\n * (`is_buffering`) still surfaces, since that's a real problem even at the end.\n */\n/**\n * True when the playhead is within LOW_BUFFER_MS of the media end, where a low\n * buffered-ahead is expected and must not read as critical. Shared by the\n * leader's health card (below) and the viewer buffer ring (ClientStatusOverlay)\n * so both apply the identical near-end exemption. `duration_ms === 0` (unknown /\n * older client) disables the exemption.\n */\nexport function isNearEnd(client: Pick<HealthInput, 'playhead_ms' | 'duration_ms'>): boolean {\n return client.duration_ms > 0 && client.duration_ms - client.playhead_ms <= LOW_BUFFER_MS;\n}\n\n/**\n * False when the client reports a HEALTHY buffer but a readyState that cannot\n * advance — a contradiction that means the playhead is pinned even though the\n * track this metric describes has data. Shared by the leader's health card\n * (below) and the viewer buffer ring (ClientStatusOverlay) so both read the\n * identical rule.\n *\n * Requiring a healthy buffered-ahead is what makes it safe: during startup, a\n * seek, or a genuine underrun readyState is legitimately low AND the buffer is\n * low, which the ordinary thresholds already classify. Only the combination is\n * anomalous. A negative/absent `video_ready_state` (older client) never trips\n * it.\n */\nexport function bufferHealthCanAdvance(\n client: Pick<HealthInput, 'buffered_ahead_ms'> & { video_ready_state?: number },\n): boolean {\n const rs = client.video_ready_state ?? -1;\n if (rs < 0) return true;\n return !(rs <= READY_STATE_CANNOT_ADVANCE && client.buffered_ahead_ms > LOW_BUFFER_MS);\n}\n\nexport function bufferHealthLevel(client: HealthInput): BufferHealthLevel {\n const isIdle =\n client.playhead_ms === 0 && !client.is_buffering && client.buffered_ahead_ms === 0;\n if (isIdle) return 'idle';\n if (client.is_buffering) return 'buffering';\n\n if (isNearEnd(client)) return 'healthy';\n\n // \"Plenty buffered, yet the element cannot advance\" is a contradiction, and\n // it is the ONLY field that moved during the cedar-ridge 2026-08-25 freeze:\n // readyState went 4 → 2 at the exact second the playhead pinned, while\n // buffered_ahead_ms sat at ~30s and is_buffering stayed false for 40s. The\n // health card read \"Healthy\" throughout and the user had to report it by\n // hand.\n //\n // Requiring a HEALTHY buffered-ahead is what makes this safe: during startup,\n // a seek, or a genuine underrun the readyState is legitimately low AND the\n // buffer is low, which the thresholds below already classify. Only the\n // combination is anomalous. `video_ready_state` is optional/-1 for clients\n // that do not report it, and a negative value never trips this.\n if (!bufferHealthCanAdvance(client)) return 'critical';\n\n if (client.buffered_ahead_ms <= CRITICAL_BUFFER_MS) return 'critical';\n if (client.buffered_ahead_ms <= LOW_BUFFER_MS) return 'warning';\n return 'healthy';\n}\n\nexport const BUFFER_HEALTH_LABEL: Record<BufferHealthLevel, string> = {\n idle: 'Idle',\n buffering: 'Buffering',\n critical: 'Critical',\n warning: 'Low Buffer',\n healthy: 'Healthy',\n};\n","/**\n * Synthesized ready-check notification cues via Web Audio API.\n *\n * - playReadyCheckStartSound() — \"proximityAlert\": pulsing radar tones\n * rising to a clear note. Played when a ready check opens.\n * - playAllReadySound() — \"syncComplete\": three staggered FM bells\n * with a slight detune. Played when every participant is ready.\n * - playReadyToggleOnSound() — a quick two-note rise. Played when any\n * participant marks themselves ready, INCLUDING the final one.\n * - playReadyToggleOffSound() — the same shape inverted, for un-readying.\n *\n * Both ≤500ms, peak gain ≤0.17. These two were chosen from a wider menu\n * of 40+ variants during sound-design iteration; the alternatives and the\n * offline mp3-generator script that produced their preview audio live in\n * git history (see commits up to b2e392e).\n *\n * Autoplay policy: viewers with the video element actively playing already\n * satisfy the page's audio activation; the AudioContext resumes silently.\n * A brand-new context (no prior interaction) may stay suspended — we\n * attempt resume() and swallow rejections rather than crash.\n *\n * Preview from devtools without triggering a real ready check:\n * __WP_SOUNDS__.start() .finish() .on() .off()\n */\n\nimport { registerDebugGlobal } from './debugGlobals';\n\nlet _ctx: AudioContext | null = null;\n\nfunction getCtx(): AudioContext | null {\n if (_ctx) return _ctx;\n try {\n const AC = (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })\n .AudioContext\n ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (AC) _ctx = new AC();\n } catch { /* no Web Audio — degrade silently */ }\n return _ctx;\n}\n\nfunction ensureRunning(ctx: AudioContext): void {\n if (ctx.state === 'suspended') ctx.resume().catch(() => { /* still suspended */ });\n}\n\nfunction envelope(g: GainNode, t0: number, peak: number, durSec: number): void {\n g.gain.setValueAtTime(0, t0);\n g.gain.linearRampToValueAtTime(peak, t0 + 0.005);\n g.gain.exponentialRampToValueAtTime(0.0001, t0 + durSec);\n}\n\n/** Sine tone with the standard 5ms-attack / exponential-decay envelope. */\nfunction playTone(freq: number, durationMs: number, gain = 0.15, when = 0): void {\n const ctx = getCtx();\n if (!ctx) return;\n ensureRunning(ctx);\n const t0 = ctx.currentTime + when;\n const osc = ctx.createOscillator();\n const g = ctx.createGain();\n osc.type = 'sine';\n osc.frequency.value = freq;\n envelope(g, t0, gain, durationMs / 1000);\n osc.connect(g).connect(ctx.destination);\n osc.start(t0);\n osc.stop(t0 + durationMs / 1000 + 0.05);\n}\n\n/**\n * FM-synth bell tone — carrier sine frequency-modulated by a second sine.\n * Web Audio idiom: modulator → gain (depth = modIndex·modHz) → carrier.frequency.\n */\nfunction playFM(\n carrierHz: number,\n modHz: number,\n modIndex: number,\n durationMs: number,\n gain = 0.15,\n when = 0,\n): void {\n const ctx = getCtx();\n if (!ctx) return;\n ensureRunning(ctx);\n const t0 = ctx.currentTime + when;\n const carrier = ctx.createOscillator();\n const mod = ctx.createOscillator();\n const modDepth = ctx.createGain();\n const g = ctx.createGain();\n carrier.type = 'sine'; carrier.frequency.value = carrierHz;\n mod.type = 'sine'; mod.frequency.value = modHz;\n modDepth.gain.value = modIndex * modHz;\n mod.connect(modDepth).connect(carrier.frequency);\n envelope(g, t0, gain, durationMs / 1000);\n carrier.connect(g).connect(ctx.destination);\n carrier.start(t0); carrier.stop(t0 + durationMs / 1000 + 0.05);\n mod.start(t0); mod.stop(t0 + durationMs / 1000 + 0.05);\n}\n\n/** Pulsing radar tones rising to a clear note — played when a ready check opens. */\nexport function playReadyCheckStartSound(): void {\n playTone(700, 60, 0.14, 0);\n playTone(700, 60, 0.14, 0.10);\n playTone(932, 120, 0.17, 0.20);\n}\n\n/**\n * A quick two-note rise — played when someone marks themselves ready.\n *\n * Deliberately quieter (0.07 peak) and shorter (~90ms total) than the two cues\n * above. Those fire once per ready check; this one fires once per PERSON per\n * toggle, so at eight viewers changing their minds it is the sound the room\n * hears most. Loud enough to notice beside the video, quiet enough to sit\n * under it.\n */\nexport function playReadyToggleOnSound(): void {\n playTone(784, 45, 0.07, 0); // G5\n playTone(1047, 60, 0.07, 0.045); // C6 — up a fourth, reads as \"yes\"\n}\n\n/** The same shape inverted — played when someone un-readies. */\nexport function playReadyToggleOffSound(): void {\n playTone(659, 45, 0.06, 0); // E5\n playTone(494, 70, 0.06, 0.045); // B4 — down, reads as \"undone\"\n}\n\n/** Three staggered FM bells with a slight detune — played when everyone is ready. */\nexport function playAllReadySound(): void {\n playFM(587, 1174, 3, 320, 0.12, 0);\n playFM(880, 1760, 3, 320, 0.12, 0.06);\n playFM(884, 1768, 3, 320, 0.08, 0.06);\n}\n\n// Tiny console preview — handy from devtools without triggering a real ready check.\nregisterDebugGlobal('__WP_SOUNDS__', {\n start: playReadyCheckStartSound,\n finish: playAllReadySound,\n on: playReadyToggleOnSound,\n off: playReadyToggleOffSound,\n});\n","/**\n * Which ready-check toggle cues to play, given consecutive ready sets.\n *\n * The existing ready-check sounds are deliberately room-wide (\"Notification\n * sounds for every participant\", `ClientStatusOverlay`), so these are too: you\n * hear the room readying up, not just yourself. That is the point of the cue —\n * a watch party where people are visibly waiting on each other.\n *\n * **Fires on EVERY toggle, including the one that completes the check.** That\n * click makes two sounds — this cue and `playAllReadySound` — and that is\n * intended: the person who clicked should get the same feedback everyone\n * else's click gave them, whether or not they happened to be last.\n *\n * Two suppressions, both load-bearing, both about not firing a burst of cues\n * for something that was never a click:\n *\n * 1. **Only while the check is active on BOTH sides.** Ending a ready check\n * clears the ready list, which as a raw diff looks like everyone\n * un-readying at once — N \"off\" blips for a check that just succeeded.\n * 2. **A first observation is not a transition.** A client joining mid-check\n * receives the whole ready set in one payload; diffing that against an\n * empty initial ref would play a cue per person already ready. The caller\n * lazy-initialises its ref to the current set for exactly this reason, and\n * `wasActive === false` covers the rest.\n */\n\ninterface ReadyToggleCues {\n /** How many clients newly became ready. */\n on: number;\n /** How many clients newly became not-ready. */\n off: number;\n}\n\nconst NONE: ReadyToggleCues = { on: 0, off: 0 };\n\nexport function diffReadyToggles(\n prev: readonly string[],\n next: readonly string[],\n wasActive: boolean,\n isActive: boolean,\n): ReadyToggleCues {\n // A check starting or ending is not a set of individual toggles.\n if (!wasActive || !isActive) return NONE;\n\n const before = new Set(prev);\n const after = new Set(next);\n\n let on = 0;\n for (const id of after) if (!before.has(id)) on++;\n\n let off = 0;\n // A client that disconnects mid-check also leaves the ready set. That reads\n // as un-readying, which is honest — the room is waiting on them again.\n for (const id of before) if (!after.has(id)) off++;\n\n return on === 0 && off === 0 ? NONE : { on, off };\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { usePersonColor } from '../lib/pictureColor';\nimport AvatarImage from './AvatarImage';\nimport { INITIAL_BUFFER_MS } from '@watchparty/shared';\nimport type { TBufferClient, TBufferStatus } from '@watchparty/shared';\nimport { isNearEnd, bufferHealthCanAdvance } from './bufferHealth';\nimport { useIsLeader } from '../hooks/useStoreSelectors';\nimport { emitReadyCheckStart, emitReadyCheckToggle } from '../lib/socket';\nimport {\n playReadyCheckStartSound,\n playAllReadySound,\n playReadyToggleOnSound,\n playReadyToggleOffSound,\n} from '../lib/notificationSound';\nimport { diffReadyToggles } from '../lib/readyToggleCue';\nimport { Icon } from '@wolffm/themes';\n\n/**\n * Unified per-client status overlay. Rendered via <FullscreenPortal> so it\n * stays visible in chat-closed and fullscreen modes (in fullscreen it is\n * portaled into the fullscreen element — see FullscreenPortal). Two modes:\n *\n * 1. Buffer loading — viewers below the INITIAL_BUFFER_MS threshold get a\n * progress ring around their avatar, color shifting hue 0→120 (red→green)\n * as the buffer fills. Auto-fades 1.2s after they cross the threshold.\n *\n * 2. Ready-check (leader-triggered) — every roster member is shown with a\n * ✓ ready / ⏳ pending badge. The local user's avatar is clickable to\n * toggle their ready state. Leader sees a \"Start ready check\" pill when\n * neither mode is active.\n *\n * Replaces BufferLoadingOverlay (which only did mode 1) AND the Roster-pane\n * ready-check banner + per-row badges (which weren't visible in fullscreen).\n */\n\nconst FADE_DURATION_MS = 1200;\nconst RING_SIZE = 52;\nconst RING_STROKE = 4;\nconst RING_RADIUS = (RING_SIZE - RING_STROKE) / 2;\nconst CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;\nconst SPRITE_SIZE = RING_SIZE - RING_STROKE * 2 - 4;\n\nfunction isBufferingSufficient(client: TBufferClient): boolean {\n // Near end-of-stream there is legitimately little content ahead, so a low\n // buffered_ahead_ms is expected — don't show the (red) ring. Same exemption\n // the leader's health card uses (reported as a false positive by the user\n // near clip/episode end, cedar-ridge 2026-07-09).\n if (isNearEnd(client)) return true;\n // A client whose element cannot advance despite a healthy buffer is stalled,\n // not sufficient — the audio-starvation shape from cedar-ridge 2026-08-25,\n // where buffered_ahead_ms read ~30s while the playhead was pinned. Same rule\n // as bufferHealthLevel(); kept in sync via the shared thresholds.\n if (!bufferHealthCanAdvance(client)) return false;\n return client.buffered_ahead_ms >= INITIAL_BUFFER_MS;\n}\n\nfunction ringColor(progress: number): string {\n const hue = Math.round(progress * 120);\n return `hsl(${hue}, 90%, 55%)`;\n}\n\ninterface BufferRingProps {\n client: TBufferClient;\n /** Their avatar picture, from the roster: buffer status carries none. */\n pictureUrl: string | undefined;\n fading: boolean;\n onFadeEnd: () => void;\n}\n\nfunction BufferRing({ client, pictureUrl, fading, onFadeEnd }: BufferRingProps) {\n const progress = Math.min(1, client.buffered_ahead_ms / INITIAL_BUFFER_MS);\n const offset = CIRCUMFERENCE * (1 - progress);\n const color = ringColor(progress);\n const borderColor = usePersonColor(client.sprite_id, pictureUrl);\n\n return (\n <div\n data-testid=\"buffer-ring\"\n data-client-id={client.client_id}\n className=\"flex flex-col items-center gap-1\"\n style={fading ? {\n animation: `bufferFadeOut ${FADE_DURATION_MS}ms ease forwards`,\n } : undefined}\n onAnimationEnd={fading ? onFadeEnd : undefined}\n >\n <div className=\"relative flex items-center justify-center\" style={{ width: RING_SIZE, height: RING_SIZE }}>\n <svg\n width={RING_SIZE}\n height={RING_SIZE}\n className=\"absolute inset-0\"\n style={{ transform: 'rotate(-90deg)' }}\n >\n <circle\n cx={RING_SIZE / 2}\n cy={RING_SIZE / 2}\n r={RING_RADIUS}\n fill=\"none\"\n stroke=\"rgba(255,255,255,0.15)\"\n strokeWidth={RING_STROKE}\n />\n <circle\n cx={RING_SIZE / 2}\n cy={RING_SIZE / 2}\n r={RING_RADIUS}\n fill=\"none\"\n stroke={color}\n strokeWidth={RING_STROKE}\n strokeLinecap=\"round\"\n strokeDasharray={CIRCUMFERENCE}\n strokeDashoffset={offset}\n style={{ transition: 'stroke-dashoffset 0.4s ease, stroke 0.4s ease' }}\n />\n </svg>\n <AvatarImage\n sprite={client.sprite_id}\n url={pictureUrl}\n alt={client.name}\n className=\"rounded-full object-cover flex-shrink-0\"\n style={{ width: SPRITE_SIZE, height: SPRITE_SIZE, border: `2px solid ${borderColor}` }}\n fallback={\n <div\n className=\"rounded-full bg-bg-alt flex-shrink-0\"\n style={{ width: SPRITE_SIZE, height: SPRITE_SIZE, border: `2px solid ${borderColor}` }}\n />\n }\n />\n </div>\n <span\n className=\"text-xs font-medium leading-none\"\n style={{\n color: borderColor,\n textShadow: '-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000',\n maxWidth: 64,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n }}\n >\n {client.name}\n </span>\n </div>\n );\n}\n\ninterface ReadyAvatarProps {\n clientId: string;\n sprite: string | null;\n pictureUrl: string | undefined;\n name: string;\n isReady: boolean;\n isSelf: boolean;\n onToggle: () => void;\n}\n\nfunction ReadyAvatar({ clientId, sprite, pictureUrl, name, isReady, isSelf, onToggle }: ReadyAvatarProps) {\n const borderColor = usePersonColor(sprite, pictureUrl);\n const badgeFamily = isReady ? 'success' : 'neutral';\n const badge = isReady ? <Icon name=\"tick\" /> : <Icon name=\"hourglass\" />;\n return (\n <button\n type=\"button\"\n data-testid=\"ready-avatar\"\n data-client-id={clientId}\n data-ready={isReady ? '1' : '0'}\n data-self={isSelf ? '1' : '0'}\n onClick={isSelf ? onToggle : undefined}\n disabled={!isSelf}\n title={isSelf ? (isReady ? 'Click to un-ready' : 'Click to mark ready') : `${name} ${isReady ? 'is ready' : 'is still loading'}`}\n className={`flex flex-col items-center gap-1 bg-transparent border-0 p-0 ${isSelf ? 'cursor-pointer' : 'cursor-default'}`}\n style={{ pointerEvents: 'auto' }}\n >\n <div className=\"relative flex items-center justify-center\" style={{ width: RING_SIZE, height: RING_SIZE }}>\n <AvatarImage\n sprite={sprite}\n url={pictureUrl}\n alt={name}\n className=\"rounded-full object-cover flex-shrink-0\"\n style={{ width: SPRITE_SIZE, height: SPRITE_SIZE, border: `2px solid ${borderColor}`, opacity: isReady ? 1 : 0.55 }}\n fallback={\n <div\n className=\"rounded-full bg-bg-alt flex-shrink-0\"\n style={{ width: SPRITE_SIZE, height: SPRITE_SIZE, border: `2px solid ${borderColor}` }}\n />\n }\n />\n <span\n aria-label={isReady ? 'ready' : 'pending'}\n className=\"absolute -bottom-1 -right-1 text-xs font-bold rounded-full\"\n style={{\n background: `var(--color-${badgeFamily})`,\n color: `var(--color-on-${badgeFamily})`,\n width: 20, height: 20,\n display: 'flex', alignItems: 'center', justifyContent: 'center',\n lineHeight: 1,\n }}\n >\n {badge}\n </span>\n </div>\n <span\n className=\"text-xs font-medium leading-none\"\n style={{\n color: borderColor,\n textShadow: '-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000',\n maxWidth: 64,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n }}\n >\n {name}\n </span>\n </button>\n );\n}\n\ninterface ClientEntry {\n client: TBufferClient;\n /** Crossed the threshold after being seen below it: the ring fades out. */\n completed: boolean;\n}\n\n/** The buffer rings on screen, and what they were derived from. */\ninterface BufferTrack {\n status: TBufferStatus | null;\n entries: ClientEntry[];\n /** Clients that crossed the threshold and are fading out. */\n completed: ReadonlySet<string>;\n /** Clients seen below the threshold since their last fade. */\n below: ReadonlySet<string>;\n}\n\nconst EMPTY_TRACK: BufferTrack = { status: null, entries: [], completed: new Set(), below: new Set() };\n\n/** Advance the rings to a new buffer status. An empty status hides them but forgets no one. */\nfunction advanceTrack(prev: BufferTrack, status: TBufferStatus | null): BufferTrack {\n if (!status || status.clients.length === 0) return { ...prev, status, entries: [] };\n const completed = new Set(prev.completed);\n const below = new Set(prev.below);\n const entries: ClientEntry[] = [];\n for (const client of status.clients) {\n const id = client.client_id;\n if (!isBufferingSufficient(client)) {\n completed.delete(id);\n below.add(id);\n entries.push({ client, completed: false });\n } else if (below.has(id) && !completed.has(id)) {\n completed.add(id);\n entries.push({ client, completed: true });\n } else if (completed.has(id)) {\n entries.push({ client, completed: true });\n }\n }\n return { status, entries, completed, below };\n}\n\nexport default function ClientStatusOverlay() {\n const bufferStatus = useAppStore(s => s.bufferStatus);\n const readyCheck = useAppStore(s => s.readyCheck);\n const roster = useAppStore(s => s.roster);\n const myClientId = useAppStore(s => s.me.clientId);\n const isPlaying = useAppStore(s => s.snapshot.playing);\n const isLeader = useIsLeader();\n\n // ─── Buffer-loading mode (existing behavior) ─────────────\n const [track, setTrack] = useState(() => advanceTrack(EMPTY_TRACK, bufferStatus));\n if (track.status !== bufferStatus) setTrack(advanceTrack(track, bufferStatus));\n const entries = track.entries;\n\n const handleFadeEnd = (clientId: string) => {\n setTrack(prev => {\n const completed = new Set(prev.completed);\n const below = new Set(prev.below);\n completed.delete(clientId);\n below.delete(clientId);\n return { ...prev, completed, below, entries: prev.entries.filter(e => e.client.client_id !== clientId) };\n });\n };\n\n // ─── Ready-check mode ────────────────────────────────────\n const readySet = new Set(readyCheck.ready);\n const users = Object.values(roster);\n const iAmReady = !!myClientId && readySet.has(myClientId);\n const allReady = readyCheck.active && users.length > 0 && readyCheck.ready.length >= users.length;\n\n // Notification sounds for every participant. Refs are lazy-initialised to\n // current state so a client joining mid-ready-check doesn't fire the cue\n // (it already started); subsequent transitions on the live socket do.\n const wasActiveRef = useRef<boolean>(readyCheck.active);\n const wasAllReadyRef = useRef<boolean>(allReady);\n useEffect(() => {\n if (!wasActiveRef.current && readyCheck.active) playReadyCheckStartSound();\n wasActiveRef.current = readyCheck.active;\n }, [readyCheck.active]);\n useEffect(() => {\n if (!wasAllReadyRef.current && allReady) playAllReadySound();\n wasAllReadyRef.current = allReady;\n }, [allReady]);\n\n // Per-person toggle cue. Lazy-initialised to the CURRENT set for the same\n // reason as the refs above: a client joining mid-check receives everyone who\n // is already ready in one payload, and diffing that against an empty ref\n // would play a cue per person for clicks that happened before they arrived.\n const prevReadyRef = useRef<string[]>(readyCheck.ready);\n const prevActiveRef = useRef<boolean>(readyCheck.active);\n useEffect(() => {\n const { on, off } = diffReadyToggles(\n prevReadyRef.current,\n readyCheck.ready,\n prevActiveRef.current,\n readyCheck.active,\n );\n // One cue per direction, not per person: four people readying inside one\n // payload should be a blip, not a four-note chord.\n if (on > 0) playReadyToggleOnSound();\n if (off > 0) playReadyToggleOffSound();\n prevReadyRef.current = readyCheck.ready;\n prevActiveRef.current = readyCheck.active;\n }, [readyCheck.ready, readyCheck.active]);\n\n // Decide which mode wins. Ready-check has priority — if the leader started\n // one we want it visible even if a buffer-load happens to be in flight.\n // The \"Start ready check\" pill is a pre-playback affordance — hide it while\n // media is playing (a ready check mid-playback is meaningless and the pill\n // just clutters the video). It stays available when paused/idle.\n const showReadyCheck = readyCheck.active;\n const showBufferLoading = !showReadyCheck && entries.length > 0;\n const showStartButton = !showReadyCheck && !showBufferLoading && isLeader && users.length > 0 && !isPlaying;\n\n if (!showReadyCheck && !showBufferLoading && !showStartButton) return null;\n\n return (\n <>\n <style>{`\n @keyframes bufferFadeOut {\n from { opacity: 1; transform: translateY(0); }\n to { opacity: 0; transform: translateY(-24px); }\n }\n `}</style>\n <div\n data-testid=\"client-status-overlay\"\n data-mode={showReadyCheck ? 'ready-check' : showBufferLoading ? 'buffer-load' : 'idle'}\n // Anchored top-RIGHT, shifted DOWN (top-16) so the column sits\n // BELOW the MediaTitle row that appears on hover (top-3 right-5,\n // ~44px tall). items-end keeps every child (tally, avatars column,\n // Start pill) flush with the right edge.\n className=\"absolute top-16 right-4 z-20 flex flex-col items-end gap-3 pointer-events-none\"\n >\n {showReadyCheck && (\n <>\n <div\n data-testid=\"ready-tally\"\n className=\"px-2 py-1 rounded bg-black/55 text-xs font-semibold tracking-wide\"\n style={{ color: '#fff', textShadow: '0 1px 0 rgba(0,0,0,0.6)' }}\n >\n Ready {readyCheck.ready.length}/{users.length}\n </div>\n <div className=\"flex flex-col gap-3\">\n {users.map(u => (\n <ReadyAvatar\n key={u.clientId}\n clientId={u.clientId}\n sprite={u.sprite}\n pictureUrl={u.avatarUrl}\n name={u.name || 'Anon'}\n isReady={readySet.has(u.clientId)}\n isSelf={u.clientId === myClientId}\n onToggle={() => emitReadyCheckToggle(!iAmReady)}\n />\n ))}\n </div>\n </>\n )}\n {showBufferLoading && entries.map(({ client, completed }) => (\n <BufferRing\n key={client.client_id}\n client={client}\n pictureUrl={roster[client.client_id]?.avatarUrl}\n fading={completed}\n onFadeEnd={() => handleFadeEnd(client.client_id)}\n />\n ))}\n {showStartButton && (\n <button\n type=\"button\"\n data-testid=\"start-ready-check\"\n onClick={() => emitReadyCheckStart()}\n className=\"px-2.5 py-1 rounded bg-black/55 text-xs font-semibold tracking-wide hover:bg-black/75 transition-colors\"\n style={{ color: '#fff', pointerEvents: 'auto' }}\n title=\"Ask everyone if they're ready\"\n >\n Ready check\n </button>\n )}\n </div>\n </>\n );\n}\n","/**\n * The global tierlist, as a full-screen popout.\n *\n * A modal rather than a sidebar panel because the board needs room: six rows of\n * cards plus a tray does not fit a 320px column, and the sidebar version was\n * cramped enough to be unpleasant to use. It is also reachable from anywhere and\n * by anyone — no leader gate, no loaded playlist, no room state.\n *\n * Two boards are on screen at once and they are NOT the same thing:\n * - YOURS, editable, held in `tierlistDraft` and sent only when you press\n * Save. Nothing you drag leaves the browser until then.\n * - THE SHARED one, read-only, recomputed by the server from everyone's saved\n * ballots. It updates live when anyone else saves.\n */\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { TIER_ORDER, type TTier, type TTierlistSnapshotOut } from '@watchparty/shared';\nimport { useAppStore } from '../lib/store';\nimport { refreshTierlist, saveTierlist } from '../lib/tierlistApi';\nimport { resolveCardImage } from '../lib/mediaUrls';\nimport { usePersonColor } from '../lib/pictureColor';\nimport AvatarImage from './AvatarImage';\n\nconst TIER_VAR: Record<TTier, string> = {\n S: 'var(--color-tier-s)', A: 'var(--color-tier-a)', B: 'var(--color-tier-b)',\n C: 'var(--color-tier-c)', D: 'var(--color-tier-d)', F: 'var(--color-tier-f)',\n};\n\nconst UNRANKED = 'unranked' as const;\ntype Zone = TTier | typeof UNRANKED;\n\ninterface Entry { id: string; displayName: string; thumbnail?: string }\n\n/**\n * Mark an image as loaded so CSS can fade it in.\n *\n * A ref rather than `onLoad` alone: a cached image can finish decoding before\n * React attaches the handler, and that one never fires — leaving the card\n * permanently at opacity 0. Checking `complete` at attach time covers it.\n */\nfunction markLoaded(el: HTMLImageElement | null): void {\n if (el?.complete && el.naturalWidth > 0) el.dataset.loaded = 'true';\n}\n\n// ── Card ────────────────────────────────────────────────────────────────────\n\nfunction Card({\n entry, draggable, picked, onPick, onDragStart,\n}: {\n entry: Entry; draggable: boolean; picked: boolean;\n onPick?: () => void; onDragStart?: () => void;\n}) {\n return (\n <article\n data-item-id={entry.id}\n draggable={draggable}\n tabIndex={draggable ? 0 : -1}\n role={draggable ? 'button' : undefined}\n aria-label={draggable ? `${entry.displayName} — pick up, then choose a tier` : entry.displayName}\n onClick={onPick}\n onKeyDown={draggable ? e => {\n if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPick?.(); }\n } : undefined}\n onDragStart={e => {\n e.dataTransfer.setData('text/plain', entry.id);\n e.dataTransfer.effectAllowed = 'move';\n onDragStart?.();\n }}\n className={`w-[124px] shrink-0 rounded-md overflow-hidden bg-bg-alt border transition-colors ${\n draggable ? 'cursor-grab active:cursor-grabbing hover:border-text-muted' : ''\n } ${picked ? 'border-primary ring-1 ring-primary' : 'border-border'}`}\n >\n <img\n src={resolveCardImage(entry.id, entry.thumbnail)}\n alt=\"\"\n // eager, not lazy: the board is a handful of cards and they are wanted\n // all at once. Lazy defers the ones below the fold, which is precisely\n // the staggered fill-in this is meant to remove.\n loading=\"eager\"\n decoding=\"async\"\n draggable={false}\n ref={markLoaded}\n onLoad={e => { e.currentTarget.dataset.loaded = 'true'; }}\n onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}\n className=\"tl-card-img w-full aspect-video object-cover bg-bg-card\"\n />\n <div className=\"px-1.5 py-1 text-[11px] leading-tight text-text-secondary line-clamp-2 min-h-[30px]\">\n {entry.displayName}\n </div>\n </article>\n );\n}\n\n// ── Hover detail ────────────────────────────────────────────────────────────\n\nfunction VoterFace({ sprite, url }: { sprite: string | undefined; url: string | undefined }) {\n const color = usePersonColor(sprite, url);\n return (\n <AvatarImage\n sprite={sprite}\n url={url}\n className=\"w-5 h-5 rounded-full shrink-0 object-cover\"\n style={{ background: color }}\n fallback={<span className=\"w-5 h-5 rounded-full shrink-0\" style={{ background: color }} />}\n />\n );\n}\n\nfunction VoterCard({\n anchor, itemId, snap,\n}: { anchor: DOMRect; itemId: string; snap: TTierlistSnapshotOut }) {\n const ref = useRef<HTMLDivElement | null>(null);\n const [pos, setPos] = useState<{ left: number; top: number } | null>(null);\n const entry = snap.placed.find(p => p.mediaId === itemId);\n\n // Measure after paint: the card's height depends on how many people voted.\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const r = el.getBoundingClientRect();\n let left = anchor.left + anchor.width / 2 - r.width / 2;\n left = Math.max(8, Math.min(left, window.innerWidth - r.width - 8));\n let top = anchor.top - r.height - 8;\n if (top < 8) top = anchor.bottom + 8;\n setPos({ left: Math.round(left), top: Math.round(top) });\n }, [anchor, itemId]);\n\n if (!entry) return null;\n const name = snap.items.find(i => i.id === itemId)?.displayName ?? itemId;\n\n return createPortal(\n <div\n ref={ref}\n role=\"tooltip\"\n style={{ position: 'fixed', left: pos?.left ?? -9999, top: pos?.top ?? -9999, visibility: pos ? 'visible' : 'hidden' }}\n className=\"z-[70] w-[220px] pointer-events-none rounded-md border border-border bg-bg-card shadow-lg px-2.5 py-2\"\n >\n <div className=\"text-[11px] font-semibold text-text leading-snug pb-1.5 mb-1.5 border-b border-border\">\n {name}\n </div>\n {Object.entries(entry.votes).map(([voterId, tier]) => {\n return (\n <div key={voterId} className=\"flex items-center gap-2 py-0.5\">\n <VoterFace sprite={snap.sprites[voterId]} url={snap.pictures?.[voterId]} />\n <span className=\"flex-1 text-[11px] text-text-secondary truncate\">\n {snap.names[voterId] ?? 'viewer'}\n </span>\n <span className=\"w-5 h-4 grid place-items-center rounded text-[10px] font-bold shrink-0\"\n style={{ background: TIER_VAR[tier], color: 'var(--color-tier-ink)' }}>\n {tier}\n </span>\n </div>\n );\n })}\n </div>,\n document.body,\n );\n}\n\n// ── Modal ───────────────────────────────────────────────────────────────────\n\nexport default function TierlistModal() {\n const open = useAppStore(s => s.tierlistOpen);\n const setOpen = useAppStore(s => s.setTierlistOpen);\n const snap = useAppStore(s => s.tierlist);\n const draft = useAppStore(s => s.tierlistDraft);\n const saving = useAppStore(s => s.tierlistSaving);\n const move = useAppStore(s => s.moveTierlistItem);\n\n const [picked, setPicked] = useState<string | null>(null);\n const [dragOver, setDragOver] = useState<Zone | null>(null);\n const [hover, setHover] = useState<{ anchor: DOMRect; itemId: string } | null>(null);\n const [savedAt, setSavedAt] = useState<number | null>(null);\n // Persists across close/reopen on purpose — reopening where you left off\n // beats being bounced back, and `showing` below already falls back to 'mine'\n // for anyone who has not voted.\n const [tab, setTab] = useState<'mine' | 'everyone'>('mine');\n\n // Adopt the server's copy of MY board on open — this is one of the two\n // moments the draft is not ahead of the server (the other is a save).\n // Fetch on MOUNT, not on open. Opening used to show six empty rows for as\n // long as the request took and then fill in all at once; the JSON is tiny\n // (item list + placements, no media), so paying for it during room load buys\n // an instant, already-populated board when the button is finally clicked.\n useEffect(() => {\n void refreshTierlist(true);\n }, []);\n\n // Re-sync on each open: someone else may have saved since the mount fetch,\n // and this is one of the two moments the draft is not ahead of the server.\n useEffect(() => {\n if (!open) return;\n void refreshTierlist(true);\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open, setOpen]);\n\n const items = useMemo(() => new Map((snap?.items ?? []).map(i => [i.id, i])), [snap]);\n\n const byZone = useMemo(() => {\n const out: Record<Zone, Entry[]> = { S: [], A: [], B: [], C: [], D: [], F: [], [UNRANKED]: [] };\n const placed = new Set<string>();\n for (const tier of TIER_ORDER) {\n for (const id of draft[tier] ?? []) {\n const it = items.get(id);\n if (!it) continue; // dropped from the config since the save\n out[tier].push(it);\n placed.add(id);\n }\n }\n for (const it of items.values()) if (!placed.has(it.id)) out[UNRANKED].push(it);\n return out;\n }, [draft, items]);\n\n /** Which slot in this row the pointer is over. Measured against the cards in\n * the DOM, skipping the dragged one — otherwise nudging a card one place\n * right lands it back where it started, because its own width shifts every\n * midpoint it passes. */\n const dropIndex = (container: HTMLElement, clientX: number, draggedId: string): number => {\n const cards = [...container.querySelectorAll<HTMLElement>('article[data-item-id]')]\n .filter(el => el.dataset.itemId !== draggedId);\n let i = 0;\n for (const el of cards) {\n const r = el.getBoundingClientRect();\n if (clientX > r.left + r.width / 2) i++;\n }\n return i;\n };\n\n const zone = (z: Zone) => ({\n onDragOver: (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOver(z); },\n onDragLeave: () => setDragOver(cur => (cur === z ? null : cur)),\n onDrop: (e: React.DragEvent) => {\n e.preventDefault(); setDragOver(null);\n const id = e.dataTransfer.getData('text/plain');\n if (id) move(id, z === UNRANKED ? null : z, dropIndex(e.currentTarget as HTMLElement, e.clientX, id));\n },\n onClick: () => { if (picked) { move(picked, z === UNRANKED ? null : z); setPicked(null); } },\n });\n\n const onSave = useCallback(async () => {\n const ok = await saveTierlist(useAppStore.getState().tierlistDraft);\n if (ok) setSavedAt(Date.now());\n }, []);\n\n // \"Saved\" fades on its own — a status that never clears reads as stale.\n useEffect(() => {\n if (savedAt === null) return;\n const t = setTimeout(() => setSavedAt(null), 2500);\n return () => clearTimeout(t);\n }, [savedAt]);\n\n if (!open) return null;\n\n const dirty = JSON.stringify(draft) !== JSON.stringify(snap?.myBoard ?? {});\n\n /**\n * You see the shared board only once you have voted.\n *\n * Read from the SERVER's copy of your board, not the draft — dragging a card\n * is not voting, and gating on the draft would reveal everyone else's answers\n * the moment you touched a single item. It is also why this survives a\n * reload: having voted before is a fact about the stored board, not about\n * this session.\n */\n const hasVoted = Object.values(snap?.myBoard ?? {}).some(r => (r?.length ?? 0) > 0);\n const showing: 'mine' | 'everyone' = hasVoted ? tab : 'mine';\n\n return createPortal(\n <div\n className=\"fixed inset-0 z-[60] bg-black/70 flex items-center justify-center p-4\"\n onClick={e => { if (e.target === e.currentTarget) setOpen(false); }}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Tierlist\"\n className=\"w-full max-w-[1100px] max-h-[92vh] flex flex-col rounded-lg border border-border bg-bg shadow-xl overflow-hidden\"\n >\n {/* Header */}\n <div className=\"flex items-center gap-3 px-4 py-2.5 border-b border-border bg-bg-alt shrink-0\">\n {/* A <span>, so Tailwind's type utilities actually apply here — see\n the .tl-* classes in styles.css for why the buttons cannot use\n them. Sized above the controls so the title reads as the title. */}\n <span className=\"text-[15px] font-semibold text-text leading-none\">Tierlist</span>\n\n {/* Only offered once you have voted — see `hasVoted`. With one tab\n there is nothing to choose between, so the strip is absent\n entirely rather than shown with a single dead option. */}\n {hasVoted && (\n <div className=\"tl-tabs ml-1\" role=\"tablist\" aria-label=\"Tierlist view\">\n {([['mine', 'Mine'], ['everyone', 'Everyone']] as const).map(([id, label]) => (\n <button key={id} role=\"tab\" aria-selected={showing === id} className=\"tl-tab\"\n onClick={() => setTab(id)}>\n {label}\n </button>\n ))}\n </div>\n )}\n\n <div className=\"flex-1\" />\n {savedAt !== null && <span className=\"text-[11px] bg-success-bg text-on-success-bg px-1.5 py-1 rounded leading-none\">Saved</span>}\n <button className=\"tl-save\" disabled={saving || !dirty} onClick={() => { void onSave(); }}>\n {saving ? 'Saving…' : 'Save'}\n </button>\n <button className=\"tl-close\" onClick={() => setOpen(false)} aria-label=\"Close tierlist\">\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto\">\n {showing === 'mine' && <>\n {/* Your board */}\n {TIER_ORDER.map(tier => (\n <div key={tier} className=\"grid grid-cols-[56px_1fr] border-b border-border/50 min-h-[86px]\">\n <div\n className=\"grid place-items-center select-none cursor-pointer\"\n style={{ background: TIER_VAR[tier], color: 'var(--color-tier-ink)' }}\n onClick={() => { if (picked) { move(picked, tier); setPicked(null); } }}\n title={`Move the picked item to ${tier}`}\n >\n <span className=\"text-2xl font-bold leading-none\">{tier}</span>\n </div>\n <div\n {...zone(tier)}\n className={`flex items-center gap-2 p-2 overflow-x-auto transition-colors ${\n dragOver === tier ? 'bg-primary/15 ring-1 ring-inset ring-primary' : 'bg-bg-card'\n }`}\n >\n {byZone[tier].map(e => (\n <Card key={e.id} entry={e} draggable picked={picked === e.id}\n onPick={() => setPicked(c => (c === e.id ? null : e.id))}\n onDragStart={() => setPicked(null)} />\n ))}\n </div>\n </div>\n ))}\n\n {/* Unranked */}\n <div\n {...zone(UNRANKED)}\n className={`flex items-center gap-2 p-2 overflow-x-auto min-h-[96px] border-t border-dashed border-border transition-colors ${\n dragOver === UNRANKED ? 'bg-primary/15 ring-1 ring-inset ring-primary' : 'bg-bg'\n }`}\n >\n {byZone[UNRANKED].map(e => (\n <Card key={e.id} entry={e} draggable picked={picked === e.id}\n onPick={() => setPicked(c => (c === e.id ? null : e.id))}\n onDragStart={() => setPicked(null)} />\n ))}\n </div>\n </>}\n\n {/* The shared board — its own tab, not stacked underneath. */}\n {showing === 'everyone' && snap && (\n <>\n {TIER_ORDER.map(tier => {\n const row = snap.placed.filter(p => p.tier === tier);\n return (\n <div key={tier} className=\"grid grid-cols-[56px_1fr] border-b border-border/50 min-h-[86px]\">\n <div className=\"grid place-items-center select-none\"\n style={{ background: TIER_VAR[tier], color: 'var(--color-tier-ink)' }}>\n <span className=\"text-2xl font-bold leading-none\">{tier}</span>\n </div>\n <div className=\"flex items-stretch gap-2 p-2 overflow-x-auto bg-bg-card\">\n {row.map(p => {\n const it = items.get(p.mediaId);\n return (\n <div\n key={p.mediaId}\n tabIndex={0}\n onMouseEnter={e => setHover({ anchor: e.currentTarget.getBoundingClientRect(), itemId: p.mediaId })}\n onMouseLeave={() => setHover(null)}\n onFocus={e => setHover({ anchor: e.currentTarget.getBoundingClientRect(), itemId: p.mediaId })}\n onBlur={() => setHover(null)}\n aria-label={`${it?.displayName ?? p.mediaId} — score ${p.score.toFixed(2)}. ${\n Object.entries(p.votes).map(([v, t]) => `${snap.names[v] ?? 'viewer'} ${t}`).join(', ')\n }`}\n className=\"w-[124px] shrink-0 rounded-md overflow-hidden bg-bg-alt border border-border hover:border-primary transition-colors\"\n >\n <img src={resolveCardImage(p.mediaId, it?.thumbnail)} alt=\"\"\n loading=\"eager\" decoding=\"async\" ref={markLoaded}\n onLoad={e => { e.currentTarget.dataset.loaded = 'true'; }}\n onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}\n className=\"tl-card-img w-full aspect-video object-cover bg-bg-card\" />\n <div className=\"px-1.5 pt-1 text-[11px] leading-tight text-text-secondary line-clamp-2 min-h-[28px]\">\n {it?.displayName ?? p.mediaId}\n </div>\n <div className=\"px-1.5 pb-1 flex items-center gap-1 text-[10px] tabular-nums text-text-muted\">\n <span>score</span>\n <span className=\"text-text-secondary\">{p.score.toFixed(2)}</span>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n })}\n </>\n )}\n </div>\n </div>\n\n {hover && snap && <VoterCard anchor={hover.anchor} itemId={hover.itemId} snap={snap} />}\n </div>,\n document.body,\n );\n}\n","import React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\n/**\n * Renders its children inline normally, but portals them into the active\n * fullscreen element while fullscreen is engaged.\n *\n * Why: overlays like the ready-check (ClientStatusOverlay) and floating chat\n * live as siblings of the video container in Room.tsx. Fullscreen is requested\n * on the video container itself, so only that element and its descendants are\n * painted — sibling overlays vanish the moment the video goes fullscreen.\n * Portaling the children into the fullscreen element keeps them on screen.\n *\n * The children use absolute positioning; both the inline parent and the\n * fullscreen element (the MediaPlayer root div) are `position: relative`, so\n * the anchoring is correct in either mode.\n */\nexport default function FullscreenPortal({ children }: { children: React.ReactNode }) {\n const [fsEl, setFsEl] = useState<Element | null>(null);\n\n useEffect(() => {\n const sync = () => setFsEl(document.fullscreenElement);\n sync();\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n }, []);\n\n if (fsEl) return createPortal(children, fsEl);\n return <>{children}</>;\n}\n","\ninterface IconProps {\n className?: string;\n}\n\nexport function StarIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} fill=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\" />\n </svg>\n );\n}\n\nexport function PlayIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n );\n}\n\nexport function PauseIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M6 19h4V5H6v14zm8-14v14h4V5h-4z\" />\n </svg>\n );\n}\n\nexport function VolumeMutedIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2\" />\n </svg>\n );\n}\n\nexport function VolumeLowIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.536 8.464a5 5 0 010 7.072\" />\n </svg>\n );\n}\n\nexport function VolumeHighIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.536 8.464a5 5 0 010 7.072M18.364 5.636a9 9 0 010 12.728\" />\n </svg>\n );\n}\n\nexport function FullscreenIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4\" />\n </svg>\n );\n}\n\nexport function ExitFullscreenIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M9 9V4H4m0 0l5 5M9 15v5H4m0 0l5-5M15 9V4h5m0 0l-5 5M15 15v5h5m0 0l-5-5\" />\n </svg>\n );\n}\n\nexport function ChevronLeftIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n );\n}\n\nexport function ChevronRightIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n );\n}\n\nexport function SidebarCloseIcon({ className = 'w-4 h-4' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M4 4v16\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15 12l-5 4v-8l5 4z\" />\n </svg>\n );\n}\n\nexport function SidebarOpenIcon({ className = 'w-4 h-4' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M20 4v16\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M9 12l5 4v-8l-5 4z\" />\n </svg>\n );\n}\n\n/** A cross — stops the running game (RoomCornerControls). */\nexport function CloseIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n );\n}\n\nexport function EjectIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 5l8 11H4z\" />\n <rect x=\"4\" y=\"17\" width=\"16\" height=\"3\" rx=\"0.5\" />\n </svg>\n );\n}\n\n\n/** Film strip — the media catalog. Same glyph the chat toolbar's\n * \"Select Media\" button uses, so both entry points read as one action. */\nexport function FilmIcon({ className = 'w-5 h-5' }: IconProps) {\n return (\n <svg className={className} fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 4h16a1 1 0 011 1v14a1 1 0 01-1 1H4a1 1 0 01-1-1V5a1 1 0 011-1z\" />\n </svg>\n );\n}\n","import { openMediaDrawer } from '../../lib/openMediaDrawer';\nimport { FilmIcon } from './Icons';\n\n/**\n * Leader-only \"Select Media\" for the control bar and the room corner.\n *\n * The primary affordance lives in the chat toolbar (ChatPanel), but that panel\n * is not always on screen: it returns null when `chatCollapsed && mediaId`, and\n * it renders as a SIBLING of the fullscreen element so it stops painting in\n * fullscreen. In either state a leader had NO way to open the media drawer —\n * the exact regression media-reload.spec.ts:111 exists to catch, reappearing\n * through a different route than the original `!mediaInfo` gate.\n *\n * ControlBar (or RoomCornerControls, while a game covers ControlBar) renders\n * this only when the ChatPanel button is unavailable, so\n * exactly ONE element with aria-label=\"Select Media\" exists in the DOM at any\n * time. That matters beyond tidiness: ready-check-fixes.spec.ts drives\n * fullscreen AND locates this button, and a second match would be a Playwright\n * strict-mode violation.\n */\nexport default function SelectMediaButton({ site = 'control bar', className = 'ctr-btn' }: {\n site?: string;\n className?: string;\n}) {\n return (\n <button\n onClick={() => openMediaDrawer(site)}\n className={className}\n title=\"Select Media\"\n aria-label=\"Select Media\"\n >\n <FilmIcon />\n </button>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { debug, warn } from '../lib/debug';\nimport { useIsFullscreen } from '../lib/useIsFullscreen';\nimport { isChatPanelHidden } from '../lib/mediaDrawerAffordance';\n\n/**\n * The Hide/Show chat action, shared by its two sites (ControlBar and\n * RoomCornerControls, chosen by `chatToggleSite`) so they cannot behave\n * differently. \"Show\" while the panel is hidden; in fullscreen the panel\n * cannot paint at all (it is a sibling of the fullscreen element), so Show\n * chat also leaves fullscreen.\n */\nexport function useChatToggle(): { panelHidden: boolean; toggle: () => void } {\n const chatCollapsed = useAppStore(s => s.chatCollapsed);\n const toggleChatCollapsed = useAppStore(s => s.toggleChatCollapsed);\n const hasMedia = useAppStore(s => !!s.snapshot.mediaId);\n const isFullscreen = useIsFullscreen();\n const panelHidden = isChatPanelHidden({ hasMedia, chatCollapsed, isFullscreen });\n\n const toggle = () => {\n debug('chat', panelHidden ? 'Show chat' : 'Hide chat', { isFullscreen, chatCollapsed });\n if (!panelHidden) {\n toggleChatCollapsed();\n return;\n }\n if (chatCollapsed) toggleChatCollapsed();\n if (document.fullscreenElement) {\n document.exitFullscreen().catch((err: unknown) => {\n warn('ui', 'Exit fullscreen failed', { error: (err as Error)?.message });\n });\n }\n };\n return { panelHidden, toggle };\n}\n","import { useChatToggle } from '../../hooks/useChatToggle';\nimport { SidebarCloseIcon, SidebarOpenIcon } from './Icons';\n\n/** The one Hide/Show chat button; where it renders is `chatToggleSite`'s call. */\nexport default function ChatToggleButton({ className }: { className: string }) {\n const { panelHidden, toggle } = useChatToggle();\n const label = panelHidden ? 'Show chat' : 'Hide chat';\n return (\n <button\n type=\"button\"\n onClick={toggle}\n className={className}\n title={label}\n aria-label={label}\n aria-expanded={!panelHidden}\n >\n {panelHidden ? <SidebarOpenIcon className=\"w-5 h-5\" /> : <SidebarCloseIcon className=\"w-5 h-5\" />}\n </button>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { emitGameSelect } from '../lib/socket';\nimport { debug } from '../lib/debug';\nimport { useIsFullscreen } from '../lib/useIsFullscreen';\nimport { chatToggleSite, selectMediaAffordance } from '../lib/mediaDrawerAffordance';\nimport { useActiveGame, useIsLeader, useLeaderAffordance } from '../hooks/useStoreSelectors';\nimport SelectMediaButton from './controls/SelectMediaButton';\nimport ChatToggleButton from './controls/ChatToggleButton';\nimport { CloseIcon } from './controls/Icons';\n\n/**\n * The top-right corner of the room's main area: the controls that must stay\n * reachable whatever covers that area.\n *\n * A running game sits over the player at z-30 and hides its ControlBar, so\n * anything that lived only there was unreachable for the life of the game —\n * which, for a game bundle, had no Stop of its own (cardtable\n * deliberately never calls `ctx.close()`: closing is the host's control).\n * Rendered inside Room's FullscreenPortal, so it also paints in fullscreen.\n *\n * - Select Media — only when `selectMediaAffordance` says 'room-corner'\n * (chat collapsed over media while a game covers ControlBar). Exactly one\n * such button may exist; see lib/mediaDrawerAffordance.ts.\n * - Stop game — leader only; viewers get none. Not in\n * fullscreen: the game is a sibling of the fullscreen element and does not\n * paint, so a Stop there would act on something off screen.\n * - The chat toggle — only while a game covers the control bar, where it\n * normally lives (`chatToggleSite`), so chat hidden during a game can\n * still come back.\n */\nexport default function RoomCornerControls() {\n const chatCollapsed = useAppStore(s => s.chatCollapsed);\n const hasMedia = useAppStore(s => !!s.snapshot.mediaId);\n const isFullscreen = useIsFullscreen();\n const activeGame = useActiveGame();\n const isLeader = useIsLeader();\n const leaderAffordance = useLeaderAffordance();\n\n const showSelectMedia = selectMediaAffordance({\n isLeader: leaderAffordance, hasMedia, chatCollapsed, isFullscreen, gameOverlay: activeGame !== null,\n }) === 'room-corner';\n const showStop = isLeader && activeGame !== null && !isFullscreen;\n const showChatToggle = chatToggleSite({ hasMedia, isFullscreen, gameOverlay: activeGame !== null }) === 'room-corner';\n\n if (!showSelectMedia && !showStop && !showChatToggle) return null;\n\n const stopGame = () => {\n debug('game', 'game_select', { gameId: null, source: 'room corner' });\n emitGameSelect(null);\n };\n\n return (\n <div className=\"absolute top-2 right-2 z-40 flex items-center gap-2\" data-testid=\"room-corner-controls\">\n {showSelectMedia && <SelectMediaButton site=\"room corner\" className=\"corner-btn\" />}\n {showStop && (\n <button type=\"button\" onClick={stopGame} className=\"corner-btn\" title=\"Stop game\" aria-label=\"Stop game\">\n <CloseIcon />\n </button>\n )}\n {showChatToggle && <ChatToggleButton className=\"corner-btn\" />}\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport { listenForAvatarChanges, requestedSpriteOf } from '../lib/avatar';\nimport { sendAvatarChange } from '../lib/socket';\nimport { debug } from '../lib/debug';\n\n/**\n * Carry a settings-popout avatar change into the room live (character and\n * picture), for as long as the room is mounted. Used by both `Room` and the\n * embeddable `RoomChrome`; the listener is removed on unmount, which the\n * chrome's contract with its host requires (e2e/room-chrome.spec.ts).\n */\nexport function useAvatarChanges(): void {\n useEffect(() => listenForAvatarChanges((settings) => {\n const sprite = requestedSpriteOf(settings);\n debug('avatar', 'Avatar changed in the settings popout', { sprite, source: settings.avatar?.source });\n sendAvatarChange(sprite);\n }), []);\n}\n","/**\n * Audio unlock queue.\n *\n * Browsers block autoplay for HTMLMediaElement until the document has sticky\n * user activation — i.e. the user has clicked, tapped, or pressed a key once.\n * After that, subsequent play() calls in the same document succeed without\n * needing a fresh gesture.\n *\n * This module just maintains a one-shot queue of callbacks that should fire\n * on the next user interaction. `seekCoordinator` pushes a retry callback\n * whenever `play()` rejects with NotAllowedError. When the user next\n * interacts anywhere on the document, the queue drains and the retries\n * run — this time inside the user activation, so they succeed.\n *\n * Nothing here touches `audio.muted`, `mediaPrefs.muted`, or creates throwaway\n * AudioContexts. Those were tried in earlier iterations and made things worse\n * (AudioContext resume does NOT grant HTMLAudioElement.play() permission, and\n * auto-muting actively sabotages the browser's Media Engagement Index, which\n * is the mechanism that eventually grants autoplay-with-sound for the origin).\n */\n\nimport { warn, errorFields } from './debug';\n\nconst pending: Array<() => void> = [];\n\n/** Register a callback to run on the next user interaction. Fires once. */\nexport function onNextInteraction(cb: () => void): void {\n pending.push(cb);\n}\n\nfunction drain() {\n const cbs = pending.splice(0);\n for (const cb of cbs) {\n try { cb(); } catch (e: unknown) {\n warn('audio', 'audio-unlock callback threw', errorFields(e));\n }\n }\n}\n\nlet registered = false;\nexport function initAudioUnlock(): () => void {\n if (registered) return () => {};\n registered = true;\n\n // Capture phase so we catch the interaction before child handlers consume it.\n document.addEventListener('click', drain, { capture: true, passive: true });\n document.addEventListener('touchend', drain, { capture: true, passive: true });\n document.addEventListener('keydown', drain, { capture: true, passive: true });\n\n return () => {\n document.removeEventListener('click', drain, { capture: true });\n document.removeEventListener('touchend', drain, { capture: true });\n document.removeEventListener('keydown', drain, { capture: true });\n registered = false;\n };\n}\n","import { getRuntimeServerOrigin } from './config';\nimport { debug, warn, errorFields } from './debug';\nimport { useAppStore } from './store';\nimport type { RoomKeyType } from './leaderAffordance';\n\n/**\n * Ask the server what the room key in the URL is, so the presenter-only\n * toolbar buttons can render before the socket join resolves leadership.\n * See lib/leaderAffordance.ts for why that is worth a request.\n *\n * Deliberately NOT tied to a React effect's AbortController. That mistake is\n * already documented in CLAUDE.md for `loadServerDeliveryFlag`, which rode the\n * health-check effect's controller and silently dropped its result whenever\n * that effect cleaned up first — producing two runs of the same build that\n * disagreed completely. The shape here is the same: an idempotent GET whose\n * only job is to land, with one retry, and whose failure mode is simply the\n * shape guess staying in place until the socket says otherwise.\n */\n\nconst RETRY_DELAY_MS = 400;\n\nfunction parseType(value: unknown): RoomKeyType | null {\n return value === 'leader' || value === 'viewer' ? value : null;\n}\n\nasync function attempt(roomKey: string): Promise<RoomKeyType | null> {\n const origin = getRuntimeServerOrigin();\n const res = await fetch(`${origin}/api/rooms/validate/${encodeURIComponent(roomKey)}`);\n // A 404 is a real answer (bad key) — the room page will surface that on its\n // own. Anything else non-OK is worth one retry.\n if (res.status === 404) return null;\n if (!res.ok) throw new Error(`validate returned ${res.status}`);\n const data: unknown = await res.json();\n return parseType((data as { type?: unknown } | null)?.type);\n}\n\nexport async function probeRoomKeyType(roomKey: string): Promise<void> {\n for (let i = 0; i < 2; i++) {\n try {\n const type = await attempt(roomKey);\n useAppStore.getState().setRoomKeyType({ roomKey, type });\n debug('room', 'Room key type probed', { roomKey, type });\n return;\n } catch (err) {\n if (i === 0) {\n await new Promise(r => setTimeout(r, RETRY_DELAY_MS));\n continue;\n }\n // Both attempts failed: leave the store null so the affordance falls\n // back to socket truth rather than guessing.\n warn('room', 'Room key type probe failed, falling back to socket truth', errorFields(err));\n }\n }\n}\n","import { CORE_EVENTS } from '@watchparty/shared';\nimport { getSocket } from './socket';\nimport { useAppStore } from './store';\n\n// Unified control sender ensuring sequential client_seq for EVERY control (including load/home)\nexport function sendControl(kind: string, extra?: Record<string, unknown>) {\n const st = useAppStore.getState();\n const client_seq = st.allocateClientSeq();\n const payload = { kind, client_seq, ...(extra || {}) };\n getSocket().emit(CORE_EVENTS.CONTROL, payload);\n return client_seq;\n}\n\nexport function sendLoad(mediaId: string) {\n return sendControl('load', { media_id: mediaId });\n}\n\nexport function sendHome() {\n return sendControl('home');\n}\n","import { sendControl } from '../../lib/controls';\nimport { useIsLeader, useReadiness, useReadinessOverride, useSnapshot } from '../../hooks/useStoreSelectors';\nimport { PlayIcon, PauseIcon } from './Icons';\nimport { debug, warn } from '../../lib/debug';\n\nexport default function PlayPauseButton() {\n const snapshot = useSnapshot();\n const readiness = useReadiness();\n const isLeader = useIsLeader();\n const overrideReady = useReadinessOverride();\n const canOverride = isLeader && overrideReady && !readiness.ready;\n const disabled = !readiness.ready && !canOverride;\n\n const handleClick = () => {\n if (!isLeader) {\n warn('controls', 'Play/pause click blocked: not leader');\n return;\n }\n if (disabled) {\n warn('controls', 'Play/pause click blocked: not ready', { readyCount: readiness.readyCount, total: readiness.total });\n return;\n }\n if (canOverride) {\n warn('controls', 'Play/pause: leader overriding stuck readiness gate', { readyCount: readiness.readyCount, total: readiness.total });\n }\n debug('controls', 'Play/pause clicked', { action: snapshot.playing ? 'pause' : 'play' });\n sendControl(snapshot.playing ? 'pause' : 'play');\n };\n\n const getTooltip = () => {\n if (canOverride) return 'Some viewers still loading — play anyway';\n if (disabled) return 'Waiting for sync...';\n if (!isLeader) return 'Only the leader can control playback';\n return snapshot.playing ? 'Pause' : 'Play';\n };\n \n return (\n <button\n data-testid=\"play-pause-button\"\n disabled={disabled || !isLeader}\n onClick={handleClick}\n className=\"ctr-btn\"\n title={getTooltip()}\n data-active={!disabled && isLeader && snapshot.playing ? \"1\" : \"0\"}\n >\n {snapshot.playing ? <PauseIcon /> : <PlayIcon />}\n </button>\n );\n}\n","import { sendHome } from '../../lib/controls';\nimport { debug } from '../../lib/debug';\nimport { EjectIcon } from './Icons';\n\n/**\n * Leader-only unload: clear the room's loaded media and return everyone\n * to the idle starfield/lobby state. Server-side this fires control/home\n * (kind='home') which calls resetMediaState(room, null).\n *\n * Rendered inside the leader cluster in ControlBar — visibility is gated\n * by the parent (only mounted while `isLeader && mediaId`).\n */\nexport default function UnloadButton() {\n function handleClick() {\n debug('controls', 'Unload episode clicked');\n sendHome();\n }\n return (\n <button\n onClick={handleClick}\n className=\"ctr-btn\"\n title=\"Unload episode (return to lobby)\"\n aria-label=\"Unload episode\"\n >\n <EjectIcon />\n </button>\n );\n}\n","/**\n * Whether the seek input should be non-interactive.\n *\n * Non-leaders get a READ-ONLY timeline: they see the playhead position + total\n * duration (so they know how much time is left) but cannot scrub — only the\n * leader drives playback. The leader's bar is interactive once sync-ready (or\n * via the readiness override), and is inert when the duration is still unknown.\n */\nexport function seekDisabled(o: {\n isLeader: boolean;\n ready: boolean;\n canOverride: boolean;\n durationMs: number;\n}): boolean {\n if (!o.isLeader) return true; // viewers: read-only timeline\n if (o.durationMs === 0) return true; // no duration yet → nothing to seek\n return !o.ready && !o.canOverride; // leader: gated until sync-ready\n}\n","/**\n * Pure seek-bar value/progress helpers, shared by SeekBar and unit-tested here.\n *\n * Problem context (cedar-ridge 2026-07-09, \"playback bar does not reset on\n * playlist switch\"): the bar's fill is `displayValue / durationMs`. On a media\n * switch the new clip's playhead is 0, but if a stale `localValue` (a pending\n * seek from the previous clip) or a stale `displayPlayheadMs` survives the\n * switch, the fill is computed from the OLD position over the NEW (often\n * shorter) duration — stranding or overflowing the bar. SeekBar resets those on\n * media change; these helpers isolate the math so both the reset target (fresh\n * state → 0) and the hazard (stale state) are testable.\n */\n\n/**\n * The value (ms) the bar should display: a pending local seek wins; otherwise\n * the live element playhead, falling back to the server snapshot playhead.\n */\nexport function resolveSeekValue(opts: {\n localValue: number | null;\n displayPlayheadMs: number | null | undefined;\n snapshotPlayheadMs: number;\n}): number {\n if (opts.localValue !== null) return opts.localValue;\n return opts.displayPlayheadMs ?? opts.snapshotPlayheadMs;\n}\n\n/** Fill fraction 0..1 for the bar. 0 when duration is unknown (0). Not clamped. */\nexport function seekProgress(valueMs: number, durationMs: number): number {\n if (durationMs <= 0) return 0;\n return valueMs / durationMs;\n}\n","import React, { useRef, useState, useCallback, useEffect } from 'react';\nimport { useAppStore } from '../../lib/store';\nimport { sendControl } from '../../lib/controls';\nimport { CRITICAL_BUFFER_MS } from '@watchparty/shared';\nimport { debug, warn } from '../../lib/debug';\nimport { useIsLeader, useReadinessOverride } from '../../hooks/useStoreSelectors';\nimport { seekDisabled } from './seekState';\nimport { resolveSeekValue, seekProgress } from '../../lib/seekBarValue';\n\ninterface Props {\n durationMs?: number;\n displayPlayheadMs?: number;\n}\n\n// Format milliseconds as HH:MM:SS or MM:SS\nfunction formatTime(ms: number): string {\n const totalSecs = Math.floor(ms / 1000);\n const hours = Math.floor(totalSecs / 3600);\n const mins = Math.floor((totalSecs % 3600) / 60);\n const secs = totalSecs % 60;\n \n if (hours > 0) {\n return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n }\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nexport default function SeekBar({ durationMs: propDurationMs, displayPlayheadMs }: Props) {\n const snapshot = useAppStore(s => s.snapshot);\n const readiness = useAppStore(s => s.readiness);\n const isLeader = useIsLeader();\n // Mirror PlayPauseButton's leader-override: one stuck viewer can otherwise\n // pin the scrub bar disabled even after the leader has overridden play.\n // Was Bug 8 in the 2026-05-20 retrospective — leader could play but never\n // seek until the server was restarted. See useReadinessOverride.\n const overrideReady = useReadinessOverride();\n const canOverride = isLeader && overrideReady && !readiness.ready;\n const ref = useRef<HTMLInputElement>(null);\n const [hoverPosition, setHoverPosition] = useState<number | null>(null);\n const [hoverTimeMs, setHoverTimeMs] = useState<number>(0);\n\n // Local state for dragging (shows immediate feedback without sending seeks)\n const [isDragging, setIsDragging] = useState(false);\n const [localValue, setLocalValue] = useState<number | null>(null);\n const lastSentRef = useRef<number>(0);\n\n // Clear any pending-seek override when the media changes. A leader who scrubbed\n // right before switching clips would otherwise leave localValue holding the old\n // clip's target; the new clip starts at 0 so the \"caught up\" check below never\n // fires and the bar strands at the old value for the full 10s fallback\n // (cedar-ridge 2026-07-09).\n const [prevMediaId, setPrevMediaId] = useState(snapshot.mediaId);\n if (prevMediaId !== snapshot.mediaId) {\n setPrevMediaId(snapshot.mediaId);\n setLocalValue(null);\n setIsDragging(false);\n }\n\n // Clear local override once the actual playhead catches up to the seek target.\n // This prevents the seek bar from snapping back to the old position while buffering.\n const actualPlayheadMs = displayPlayheadMs ?? snapshot.playheadMs;\n if (!isDragging && localValue !== null && Math.abs(actualPlayheadMs - localValue) < CRITICAL_BUFFER_MS) {\n setLocalValue(null);\n }\n\n const durationMs = propDurationMs || 0;\n\n // Show local value while dragging, otherwise show live playhead from video element\n const val = resolveSeekValue({ localValue, displayPlayheadMs, snapshotPlayheadMs: snapshot.playheadMs });\n\n // Seeks are only sent on drag release (mouseup / mouseleave-while-dragging)\n // and on no-drag clicks. We deliberately do not send mid-drag seeks: each\n // control/seek causes every viewer's FragmentManager to abort + restart its\n // buffer fill, so a slow scrub through N intermediate positions cascades\n // into N abort-and-restart cycles room-wide. The localValue override keeps\n // the scrub bar visually responsive during the drag without flooding the\n // room with seek events.\n\n // When the local override was set, for the 10s fallback below.\n const localValueSetAt = useRef<number>(0);\n\n useEffect(() => {\n if (isDragging || localValue === null) return;\n\n // Clear after 10s if playhead never caught up — indicates a seek that didn't\n // complete (buffer gate timeout, fetch failure, or gap). Log so we can trace why.\n const remaining = 10000 - (Date.now() - localValueSetAt.current);\n const timer = setTimeout(() => {\n warn('seek', 'SeekBar local override timed out — seek never completed', {\n target: Math.round(localValue), actual: Math.round(actualPlayheadMs),\n delta: Math.round(Math.abs(actualPlayheadMs - localValue)),\n });\n setLocalValue(null);\n }, Math.max(0, remaining));\n return () => clearTimeout(timer);\n }, [actualPlayheadMs, isDragging, localValue]);\n\n const handleMouseMove = useCallback((e: React.MouseEvent<HTMLInputElement>) => {\n const rect = e.currentTarget.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const ratio = x / rect.width;\n const timeMs = Math.round(ratio * durationMs);\n setHoverPosition(x);\n setHoverTimeMs(Math.max(0, Math.min(timeMs, durationMs)));\n }, [durationMs]);\n\n const handleMouseLeave = useCallback(() => {\n setHoverPosition(null);\n }, []);\n\n return (\n <div className=\"flex items-center gap-2 flex-1\">\n {/* Current time */}\n <span className=\"text-[11px] font-mono text-text-secondary w-[45px] text-right shrink-0\">\n {formatTime(val)}\n </span>\n\n {/* Seek slider with tooltip */}\n <div className=\"flex-1 relative\">\n <input\n type=\"range\"\n min={0}\n max={durationMs}\n value={val}\n disabled={seekDisabled({ isLeader, ready: readiness.ready, canOverride, durationMs })}\n style={{ ['--progress' as string]: `${seekProgress(val, durationMs) * 100}%` }}\n onChange={(e) => {\n const to_ms = Number(e.target.value);\n // Update local value immediately for responsive UI. We deliberately\n // do NOT send a seek here — the room only learns the new position\n // on drag release. See comment above lastSentRef.\n setLocalValue(to_ms);\n localValueSetAt.current = Date.now();\n }}\n onMouseDown={() => {\n setIsDragging(true);\n lastSentRef.current = displayPlayheadMs ?? snapshot.playheadMs;\n }}\n onMouseUp={() => {\n setIsDragging(false);\n // Drag release (or simple click, which also triggers mouseup) — send\n // the final position exactly once. Guard against duplicate sends by\n // comparing against lastSentRef.\n if (localValue !== null && localValue !== lastSentRef.current) {\n debug('seek', 'Final seek on mouseup', { to_ms: localValue });\n sendControl('seek', { to_ms: localValue });\n lastSentRef.current = localValue;\n }\n }}\n onMouseMove={handleMouseMove}\n onMouseLeave={() => {\n handleMouseLeave();\n // If dragging and mouse leaves the element, treat as release.\n if (isDragging && localValue !== null && localValue !== lastSentRef.current) {\n setIsDragging(false);\n debug('seek', 'Final seek on mouseleave-while-dragging', { to_ms: localValue });\n sendControl('seek', { to_ms: localValue });\n lastSentRef.current = localValue;\n }\n }}\n className=\"w-full range-fill\"\n ref={ref}\n aria-label=\"Seek bar\"\n />\n \n {/* Hover tooltip */}\n {hoverPosition !== null && (\n <div\n className=\"absolute bottom-full mb-2 px-2 py-1 bg-bg-card text-white text-xs rounded pointer-events-none transform -translate-x-1/2 whitespace-nowrap\"\n style={{ left: hoverPosition }}\n >\n {formatTime(hoverTimeMs)}\n </div>\n )}\n </div>\n\n {/* Duration */}\n <span className=\"text-[11px] font-mono text-text-muted w-[45px] text-left shrink-0\">\n {formatTime(durationMs)}\n </span>\n </div>\n );\n}\n","import React, { useEffect } from 'react';\nimport { useAppStore } from '../../lib/store';\nimport { VolumeMutedIcon, VolumeLowIcon, VolumeHighIcon } from './Icons';\n\ninterface Props {\n videoRef: React.RefObject<HTMLVideoElement | null>;\n}\n\nexport default function VolumeSlider({ videoRef }: Props) {\n const volume = useAppStore(s => s.mediaPrefs.volume);\n const muted = useAppStore(s => s.mediaPrefs.muted);\n const setVolumeStore = useAppStore(s => s.setVolume);\n const setMutedStore = useAppStore(s => s.setMuted);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) {\n video.volume = volume;\n video.muted = muted;\n }\n }, [volume, muted, videoRef]);\n\n const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const newVolume = parseFloat(e.target.value);\n setVolumeStore(newVolume);\n if (newVolume > 0 && muted) {\n setMutedStore(false);\n }\n };\n\n const toggleMute = () => {\n setMutedStore(!muted);\n };\n\n const volumeIcon = () => {\n if (muted || volume === 0) return <VolumeMutedIcon />;\n if (volume < 0.5) return <VolumeLowIcon />;\n return <VolumeHighIcon />;\n };\n\n const currentVolume = muted ? 0 : volume;\n const volumePercent = Math.round(currentVolume * 100);\n\n return (\n <div className=\"flex items-center gap-2\">\n <button\n onClick={toggleMute}\n className=\"hover:opacity-80 transition-opacity\"\n title={muted ? 'Unmute' : 'Mute'}\n aria-label={muted ? 'Unmute' : 'Mute'}\n >\n {volumeIcon()}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.05\"\n value={currentVolume}\n onChange={handleVolumeChange}\n style={{ ['--progress' as string]: `${volumePercent}%` }}\n className=\"range-fill volume-slider\"\n title={`Volume: ${volumePercent}%`}\n aria-label=\"Volume\"\n />\n <span className=\"text-[10px] opacity-60 w-8 text-right\">\n {volumePercent}%\n </span>\n </div>\n );\n}\n","\nexport interface AudioTrack {\n index: number;\n lang: string;\n label: string;\n url: string;\n}\n\ninterface Props {\n tracks: AudioTrack[];\n selectedIndex: number;\n onSelect: (index: number) => void;\n}\n\nexport default function AudioTrackSelector({\n tracks,\n selectedIndex,\n onSelect,\n}: Props) {\n if (tracks.length === 0) {\n return null;\n }\n\n const single = tracks.length === 1;\n\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[10px] opacity-60 uppercase tracking-wide\">Audio</span>\n <select\n value={selectedIndex}\n onChange={(e) => onSelect(Number(e.target.value))}\n disabled={single}\n className={`text-xs rounded px-2 py-1 ${single ? 'opacity-60 cursor-default' : 'cursor-pointer'}`}\n title={single ? tracks[0].label : 'Select audio track'}\n aria-label=\"Audio track selection\"\n >\n {tracks.map((track) => (\n <option key={track.index} value={track.index}>\n {track.label}\n </option>\n ))}\n </select>\n </div>\n );\n}\n","\nexport interface SubtitleTrack {\n index: number;\n lang: string;\n label: string;\n url: string;\n}\n\ninterface Props {\n tracks: SubtitleTrack[];\n selectedIndex: number; // -1 means off\n onSelect: (index: number) => void;\n enabled: boolean;\n onToggle: () => void;\n}\n\nexport default function SubtitleSelector({ \n tracks, \n selectedIndex, \n onSelect, \n enabled, \n onToggle \n}: Props) {\n if (tracks.length === 0) {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2\">\n {/* CC Toggle Button */}\n <button\n onClick={onToggle}\n className={`text-xs px-2 py-1 rounded transition-colors ${\n enabled \n ? 'bg-success text-black font-semibold'\n : 'bg-bg-alt text-text-secondary hover:bg-bg-card'\n }`}\n title={enabled ? 'Disable subtitles' : 'Enable subtitles'}\n aria-label=\"Toggle subtitles\"\n >\n CC\n </button>\n\n {/* Language Dropdown — always visible when tracks exist, includes Off option */}\n <select\n value={enabled ? selectedIndex : -1}\n onChange={(e) => {\n const val = Number(e.target.value);\n if (val === -1) { if (enabled) onToggle(); }\n else { if (!enabled) onToggle(); onSelect(val); }\n }}\n className=\"text-xs rounded px-2 py-1 cursor-pointer\"\n title=\"Select subtitle language\"\n aria-label=\"Subtitle language selection\"\n >\n <option value={-1}>Off</option>\n {tracks.map((track) => (\n <option key={track.index} value={track.index}>\n {track.label}\n </option>\n ))}\n </select>\n </div>\n );\n}\n","import type { SubtitleStyle } from '../SubtitleOverlay';\n\ninterface Props {\n style: SubtitleStyle;\n onStyleChange: (style: SubtitleStyle) => void;\n enabled: boolean;\n}\n\nconst STYLE_OPTIONS: { value: SubtitleStyle; label: string }[] = [\n { value: 'classic', label: 'Classic' },\n { value: 'cinema', label: 'Cinema' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'contrast', label: 'High Contrast' },\n];\n\nexport default function SubtitleStyleSelector({ style, onStyleChange, enabled }: Props) {\n if (!enabled) {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[10px] opacity-60 uppercase tracking-wide\">Style</span>\n <select\n value={style}\n onChange={(e) => onStyleChange(e.target.value as SubtitleStyle)}\n className=\"text-xs rounded px-2 py-1 cursor-pointer\"\n title=\"Select subtitle style\"\n aria-label=\"Subtitle style selection\"\n >\n {STYLE_OPTIONS.map(opt => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </div>\n );\n}\n","import React, { useState, useEffect } from 'react';\nimport { FullscreenIcon, ExitFullscreenIcon } from './Icons';\nimport { warn } from '../../lib/debug';\n\ninterface Props {\n videoRef: React.RefObject<HTMLVideoElement | null>;\n}\n\nexport default function FullscreenButton({ videoRef }: Props) {\n const [isFullscreen, setIsFullscreen] = useState(false);\n\n useEffect(() => {\n const handleFullscreenChange = () => {\n setIsFullscreen(!!document.fullscreenElement);\n };\n\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);\n }, []);\n\n const toggleFullscreen = async () => {\n const video = videoRef.current;\n if (!video) return;\n\n try {\n if (!document.fullscreenElement) {\n // Request fullscreen on the video's parent container\n const container = video.parentElement;\n if (container?.requestFullscreen) {\n await container.requestFullscreen();\n }\n } else {\n // Exit fullscreen\n if (document.exitFullscreen) {\n await document.exitFullscreen();\n }\n }\n } catch (err) {\n warn('ui', 'Fullscreen toggle failed', { error: (err as Error)?.message });\n }\n };\n\n return (\n <button\n onClick={() => { void toggleFullscreen(); }}\n className=\"ctr-btn\"\n title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}\n >\n {isFullscreen ? <ExitFullscreenIcon /> : <FullscreenIcon />}\n </button>\n );\n}\n","import React, { useRef, useState, useEffect, useCallback } from 'react';\nimport { useIsLeader } from '../../hooks/useStoreSelectors';\nimport { Icon } from '@wolffm/themes';\nimport ChatAttachmentStrip from '../ChatAttachmentStrip';\nimport MemePicker from '../../lib/chatMedia/react/MemePicker';\nimport { useChatImageAttachments } from '../../hooks/useChatImageAttachments';\nimport PlayPauseButton from './PlayPauseButton';\nimport UnloadButton from './UnloadButton';\nimport SeekBar from './SeekBar';\nimport VolumeSlider from './VolumeSlider';\nimport AudioTrackSelector, { type AudioTrack } from './AudioTrackSelector';\nimport SubtitleSelector, { type SubtitleTrack } from './SubtitleSelector';\nimport SubtitleStyleSelector from './SubtitleStyleSelector';\nimport FullscreenButton from './FullscreenButton';\nimport SelectMediaButton from './SelectMediaButton';\nimport ChatToggleButton from './ChatToggleButton';\nimport { useIsFullscreen } from '../../lib/useIsFullscreen';\nimport { useActiveGame, useLeaderAffordance } from '../../hooks/useStoreSelectors';\nimport { chatToggleSite, selectMediaAffordance } from '../../lib/mediaDrawerAffordance';\n// No overlay-toggle button: the chat overlay is hardcoded on for everyone.\n// (The unrendered OverlayToggleButton component was removed 2026-08-13; it\n// lives in git history if the toggle is ever re-enabled.)\nimport type { SubtitleStyle } from '../SubtitleOverlay';\nimport { ChevronLeftIcon, ChevronRightIcon } from './Icons';\nimport { DEFAULT_CHAT_MAX_LENGTH, DEFAULT_CHAT_COOLDOWN_MS } from '@watchparty/shared';\nimport { useAppStore } from '../../lib/store';\nimport { sendChat } from '../../lib/socket';\n\ninterface Props {\n videoRef: React.RefObject<HTMLVideoElement | null>;\n mediaId: string | null;\n durationMs?: number;\n visible?: boolean; // Parent-controlled visibility\n // Track state passed from parent\n audioTracks: AudioTrack[];\n selectedAudioIndex: number;\n onAudioSelect: (index: number) => void;\n subtitleTracks: SubtitleTrack[];\n selectedSubtitleIndex: number;\n onSubtitleSelect: (index: number) => void;\n subtitlesEnabled: boolean;\n onSubtitlesToggle: () => void;\n subtitleStyle: SubtitleStyle;\n onSubtitleStyleChange: (style: SubtitleStyle) => void;\n displayPlayheadMs?: number;\n onChatActivity?: () => void;\n /** True while one of the bar's native <select>s has focus. Its popup\n * swallows mousemove, so without a hold the idle timer fades the bar\n * out from under a menu that is still open. */\n onMenuHeld?: (held: boolean) => void;\n}\n\nexport default function ControlBar({\n videoRef,\n mediaId,\n durationMs,\n visible: parentVisible,\n audioTracks,\n selectedAudioIndex,\n onAudioSelect,\n subtitleTracks,\n selectedSubtitleIndex,\n onSubtitleSelect,\n subtitlesEnabled,\n onSubtitlesToggle,\n subtitleStyle,\n onSubtitleStyleChange,\n displayPlayheadMs,\n onChatActivity,\n onMenuHeld,\n}: Props) {\n const isLeader = useIsLeader();\n // The chat toolbar owns the primary \"Select Media\" button, but ChatPanel is\n // absent when collapsed-with-media and unpainted in fullscreen (it is a\n // sibling of the fullscreen element). Mirror those two states so the leader\n // always has exactly one reachable way into the media drawer — unless a game\n // covers this bar, when RoomCornerControls carries it instead.\n const chatCollapsed = useAppStore(s => s.chatCollapsed);\n const isFullscreen = useIsFullscreen();\n // ControlBar only mounts inside MediaPlayer, so media is loaded by definition.\n // Must be the SAME leader-ness ChatPanel feeds the rule, or the\n // exactly-one-Select-Media invariant can render zero or two.\n const leaderAffordance = useLeaderAffordance();\n const gameOverlay = useActiveGame() !== null;\n const showSelectMedia = selectMediaAffordance({\n isLeader: leaderAffordance, hasMedia: true, chatCollapsed, isFullscreen,\n gameOverlay,\n }) === 'control-bar';\n // ControlBar renders only with media loaded (it returns null without).\n const showChatToggle = chatToggleSite({ hasMedia: true, isFullscreen, gameOverlay }) === 'control-bar';\n const floatingChatEnabled = useAppStore(s => s.floatingChatEnabled);\n const scrollRef = useRef<HTMLDivElement>(null);\n const chatInputRef = useRef<HTMLInputElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n const [chatText, setChatText] = useState('');\n const [chatCooldown, setChatCooldown] = useState(false);\n // Same hook as the sidebar ChatInput — paste/drop must behave identically in\n // both, or the one nobody tested silently loses the feature.\n const attach = useChatImageAttachments();\n const [memeOpen, setMemeOpen] = useState(false);\n const [memeUnavailable, setMemeUnavailable] = useState(false);\n\n const handleChatKeyDown = useCallback(async (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key !== 'Enter') return;\n const trimmed = chatText.trim();\n // Empty text is a real message when images are attached.\n if ((!trimmed && !attach.pending.length) || chatCooldown || attach.uploading) return;\n // Upload before clearing: a failed upload must not cost the user their\n // text or their attachments.\n const images = await attach.uploadPending();\n if (images === null) return;\n sendChat(trimmed, images);\n setChatText('');\n attach.clear();\n setChatCooldown(true);\n setTimeout(() => setChatCooldown(false), DEFAULT_CHAT_COOLDOWN_MS);\n // Only refocus if watchparty is the active tab. Guards against\n // late focus() pulling attention back after the user alt-tabs.\n if (!document.hidden && document.hasFocus()) {\n chatInputRef.current?.focus();\n }\n }, [chatText, chatCooldown, attach]);\n\n // A select removed while focused (media unloaded) fires no blur, which\n // would leave the controls held visible for good.\n useEffect(() => () => onMenuHeld?.(false), [mediaId, onMenuHeld]);\n\n // Use parent visibility if provided, otherwise default to showing\n const visible = parentVisible ?? true;\n\n // Check scroll state\n const updateScrollState = useCallback(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n setCanScrollLeft(scrollLeft > 5);\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 5);\n }, []);\n\n // Update scroll state on scroll, resize, and when visible\n useEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n // Initial check\n updateScrollState();\n\n // Listen for scroll\n el.addEventListener('scroll', updateScrollState);\n\n // Listen for resize\n const resizeObserver = new ResizeObserver(updateScrollState);\n resizeObserver.observe(el);\n\n return () => {\n el.removeEventListener('scroll', updateScrollState);\n resizeObserver.disconnect();\n };\n }, [updateScrollState, visible]);\n\n // Scroll by a fixed amount\n const scrollBy = useCallback((direction: 'left' | 'right') => {\n const el = scrollRef.current;\n if (!el) return;\n const amount = direction === 'left' ? -200 : 200;\n el.scrollBy({ left: amount, behavior: 'smooth' });\n }, []);\n\n if (!mediaId) return null;\n\n\n // Build class names\n const barClasses = [\n 'ctr-bar',\n visible && 'show',\n ].filter(Boolean).join(' ');\n\n const showScrollButtons = canScrollLeft || canScrollRight;\n\n return (\n <div\n className={`ctr-bar-wrapper ${visible ? 'show' : ''}`}\n onFocus={(e) => { if (e.target instanceof HTMLSelectElement) onMenuHeld?.(true); }}\n onBlur={(e) => { if (e.target instanceof HTMLSelectElement) onMenuHeld?.(false); }}\n // A made choice ends the interaction: drop focus so the hold releases\n // and the bar fades on its normal timer.\n onChange={(e) => { if (e.target instanceof HTMLSelectElement) e.target.blur(); }}\n >\n <div ref={scrollRef} className={barClasses}>\n {/* Leader controls cluster */}\n {isLeader && (\n <div className=\"cluster\">\n <PlayPauseButton />\n {showSelectMedia && <SelectMediaButton />}\n <UnloadButton />\n </div>\n )}\n\n {/* Seek bar cluster — flexible width, min 25vw. Shown to EVERYONE so\n viewers can see the timeline + time remaining; SeekBar itself is\n read-only for non-leaders (only the leader can scrub). */}\n <div className=\"cluster-seek\">\n <SeekBar durationMs={durationMs} displayPlayheadMs={displayPlayheadMs} />\n </div>\n\n {/* Volume + Audio track selector grouped together */}\n <div className=\"cluster\">\n <VolumeSlider videoRef={videoRef} />\n {audioTracks.length > 1 && (\n <>\n <div className=\"w-px h-5 bg-white/20\" /> {/* Divider */}\n <AudioTrackSelector\n tracks={audioTracks}\n selectedIndex={selectedAudioIndex}\n onSelect={onAudioSelect}\n />\n </>\n )}\n </div>\n\n {/* Subtitle controls */}\n {subtitleTracks.length > 0 && (\n <div className=\"cluster\">\n <SubtitleSelector\n tracks={subtitleTracks}\n selectedIndex={selectedSubtitleIndex}\n onSelect={onSubtitleSelect}\n enabled={subtitlesEnabled}\n onToggle={onSubtitlesToggle}\n />\n <SubtitleStyleSelector\n style={subtitleStyle}\n onStyleChange={onSubtitleStyleChange}\n enabled={subtitlesEnabled}\n />\n </div>\n )}\n\n {/* Inline chat input when no chat UI is visible */}\n {chatCollapsed && floatingChatEnabled && (\n <div\n className=\"cluster px-2 relative\"\n onPaste={attach.onPaste}\n onDrop={attach.onDrop}\n onDragOver={attach.onDragOver}\n >\n <ChatAttachmentStrip pending={attach.pending} uploading={attach.uploading} onRemove={attach.remove} />\n {/* Same picker as the sidebar input. Both composers get every\n attachment path or the two silently diverge — the reason\n `useChatImageAttachments` and the strip are shared already. */}\n <MemePicker\n open={memeOpen}\n onClose={() => setMemeOpen(false)}\n onPick={attach.addMeme}\n onUnavailable={() => { setMemeUnavailable(true); setMemeOpen(false); }}\n />\n <input\n ref={chatInputRef}\n type=\"text\"\n value={chatText}\n onChange={(e) => { setChatText(e.target.value.slice(0, DEFAULT_CHAT_MAX_LENGTH)); onChatActivity?.(); }}\n onKeyDown={(e) => { void handleChatKeyDown(e); }}\n placeholder=\"Message...\"\n className=\"bg-transparent border-none outline-none text-sm max-w-[240px] w-48 h-7 px-2\"\n style={{ color: 'var(--color-chip-text)' }}\n />\n {!memeUnavailable && (\n <button\n type=\"button\"\n onClick={() => setMemeOpen((v) => !v)}\n aria-label=\"Add a meme\"\n aria-expanded={memeOpen}\n title=\"Add a meme\"\n data-testid=\"meme-button-inline\"\n className={`shrink-0 px-1 ${memeOpen ? 'text-text' : 'text-text-secondary'} hover:text-text`}\n >\n <Icon name=\"image\" />\n </button>\n )}\n </div>\n )}\n\n {/* Chat toggle + fullscreen. The toggle lives here so it fades with the\n controls; a game covering the bar moves it to the room corner\n (chatToggleSite). */}\n <div className=\"cluster cluster-tight\">\n {showChatToggle && <ChatToggleButton className=\"ctr-btn\" />}\n <FullscreenButton videoRef={videoRef} />\n </div>\n </div>\n\n {/* Scroll arrows - persistent group on right side */}\n {showScrollButtons && (\n <div className=\"ctr-scroll-group\">\n <button\n className={`ctr-scroll-btn ${canScrollLeft ? 'visible' : ''}`}\n onClick={() => scrollBy('left')}\n aria-label=\"Scroll left\"\n >\n <ChevronLeftIcon />\n </button>\n <button\n className={`ctr-scroll-btn ${canScrollRight ? 'visible' : ''}`}\n onClick={() => scrollBy('right')}\n aria-label=\"Scroll right\"\n >\n <ChevronRightIcon />\n </button>\n </div>\n )}\n </div>\n );\n}\n","/**\n * Background play: the rules behind `useBackgroundPlay`.\n *\n * A phone with its screen off, or with Chrome in the background, is still in\n * the room. Three things decide whether it keeps playing:\n *\n * 1. **The browser may pause a hidden video.** Nothing in the sync model\n * re-asserts play afterwards: `usePlaybackSync` only acts when the ROOM's\n * play state changes, and `useStallRecovery` reads any element pause as\n * \"paused on purpose\" and disarms its watchdog. So a viewer the OS paused\n * stayed silent until they came back, with every metric reading healthy.\n * `shouldKeepAlive` is the rule for re-issuing play.\n * 2. **The lock screen has its own play/pause.** The Media Session API puts\n * them in the notification. A leader's press is a room control, exactly\n * like the play button. A viewer cannot control the room, so their pause is\n * a LOCAL silence that ends when they press play or return to the page\n * (`mediaSessionAction`). Ending it on return matters: a viewer has no\n * in-app play button, so a pause that outlived the lock screen would strand\n * them.\n * 3. **Coming back must land on the room's position.** If the OS suspended\n * audio for minutes, the element is behind. `resyncPlan` decides between a\n * plain resume and a seek. This is a one-shot on an EVENT (the page\n * becoming visible), not a drift timer — see \"DO NOT add drift correction\n * timers\" in AGENTS.md.\n *\n * Our OWN pauses must never be undone: a room pause (room not playing) and a\n * coordinator seek (`seekInFlight`, set before it pauses) both rule\n * keep-alive out.\n */\n\n/** A lock-screen pause within this long of the element's pause event is the user's, not the OS's. */\nexport const USER_PAUSE_WINDOW_MS = 1000;\n\nexport interface KeepAliveInput {\n hidden: boolean;\n roomPlaying: boolean;\n seekInFlight: boolean;\n ended: boolean;\n /** A lock-screen pause is holding playback (viewer local silence). */\n localPause: boolean;\n /** ms since the last lock-screen pause press; Infinity when none. */\n sinceUserPauseMs: number;\n}\n\n/** Should a pause event on a hidden page be answered with play()? */\nexport function shouldKeepAlive(i: KeepAliveInput): boolean {\n return i.hidden\n && i.roomPlaying\n && !i.seekInFlight\n && !i.ended\n && !i.localPause\n && i.sinceUserPauseMs >= USER_PAUSE_WINDOW_MS;\n}\n\ntype MediaSessionResult =\n | { kind: 'control'; control: 'play' | 'pause' }\n | { kind: 'local-pause' }\n | { kind: 'resync' }\n | { kind: 'none' };\n\n/** What a lock-screen play or pause press does. */\nexport function mediaSessionAction(\n action: 'play' | 'pause',\n s: { isLeader: boolean; roomPlaying: boolean },\n): MediaSessionResult {\n if (s.isLeader) {\n // A press that matches the room's state is a no-op, not a second control.\n if (action === 'play' && s.roomPlaying) return { kind: 'resync' };\n if (action === 'pause' && !s.roomPlaying) return { kind: 'none' };\n return { kind: 'control', control: action };\n }\n if (action === 'pause') return { kind: 'local-pause' };\n return s.roomPlaying ? { kind: 'resync' } : { kind: 'none' };\n}\n\ntype ResyncPlan = { kind: 'none' } | { kind: 'resume' } | { kind: 'seek'; targetMs: number };\n\n/**\n * How far the element may be from the room before a return to the page seeks.\n *\n * NOT the seek effect's 80ms (`DEFAULT_DRIFT_SOFT_MS`). That one judges a\n * position the server just sent. Here the room position is an ESTIMATE, the\n * last snapshot plus wall-clock time since it arrived, and its own error\n * (the snapshot's network delay, timer jitter in a hidden tab) is routinely\n * past 80ms. A video that kept playing while hidden runs at the room's 1x, so\n * an 80ms tolerance mostly seeked on the estimate's noise. On Windows Chrome\n * reports an occluded window as hidden, so every alt-tab back to the room cost\n * a 120-315ms seek stall: four in two minutes for one viewer (cedar-ridge,\n * 2026-10-31, `SEEK-DIGEST [bg-resync:visible]`). A second still catches what\n * this exists for: a hidden tab the browser paused or starved for longer.\n */\nexport const BG_RESYNC_TOLERANCE_MS = 1000;\n\n/**\n * On returning to the page (or a lock-screen play), bring the element back\n * to the room: a seek past `driftToleranceMs` (`BG_RESYNC_TOLERANCE_MS`), else\n * a resume if the element is paused.\n */\nexport function resyncPlan(s: {\n roomPlaying: boolean;\n seekInFlight: boolean;\n elementPaused: boolean;\n elementMs: number;\n roomMs: number;\n durationMs: number;\n driftToleranceMs: number;\n}): ResyncPlan {\n if (!s.roomPlaying || s.seekInFlight) return { kind: 'none' };\n const target = s.durationMs > 0 ? Math.min(s.roomMs, s.durationMs) : s.roomMs;\n if (Math.abs(target - s.elementMs) > s.driftToleranceMs) return { kind: 'seek', targetMs: Math.round(target) };\n return s.elementPaused ? { kind: 'resume' } : { kind: 'none' };\n}\n\n/** The room's playhead right now, as the server's stopwatch computes it. */\nexport function roomPlayheadNow(snapshot: { playheadMs: number; playing: boolean }, receivedAt: number, now: number): number {\n return snapshot.playheadMs + (snapshot.playing ? Math.max(0, now - receivedAt) : 0);\n}\n\n/**\n * Human title for a mediaId: the last two path segments,\n * `anime/Frieren/frieren-s01e01` -> `Frieren frieren-s01e01`. Shared by the\n * on-screen title and the lock-screen notification so they cannot disagree.\n */\nexport function mediaDisplayName(mediaId: string): string {\n const parts = mediaId.split('/').filter(Boolean);\n if (parts.length >= 2) return parts.slice(-2).join(' ');\n return parts[parts.length - 1] || mediaId;\n}\n\n/**\n * Where a leaf's poster may be, best first. Playlist clips carry their own\n * (`<leaf>/poster.jpg`); anime episodes share the series' (`<series>/poster.jpg`,\n * the parent dir). Measured on the library 2026-10-19: 76 of 76 playlist leaves\n * have a leaf poster, 0 of 240 anime leaves do, and 14 anime series have one.\n */\nexport function artworkCandidates(mediaId: string): string[] {\n const parts = mediaId.split('/').filter(Boolean);\n const out = [`${parts.join('/')}/poster.jpg`];\n if (parts.length >= 2) out.push(`${parts.slice(0, -1).join('/')}/poster.jpg`);\n return out;\n}\n","import { mediaDisplayName } from '../lib/backgroundPlay';\n\ninterface Props {\n mediaId: string | null;\n visible: boolean;\n}\n\nexport default function MediaTitle({ mediaId, visible }: Props) {\n if (!mediaId) return null;\n\n return (\n <div className={`media-title ${visible ? 'show' : ''}`}>\n {mediaDisplayName(mediaId)}\n </div>\n );\n}\n","/**\n * Pure utility functions and types extracted from FragmentManager.\n * None of these touch pendingFetchIndices, appendQueue, or SourceBuffer mutation.\n */\n\nexport interface Fragment {\n start_ms: number;\n end_ms: number;\n start_byte: number;\n end_byte: number;\n keyframe?: boolean;\n}\n\nexport type FragmentState = 'pending' | 'loading' | 'loaded' | 'error';\nexport interface FetchJob { index: number; abortController: AbortController; promise: Promise<void> }\nexport interface AppendWaiter { index: number; resolve: () => void; reject: (err: Error) => void }\n\nexport interface TargetResolution {\n status: 'buffered' | 'loading' | 'nearby-pending' | 'far';\n fragmentIndex: number;\n existingPromise?: Promise<void>;\n}\n\nexport const MAX_RETRIES = 3;\nconst RETRY_DELAY_MS = 1000;\nconst RETRY_CAP_MS = 15_000;\n\n/**\n * SourceBuffer time-tolerance values (in seconds).\n *\n * **Why these exist:** fMP4 container overhead can push `buffered.start(0)` up\n * to ~0.6s past the fragment's nominal start time. Late-IDR fragments compound\n * this — MSE buffers from the IDR's presentation time, not from\n * `baseMediaDecodeTime`, so a fragment spanning 22.0–24.0s with an IDR at ~23.9s\n * appears as `[23.36, 24.15]` in `sb.buffered`.\n *\n * **Invariant:** `EQUAL` is the smallest meaningful comparison delta. `POINT`\n * and `RANGE` must be ≥ 0.08 — anything smaller false-negatives on the\n * `buffered.start(0)` offset and makes the buffer gate spin for 20s before\n * timing out, which inflates the server's stopwatch and causes viewer drift.\n *\n * - `EQUAL` 0.001 — float-equality slop for `sb.buffered.start(i) ≈ expected.start`.\n * - `POINT` 0.05 — \"point t falls inside this range\" (used by findContainingRangeIndex).\n * - `RANGE` 0.1 — \"this fragment's range overlaps this buffered range\" (fragmentOverlapsSB, isBuffered, getBufferedAheadMs).\n * - `FALLBACK` 1.0 — getBufferedEnd \"nearest range starting within Xs ahead\" fallback when no containing range is found.\n * - `CONTAINER` 0.6 — observed maximum fMP4 container-overhead drift for buffered.start(0). Documented invariant; not currently used as a runtime constant.\n */\n/**\n * Buffered-range comparison tolerances, in seconds. **DO NOT use 0.**\n *\n * fMP4 container overhead means `buffered.start(0)` is ~0.08s, not 0.0 — so an\n * exact comparison false-negatives on a fragment that is fully buffered.\n * Concretely, `isBuffered(0)` with a tolerance under 0.08 returns false even\n * when fragment #0 is present, which makes the pre-play buffer gate spin for\n * its full 20s timeout. That delays playback start AND inflates the server's\n * playhead stopwatch relative to the leader's actual video position — the\n * observed result was 20-98s of viewer drift, which reads as a sync bug rather\n * than a comparison bug.\n *\n * - RANGE (0.1) `isBuffered()` — buffer gate, stall recovery, waitForBuffered\n * - POINT (0.05) `getBufferedEnd()` — plus a nearest-range-ahead fallback\n * - FALLBACK (1.0) how far ahead that fallback will look\n * - CONTAINER (0.6) the container-overhead allowance itself\n */\nexport const SB_TOLERANCE = {\n EQUAL: 0.001,\n POINT: 0.05,\n RANGE: 0.1,\n FALLBACK: 1.0,\n CONTAINER: 0.6,\n} as const;\n\n/** Gap-skip edge threshold (seconds). `findNextRangeStart` treats the playhead\n * as sitting on a gap's lip — and is therefore eligible to skip to the next\n * buffered range — only when it has at most this much real data ahead of it in\n * its containing range. Above this, the playhead is comfortably mid-range\n * (a normal decode hiccup, not a structural gap) and must not be jumped. */\nexport const GAP_SKIP_EDGE_SEC = 0.5;\n\n/** Gap-skip window (ms): the max gap `findNextRangeStart` will jump over to the\n * next buffered range when stalled at a range edge. MUST exceed one fragment\n * duration — a single missing fragment is the common unfillable gap, and at\n * 24000-timescale / 23.976fps a fragment is **2002ms**, not 2000ms. The old\n * hard-coded 2000 was *narrower than one fragment*, so even after the\n * trailing-edge fix the gap (#300.end 602.602 → #302.start 604.604 = 2002ms)\n * measured 2002 > 2000 and was never skipped — the second half of the\n * 2026-06-22 ember-cove freeze. 3000 clears a single fragment (any frame rate)\n * with margin while staying below a 2-fragment (~4s) hole, which is rarer and\n * falls through to re-fetch / the stuck-reload watchdog instead. */\nexport const GAP_SKIP_MAX_MS = 3000;\n/** Tuning for {@link getRetryDelay}. All fields optional; omitted ones fall back\n * to the FragmentManager defaults (1s base, 15s cap, 25% jitter). */\ninterface RetryDelayOptions {\n /** First-attempt delay before exponential growth (ms). */\n baseMs?: number;\n /** Hard ceiling on the computed delay (ms). */\n capMs?: number;\n /** Jitter fraction [0..1] of the delay added randomly. Pass 0 for deterministic delays. */\n jitter?: number;\n}\n\n/**\n * Exponential backoff with optional jitter for retry delays. Prevents\n * thundering herd. `attempt` is 1-based: attempt 1 → ~baseMs, attempt 2 →\n * ~2×baseMs, etc., capped at `capMs`.\n */\nexport function getRetryDelay(attempt: number, opts: RetryDelayOptions = {}): number {\n const { baseMs = RETRY_DELAY_MS, capMs = RETRY_CAP_MS, jitter = 0.25 } = opts;\n const base = Math.min(baseMs * Math.pow(2, attempt - 1), capMs);\n const jitterMs = jitter > 0 ? Math.round(base * Math.random() * jitter) : 0;\n return base + jitterMs;\n}\nexport const NEARBY_FRAGMENT_THRESHOLD = 15;\n/** How long a seek's target stays protected from our own eviction. A seek that\n * is abandoned outright (promise rejected, media switched under it) never\n * reaches its target, and without a bound its region would be shielded for the\n * life of the media. Sits just above waitForAppend's own 30s ceiling so a seek\n * that is merely slow is never stripped of protection while still trying. */\nexport const PENDING_SEEK_PROTECT_MS = 35_000;\n/** How close the playhead must get before a seek counts as landed and its\n * target stops being protected. One fragment (~2s) is comfortably more than the\n * sub-second overshoot MSE introduces, and comfortably less than any gap worth\n * evicting. */\nexport const SEEK_LANDED_TOLERANCE_MS = 2_000;\nexport const MAX_DOWNLOAD_SAMPLES = 10;\n\n/** Binary search for the fragment containing `timeMs`. */\nexport function findFragmentIndex(fragments: Fragment[], timeMs: number): number {\n if (fragments.length === 0) return 0;\n if (timeMs >= fragments[fragments.length - 1].end_ms) return fragments.length - 1;\n let lo = 0, hi = fragments.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n fragments[mid].end_ms <= timeMs ? (lo = mid + 1) : (hi = mid);\n }\n return lo;\n}\n\n/** Find the nearest keyframe fragment at or before `timeMs`.\n * Scans backward from the fragment containing `timeMs` to find the closest\n * fragment with `keyframe === true`. Falls back to `findFragmentIndex` if\n * no keyframe info exists (old index.json without keyframe field). */\nexport function findKeyframeIndex(fragments: Fragment[], timeMs: number): number {\n const idx = findFragmentIndex(fragments, timeMs);\n // If no keyframe info, fall back to fragment index (backward compat)\n if (fragments.length === 0 || fragments[0].keyframe === undefined) return idx;\n // Scan backward for nearest keyframe\n for (let i = idx; i >= 0; i--) {\n if (fragments[i].keyframe) return i;\n }\n // No keyframe found before target — scan forward\n for (let i = idx + 1; i < fragments.length; i++) {\n if (fragments[i].keyframe) return i;\n }\n return idx; // fallback\n}\n\n/** Find which buffered TimeRange contains `timeSec`, or -1. */\nexport function findContainingRangeIndex(buffered: TimeRanges, timeSec: number, tolerance: number = SB_TOLERANCE.POINT): number {\n for (let i = 0; i < buffered.length; i++) {\n if (timeSec >= buffered.start(i) - tolerance && timeSec <= buffered.end(i)) return i;\n }\n return -1;\n}\n\n/** Check if a fragment's time range overlaps ANY buffered range in the SourceBuffer.\n * Unlike `findContainingRangeIndex` (point-in-range), this handles fragments where\n * the IDR is late: MSE creates a buffered range only from the IDR's presentation\n * time, which may not overlap the fragment's midpoint. */\nexport function fragmentOverlapsSB(buffered: TimeRanges, startMs: number, endMs: number, tolerance: number = SB_TOLERANCE.RANGE): boolean {\n const startSec = startMs / 1000;\n const endSec = endMs / 1000;\n for (let i = 0; i < buffered.length; i++) {\n // Two ranges overlap if neither is entirely before or after the other\n if (buffered.start(i) < endSec + tolerance && buffered.end(i) > startSec - tolerance) return true;\n }\n return false;\n}\n\n/** Format SourceBuffer.buffered ranges as human-readable strings. */\nexport function formatBufferedRanges(buffered: TimeRanges | undefined): string[] {\n if (!buffered) return [];\n return Array.from({ length: buffered.length }, (_, i) => `${buffered.start(i).toFixed(2)}-${buffered.end(i).toFixed(2)}`);\n}\n\n/** Average download rate in bytes/sec from rolling samples. */\nexport function getAvgDownloadRate(samples: { bytes: number; durationMs: number }[]): number {\n if (samples.length === 0) return 0;\n const b = samples.reduce((s, d) => s + d.bytes, 0);\n const ms = samples.reduce((s, d) => s + d.durationMs, 0);\n return ms === 0 ? 0 : (b / ms) * 1000;\n}\n\n// Reference size used to estimate download time. Represents a typical fMP4 fragment;\n// actual fragments vary, so a 4× safety factor is applied on top.\nconst REFERENCE_FRAGMENT_BYTES = 800_000;\nconst TIMEOUT_SAFETY_FACTOR = 4;\n\n/** Adaptive fetch timeout: cold start 60s, rate-based 30s–120s once samples exist.\n * Rate is bytes/sec; formula: (REFERENCE_FRAGMENT_BYTES / rate) × 1000 ms/s × TIMEOUT_SAFETY_FACTOR */\nexport function getAdaptiveTimeout(avgRateBytesPerSec: number): number {\n if (avgRateBytesPerSec === 0) return 60_000;\n const estimatedMs = (REFERENCE_FRAGMENT_BYTES / avgRateBytesPerSec) * 1000 * TIMEOUT_SAFETY_FACTOR;\n return Math.max(30_000, Math.min(120_000, Math.round(estimatedMs)));\n}\n\n/** Scan backward from `index` to find the nearest keyframe fragment.\n * Returns -1 if no keyframe exists at or before `index`. Used by the\n * append-queue to detect non-keyframe fragments whose IDR predecessor\n * is in error/missing, which MSE would silently discard. */\nexport function findKeyframePredecessor(fragments: Fragment[], index: number): number {\n for (let i = index; i >= 0; i--) {\n if (fragments[i]?.keyframe) return i;\n }\n return -1;\n}\n\n/** True if a gap (in SB time) overlaps ANY fragment currently in-flight.\n * Used by the SB updateend gap-detection code: a gap with matching in-flight\n * fragments is being filled, not a real unfillable hole.\n *\n * Tolerances: ±100ms container-overhead slack. ASS/ffmpeg fMP4 containers\n * can offset buffered edges by up to ~80ms from the nominal fragment bounds. */\nexport function gapOverlapsPendingFetch(\n gapStartSec: number,\n gapEndSec: number,\n fragments: Fragment[],\n inFlightIndices: Iterable<number>,\n): boolean {\n const gapStartMs = gapStartSec * 1000 - 100;\n const gapEndMs = gapEndSec * 1000 + 100;\n for (const idx of inFlightIndices) {\n const f = fragments[idx];\n if (!f) continue;\n if (f.start_ms < gapEndMs && f.end_ms > gapStartMs) return true;\n }\n return false;\n}\n","import React, { useEffect, useState, useRef, useCallback } from 'react';\nimport { getRetryDelay } from '../lib/bufferUtils';\nimport { warn, error as logError, errorFields } from '../lib/debug';\n\nexport type SubtitleStyle = 'classic' | 'cinema' | 'minimal' | 'contrast';\n\ntype CueAlign = 'start' | 'center' | 'end';\n\ninterface VttCue {\n id: string;\n startTime: number;\n endTime: number;\n text: string;\n /** VTT `line:` cue setting as a 0-1 vertical fraction. `line:10%` → 0.1.\n * Only honored when explicitly set — otherwise the cue renders at the\n * default bottom position.\n */\n linePercent?: number;\n /** VTT `position:` as a 0-1 horizontal fraction. `position:15%` → 0.15.\n * Used together with align to place corner/edge cues. */\n positionPercent?: number;\n /** VTT `align:` cue setting. Maps to text-align for horizontal anchor. */\n align?: CueAlign;\n}\n\ninterface Props {\n subtitleUrl: string | null;\n videoRef: React.RefObject<HTMLVideoElement | null>;\n style: SubtitleStyle;\n enabled: boolean;\n}\n\n// Parse VTT file into cues\nfunction parseVtt(vttText: string): VttCue[] {\n const cues: VttCue[] = [];\n const lines = vttText.replace(/\\r\\n/g, '\\n').split('\\n');\n \n let i = 0;\n // Skip header\n while (i < lines.length && !lines[i].includes('-->')) {\n i++;\n }\n \n while (i < lines.length) {\n const line = lines[i].trim();\n \n // Look for timestamp line\n if (line.includes('-->')) {\n const timeParts = line.split('-->');\n const startTime = parseTimestamp(timeParts[0].trim());\n // Right side: end timestamp followed by optional cue settings\n // (space-separated key:value pairs like `line:10%` or `position:50%`).\n const rightSide = timeParts[1].trim().split(/\\s+/);\n const endTime = parseTimestamp(rightSide[0]);\n let linePercent: number | undefined;\n let positionPercent: number | undefined;\n let align: CueAlign | undefined;\n for (let si = 1; si < rightSide.length; si++) {\n const setting = rightSide[si];\n if (setting.startsWith('line:')) {\n const raw = setting.slice(5);\n if (raw.endsWith('%')) {\n const pct = parseFloat(raw.slice(0, -1));\n if (!isNaN(pct)) linePercent = pct / 100;\n }\n } else if (setting.startsWith('position:')) {\n const raw = setting.slice(9);\n if (raw.endsWith('%')) {\n const pct = parseFloat(raw.slice(0, -1));\n if (!isNaN(pct)) positionPercent = pct / 100;\n }\n } else if (setting.startsWith('align:')) {\n const raw = setting.slice(6);\n if (raw === 'start' || raw === 'center' || raw === 'end') align = raw;\n }\n }\n\n // Collect cue text\n const textLines: string[] = [];\n i++;\n while (i < lines.length && lines[i].trim() !== '') {\n textLines.push(lines[i].trim());\n i++;\n }\n\n if (startTime !== null && endTime !== null && textLines.length > 0) {\n cues.push({\n id: `cue-${cues.length}`,\n startTime,\n endTime,\n text: textLines.join('\\n'),\n linePercent,\n positionPercent,\n align,\n });\n }\n } else {\n i++;\n }\n }\n \n return cues;\n}\n\n// Parse VTT timestamp (00:00:00.000) to seconds\nfunction parseTimestamp(ts: string): number | null {\n const parts = ts.split(':');\n if (parts.length < 2) return null;\n \n let hours = 0;\n let minutes: number;\n let seconds: number;\n \n if (parts.length === 3) {\n hours = parseInt(parts[0], 10);\n minutes = parseInt(parts[1], 10);\n seconds = parseFloat(parts[2]);\n } else {\n minutes = parseInt(parts[0], 10);\n seconds = parseFloat(parts[1]);\n }\n \n if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return null;\n return hours * 3600 + minutes * 60 + seconds;\n}\n\n// Strip common subtitle formatting tags and collapse hard line breaks.\n//\n// ASS authors hard-wrap lines with \\N for their native 640×360 canvas. Our\n// overlay sits over a 1920-wide (or wider) video, so those hard breaks end\n// up splitting short dialog across two rows for no visual reason (e.g. a\n// 56-character line that would easily fit on one). Collapsing \\N / embedded\n// newlines to spaces lets CSS wrapping use the overlay's own width budget\n// (max-w-[95%] of the container). Edge cases where the source intentionally\n// stacked two speakers become a single longer line — readable, just not\n// visually separated.\nfunction cleanSubtitleText(text: string): string {\n return text\n .replace(/<\\/?[^>]+(>|$)/g, '') // HTML tags\n .replace(/\\{[^}]+\\}/g, '') // ASS/SSA tags\n .replace(/\\\\N/g, ' ') // literal \\N → space\n .replace(/\\s*\\n\\s*/g, ' ') // embedded newlines → single space\n .replace(/\\s{2,}/g, ' ') // collapse runs of whitespace\n .trim();\n}\n\n// Style presets - each visually distinct for a different use-case\n// All styles use hardcoded white text because subtitles always overlay video,\n// regardless of the UI theme (light themes would otherwise make text dark/invisible).\nconst STYLE_CLASSES: Record<SubtitleStyle, string> = {\n // Standard CC look: white text with thick black outline stroke\n classic: 'text-white [text-shadow:_-2px_-2px_0_#000,_2px_-2px_0_#000,_-2px_2px_0_#000,_2px_2px_0_#000,_0_-2px_0_#000,_0_2px_0_#000,_-2px_0_0_#000,_2px_0_0_#000]',\n // DVD/theater style: yellow text on dark translucent box\n cinema: 'text-[#fde047] bg-black/70 px-3 py-1 rounded',\n // Clean & unobtrusive: lighter weight white text with subtle shadow\n minimal: 'text-white/90 font-normal drop-shadow-[0_1px_3px_rgba(0,0,0,0.8)]',\n // Accessibility-focused: bold white text on solid black box\n contrast: 'text-white font-bold bg-black px-3 py-1',\n};\n\nexport default function SubtitleOverlay({ subtitleUrl, videoRef, style, enabled }: Props) {\n const [cues, setCues] = useState<VttCue[]>([]);\n const [activeCues, setActiveCues] = useState<VttCue[]>([]);\n const containerRef = useRef<HTMLDivElement>(null);\n const [bottomOffset, setBottomOffset] = useState(40);\n const [fontSize, setFontSize] = useState(18);\n\n // Drop the cues as soon as subtitles are turned off.\n const [prevSubtitleUrl, setPrevSubtitleUrl] = useState(subtitleUrl);\n if (prevSubtitleUrl !== subtitleUrl) {\n setPrevSubtitleUrl(subtitleUrl);\n if (!subtitleUrl) setCues([]);\n }\n\n // Fetch and parse VTT file\n useEffect(() => {\n if (!subtitleUrl) return;\n \n let cancelled = false;\n const MAX_RETRY = 2; // retries after the initial attempt: ~1s, then ~2s\n\n (async () => {\n for (let attempt = 0; attempt <= MAX_RETRY; attempt++) {\n if (cancelled) return;\n try {\n const res = await fetch(subtitleUrl);\n if (!res.ok) throw new Error(`Failed to fetch subtitles (status ${res.status})`);\n const text = await res.text();\n if (cancelled) return;\n const parsed = parseVtt(text);\n setCues(parsed);\n return; // success — exit retry loop\n } catch (err) {\n if (cancelled) return;\n if (attempt < MAX_RETRY) {\n const retryMs = getRetryDelay(attempt + 1, { baseMs: 1000, capMs: 2000, jitter: 0 });\n warn('subtitles', `Fetch failed, retrying in ${retryMs}ms (attempt ${attempt + 1}/${MAX_RETRY + 1})`, { error: (err as Error)?.message });\n await new Promise(resolve => setTimeout(resolve, retryMs));\n } else {\n warn('subtitles', 'All fetch attempts failed', { error: (err as Error)?.message });\n setCues([]);\n }\n }\n }\n })().catch((err: unknown) => logError('subtitles', 'Subtitle load threw', errorFields(err)));\n\n return () => { cancelled = true; };\n }, [subtitleUrl]);\n\n // Update active cues based on video time\n const updateActiveCues = useCallback(() => {\n const video = videoRef.current;\n if (!video || !enabled || cues.length === 0) {\n setActiveCues([]);\n return;\n }\n \n const currentTime = video.currentTime;\n const active = cues.filter(\n cue => currentTime >= cue.startTime && currentTime < cue.endTime\n );\n setActiveCues(active);\n }, [videoRef, enabled, cues]);\n\n // Listen to video timeupdate\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n \n const handleTimeUpdate = () => updateActiveCues();\n video.addEventListener('timeupdate', handleTimeUpdate);\n \n // Also update on seek\n video.addEventListener('seeked', handleTimeUpdate);\n \n return () => {\n video.removeEventListener('timeupdate', handleTimeUpdate);\n video.removeEventListener('seeked', handleTimeUpdate);\n };\n }, [videoRef, updateActiveCues]);\n\n // Calculate letterbox offset for proper positioning\n const hasActiveCues = activeCues.length > 0;\n useEffect(() => {\n const video = videoRef.current;\n const container = containerRef.current?.parentElement;\n if (!video || !container) return;\n\n const calculateOffset = () => {\n const containerHeight = container.clientHeight;\n const containerWidth = container.clientWidth;\n const videoWidth = video.videoWidth || 1920;\n const videoHeight = video.videoHeight || 1080;\n \n // Calculate displayed video dimensions\n const scale = Math.min(containerWidth / videoWidth, containerHeight / videoHeight);\n const displayedHeight = videoHeight * scale;\n const letterbox = (containerHeight - displayedHeight) / 2;\n \n // Position above letterbox with some margin\n const baseFontPx = Math.max(16, displayedHeight * 0.035);\n const margin = Math.max(baseFontPx * 0.8, displayedHeight * 0.055);\n setBottomOffset(Math.max(letterbox + margin, 40));\n setFontSize(baseFontPx);\n };\n\n calculateOffset();\n window.addEventListener('resize', calculateOffset);\n video.addEventListener('loadedmetadata', calculateOffset);\n \n return () => {\n window.removeEventListener('resize', calculateOffset);\n video.removeEventListener('loadedmetadata', calculateOffset);\n };\n }, [videoRef, hasActiveCues]); // re-run when cues first render so containerRef is valid\n\n if (!enabled || activeCues.length === 0) {\n return null;\n }\n\n // Bucket active cues into a 2×3 grid of screen regions:\n // top-start | top-center | top-end\n // bot-start | bot-center | bot-end\n // This preserves the visual separation that source ASS encodes via\n // `\\anN` (numpad layout): e.g. `\\an8` → top-center, `\\an9` → top-end,\n // `\\an1` → bottom-start. Cues without explicit positioning default to\n // bottom-center — the standard dialog position.\n type Bucket = 'tl' | 'tc' | 'tr' | 'bl' | 'bc' | 'br';\n\n function bucketFor(cue: VttCue): Bucket {\n const top = cue.linePercent !== undefined && cue.linePercent <= 0.5;\n // Prefer explicit `align:` over `position:` — anime sources usually\n // emit both consistently (e.g. `position:15% align:start`).\n let horiz: 'l' | 'c' | 'r' = 'c';\n if (cue.align === 'start') horiz = 'l';\n else if (cue.align === 'end') horiz = 'r';\n else if (cue.positionPercent !== undefined) {\n if (cue.positionPercent < 0.3) horiz = 'l';\n else if (cue.positionPercent > 0.7) horiz = 'r';\n }\n return (top ? 't' : 'b') + horiz as Bucket;\n }\n\n const buckets: Record<Bucket, VttCue[]> = {\n tl: [], tc: [], tr: [],\n bl: [], bc: [], br: [],\n };\n for (const cue of activeCues) buckets[bucketFor(cue)].push(cue);\n\n const renderGroup = (group: VttCue[], alignClass: string, textAlignClass: string) =>\n group.map(cue => (\n <div\n key={cue.id}\n // w-full stretches this cue wrapper to its column's full width so\n // the `max-w-[95%]` below references a stable parent width. Without\n // this, `flex-col items-*` shrink-wraps the wrapper to its min-content\n // width and every long line wraps to single-word width.\n className={`flex flex-col ${alignClass} w-full`}\n style={{ fontSize: fontSize + 'px' }}\n >\n <div\n // inline-block with a 95% width cap lets the box grow to fit the\n // text and only wraps when the line genuinely exceeds the video\n // width. cleanSubtitleText collapses source newlines so CSS is\n // the sole authority on when to wrap. `antialiased` switches the\n // browser from RGB-subpixel AA to grayscale AA; subpixel AA over\n // a moving video background creates visible RGB fringing on the\n // edges of the white text + heavy black text-shadow stroke.\n className={`${textAlignClass} font-medium max-w-[95%] inline-block antialiased ${STYLE_CLASSES[style]}`}\n >\n {cleanSubtitleText(cue.text)}\n </div>\n </div>\n ));\n\n // Render each row as three layered containers, all absolutely positioned.\n // The center container spans the full video width so dialog has the same\n // wrapping budget (max-w-[95%]) it had before the redesign. Left/right\n // containers are narrower overlays pinned to the edge — only populated by\n // explicit \\an7/\\an9/\\an1/\\an3 corner cues, which are rare.\n const renderRow = (\n leftCues: VttCue[], centerCues: VttCue[], rightCues: VttCue[],\n verticalStyle: React.CSSProperties, centerRef?: React.RefObject<HTMLDivElement | null>,\n ) => (\n <>\n {leftCues.length > 0 && (\n <div\n className=\"absolute left-0 flex flex-col items-start gap-2 pointer-events-none z-10 px-3 max-w-[35%]\"\n style={verticalStyle}\n >\n {renderGroup(leftCues, 'items-start', 'text-left')}\n </div>\n )}\n {centerCues.length > 0 && (\n <div\n ref={centerRef}\n className=\"absolute left-0 right-0 flex flex-col items-center gap-2 pointer-events-none z-10\"\n style={verticalStyle}\n >\n {renderGroup(centerCues, 'items-center', 'text-center')}\n </div>\n )}\n {rightCues.length > 0 && (\n <div\n className=\"absolute right-0 flex flex-col items-end gap-2 pointer-events-none z-10 px-3 max-w-[35%]\"\n style={verticalStyle}\n >\n {renderGroup(rightCues, 'items-end', 'text-right')}\n </div>\n )}\n </>\n );\n\n return (\n <>\n {renderRow(buckets.tl, buckets.tc, buckets.tr, { top: `${bottomOffset}px` })}\n {renderRow(buckets.bl, buckets.bc, buckets.br, { bottom: `${bottomOffset}px` }, containerRef)}\n </>\n );\n}\n","/**\n * Ad-hoc performance profiler for the v2 audit.\n *\n * Instruments FragmentManager, SeekCoordinator, and useStallRecovery without\n * changing any behavior. All methods are pure side-effects on internal arrays.\n *\n * Access from E2E tests:\n * await page.evaluate(() => window.__WP_PERF__.report())\n *\n * Access from console:\n * window.__WP_PERF__.report()\n * window.__WP_PERF__.reset()\n */\n\nimport { registerDebugGlobal } from './debugGlobals';\n\nconst MAX_SAMPLES = 300;\nconst MAX_TIMEOUT_RECORDS = 50;\nconst MAX_SEEK_RECORDS = 100;\nconst MAX_STALL_RECORDS = 100;\nconst MAX_EVICT_RECORDS = 100;\nconst HIGH_AUDIO_DRIFT_MS = 100;\n\n// ── Stat helpers ──────────────────────────────────────────────────────────────\n\nfunction pct(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(Math.floor((p / 100) * sorted.length), sorted.length - 1);\n return sorted[idx];\n}\n\ninterface Stats {\n count: number;\n mean: number;\n p50: number;\n p90: number;\n p99: number;\n max: number;\n}\n\nfunction makeStats(values: number[]): Stats {\n if (values.length === 0) return { count: 0, mean: 0, p50: 0, p90: 0, p99: 0, max: 0 };\n const s = [...values].sort((a, b) => a - b);\n const sum = s.reduce((acc, v) => acc + v, 0);\n return {\n count: s.length,\n mean: Math.round(sum / s.length),\n p50: Math.round(pct(s, 50)),\n p90: Math.round(pct(s, 90)),\n p99: Math.round(pct(s, 99)),\n max: Math.round(s[s.length - 1]),\n };\n}\n\nfunction cap<T>(arr: T[], max = MAX_SAMPLES): void {\n if (arr.length > max) arr.splice(0, arr.length - max);\n}\n\n// ── Sample record types ───────────────────────────────────────────────────────\n\ninterface FetchRecord { index: number; bytes: number; durationMs: number; ts: number }\ninterface AppendRecord { index: number; queueDepth: number; latencyMs: number; ts: number }\ninterface AppendTimeoutRecord { index: number; timeoutMs: number; queueDepth: number; ts: number }\ninterface SeekRecord { caller: string; targetMs: number; durationMs: number; ts: number }\ninterface StallRecord { currentMs: number; aheadMs: number; durationMs: number; ts: number }\ninterface EvictRecord { reason: string; start: number; end: number; ts: number }\n\n// ── PerfCollector ─────────────────────────────────────────────────────────────\n\nclass PerfCollector {\n // Fetch\n private _fetches: FetchRecord[] = [];\n private _fetchTimeoutCount = 0;\n private _fetchErrorCount = 0;\n\n // Append\n private _appends: AppendRecord[] = [];\n private _appendStarts = new Map<number, { time: number; queueDepth: number }>();\n private _appendTimeouts: AppendTimeoutRecord[] = [];\n\n // Seek\n private _seeks: SeekRecord[] = [];\n private _waitForSeekedTimeouts = 0;\n\n // Audio drift\n private _audioDrifts: number[] = [];\n\n // Stalls\n private _stalls: StallRecord[] = [];\n private _stallStart: { currentMs: number; aheadMs: number; ts: number } | null = null;\n\n // Evictions\n private _evictCounts: Record<string, number> = {};\n private _evicts: EvictRecord[] = [];\n\n // Browser quota eviction — the browser silently drops SourceBuffer data at\n // its own memory limit (MSE coded-frame eviction). No event fires; detected\n // only when reconcileLoadedStates() finds 'loaded' fragments gone from\n // sb.buffered. This is the failure mode behind the backward-seek freeze.\n private _browserEvictions: { fragmentCount: number; survivingRanges: string; ts: number }[] = [];\n\n // ── Recording API ─────────────────────────────────────────────────────────\n\n /** Called on every successful fragment fetch. */\n recordFetch(index: number, bytes: number, durationMs: number): void {\n this._fetches.push({ index, bytes, durationMs, ts: performance.now() });\n cap(this._fetches);\n }\n\n /** Called when a fetch times out (before retry). */\n recordFetchTimeout(_index: number): void {\n this._fetchTimeoutCount++;\n }\n\n /** Called when a fragment exhausts all retries and ends in error state. */\n recordFetchError(_index: number): void {\n this._fetchErrorCount++;\n }\n\n /**\n * Called when a fragment's buffer enters the append queue.\n * Must be paired with recordAppendLanded or the entry will leak — callers\n * must ensure they call recordAppendLanded from the updateend handler.\n */\n recordAppendQueued(index: number, queueDepth: number): void {\n this._appendStarts.set(index, { time: performance.now(), queueDepth });\n }\n\n /** Called from the updateend handler when MSE confirms the fragment landed. */\n recordAppendLanded(index: number): void {\n const start = this._appendStarts.get(index);\n if (!start) return;\n this._appendStarts.delete(index);\n this._appends.push({\n index,\n queueDepth: start.queueDepth,\n latencyMs: performance.now() - start.time,\n ts: performance.now(),\n });\n cap(this._appends);\n }\n\n /** Called when waitForAppend times out. */\n recordAppendTimeout(index: number, timeoutMs: number, queueDepth: number): void {\n this._appendTimeouts.push({ index, timeoutMs, queueDepth, ts: performance.now() });\n cap(this._appendTimeouts, MAX_TIMEOUT_RECORDS);\n }\n\n /** Called when a seek completes (COMPLETED path in _execute). */\n recordSeekCompleted(caller: string, targetMs: number, durationMs: number): void {\n this._seeks.push({ caller, targetMs, durationMs, ts: performance.now() });\n cap(this._seeks, MAX_SEEK_RECORDS);\n }\n\n /** Called when waitForSeeked resolves via timeout rather than the 'seeked' event. */\n recordWaitForSeekedTimeout(): void {\n this._waitForSeekedTimeouts++;\n }\n\n /**\n * Called ~500ms after _resume() syncs audio. Captures actual A/V drift\n * after both elements have started playing, not at the sync assignment moment.\n */\n recordAudioDrift(driftMs: number): void {\n this._audioDrifts.push(driftMs);\n cap(this._audioDrifts);\n }\n\n /** Called when the video 'waiting' event fires. */\n recordStallStart(currentMs: number, aheadMs: number): void {\n // Overwrite any open stall without closing it — consecutive waiting events\n // without an intervening playing event just refresh the stall start.\n this._stallStart = { currentMs, aheadMs, ts: performance.now() };\n }\n\n /** Called when the video 'playing' event fires (stall resolved). */\n recordStallEnd(): void {\n if (!this._stallStart) return;\n this._stalls.push({\n ...this._stallStart,\n durationMs: Math.round(performance.now() - this._stallStart.ts),\n });\n cap(this._stalls, MAX_STALL_RECORDS);\n this._stallStart = null;\n }\n\n /** Called from _doEvict with the reason tag passed by the caller. */\n recordEvict(reason: string, start: number, end: number): void {\n this._evictCounts[reason] = (this._evictCounts[reason] ?? 0) + 1;\n this._evicts.push({ reason, start, end, ts: performance.now() });\n cap(this._evicts, MAX_EVICT_RECORDS);\n }\n\n /** Called from FragmentManager.reconcileLoadedStates() when the browser's\n * own silent quota eviction is detected (fragments 'loaded' but gone from\n * sb.buffered). The MSE spec fires no event for this, so reconcile is our\n * only signal — without this counter the eviction is invisible. */\n recordBrowserEviction(fragmentCount: number, survivingRanges: string): void {\n this._browserEvictions.push({ fragmentCount, survivingRanges, ts: performance.now() });\n cap(this._browserEvictions);\n }\n\n // ── Reporting ─────────────────────────────────────────────────────────────\n\n report() {\n const fetchDurations = this._fetches.map(f => f.durationMs);\n const fetchThroughputs = this._fetches\n .filter(f => f.bytes > 0 && f.durationMs > 0)\n .map(f => Math.round((f.bytes / f.durationMs) * 1000)); // bytes/sec\n\n const appendLatencies = this._appends.map(a => a.latencyMs);\n const appendDepths = this._appends.map(a => a.queueDepth);\n\n const seekDurations = this._seeks.map(s => s.durationMs);\n const stallDurations = this._stalls.map(s => s.durationMs);\n\n // Per-caller seek breakdown\n const callerMap: Record<string, number[]> = {};\n for (const s of this._seeks) {\n (callerMap[s.caller] ??= []).push(s.durationMs);\n }\n const seekByCaller = Object.fromEntries(\n Object.entries(callerMap).map(([caller, durations]) => [caller, makeStats(durations)])\n );\n\n return {\n fetch: {\n ...makeStats(fetchDurations),\n throughputBytesPerSec: makeStats(fetchThroughputs),\n timeoutCount: this._fetchTimeoutCount,\n errorCount: this._fetchErrorCount,\n },\n append: {\n queueDepth: makeStats(appendDepths),\n latencyMs: makeStats(appendLatencies),\n timeoutCount: this._appendTimeouts.length,\n timeouts: this._appendTimeouts.slice(-10).map(t => ({\n index: t.index,\n timeoutMs: t.timeoutMs,\n queueDepth: t.queueDepth,\n })),\n },\n seek: {\n count: this._seeks.length,\n durationMs: makeStats(seekDurations),\n byCaller: seekByCaller,\n waitForSeekedTimeouts: this._waitForSeekedTimeouts,\n },\n audio: {\n driftMs: makeStats(this._audioDrifts),\n highDriftCount: this._audioDrifts.filter(d => d > HIGH_AUDIO_DRIFT_MS).length,\n samples: this._audioDrifts.slice(-20).map(d => Math.round(d)),\n },\n stalls: {\n count: this._stalls.length + (this._stallStart ? 1 : 0),\n durationMs: makeStats(stallDurations),\n openStall: this._stallStart\n ? {\n currentMs: Math.round(this._stallStart.currentMs),\n aheadMs: Math.round(this._stallStart.aheadMs),\n elapsedMs: Math.round(performance.now() - this._stallStart.ts),\n }\n : null,\n },\n evictions: { ...this._evictCounts },\n browserEviction: {\n detectionCount: this._browserEvictions.length,\n fragmentsAffected: this._browserEvictions.reduce((s, e) => s + e.fragmentCount, 0),\n recent: this._browserEvictions.slice(-10).map(e => ({\n fragmentCount: e.fragmentCount,\n survivingRanges: e.survivingRanges,\n })),\n },\n raw: {\n fetches: this._fetches.slice(-20).map(f => ({\n index: f.index,\n bytes: f.bytes,\n durationMs: Math.round(f.durationMs),\n })),\n seeks: this._seeks.slice(-10).map(s => ({\n caller: s.caller,\n targetMs: Math.round(s.targetMs),\n durationMs: s.durationMs,\n })),\n audioDrifts: this._audioDrifts.slice(-10).map(d => Math.round(d)),\n stalls: this._stalls.slice(-5).map(s => ({\n currentMs: Math.round(s.currentMs),\n aheadMs: Math.round(s.aheadMs),\n durationMs: s.durationMs,\n })),\n evictions: this._evicts.slice(-10).map(e => ({\n reason: e.reason,\n range: `${e.start.toFixed(1)}-${e.end.toFixed(1)}`,\n })),\n },\n };\n }\n\n reset(): void {\n this._fetches = [];\n this._fetchTimeoutCount = 0;\n this._fetchErrorCount = 0;\n this._appends = [];\n this._appendStarts.clear();\n this._appendTimeouts = [];\n this._seeks = [];\n this._waitForSeekedTimeouts = 0;\n this._audioDrifts = [];\n this._stalls = [];\n this._stallStart = null;\n this._evictCounts = {};\n this._evicts = [];\n this._browserEvictions = [];\n }\n}\n\nexport const perf = new PerfCollector();\n\ndeclare global {\n interface Window {\n __WP_PERF__?: PerfCollector;\n }\n}\n// Published only when the host opts in (lib/debugGlobals.ts).\nregisterDebugGlobal('__WP_PERF__', perf);\n","import type { Fragment } from './bufferUtils';\nimport { findKeyframeIndex } from './bufferUtils';\n\ninterface MediaCache {\n init?: ArrayBuffer;\n fragments: Map<number, ArrayBuffer>;\n}\n\nclass PlaylistFragmentCache {\n private cache = new Map<string, MediaCache>();\n\n private getOrCreate(mediaId: string): MediaCache {\n let mc = this.cache.get(mediaId);\n if (!mc) { mc = { fragments: new Map() }; this.cache.set(mediaId, mc); }\n return mc;\n }\n\n storeInit(mediaId: string, buf: ArrayBuffer): void {\n this.getOrCreate(mediaId).init = buf;\n }\n\n getInit(mediaId: string): ArrayBuffer | undefined {\n return this.cache.get(mediaId)?.init;\n }\n\n storeFragment(mediaId: string, index: number, buf: ArrayBuffer): void {\n this.getOrCreate(mediaId).fragments.set(index, buf);\n }\n\n getFragment(mediaId: string, index: number): ArrayBuffer | undefined {\n return this.cache.get(mediaId)?.fragments.get(index);\n }\n\n hasFragment(mediaId: string, index: number): boolean {\n return this.cache.get(mediaId)?.fragments.has(index) ?? false;\n }\n\n /**\n * Returns how many ms of consecutive fragments starting from the keyframe at/before time 0\n * are currently cached. Used by usePlaylistPreBuffer to decide whether pre-buffering is complete.\n */\n getBufferedMs(mediaId: string, fragments: Fragment[]): number {\n if (fragments.length === 0) return 0;\n const mc = this.cache.get(mediaId);\n if (!mc) return 0;\n const startIdx = findKeyframeIndex(fragments, 0);\n let maxMs = 0;\n for (let i = startIdx; i < fragments.length; i++) {\n if (!mc.fragments.has(i)) break;\n maxMs = fragments[i].end_ms;\n }\n return maxMs;\n }\n\n clear(): void {\n this.cache.clear();\n }\n}\n\nexport const playlistFragmentCache = new PlaylistFragmentCache();\n","/**\n * SourceBuffer eviction strategy. Owns the 'is a remove() in progress' state\n * and the decision logic for which ranges to evict.\n *\n * Two independent policies:\n * 1. **evictSeekGap(targetMs)** — runs synchronously at seek time. Removes\n * one range per call (chained via updateend). Backward pass drops ranges\n * ending more than 5s before the seek target with a gap after them\n * (prevents Firefox stalling on a preceding discontinuity). Forward pass\n * drops orphan islands beyond the prefetch window.\n * 2. **evictBefore(currentMs)** — runs on playhead advance. Sub-trims the\n * stale behind-tail of a range straddling the playhead (the common\n * continuous-playback case the whole-range checks below all miss).\n * Otherwise removes ranges behind the playhead, far-ahead islands,\n * orphaned gap ranges, and forward-seek orphans. One range per call;\n * updateend re-arms via onUpdateendComplete() until nothing left to evict.\n *\n * TWO RELATED FIXES WERE DELIBERATELY NOT SHIPPED — do not add them back\n * without first seeing an `EVICT-STUCK` in production:\n * (a) Splitting the updateend `else if (isEvicting)` into an unconditional\n * clear. No real sequence reaches the state it guards: `appendingIndex`\n * is assigned only AFTER a successful `appendBuffer()`, is cleared on\n * every updateend branch, and `remove()` cannot start mid-append because\n * the browser holds `sb.updating`.\n * (b) `reset()` in setIndex()/teardown. A genuine media-swap hazard since the\n * FM is reused across loads, but the watchdog already bounds it to\n * EVICT_STUCK_MS.\n *\n * WHEN DEBUGGING A FREEZE HERE, three confident conclusions have all been wrong\n * before — check each against the data rather than assuming:\n * 1. \"The behind-buffer hit Chrome's MSE quota.\" Read the `kf=` token on\n * APPEND-DISCARDED first: `kf=n/a` means AUDIO, which averages ~48KB/frag\n * against video's ~2MB, so buffer-size reasoning is off by ~40x.\n * 2. \"The eviction leak caused it.\" Audio starvation presents identically and\n * is far more common; the watchdog does not address that trigger.\n * 3. \"`fragments_loaded` froze, so state stopped changing.\" Those counters\n * balance in steady state because eviction returns fragments to `pending`\n * as fast as prefetch loads new ones — they say NOTHING about eviction.\n * Read `sbRanges` out of the APPEND-DISCARDED lines instead; it is the only\n * place real buffer geometry reaches a session log.\n *\n * The class deliberately does NOT own `fragmentStates` or `fragments`; it\n * receives them via deps so FragmentManager's setIndex() reassignments stay\n * authoritative. `fragmentStates` is mutated in place (loaded → pending for\n * evicted ranges) — passing the Map ref works because nobody reassigns it.\n */\nimport { debug, warn } from './debug';\nimport { perf } from './perfCollector';\nimport { BUFFER_AHEAD_MS } from '@watchparty/shared';\nimport { SB_TOLERANCE, type Fragment, type FragmentState } from './bufferUtils';\n\n/** How much behind-the-playhead buffer to retain when sub-trimming a\n * contiguous range that straddles the playhead. The remaining whole-range\n * evictBefore criteria can't trim such a range (it matches none of them),\n * so without sub-trim the behind-buffer grows unbounded during smooth\n * continuous playback — eventually the browser hits its own MSE memory\n * quota and silently evicts, which desyncs fragmentStates and freezes\n * backward seeks (see fragmentManager.reconcileLoadedStates). */\nconst BEHIND_KEEP_SEC = 60;\n\ninterface EvictionDeps {\n /** Live getter — FragmentManager.setIndex reassigns fragments, so we can't\n * capture a snapshot in the constructor. */\n getFragments: () => Fragment[];\n getSourceBuffer: () => SourceBuffer | null;\n getMediaSource: () => MediaSource | null;\n isDisposed: () => boolean;\n /** Shared Map — mutated in place when evicted ranges reset fragments to\n * pending. FM owns the Map; we just hold the reference. */\n fragmentStates: Map<number, FragmentState>;\n /** Milliseconds a seek is currently trying to reach, or null when none is in\n * flight. Eviction MUST NOT touch the range a pending seek is waiting for:\n * every whole-range rule below is written against `video.currentTime`, and a\n * forward seek does not move currentTime until its data is buffered — so the\n * target it just fetched reads as an island stranded ahead of the playhead,\n * which is exactly what `isForwardOrphan` deletes. See getPendingSeekTargetMs\n * at the FragmentManager for why this is a getter rather than a constructor\n * value. */\n getPendingSeekTargetMs: () => number | null;\n}\n\n/** A `remove()` resolves in milliseconds. If `_isEvicting` is still set this\n * long afterwards, its `updateend` never reached onUpdateendComplete() and the\n * flag is stuck — which silently disables all eviction on THIS FragmentManager\n * (every entry point below early-returns on it), and there is one strategy per\n * FM: video plus each audio track. Self-heal instead of wedging: see the\n * cedar-ridge 2026-08-18 note on clearIfStuck(). */\nconst EVICT_STUCK_MS = 5000;\n\nexport class EvictionStrategy {\n private _isEvicting = false;\n private pendingEvictionMs = 0;\n /** Wall-clock when the in-flight remove() started; 0 when not evicting. */\n private evictStartedAt = 0;\n\n constructor(private deps: EvictionDeps) {}\n\n get isEvicting(): boolean { return this._isEvicting; }\n\n /** Called by FragmentManager from the updateend handler when a remove()\n * has just completed. Clears the flag and re-arms evictBefore with the\n * stored playhead/seek target — removes are one-at-a-time so we need a\n * second pass if more ranges match. */\n onUpdateendComplete(): void {\n this._isEvicting = false;\n this.evictStartedAt = 0;\n this.evictBefore(this.pendingEvictionMs);\n }\n\n /** Re-arm eviction from an APPEND updateend, using the last playhead an\n * evictBefore/evictSeekGap call supplied.\n *\n * Eviction is otherwise driven only by the 2s prefetch tick, and every entry\n * point early-returns on `sb.updating`. That is fine while appends are\n * sporadic and fatal once they are not: with a backed-up append queue the\n * SourceBuffer is updating almost continuously, so the tick nearly always\n * lands mid-append and nothing is ever trimmed. The behind-buffer then grows\n * unbounded, which is the precondition for the browser's own quota eviction\n * (see FragmentManager.reconcileLoadedStates).\n *\n * Observed cedar-ridge 2026-08-25 on TWO independent clients: the audio\n * buffer's START stayed pinned (1026.43s / 50.03s) across the whole freeze\n * while its end crawled forward, leaving ~102s of behind-buffer against\n * BEHIND_KEEP_SEC=60. EVICT-STUCK never fired on either, so the flag was not\n * leaked — the tick simply never found an idle SourceBuffer.\n *\n * An `updateend` is by definition a moment when `sb.updating` is false, so\n * this is the one place the trim is guaranteed a window. Cheap when there is\n * nothing to do: evictBefore matches no range and returns. */\n retryPendingEviction(): void {\n if (this.pendingEvictionMs <= 0) return;\n this.evictBefore(this.pendingEvictionMs);\n }\n\n /** Last-resort watchdog for a stuck `_isEvicting`.\n *\n * A leaked flag is unrecoverable and SILENT: `evictBefore`/`evictSeekGap`\n * both early-return on it, so nothing is ever trimmed again on THIS\n * FragmentManager — and there is one strategy per FM, i.e. one per audio\n * track as well as the video one.\n *\n * Observed cedar-ridge 2026-08-18 (TenmakuNoJaadugar s01e07), on an AUDIO FM:\n * the buffered range start froze at 101.33s while the playhead advanced to\n * 210.9s — 109.6s of behind-buffer against BEHIND_KEEP_SEC=60 — across 50s of\n * 2s ticks that should each have trimmed. `prefetchAround` and `evictBefore`\n * are called back-to-back in the SAME interval tick and prefetch kept working,\n * so the driver was alive.\n *\n * Two things that entry deliberately does NOT claim, because both were\n * asserted during the investigation and turned out false:\n * - It is NOT a memory story. The frozen range held ~55 AUDIO fragments,\n * ~2.6MB (audio averages 48KB/frag), nowhere near any SourceBuffer quota.\n * An early draft of this comment said ~109MB by mis-attributing the range\n * to video; video fragments average 1.97MB and had ZERO discards.\n * - It did NOT cause that freeze. The freeze was audio starvation — 10\n * APPEND-DISCARDED, all audio (kf=n/a; audio fragments carry no keyframe\n * field), effective fill 0.44x realtime, playhead meeting the audio buffer\n * edge at exactly 214.569s = audio fragment #106's end. Why those appends\n * were discarded is UNSOLVED and is not addressed here.\n *\n * So this is a bounded-blast-radius fix plus an instrument: it stops one FM's\n * eviction from dying permanently, and EVICT-STUCK is the first signal we\n * will ever have that the flag genuinely leaks (the EVICT-START/EVICT-DONE\n * lines are debug() and are not forwarded to session logs).\n *\n * Over-clearing is cheap (at worst one chained eviction pass is skipped and\n * the 2s tick re-arms it); under-clearing is fatal. Bias accordingly. */\n private clearIfStuck(): boolean {\n if (!this._isEvicting) return false;\n // No stamp means someone set the flag without going through _doEvict.\n // Stamp it now so this same watchdog can still time it out, rather than\n // trusting a value we can't measure and wedging forever.\n if (this.evictStartedAt === 0) { this.evictStartedAt = Date.now(); return true; }\n const heldMs = Date.now() - this.evictStartedAt;\n if (heldMs < EVICT_STUCK_MS) return true;\n warn('buffer', `EVICT-STUCK isEvicting held ${heldMs}ms — clearing; a remove() updateend was lost, eviction was disabled`);\n perf.recordEvict('stuck-recovered', 0, 0);\n this._isEvicting = false;\n this.evictStartedAt = 0;\n return false;\n }\n\n /** Called on dispose, on SourceBuffer error, and on media change\n * (FragmentManager.setIndex) — see the note at that call site. */\n reset(): void {\n this._isEvicting = false;\n this.evictStartedAt = 0;\n this.pendingEvictionMs = 0;\n }\n\n /** Remove SB ranges that would create gaps around a new seek target.\n * Called synchronously at seek time. HTTP fetches can overlap because\n * processAppendQueue() already blocks while sb.updating === true. One\n * range per call; the updateend chain handles any remainder. */\n evictSeekGap(targetMs: number): void {\n const sb = this.deps.getSourceBuffer();\n const ms = this.deps.getMediaSource();\n if (!sb || sb.updating || this.deps.isDisposed()\n || ms?.readyState !== 'open') return;\n if (this.clearIfStuck()) return;\n // Keep pendingEvictionMs in sync so the updateend re-check uses the seek\n // target, not a stale playhead from a prior evictBefore() call.\n this.pendingEvictionMs = targetMs;\n const targetSec = targetMs / 1000;\n const b = sb.buffered;\n\n // Pass 1: backward — gap-creating ranges before targetMs\n for (let i = 0; i < b.length; i++) {\n const rangeEnd = b.end(i);\n if (rangeEnd >= targetSec - 5) continue;\n const nextStart = i < b.length - 1 ? b.start(i + 1) : Infinity;\n if (nextStart - rangeEnd <= SB_TOLERANCE.RANGE) continue; // contiguous — no gap\n debug('buffer', `EVICT-SEEK-GAP [backward] range=${b.start(i).toFixed(2)}-${rangeEnd.toFixed(2)} targetSec=${targetSec.toFixed(2)}`);\n this._doEvict(b.start(i), rangeEnd, 'seek-gap-backward');\n return;\n }\n\n // Pass 2: forward — orphaned islands beyond the prefetch window\n const forwardThreshold = targetSec + BUFFER_AHEAD_MS / 1000;\n for (let i = 0; i < b.length; i++) {\n const rangeStart = b.start(i);\n if (rangeStart <= forwardThreshold) continue;\n const prevEnd = i > 0 ? b.end(i - 1) : 0;\n if (rangeStart - prevEnd <= SB_TOLERANCE.FALLBACK) continue; // contiguous with preceding range — keep it\n debug('buffer', `EVICT-SEEK-GAP [forward] range=${rangeStart.toFixed(2)}-${b.end(i).toFixed(2)} threshold=${forwardThreshold.toFixed(2)}`);\n this._doEvict(rangeStart, b.end(i), 'seek-gap-forward');\n return;\n }\n }\n\n /** Evict orphaned SB ranges relative to the current playhead. One range\n * per call; caller (updateend) re-arms via onUpdateendComplete. */\n evictBefore(currentMs: number): void {\n // Record the playhead BEFORE the guards. The guards are the common case\n // during a busy period — `sb.updating` is true for most of a backed-up\n // append queue — and a call that returns without storing anything leaves\n // retryPendingEviction() with no playhead to trim against, so the\n // updateend re-arm can never fire. Storing it is free and is what makes\n // \"trim at the next idle moment\" possible at all.\n this.pendingEvictionMs = currentMs;\n const sb = this.deps.getSourceBuffer();\n const ms = this.deps.getMediaSource();\n if (!sb || sb.updating || this.deps.isDisposed() || ms?.readyState !== 'open') return;\n if (this.clearIfStuck()) return;\n const sec = currentMs / 1000, b = sb.buffered;\n // Read ONCE per pass: a seek can complete mid-loop, and a target that\n // changes between ranges would protect one range and not the next.\n const pendingMs = this.deps.getPendingSeekTargetMs();\n const pendingSec = pendingMs === null ? null : pendingMs / 1000;\n for (let i = 0; i < b.length; i++) {\n const s = b.start(i), e = b.end(i);\n // Sub-trim a contiguous range that straddles the playhead with a stale\n // behind-tail. The whole-range checks below all skip this case (none\n // of `isBehind`/`isFarAhead`/orphan match a range spanning the\n // playhead), so without this trim the behind-buffer grows unbounded\n // during smooth continuous playback → browser quota eviction →\n // backward-seek freeze. Trim [s, sec-60s] so we keep BEHIND_KEEP_SEC\n // of recent history; updateend re-arms via onUpdateendComplete.\n if (s < sec - BEHIND_KEEP_SEC && e > sec) {\n const trimEnd = sec - BEHIND_KEEP_SEC;\n debug('buffer', `EVICT-START [behind-tail-trim] range=${s.toFixed(2)}-${e.toFixed(2)} → trim [${s.toFixed(2)}-${trimEnd.toFixed(2)}] playhead=${sec.toFixed(2)}`);\n this._doEvict(s, trimEnd, 'behind-tail-trim');\n return;\n }\n if (s < 0.5 && e > sec) continue; // range spans start to playhead\n // Never evict the range a pending seek is waiting on. Reproduced on the DO\n // fleet 2026-09-15 (run remote-1789440592127): fetchNearby appended the\n // target at 68.07-70.07 while currentTime was still 34.49, isForwardOrphan\n // matched, EVICT-DONE removed it, and the seek then burned its full 30s\n // waitForAppend on data it had just destroyed. 4 of 4 short forward seeks\n // timed out while 4 of 4 longer ones landed under 1.1s — the longer ones\n // route through ensureBuffered, which calls evictSeekGap first and leaves\n // nothing to orphan against.\n if (pendingSec !== null && e > pendingSec - SB_TOLERANCE.FALLBACK\n && s < pendingSec + BUFFER_AHEAD_MS / 1000) continue;\n const isBehind = e < sec - 25; // 25s: leader-controlled playback rarely seeks backward more than this\n const isFarAhead = s > sec + 4 * BUFFER_AHEAD_MS / 1000; // 4× prefetch window ahead\n // Evict orphaned ranges that create large gaps before the playhead.\n // Firefox stalls when the SourceBuffer has ranges with preceding gaps — the browser\n // checks buffer continuity and freezes even if currentTime is past the gap.\n // Threshold: range ends >25s before playhead AND gap to next range is >5s.\n const gapAfter = i < b.length - 1 ? b.start(i + 1) - e : 0;\n const isOrphanedGap = e < sec - 25 && gapAfter > 5;\n // Evict forward islands left by a backward seek: range starts beyond the prefetch\n // window AND has a genuine gap before it (not just normal prefetch spillover).\n const isForwardOrphan = s > sec + BUFFER_AHEAD_MS / 1000 && (i === 0 || b.end(i - 1) < s - SB_TOLERANCE.FALLBACK);\n if (isBehind || isFarAhead || isOrphanedGap || isForwardOrphan) {\n const reason = isBehind ? 'behind' : isFarAhead ? 'far-ahead-island' : isOrphanedGap ? 'orphaned-gap' : 'forward-orphan';\n debug('buffer', `EVICT-START [${reason}] range=${s.toFixed(2)}-${e.toFixed(2)} playhead=${sec.toFixed(2)}`);\n this._doEvict(s, e, reason);\n return;\n }\n }\n }\n\n /** Actually invoke sb.remove() and reset affected fragment states. Sets\n * isEvicting=true; caller's updateend handler calls onUpdateendComplete\n * to clear the flag and chain another pass. */\n private _doEvict(s: number, e: number, reason = 'unknown'): void {\n const sb = this.deps.getSourceBuffer();\n if (!sb) return;\n perf.recordEvict(reason, s, e);\n const fragments = this.deps.getFragments();\n for (const [idx, st] of this.deps.fragmentStates) {\n if (st !== 'loaded') continue;\n const f = fragments[idx];\n if (f && f.start_ms / 1000 < e + SB_TOLERANCE.RANGE && f.end_ms / 1000 > s - SB_TOLERANCE.RANGE) {\n this.deps.fragmentStates.set(idx, 'pending');\n }\n }\n this._isEvicting = true;\n this.evictStartedAt = Date.now();\n try {\n sb.remove(s, e);\n } catch (err) {\n this._isEvicting = false;\n this.evictStartedAt = 0;\n warn('buffer', `Eviction remove() failed: ${(err as Error).message}`);\n }\n }\n}\n","import { debug, warn, error as logError, errorFields } from './debug';\nimport { perf } from './perfCollector';\nimport {\n MAX_CONCURRENT_JOBS, BUFFER_AHEAD_MS, INITIAL_BUFFER_MS,\n INIT_RETRY_DELAY_MS, MAX_INIT_FETCH_RETRIES, END_OF_STREAM_RETRY_INTERVAL_MS,\n MAX_FETCH_ERROR_CYCLES, ERROR_RESET_AGE_MS, STABLE_GAP_DWELL_MS, APPEND_HOLD_MAX_MS,\n INIT_DIGEST_TRACK_COUNT,\n} from '@watchparty/shared';\nimport { playlistFragmentCache } from './PlaylistFragmentCache';\nimport { EvictionStrategy } from './evictionStrategy';\nimport {\n type Fragment, type FragmentState, type FetchJob, type AppendWaiter, type TargetResolution,\n MAX_RETRIES, NEARBY_FRAGMENT_THRESHOLD, MAX_DOWNLOAD_SAMPLES, SB_TOLERANCE, GAP_SKIP_EDGE_SEC,\n PENDING_SEEK_PROTECT_MS, SEEK_LANDED_TOLERANCE_MS,\n findFragmentIndex, findKeyframeIndex, findContainingRangeIndex, fragmentOverlapsSB, formatBufferedRanges,\n getAvgDownloadRate, getAdaptiveTimeout, getRetryDelay, gapOverlapsPendingFetch,\n findKeyframePredecessor,\n} from './bufferUtils';\nimport { buildFragmentUrl, isFragmentDeliveryEnabled, type DeliveryConfig, type DeliveryTarget } from './fragmentDelivery';\n\nexport type { Fragment } from './bufferUtils';\n\ndeclare global {\n interface Window {\n /** Live video/audio FMs, published by MediaPlayer for healthReporter and E2E. */\n __FRAGMENT_MANAGER__?: FragmentManager | null;\n __AUDIO_FRAGMENT_MANAGERS__?: FragmentManager[] | null;\n }\n}\n\n/** Per-fragment init-phase timings, digested by emitInitDigest. */\ninterface FragInitTiming {\n fetchStart?: number; fetchEnd?: number; queued?: number; appendDone?: number;\n src?: 'cache' | 'net'; err?: string; bytes?: number;\n ttfbMs?: number; cf?: string; // upstream-layer signal: time-to-first-byte and CF cache status\n}\n\n/** Compact digest entry: ms relative to loadStartTs, deltas only. */\ninterface FragInitDigestEntry {\n i: number; src: string;\n fs?: number; fM?: number; qD?: number; aM?: number; B?: number; tt?: number; cf?: string; e?: string;\n}\n\nexport class FragmentManager {\n private fragments: Fragment[] = [];\n private mediaUrl = '';\n /** Fragment-route addressing for this media, when available. Null => Range only. */\n private delivery: DeliveryConfig | null = null;\n /** Sticky kill-switch. Set on the FIRST fragment-route failure of any kind and never cleared\n * until the next setIndex(), so one bad response costs one extra round trip and everything\n * after it is served by the Range path that has always worked. */\n private fragmentRouteDisabled = false;\n private fragmentRouteHits = 0;\n private mediaId = '';\n private fragmentStates = new Map<number, FragmentState>();\n private sourceBuffer: SourceBuffer | null = null;\n private mediaSource: MediaSource | null = null;\n private currentJobs = new Map<number, FetchJob>();\n private appendQueue: { index: number; buffer: ArrayBuffer }[] = [];\n private appendWaiters: AppendWaiter[] = [];\n private bufferedWaiters: { timeMs: number; resolve: () => void; reject: (err: Error) => void }[] = [];\n private isAppending = false;\n private disposed = false;\n get isDisposed(): boolean { return this.disposed; }\n private downloadSamples: { bytes: number; durationMs: number }[] = [];\n private currentPlayheadMs = 0;\n /** Where a seek is trying to get to, or null when none is outstanding.\n * Eviction reads it so it cannot delete the very data the seek is waiting on\n * (evictionStrategy.ts documents the failure).\n *\n * **It must outlive the FETCH.** The first version cleared it in\n * _fetchRange's finally, which is too early: waitForAppend resolves the\n * instant the TARGET fragment appends, while the rest of that same fetch is\n * still queued — and those trailing appends re-triggered the orphan rule with\n * the protection already gone, deleting the whole island including the\n * target. Observed on the 2026-09-15 verification run: APPEND-DONE #143\n * (the target) resolved the wait, then #144's append produced\n * `EVICT-START [forward-orphan] range=286.29-290.29 playhead=227.15`.\n * So it is cleared when the PLAYHEAD ARRIVES instead — the seek is over when\n * currentTime is actually there, not when the bytes are.\n *\n * Deliberately NOT `currentPlayheadMs`: that is also written by prefetch and\n * by playback, so it could never be cleared at all. */\n private pendingSeekTargetMs: number | null = null;\n /** When pendingSeekTargetMs was set. A seek that is abandoned outright (its\n * promise rejected, the room moved on) would otherwise protect its region for\n * the life of the media. Checked lazily on read — no timer, per the\n * no-periodic-timers rule. */\n private pendingSeekSetAtMs = 0;\n /**\n * In-flight fetches, by fragment index. **Sequential append guard — DO NOT REMOVE.**\n *\n * `processAppendQueue()` holds the queue whenever any pending fetch has a\n * LOWER index than the queue head. Appending past a gap creates permanent\n * SourceBuffer gaps in **every** browser (Chrome, Firefox, Edge) — bytes that\n * can never be filled by re-fetching, because MSE will not splice them in.\n *\n * Invariants, all four required:\n * - `.add(index)` at the start of `fetchFragment()`\n * - `.delete(index)` in the `updateend` handler (data CONFIRMED in the SB)\n * - `.delete(index)` in `fetchFragment`'s `finally`, but ONLY when the\n * fragment is not queued for append (the error/abort path)\n * - `.clear()` in `abortAllJobs()`\n *\n * A queue item keeps its entry until the SourceBuffer confirms via\n * `updateend` — not when the fetch resolves.\n *\n * **The hold is BOUNDED by `APPEND_HOLD_MAX_MS` — do not remove the escape.**\n * The guard has no timeout of its own, which is correct for a fetch that will\n * land and a permanent wedge for one that will not: a fragment whose append is\n * reported discarded goes back to `pending`, is re-fetched, re-takes the lower\n * index, and so holds every later append forever. `_breakStuckAppendHold`\n * gives up on the BLOCKERS, never on ordering — it aborts their fetches and\n * marks them `fetchErrorCycles = MAX_FETCH_ERROR_CYCLES`, the same \"structural\n * gap, not a blocker\" treatment `prefetchAround` applies. **Do NOT weaken it\n * into \"append out of order after a timeout\"** — that recreates exactly the\n * permanent gaps this guard exists to prevent. The result is a bounded gap\n * that gap-skip and `reconcileLoadedStates` can act on, instead of a queue\n * that never drains. Warns `APPEND-HOLD-BREAK`; the ordinary `APPEND-HOLD`\n * line is `debug()` and never reaches a session log, which is why the wedge\n * was invisible. Regression test: `fragmentManager.appendHoldBreak.test.ts`.\n */\n private pendingFetchIndices = new Set<number>();\n private appendingIndex: number | null = null;\n /** Queue head currently held by the sequential-append guard, and when that\n * hold began. Lets processAppendQueue bound a hold that would otherwise\n * never end — see APPEND_HOLD_MAX_MS. */\n private appendHold: { index: number; since: number } | null = null;\n private eviction: EvictionStrategy = new EvictionStrategy({\n getFragments: () => this.fragments,\n getSourceBuffer: () => this.sourceBuffer,\n getMediaSource: () => this.mediaSource,\n isDisposed: () => this.disposed,\n fragmentStates: this.fragmentStates,\n getPendingSeekTargetMs: () => (\n this.pendingSeekTargetMs !== null\n && Date.now() - this.pendingSeekSetAtMs > PENDING_SEEK_PROTECT_MS\n ? null\n : this.pendingSeekTargetMs\n ),\n });\n private lastGapWarnMs = 0;\n private stableGapSignature: string | null = null; // join of sorted gap ranges; changes when gap moves/shrinks\n private stableGapSince = 0; // timestamp when current stableGapSignature first appeared\n private onBufferingChange?: (isBuffering: boolean) => void;\n private preAppendRanges: { start: number; end: number }[] = []; // snapshot for discard detection\n private discardCounts = new Map<number, number>(); // consecutive discard count per fragment\n private initState: 'pending' | 'loading' | 'loaded' = 'pending';\n private initRetryCount = 0; // consecutive fetchInit() failures (reset on success / setIndex)\n private initFailed = false; // gave up on init after MAX_INIT_FETCH_RETRIES — reject waiters fast\n private initRange: { start_byte: number; end_byte: number } | null = null;\n private errorTimestamps = new Map<number, number>();\n private fetchErrorCycles = new Map<number, number>(); // persistent cycle count across 30s resets — not cleared on seek\n\n // Init-phase telemetry (reset on setIndex). Captures first\n // INIT_DIGEST_TRACK_COUNT fragments' fetch/queue/append timings, init segment\n // timing, and APPEND-HOLD reason counts. Read by emitInitDigest() which warns\n // once at gate-pass time so the load shape is visible in session logs without\n // wp:reportLogs=true.\n private static readonly INIT_TRACK_FRAGS = INIT_DIGEST_TRACK_COUNT;\n private loadStartTs = 0;\n private initFetchStart = 0;\n private initFetchEnd = 0;\n private initAppendDone = 0;\n private initTtfbMs = -1;\n private initCfStatus = '';\n private fragInitTimings = new Map<number, FragInitTiming>();\n private initHoldCounts = { byPendingFetch: 0, byInProgressAppend: 0, byInit: 0 };\n private initDigestEmitted = false;\n\n /** Epoch tag set by MediaPlayer to coordinate with the fragments-late-arrival effect. */\n epoch = 0;\n /** True after setIndex has been called at least once on this instance. */\n get hasIndex(): boolean { return this.fragments.length > 0; }\n\n /** Cache key passed to setIndex(). For audio FMs we use\n * `${mediaId}::audio::${track_id}` so swapping tracks creates a unique key. */\n get cacheKey(): string { return this.mediaId; }\n\n /** When multiple FMs share one MediaSource (video + N audio tracks),\n * only one of them should call endOfStream at the last fragment. Audio FMs\n * set this true so they don't race to end the stream before video finishes. */\n private suppressEndOfStream = false;\n\n /** True from the moment the video FM observes its last fragment land in the\n * SB until endOfStream() successfully signals. Decoupled from the updateend\n * event loop so we can retry when a concurrent audio SB updating blocks the\n * call. See tryEndOfStream(). */\n private pendingEndOfStream = false;\n private eosRetryTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** 'video' | 'audio:<trackId>'. Purely diagnostic, but load-bearing for\n * diagnosis: video and audio run separate FragmentManagers over nearly\n * identical timestamps, so an un-labelled per-fragment warning cannot be\n * attributed to a track. That cost a full investigation cycle on the\n * 2026-09-15 AUTO-RELOAD loop — `waitForAppend timeout {index:734}` was\n * unattributable, and video has 735 fragments where audio has 734, so the\n * two readings implied opposite root causes. */\n private label: string;\n\n constructor(\n onBufferingChange?: (isBuffering: boolean) => void,\n opts?: { suppressEndOfStream?: boolean; label?: string },\n ) {\n this.onBufferingChange = onBufferingChange;\n this.suppressEndOfStream = !!opts?.suppressEndOfStream;\n this.label = opts?.label ?? 'video';\n }\n\n /** Reject every pending append + buffered waiter with the same error and\n * clear both lists. Used at teardown / SB-error / dispose paths to unblock\n * any promise awaiting buffer progress so callers don't hang for 60s. */\n private rejectAllWaiters(err: Error): void {\n for (const w of this.appendWaiters) w.reject(err);\n this.appendWaiters = [];\n for (const w of this.bufferedWaiters) w.reject(err);\n this.bufferedWaiters = [];\n }\n\n /** Return a fragment to 'pending' and drop it from pendingFetchIndices so\n * downstream code can re-fetch or re-queue. Narrow helper that collapses\n * the \"abandon this fetch\" cleanup scattered across abort/discard paths. */\n private resetFragmentState(index: number): void {\n this.pendingFetchIndices.delete(index);\n this.fragmentStates.set(index, 'pending');\n }\n\n /**\n * Try to fetch bytes via the edge-cacheable fragment route.\n *\n * Returns the buffer on success, or **null meaning \"use the Range path\"** — never throws.\n * That is the whole safety contract: every failure mode here degrades to the delivery path\n * this app has always used, within the SAME attempt, so a broken/absent/misconfigured\n * fragment route costs one wasted round trip and is otherwise invisible to playback.\n *\n * Any failure also sets the sticky `fragmentRouteDisabled`, so we probe once per media rather\n * than paying that round trip on all ~711 fragments.\n *\n * The length check is not a formality. The route answers 200 with no Content-Range, so unlike\n * the Range path there is no 206-vs-200 signal to catch a misrouted response — a wrong-but-\n * plausible body would be appended to the SourceBuffer and surface much later as a corrupt\n * fragment. Byte-identity with the Range path is verified server-side by\n * `fragment-route.realmedia.test.ts`; this is the client-side backstop.\n */\n private async tryFragmentRoute(\n target: DeliveryTarget,\n expectedBytes: number,\n signal: AbortSignal,\n ): Promise<{ buf: ArrayBuffer; ttfbMs: number; cfStatus: string } | null> {\n if (this.fragmentRouteDisabled || !this.delivery || !isFragmentDeliveryEnabled()) return null;\n const url = buildFragmentUrl(this.delivery, target);\n if (!url) {\n this.disableFragmentRoute(target, 'could not build a URL for this config');\n return null;\n }\n try {\n const t0 = performance.now();\n // Deliberately NO Range header: this URL addresses a whole object, and sending one would\n // invite the very edge Range-collapse behaviour the route exists to sidestep.\n const res = await fetch(url, {\n headers: { 'X-Buffer-Ahead-Ms': String(Math.round(this.getBufferedAheadMs(this.currentPlayheadMs))) },\n signal,\n });\n const ttfbMs = Math.round(performance.now() - t0);\n if (res.status !== 200) {\n await res.body?.cancel().catch(() => { /* already closed */ });\n this.disableFragmentRoute(target, `HTTP ${res.status}`);\n return null;\n }\n const buf = await res.arrayBuffer();\n if (buf.byteLength !== expectedBytes) {\n this.disableFragmentRoute(target, `length ${buf.byteLength} != expected ${expectedBytes}`);\n return null;\n }\n this.fragmentRouteHits++;\n return { buf, ttfbMs, cfStatus: res.headers.get('cf-cache-status') || res.headers.get('x-cache') || '' };\n } catch (e) {\n // An abort is OUR OWN doing, not a route failure — DO NOT disable on it.\n //\n // `abortAllJobs()` fires on every seek and every media switch, and\n // `disableFragmentRoute` is sticky until the next `setIndex()`. So a single\n // mid-episode seek used to pin that viewer to the Range path for the REST of the\n // episode — and the Range path is the one with the edge-collapse bug and no origin\n // dedup, which is exactly what happened next: cedar-ridge 2026-08-25 02:23:39,\n // `reason: \"The user aborted a request.\"` after 708 successfully edge-cached\n // fragments, immediately followed by `Fragment #739 range dropped upstream — got\n // 200, expected 206` with `contentLength: 1022519875` — a 1 GB body for a 68KB\n // fragment, `cf-cache-status: BYPASS`.\n //\n // Returning null still falls back to Range for THIS attempt, which is correct and\n // costs nothing: the caller's Range fetch shares the aborted signal, throws\n // immediately, and its own `AbortError && ac.signal.aborted` branch resets the\n // fragment to pending without retrying.\n //\n // Match on the error NAME, not `signal.aborted`. `AbortSignal.timeout()` — which the\n // init path passes here — also sets `aborted`, but throws `TimeoutError`, and a\n // route that timed out IS a genuine failure worth falling back for.\n if ((e as Error)?.name === 'AbortError') return null;\n this.disableFragmentRoute(target, (e as Error)?.message || 'fetch threw');\n return null;\n }\n }\n\n private disableFragmentRoute(target: DeliveryTarget, reason: string): void {\n if (this.fragmentRouteDisabled) return;\n this.fragmentRouteDisabled = true;\n // warn, not debug: falling back is safe but it means the edge-cache path is not working,\n // which is invisible in playback and would otherwise go unnoticed in production.\n warn('buffer', 'FRAG-ROUTE disabled, falling back to Range', {\n target: String(target), reason, hitsBeforeFailure: this.fragmentRouteHits, mediaId: this.mediaId,\n });\n }\n\n setPlayhead(ms: number): void { this.currentPlayheadMs = ms; }\n\n setIndex(fragments: Fragment[], mediaUrl: string, initSegment?: { start_byte: number; end_byte: number }, mediaId = '', delivery: DeliveryConfig | null = null): void {\n this.fragments = fragments;\n this.mediaUrl = mediaUrl;\n this.mediaId = mediaId;\n // New media gets a fresh chance at the fragment route; a previous media's failure must not\n // pin the whole session to Range.\n this.delivery = delivery;\n this.fragmentRouteDisabled = false;\n this.fragmentRouteHits = 0;\n this.fragmentStates.clear();\n this.errorTimestamps.clear();\n this.discardCounts.clear();\n this.fetchErrorCycles.clear();\n this.appendHold = null;\n fragments.forEach((_, i) => this.fragmentStates.set(i, 'pending'));\n this.initRange = initSegment ?? null;\n this.initState = 'pending';\n this.initRetryCount = 0;\n this.initFailed = false;\n // Reset init-phase telemetry for this load. emitInitDigest() will warn\n // once when the buffer gate clears, summarizing what happened.\n this.loadStartTs = performance.now();\n this.initFetchStart = 0;\n this.initFetchEnd = 0;\n this.initAppendDone = 0;\n this.initTtfbMs = -1;\n this.initCfStatus = '';\n this.fragInitTimings.clear();\n this.initHoldCounts = { byPendingFetch: 0, byInProgressAppend: 0, byInit: 0 };\n this.initDigestEmitted = false;\n }\n\n /** True while we should record per-fragment init timings. We track only the\n * first INIT_TRACK_FRAGS distinct indices and stop after the digest fires\n * to keep memory bounded and avoid logging steady-state behavior. */\n private isInInitPhase(): boolean {\n return !this.initDigestEmitted && this.fragInitTimings.size < FragmentManager.INIT_TRACK_FRAGS;\n }\n\n private recordInitFragEvent(index: number, patch: FragInitTiming): void {\n if (this.initDigestEmitted) return;\n const existing = this.fragInitTimings.get(index);\n if (!existing) {\n // Don't start tracking new indices once we've hit the cap.\n if (this.fragInitTimings.size >= FragmentManager.INIT_TRACK_FRAGS) return;\n this.fragInitTimings.set(index, { ...patch });\n } else {\n Object.assign(existing, patch);\n }\n }\n\n setSourceBuffer(sb: SourceBuffer, ms: MediaSource): void {\n this.sourceBuffer = sb;\n this.mediaSource = ms;\n\n sb.addEventListener('updateend', () => {\n // Guard: during teardown the SourceBuffer may have been removed from the\n // MediaSource. Accessing sb.buffered then throws. Reject all waiters so\n // initializePlayback doesn't hang for 60s on the orphaned promise.\n if (this.disposed || this.mediaSource?.readyState !== 'open') {\n this.isAppending = false;\n if (this.appendingIndex !== null) {\n this.pendingFetchIndices.delete(this.appendingIndex);\n this.appendingIndex = null;\n }\n this.rejectAllWaiters(new Error('SourceBuffer detached during append'));\n return;\n }\n const appendedIdx = this.appendingIndex;\n // Captured BEFORE the branch below: onUpdateendComplete() clears\n // isEvicting, so afterwards there is no way to tell an eviction's\n // updateend from an append's — and an eviction has already re-armed\n // itself, so retrying here would just duplicate that pass.\n const wasEvictionUpdateend = appendedIdx === null && this.eviction.isEvicting;\n if (appendedIdx !== null) {\n this.pendingFetchIndices.delete(appendedIdx);\n this.appendingIndex = null;\n this._onAppendFinished(sb, appendedIdx);\n } else if (this.eviction.isEvicting) {\n // updateend from a sourceBuffer.remove() call — eviction complete\n debug('buffer', `EVICT-DONE sbRanges=[${formatBufferedRanges(sb.buffered).join(', ')}]`);\n // Strategy clears its isEvicting flag and re-arms evictBefore with the\n // stored playhead — remove() only handles one range per call so there\n // may be more to drop.\n this.eviction.onUpdateendComplete();\n }\n // Release the append lock ONLY in updateend — this prevents the race\n // where a synchronous re-entrant call to processAppendQueue() starts a\n // second appendBuffer() before the browser has set sourceBuffer.updating.\n this.isAppending = false;\n this._detectUnfillableGaps(sb);\n // Trim BEFORE refilling the queue. This is the only point where the\n // SourceBuffer is guaranteed idle, and processAppendQueue() below may\n // immediately start another append — see retryPendingEviction().\n if (!wasEvictionUpdateend) this.eviction.retryPendingEviction();\n this.processAppendQueue();\n this.checkAppendWaiters();\n this.checkBufferedWaiters();\n });\n sb.addEventListener('error', (e) => {\n logError('buffer', 'SourceBuffer error', { error: e });\n if (this.appendingIndex !== null) {\n this.resetFragmentState(this.appendingIndex);\n this.appendingIndex = null;\n }\n this.sourceBuffer = null; // Prevent infinite pageerror loops from polling dead SB\n // Clean up pendingFetchIndices for queued items that will never be appended\n for (const item of this.appendQueue) {\n this.pendingFetchIndices.delete(item.index);\n }\n this.appendQueue = [];\n this.isAppending = false;\n this.eviction.reset();\n this.abortAllJobs();\n this.rejectAllWaiters(new Error('SourceBuffer error'));\n });\n if (this.appendQueue.length > 0) this.processAppendQueue();\n }\n\n async fetchInit(): Promise<void> {\n if (!this.initRange || this.initState === 'loaded' || !this.sourceBuffer || this.disposed) return;\n if (this.initState === 'loading') return; // already in flight\n // MediaSource may have transitioned to 'ended' or 'closed' between the\n // queueing of this call and now (e.g. premature endOfStream errored the\n // pipeline). Appending to a detached SB throws \"SourceBuffer has been\n // removed from the parent media source\" and traps FM in a retry loop —\n // bail cleanly instead. Mirrors the updateend / processAppendQueue guards.\n if (this.mediaSource?.readyState !== 'open') {\n debug('buffer', `INIT-SKIP — mediaSource.readyState=${this.mediaSource?.readyState ?? 'null'}`);\n return;\n }\n this.initState = 'loading';\n this.initFetchStart = performance.now();\n const { start_byte, end_byte } = this.initRange;\n // Capture mediaId/mediaUrl/sourceBuffer at fetch START. If setIndex is\n // called during the await — typical for a playlist clip-switch — these\n // identifiers drift on `this` but the bytes we're holding belong to the\n // OLD media. Writing them to the cache or appending them to the NEW\n // SourceBuffer is the exact race that crashed Firefox's MP4 parser\n // (Bugs 3/4/9 from the 2026-05-20 cedar-ridge session — \"Invalid Top-\n // Level Box: mfh/hd\"). Re-check the captured values at every awaitable\n // boundary and bail out cleanly if any has drifted.\n const expectedMediaId = this.mediaId;\n const expectedMediaUrl = this.mediaUrl;\n const expectedSb = this.sourceBuffer;\n const stale = () =>\n this.disposed\n || this.mediaId !== expectedMediaId\n || this.mediaUrl !== expectedMediaUrl\n || this.sourceBuffer !== expectedSb;\n try {\n let buf: ArrayBuffer;\n const cached = expectedMediaId ? playlistFragmentCache.getInit(expectedMediaId) : undefined;\n if (cached) {\n debug('buffer', `INIT-CACHE-HIT bytes=${start_byte}-${end_byte}`);\n buf = cached;\n } else {\n debug('buffer', `INIT-FETCH bytes=${start_byte}-${end_byte} (${end_byte - start_byte + 1}B)`);\n // Fragment route first when enabled; null => fall straight through to Range below,\n // in this same attempt, so a failure here never costs playback anything.\n const viaFrag = await this.tryFragmentRoute(\n 'init', end_byte - start_byte + 1,\n AbortSignal.timeout(getAdaptiveTimeout(this.getAverageDownloadRate())),\n );\n if (viaFrag) {\n this.initTtfbMs = viaFrag.ttfbMs;\n this.initCfStatus = viaFrag.cfStatus;\n buf = viaFrag.buf;\n if (stale()) {\n warn('buffer', 'INIT-FETCH stale on completion, discarding bytes', {\n expectedMediaId, currentMediaId: this.mediaId,\n });\n return;\n }\n if (expectedMediaId) playlistFragmentCache.storeInit(expectedMediaId, buf);\n } else {\n const t0 = performance.now();\n const res = await fetch(expectedMediaUrl, {\n headers: { Range: `bytes=${start_byte}-${end_byte}` },\n signal: AbortSignal.timeout(getAdaptiveTimeout(this.getAverageDownloadRate())),\n });\n this.initTtfbMs = Math.round(performance.now() - t0);\n this.initCfStatus = res.headers.get('cf-cache-status') || res.headers.get('x-cache') || '';\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n // We sent a Range header, so the ONLY valid success is 206. A 200 means\n // a proxy/CDN edge ignored the range and returned the FULL file (~1.2GB\n // for a feature) — appending that as the init segment throws Chrome's\n // \"MediaSource buffer not sufficient\" and corrupts the SourceBuffer.\n // Reject it so the retry/recreate path runs instead (cedar-ridge\n // 2026-07-09 hardening).\n if (res.status !== 206) {\n // Cancel before throwing. A collapsed-Range 200 carries the WHOLE\n // file (measured 744MB on prod, `content-length: 744133112`, no\n // content-range); dropping the Response on the floor leaves that\n // body streaming from the origin until GC or the socket closes,\n // burning a media queue slot and residential upload for bytes\n // nobody will read. See \"Edge Range collapsing\" in CLAUDE.md.\n await res.body?.cancel().catch(() => { /* already closed */ });\n throw new Error(`init expected 206, got ${res.status}`);\n }\n buf = await res.arrayBuffer();\n if (stale()) {\n warn('buffer', 'INIT-FETCH stale on completion, discarding bytes', {\n expectedMediaId, currentMediaId: this.mediaId,\n });\n return;\n }\n if (expectedMediaId) playlistFragmentCache.storeInit(expectedMediaId, buf);\n }\n }\n this.initFetchEnd = performance.now();\n if (stale()) {\n debug('buffer', 'INIT skip append — media drifted before sb.appendBuffer');\n return;\n }\n // Append init segment directly — wait for updateend\n await new Promise<void>((resolve, reject) => {\n const sb = expectedSb;\n const onEnd = () => { sb.removeEventListener('updateend', onEnd); sb.removeEventListener('error', onErr); resolve(); };\n const onErr = (e: Event) => { sb.removeEventListener('updateend', onEnd); sb.removeEventListener('error', onErr); reject(e); };\n sb.addEventListener('updateend', onEnd);\n sb.addEventListener('error', onErr);\n sb.appendBuffer(buf);\n });\n // Last-chance staleness check — append may have completed for the OLD\n // SourceBuffer that was already replaced; don't flip initState for the\n // new media in that case (new fetchInit will take over).\n if (stale()) {\n debug('buffer', 'INIT append completed for stale media, ignoring');\n return;\n }\n this.initAppendDone = performance.now();\n this.initState = 'loaded';\n this.initRetryCount = 0;\n debug('buffer', `INIT-LOADED sbRanges=[${formatBufferedRanges(this.sourceBuffer?.buffered).join(', ')}]`);\n this.processAppendQueue(); // kick queued media fragments\n } catch (e) {\n logError('buffer', 'Init segment fetch/append failed', { error: (e as Error).message });\n // Only flip state for retry if we're still the active media — a stale\n // failure for old media must not pull the new media's initState back\n // to 'pending'.\n if (!stale()) {\n this.initState = 'pending';\n this.initRetryCount += 1;\n if (this.initRetryCount >= MAX_INIT_FETCH_RETRIES) {\n // Persistent init failure. Looping against this MediaSource won't\n // help (a stale byte range / corrupt SB / 404 stays broken). Give up\n // and reject any waiters so initializePlayback's waitForAppend(0)\n // fails fast and recreates the MediaSource — instead of blocking on\n // two 30s waitForAppend timeouts (~65s frozen, cedar-ridge 2026-06-25).\n this.initFailed = true;\n warn('buffer', 'INIT-GIVE-UP — init segment failed repeatedly; rejecting waiters for fast recovery', {\n attempts: this.initRetryCount, error: (e as Error).message,\n });\n this.rejectAllWaiters(new Error('init segment failed after repeated retries'));\n } else if (!this.disposed) {\n setTimeout(() => {\n if (!this.disposed && this.initState === 'pending') {\n this.fetchInit().catch((err: unknown) => {\n logError('buffer', 'INIT-RETRY failed', errorFields(err));\n });\n }\n }, INIT_RETRY_DELAY_MS);\n }\n }\n }\n }\n\n /** Runs inside updateend after an append completes. Decides whether MSE\n * actually buffered the fragment (landed) or silently discarded it, then\n * records the outcome + maybe fires end-of-stream.\n *\n * Two success paths (both count as landed):\n * 1. SB buffered ranges changed — a new range was created or an existing\n * one extended. Compare against preAppendRanges snapshot.\n * 2. Fragment's nominal range is fully inside an existing SB range —\n * redundant re-append after eviction reset the state to pending but\n * the data was never actually evicted.\n *\n * Pure geometric overlap (fragmentOverlapsSB) has false positives for\n * boundary fragments: e.g., [6.0-8.0s] \"overlaps\" SB [0.08-6.13] at the\n * tail by 0.13s, but MSE didn't actually buffer the 6.13-8.0s portion.\n * The before/after range comparison catches this. */\n private _onAppendFinished(sb: SourceBuffer, appendedIdx: number): void {\n const frag = this.fragments[appendedIdx];\n let inSb = false;\n if (frag && sb.buffered.length > 0) {\n const pre = this.preAppendRanges;\n if (sb.buffered.length !== pre.length) {\n inSb = true;\n } else {\n for (let ri = 0; ri < sb.buffered.length; ri++) {\n if (Math.abs(sb.buffered.start(ri) - pre[ri].start) > SB_TOLERANCE.EQUAL\n || Math.abs(sb.buffered.end(ri) - pre[ri].end) > SB_TOLERANCE.EQUAL) {\n inSb = true;\n break;\n }\n }\n }\n if (!inSb) {\n const startSec = frag.start_ms / 1000;\n const endSec = frag.end_ms / 1000;\n for (let ri = 0; ri < sb.buffered.length; ri++) {\n if (sb.buffered.start(ri) <= startSec + SB_TOLERANCE.RANGE && sb.buffered.end(ri) >= endSec - SB_TOLERANCE.RANGE) {\n inSb = true;\n break;\n }\n }\n }\n }\n\n if (inSb) {\n this.fragmentStates.set(appendedIdx, 'loaded');\n this.discardCounts.delete(appendedIdx);\n this.fetchErrorCycles.delete(appendedIdx);\n perf.recordAppendLanded(appendedIdx);\n if (this.isInInitPhase()) {\n this.recordInitFragEvent(appendedIdx, { appendDone: performance.now() });\n }\n debug('buffer', `APPEND-DONE #${appendedIdx} sbRanges=[${formatBufferedRanges(sb.buffered).join(', ')}] queueLen=${this.appendQueue.length}`);\n // Signal end of stream when the last fragment has landed in the SB.\n // Without this, the video element waits indefinitely for more data\n // instead of firing 'ended', causing a permanent stall at clip end.\n // See tryEndOfStream() for the MS.endOfStream() retry rationale.\n //\n // GUARD: a viewer who joins mid-playback near the end can land on the\n // last fragment as their FIRST append. Buffered range is then just\n // ~that one fragment, MediaSource has no data to play TO the end, and\n // endOfStream() makes the pipeline error with DEMUXER_ERROR_COULD_NOT_OPEN\n // — which tears the SourceBuffer down and traps FM in an infinite\n // init/fetch retry loop (saw this on Viewer-1 in chaos-mega-1779725490887\n // when their first append was fragment #754, the last index). Require\n // the buffered range covering the last fragment to be wide enough that\n // we actually played through to get here. A natural EOS has at least\n // BEHIND_KEEP_SEC (60s) of behind-buffer plus current; mid-join only\n // has the single fragment. 8s is a comfortable margin above any single\n // fragment duration in our media (max ~2s) but well below typical\n // end-of-clip buffer width.\n if (appendedIdx === this.fragments.length - 1\n && this.appendQueue.length === 0\n && !this.suppressEndOfStream) {\n const lastFragEndSec = this.fragments[appendedIdx].end_ms / 1000;\n const mediaDurationSec = (this.fragments[appendedIdx].end_ms - this.fragments[0].start_ms) / 1000;\n let lastRangeWidth = 0;\n for (let i = sb.buffered.length - 1; i >= 0; i--) {\n const s = sb.buffered.start(i), e = sb.buffered.end(i);\n if (e >= lastFragEndSec - SB_TOLERANCE.RANGE) {\n lastRangeWidth = e - s;\n break;\n }\n }\n // Threshold scales for very short clips: at least half the clip must\n // be buffered before declaring EOS, capped at 8s for normal-length media.\n const minWidth = Math.min(8, mediaDurationSec * 0.5);\n if (lastRangeWidth < minWidth) {\n warn('buffer', `EOS-SKIP — last frag appended but buffered span ${lastRangeWidth.toFixed(2)}s < ${minWidth.toFixed(2)}s threshold; mid-join, not end-of-clip ${JSON.stringify({\n appendedIdx, mediaDurationSec: mediaDurationSec.toFixed(1), sbRanges: formatBufferedRanges(sb.buffered),\n })}`);\n } else {\n debug('buffer', 'END-OF-STREAM requested — last fragment confirmed in SB');\n this.pendingEndOfStream = true;\n this.tryEndOfStream();\n }\n }\n } else {\n // Fragment was silently discarded by MSE. Track consecutive discards\n // so prefetchAround can stop retrying after repeated failures.\n const dc = (this.discardCounts.get(appendedIdx) ?? 0) + 1;\n this.discardCounts.set(appendedIdx, dc);\n this.fragmentStates.set(appendedIdx, 'pending');\n // The append reported success (updateend fired) but produced no buffered\n // range overlapping the fragment. Two distinct causes, told apart by the\n // fragment's OWN keyframe flag — don't guess \"missing keyframe\" blindly:\n // kf === false → MSE dropped a non-IDR fragment appended without a\n // preceding keyframe in the SB (normally prevented by keyframe-aware\n // fetch; a real structural cause).\n // kf === true (or legacy undefined) → the bytes were structurally valid\n // yet MSE still refused them. That is NOT a media defect — it is the\n // signature of the browser's own coded-frame eviction / memory-quota\n // pressure, i.e. a starved/overloaded host. A RUN of these on kf:true\n // fragments (mistral-bluff 2026-07-28, #527–534, leader on a machine\n // saturated by a background download) is a strong \"browser MSE is\n // choking\" signal — the buffer can read full while appends fail.\n const kf = this.fragments[appendedIdx]?.keyframe;\n const cause = kf === false\n ? 'non-keyframe fragment, needs preceding IDR in SB'\n : 'keyframe-valid bytes refused — likely browser MSE eviction/pressure (overloaded host?)';\n warn('buffer', `APPEND-DISCARDED #${appendedIdx} (attempt ${dc}, kf=${kf ?? 'n/a'}) not in SB after appendBuffer — ${cause}; sbRanges=[${formatBufferedRanges(sb.buffered).join(', ')}] queueLen=${this.appendQueue.length}`);\n }\n }\n\n /** Two-stage gap detection, runs on every updateend. A gap is worth warning\n * about only when:\n * (1) no in-flight fragment (appendQueue or pendingFetchIndices) overlaps\n * the gap region (stage 1 catches fetches-in-progress), AND\n * (2) the same gap pattern has been stable for ≥5s across consecutive\n * updateend events (stage 2 catches the brief idle window between\n * queue drain and the next prefetch tick, where appendQueue and\n * pendingFetchIndices are transiently empty but the gap is actively\n * shrinking as fragments batch in).\n *\n * Tracker state (stableGapSignature, stableGapSince, lastGapWarnMs) is\n * reset when the gap shape changes or resolves. */\n private _detectUnfillableGaps(sb: SourceBuffer): void {\n if (sb.buffered.length <= 1) {\n this.stableGapSignature = null;\n this.stableGapSince = 0;\n return;\n }\n const inFlightIndices = new Set<number>([\n ...this.appendQueue.map(q => q.index),\n ...this.pendingFetchIndices,\n ]);\n const unfillableGaps: string[] = [];\n const playheadSec = this.currentPlayheadMs / 1000;\n for (let i = 1; i < sb.buffered.length; i++) {\n const gapStart = sb.buffered.end(i - 1);\n const gapEnd = sb.buffered.start(i);\n // A gap the playhead has already passed will never be played, and\n // nothing is meant to fill it — eviction removes the range before it. A\n // forward seek on the nearby route leaves exactly that by design (it\n // keeps the old buffer and fetches from the target): nine false warns\n // from one 412s→458s seek in a 2026-10-20 fleet run.\n if (gapEnd <= playheadSec) continue;\n if (!gapOverlapsPendingFetch(gapStart, gapEnd, this.fragments, inFlightIndices)) {\n unfillableGaps.push(`${gapStart.toFixed(2)}-${gapEnd.toFixed(2)}`);\n }\n }\n if (unfillableGaps.length === 0) {\n this.stableGapSignature = null;\n this.stableGapSince = 0;\n return;\n }\n const signature = unfillableGaps.join('|');\n const now = Date.now();\n if (this.stableGapSignature !== signature) {\n this.stableGapSignature = signature;\n this.stableGapSince = now;\n } else if (now - this.stableGapSince >= STABLE_GAP_DWELL_MS\n && now - this.lastGapWarnMs > STABLE_GAP_DWELL_MS) {\n this.lastGapWarnMs = now;\n warn('buffer', `SB has ${sb.buffered.length} ranges with ${unfillableGaps.length} stable unfillable gap(s) ${JSON.stringify({\n ranges: formatBufferedRanges(sb.buffered),\n unfillableGaps,\n stableForMs: now - this.stableGapSince,\n appendQueue: this.appendQueue.map(q => q.index),\n pending: Array.from(this.pendingFetchIndices).sort((a, b) => a - b),\n })}`);\n }\n }\n\n private checkAppendWaiters(): void {\n const still: AppendWaiter[] = [];\n for (const w of this.appendWaiters) {\n this.fragmentStates.get(w.index) === 'loaded' ? w.resolve() : still.push(w);\n }\n this.appendWaiters = still;\n }\n\n private checkBufferedWaiters(): void {\n const still: typeof this.bufferedWaiters = [];\n for (const w of this.bufferedWaiters) {\n this.isBuffered(w.timeMs) ? w.resolve() : still.push(w);\n }\n this.bufferedWaiters = still;\n }\n\n waitForAppend(index: number, timeoutMs?: number): Promise<void> {\n if (this.disposed) return Promise.reject(new Error('FragmentManager disposed'));\n // Init gave up before this waiter registered — fail fast (no 30s timeout)\n // so the caller can recreate the MediaSource. Media fragments can never\n // append without the init segment.\n if (this.initFailed) return Promise.reject(new Error('init segment failed after repeated retries'));\n if (this.fragmentStates.get(index) === 'loaded') return Promise.resolve();\n const t = timeoutMs ?? getAdaptiveTimeout(this.getAverageDownloadRate());\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n this.appendWaiters = this.appendWaiters.filter(w => w.index !== index);\n perf.recordAppendTimeout(index, t, this.appendQueue.length);\n warn('buffer', 'waitForAppend timeout', {\n track: this.label, index, timeoutMs: t, queueDepth: this.appendQueue.length,\n totalFragments: this.fragments.length,\n state: this.fragmentStates.get(index) ?? 'absent',\n });\n reject(new Error(`Timeout waiting for fragment ${index} to append`));\n }, t);\n this.appendWaiters.push({\n index,\n resolve: () => { clearTimeout(timer); resolve(); },\n reject: (err) => { clearTimeout(timer); reject(err); },\n });\n });\n }\n\n isBuffered(timeMs: number): boolean {\n if (!this.sourceBuffer?.buffered) return false;\n // After a MediaSource error → 'ended'/'closed', reading sb.buffered throws\n // \"SourceBuffer has been removed from the parent media source\". Callers\n // (ensureBuffered guards, prefetch heuristics) expect a clean bool; return\n // false so they fall through to a normal \"not buffered yet\" path.\n if (this.mediaSource?.readyState !== 'open') return false;\n return findContainingRangeIndex(this.sourceBuffer.buffered, timeMs / 1000, SB_TOLERANCE.RANGE) >= 0;\n }\n\n /** True when `timeMs` is comfortably inside a buffered range — more than\n * GAP_SKIP_EDGE_SEC of data ahead of it. `isBuffered` is a point test and says\n * yes on a range's trailing edge, which is precisely where a stalled playhead\n * sits: `_fetchRange` bailing on it made every rescue `fetchNearby` a no-op for\n * a viewer frozen 45ms short of their audio buffer's end (mistral-bluff\n * 2026-10-20, 55s). Test: fragmentManager.trailingEdgeRefill.test.ts. */\n private hasDataAhead(timeMs: number): boolean {\n if (!this.sourceBuffer?.buffered || this.mediaSource?.readyState !== 'open') return false;\n const b = this.sourceBuffer.buffered;\n const sec = timeMs / 1000;\n const ri = findContainingRangeIndex(b, sec, SB_TOLERANCE.RANGE);\n return ri >= 0 && b.end(ri) - sec > GAP_SKIP_EDGE_SEC;\n }\n\n /** Wait for a specific time position to be buffered in the SourceBuffer.\n * Event-driven: resolves on updateend when the position enters a buffered range. */\n waitForBuffered(timeMs: number, timeoutMs = 10_000): Promise<boolean> {\n if (this.isBuffered(timeMs)) return Promise.resolve(true);\n if (this.disposed) return Promise.resolve(false);\n return new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => {\n this.bufferedWaiters = this.bufferedWaiters.filter(w => w.timeMs !== timeMs);\n resolve(false);\n }, timeoutMs);\n this.bufferedWaiters.push({\n timeMs,\n resolve: () => { clearTimeout(timer); resolve(true); },\n reject: () => { clearTimeout(timer); resolve(false); },\n });\n });\n }\n\n /**\n * Whether the buffered-geometry REPORTING getters may read `sb.buffered`.\n * `'ended'` counts: that is the normal state once the last fragment is\n * appended (`endOfStream()`), and the SourceBuffers stay attached and\n * readable. Only `'closed'` detaches them (the throw the other guards in this\n * file protect against). These two getters used to require `'open'`, so for\n * the last ~BUFFER_AHEAD of every clip they reported 0 buffered while the\n * whole rest of the clip sat in the buffer: health cards and buffer rings\n * read it as starved, and the party analyzer flagged every viewer as\n * draining at once ~25s into each short trailer (10-5-26 party). Deliberately\n * NOT applied to `isBuffered`/seek routing, which decide what to fetch.\n */\n private canReportBuffered(): boolean {\n const rs = this.mediaSource?.readyState;\n return rs === 'open' || rs === 'ended';\n }\n\n getBufferedEnd(timeMs: number): number {\n if (!this.sourceBuffer?.buffered) return 0;\n if (!this.canReportBuffered()) return 0;\n const sec = timeMs / 1000;\n const b = this.sourceBuffer.buffered;\n const ri = findContainingRangeIndex(b, sec, SB_TOLERANCE.POINT);\n if (ri >= 0) return b.end(ri) * 1000;\n for (let i = 0; i < b.length; i++) {\n if (b.start(i) > sec && b.start(i) - sec <= SB_TOLERANCE.FALLBACK) return b.end(i) * 1000;\n }\n return 0;\n }\n\n getBufferedAheadMs(currentMs: number): number {\n if (!this.sourceBuffer?.buffered?.length) return 0;\n if (!this.canReportBuffered()) return 0;\n const sec = currentMs / 1000;\n const b = this.sourceBuffer.buffered;\n const ri = findContainingRangeIndex(b, sec, SB_TOLERANCE.RANGE);\n if (ri >= 0) return (b.end(ri) - sec) * 1000;\n // Fallback: fMP4 container overhead can push buffered.start(0) up to ~0.6s\n // past the fragment's nominal start under load. If a range starts within 1s\n // ahead, count buffer from our query point through that range's end.\n for (let i = 0; i < b.length; i++) {\n if (b.start(i) > sec && b.start(i) - sec <= SB_TOLERANCE.FALLBACK) {\n return (b.end(i) - sec) * 1000;\n }\n }\n return 0;\n }\n\n /**\n * Reconcile fragmentStates against the real SourceBuffer.\n *\n * The browser runs its OWN quota-driven eviction on the SourceBuffer: when\n * the buffer grows past an internal limit it silently drops old data —\n * without firing any event and without going through our EvictionStrategy.\n * Our `fragmentStates` map is never told, so those fragments stay marked\n * 'loaded' while their data is gone. A backward seek into such a region\n * then routes to `nearby-pending`/skips them in `_fetchRange` toFetch\n * (state === 'loaded'), fetches nothing, and `v.currentTime` lands on\n * absent data — `waitForSeeked` times out and the player freezes.\n *\n * Fix: any fragment marked 'loaded' whose range is no longer present in\n * `sb.buffered` is reset to 'pending' so the seek path re-fetches it.\n *\n * Called from two places — cheap either way (one Map scan per call):\n * - top of `resolveTarget()` (seek time), and\n * - the stall paths in `useStallRecovery` (playback time).\n *\n * The stall call is NOT optional. Reconcile used to run only on seeks, so\n * during continuous playback nothing ever re-synced the ledger: a fragment\n * the browser had silently dropped stayed 'loaded' forever, `prefetchAround`\n * and `_fetchRange` both skipped it, and the playhead sat at the hole until\n * something happened to issue a seek. That is exactly the 2026-08-04\n * clover-basin freeze — a viewer stopped advancing for 36s while the room\n * played on, with zero fetches issued and zero events emitted, and only\n * recovered when the leader's pause triggered a resync seek.\n */\n reconcileLoadedStates(): void {\n const buffered = this.sourceBuffer?.buffered;\n if (!buffered) return;\n const evicted: number[] = [];\n for (const [idx, st] of this.fragmentStates) {\n if (st !== 'loaded') continue;\n const f = this.fragments[idx];\n if (!f) continue;\n if (!fragmentOverlapsSB(buffered, f.start_ms, f.end_ms)) {\n this.fragmentStates.set(idx, 'pending');\n evicted.push(idx);\n }\n }\n if (evicted.length === 0) return;\n\n // Telemetry: the MSE spec fires NO event for the browser's own quota\n // eviction — reconcile is the only place it becomes visible. Emit a loud,\n // structured warn (forwarded to server session logs) + a perf counter so\n // this failure mode is tracked, not silent.\n const survivingRanges = formatBufferedRanges(buffered).join(', ') || '(none)';\n perf.recordBrowserEviction(evicted.length, survivingRanges);\n warn('buffer', 'BROWSER-EVICTION — browser silently evicted buffered fragments at its MSE memory quota', {\n failureMode: 'browser dropped SourceBuffer data with no event; fragmentStates was stale. Uncaught, a seek into the evicted region sets currentTime onto absent data → waitForSeeked timeout → frozen player. Reconcile reset these to pending so the seek re-fetches.',\n evictedCount: evicted.length,\n evictedFragments: evicted.length <= 24 ? evicted.join(',') : `${evicted.slice(0, 24).join(',')}…+${evicted.length - 24}`,\n survivingSbRanges: survivingRanges,\n playheadMs: Math.round(this.currentPlayheadMs),\n mediaId: this.mediaId,\n });\n }\n\n resolveTarget(targetMs: number): TargetResolution {\n // Browser quota eviction can silently drop 'loaded' fragments — sync the\n // state map to the real SourceBuffer before routing the seek.\n this.reconcileLoadedStates();\n const idx = findFragmentIndex(this.fragments, targetMs);\n if (this.isBuffered(targetMs)) return { status: 'buffered', fragmentIndex: idx };\n const job = this.currentJobs.get(idx);\n if (job) return { status: 'loading', fragmentIndex: idx, existingPromise: job.promise };\n const lo = Math.max(0, idx - NEARBY_FRAGMENT_THRESHOLD);\n const hi = Math.min(this.fragments.length - 1, idx + NEARBY_FRAGMENT_THRESHOLD);\n for (let i = lo; i <= hi; i++) {\n const s = this.fragmentStates.get(i);\n if (s === 'loaded' || s === 'loading') return { status: 'nearby-pending', fragmentIndex: idx };\n }\n return { status: 'far', fragmentIndex: idx };\n }\n\n async fetchNearby(targetMs: number, signal?: AbortSignal, minimal = false): Promise<void> {\n this.currentPlayheadMs = targetMs;\n await this._fetchRange(targetMs, { abort: false, minimal, signal, caller: 'fetchNearby' });\n }\n\n async ensureBuffered(targetMs: number, caller?: string, signal?: AbortSignal): Promise<void> {\n if (this.disposed || !this.sourceBuffer || !this.fragments.length || this.isBuffered(targetMs)) return;\n if (this.mediaSource?.readyState !== 'open') {\n debug('buffer', `ENSURE-SKIP — mediaSource.readyState=${this.mediaSource?.readyState ?? 'null'}`);\n return;\n }\n if (this.sourceBuffer.buffered.length > 1)\n warn('mse', 'Buffer gaps detected during seek', { targetMs, ranges: formatBufferedRanges(this.sourceBuffer?.buffered).join(', ') });\n // Evict ranges that would create a gap before targetMs. Fires synchronously so\n // Firefox doesn't stall on a preceding discontinuity in the SourceBuffer.\n // HTTP fetches started by _fetchRange can overlap because processAppendQueue()\n // already serializes appends behind sb.updating === true.\n this.eviction.evictSeekGap(targetMs);\n await this._fetchRange(targetMs, { abort: true, signal, caller });\n }\n\n private async _fetchRange(\n targetMs: number,\n opts: { abort?: boolean; minimal?: boolean; signal?: AbortSignal; caller?: string },\n ): Promise<void> {\n if (this.disposed || !this.sourceBuffer || !this.fragments.length || this.hasDataAhead(targetMs)) return;\n if (this.mediaSource?.readyState !== 'open') return;\n const { abort = false, minimal = false, signal, caller = 'unknown' } = opts;\n if (abort) this.abortAllJobs(caller);\n\n const ti = findFragmentIndex(this.fragments, targetMs);\n // On a seek (abort=true), clear stale discard/error verdicts in the new\n // prefetch window. Verdicts are tied to the previous SourceBuffer layout:\n // after evictSeekGap() + re-fetch from the nearest keyframe, a fragment\n // that was \"discarded for missing IDR\" may decode fine. Not clearing\n // these keeps the rainbow-roost stall latent across seeks.\n if (abort) {\n const winEnd = findFragmentIndex(this.fragments, targetMs + BUFFER_AHEAD_MS);\n for (let i = ti; i <= winEnd; i++) {\n this.discardCounts.delete(i);\n this.fetchErrorCycles.delete(i);\n }\n }\n // Always start from the nearest keyframe to avoid appending non-keyframe\n // fragments that MSE silently discards (they need a preceding keyframe in\n // the SourceBuffer to be decoded). This applies to both seeks (abort=true)\n // and nearby fetches (abort=false), because after viewer correction or\n // buffer eviction the target fragment may not have its keyframe in the SB.\n const kfIdx = findKeyframeIndex(this.fragments, targetMs);\n // Only use keyframe start if the keyframe's data isn't already in the SourceBuffer.\n // If the keyframe range is already buffered, we can start from ti directly.\n const kfFrag = this.fragments[kfIdx];\n const kfInSb = kfFrag && this.sourceBuffer?.buffered\n && fragmentOverlapsSB(this.sourceBuffer.buffered, kfFrag.start_ms, kfFrag.end_ms);\n const start = kfInSb ? ti : kfIdx;\n const end = minimal ? Math.min(this.fragments.length - 1, ti + 1) : findFragmentIndex(this.fragments, targetMs + INITIAL_BUFFER_MS);\n\n // Ensure init segment is appended before any media fragments\n if (this.initRange && this.initState !== 'loaded') await this.fetchInit();\n\n const toFetch: number[] = [];\n const skipped: string[] = [];\n for (let i = start; i <= end; i++) {\n const s = this.fragmentStates.get(i);\n if (s === 'loaded') { skipped.push(`#${i}=${s}`); continue; }\n // 'loading' is only a reason to skip while something is ACTUALLY\n // fetching it. A fragment abandoned mid-flight keeps the label with no\n // job behind it; skipping it here and then awaiting waitForAppend(ti)\n // below is an unbreakable wait — the FragmentManager goes completely\n // idle and the seek burns its full ~30s timeout in silence.\n if (s === 'loading' && this.isFragmentInFlight(i)) { skipped.push(`#${i}=${s}`); continue; }\n if (s === 'loading') skipped.push(`#${i}=loading-abandoned→refetch`);\n toFetch.push(i);\n }\n\n debug('buffer', `FETCH-RANGE [${caller}] ${JSON.stringify({\n targetMs: Math.round(targetMs), targetFrag: ti,\n keyframeFrag: kfIdx !== ti ? kfIdx : undefined, kfInSb: kfIdx !== ti ? kfInSb : undefined,\n range: [start, end], abort, minimal, toFetch, skipped,\n activeJobs: Array.from(this.currentJobs.keys()),\n sbRanges: formatBufferedRanges(this.sourceBuffer?.buffered),\n })}`);\n\n if (toFetch.length === 0) {\n if (this.fragmentStates.get(ti) === 'loading') {\n debug('buffer', `FETCH-RANGE [${caller}] target #${ti} already loading, awaiting`);\n const job = this.currentJobs.get(ti);\n if (job) await job.promise;\n await this.waitForAppend(ti);\n }\n return;\n }\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n if (!abort && this.currentJobs.size >= MAX_CONCURRENT_JOBS) this.evictFurthestJob(targetMs);\n this.onBufferingChange?.(true);\n // Protect this target from our own EvictionStrategy for exactly as long as\n // we are waiting on it. Nested _fetchRange calls (prefetch racing a seek)\n // restore the previous value rather than clearing, so an inner fetch\n // finishing cannot strip the outer seek's protection.\n this.pendingSeekTargetMs = targetMs;\n this.pendingSeekSetAtMs = Date.now();\n try {\n await this.fetchFragments(toFetch);\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n await this.waitForAppend(ti);\n } finally {\n // NOT cleared here — see pendingSeekTargetMs. The trailing appends of this\n // very fetch land after waitForAppend resolves, and they are what trips\n // the orphan rule.\n this.onBufferingChange?.(false);\n }\n }\n\n // MAX_CONCURRENT_JOBS - 1: batch size is one below the outer guard so a second\n // concurrent caller (e.g. prefetchAround while _fetchRange runs) can still launch\n // one job before hitting the MAX_CONCURRENT_JOBS cap.\n private async fetchFragments(indices: number[], concurrency = MAX_CONCURRENT_JOBS - 1): Promise<void> {\n if (indices.length === 0) return;\n const sorted = [...indices].sort((a, b) => a - b);\n debug('buffer', `FETCH-BATCH-START total=${sorted.length} concurrency=${concurrency} indices=[${sorted.join(',')}]`);\n for (let i = 0; i < sorted.length; i += concurrency) {\n if (this.disposed) break;\n const batch = sorted.slice(i, i + concurrency);\n const skipped: string[] = [];\n const promises: Promise<void>[] = [];\n for (const idx of batch) {\n if (this.fragmentStates.get(idx) === 'loaded') { skipped.push(`#${idx}=loaded`); continue; }\n if (this.currentJobs.size >= MAX_CONCURRENT_JOBS) { skipped.push(`#${idx}=max-jobs`); continue; }\n promises.push(this.fetchFragment(idx).catch(e => {\n warn('buffer', 'Fragment fetch failed in batch', { index: idx, error: (e as Error).message });\n }));\n }\n debug('buffer', `FETCH-BATCH [${i / concurrency + 1}/${Math.ceil(sorted.length / concurrency)}] launching=[${batch.filter(idx => !skipped.some(s => s.startsWith(`#${idx}`))).join(',')}] skipped=[${skipped.join(',')}]`);\n if (promises.length > 0) await Promise.allSettled(promises);\n }\n debug('buffer', `FETCH-BATCH-END indices=[${sorted.join(',')}] states=${JSON.stringify(sorted.map(i => `#${i}=${this.fragmentStates.get(i)}`))}`);\n }\n\n private async fetchFragment(index: number): Promise<void> {\n if (this.disposed) return;\n const frag = this.fragments[index];\n if (!frag || this.fragmentStates.get(index) === 'loaded') return;\n const existing = this.currentJobs.get(index);\n if (existing) return existing.promise;\n\n this.fragmentStates.set(index, 'loading');\n this.pendingFetchIndices.add(index);\n const ac = new AbortController();\n const fragTimeRange = `${(frag.start_ms / 1000).toFixed(1)}-${(frag.end_ms / 1000).toFixed(1)}s`;\n const fragBytes = frag.end_byte - frag.start_byte + 1;\n // Retry loop runs INSIDE the IIFE so `finally` only fires once after all attempts.\n // Previous recursive retry (`return this.fetchFragment(index, attempt+1)`) caused a race:\n // the recursive call re-added to pendingFetchIndices/currentJobs, then attempt 1's finally\n // block deleted them, defeating sequential append enforcement and causing SourceBuffer gaps.\n const promise = (async () => {\n // Yield one microtask so `currentJobs.set(index, ...)` below runs\n // BEFORE this body executes. Without this, the cache-hit path returns\n // synchronously, `finally` runs `currentJobs.delete(index)` against an\n // empty map, then the outer `set` installs a zombie entry that never\n // gets cleaned up. Zombies count toward MAX_CONCURRENT_JOBS and starve\n // real fetches, eventually causing waitForAppend timeouts and stalls.\n await Promise.resolve();\n if (this.disposed || ac.signal.aborted) {\n this.resetFragmentState(index);\n return;\n }\n try {\n // Cache check — skip network entirely on hit\n if (this.mediaId) {\n const cached = playlistFragmentCache.getFragment(this.mediaId, index);\n if (cached) {\n debug('buffer', `FETCH-CACHE-HIT #${index} [${fragTimeRange}]`);\n if (this.isInInitPhase()) {\n const now = performance.now();\n this.recordInitFragEvent(index, { fetchStart: now, fetchEnd: now, src: 'cache', bytes: cached.byteLength });\n }\n if (!this.disposed) this.queueAppend(index, cached);\n return; // finally block handles pendingFetchIndices correctly\n }\n }\n for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {\n if (this.disposed) return;\n debug('buffer', `FETCH-START #${index} [${fragTimeRange}] attempt=${attempt} bytes=${fragBytes} pending=${JSON.stringify(Array.from(this.pendingFetchIndices).sort((a, b) => a - b))}`);\n try {\n const t0 = performance.now();\n if (this.isInInitPhase() && attempt === 1) {\n this.recordInitFragEvent(index, { fetchStart: t0, src: 'net' });\n }\n const expectedLen = frag.end_byte - frag.start_byte + 1;\n const urlAtStart = this.mediaUrl;\n // Fragment route first when enabled. A null result means \"use Range\", and we fall\n // through to it in THIS attempt — a broken fragment route costs one round trip,\n // never a retry cycle and never a stall.\n const viaFrag = await this.tryFragmentRoute(index, expectedLen, ac.signal);\n if (viaFrag) {\n const elapsedFr = performance.now() - t0;\n if (this.isInInitPhase()) {\n this.recordInitFragEvent(index, {\n fetchEnd: performance.now(), bytes: viaFrag.buf.byteLength,\n ttfbMs: viaFrag.ttfbMs, cf: viaFrag.cfStatus,\n });\n }\n debug('buffer', `FETCH-OK #${index} [${fragTimeRange}] ${viaFrag.buf.byteLength}B in ${Math.round(elapsedFr)}ms via=frag-route (ttfb=${viaFrag.ttfbMs}ms cf=${viaFrag.cfStatus || '-'})`);\n // Same download-rate bookkeeping as the Range path — getAdaptiveTimeout() and the\n // health report both read these, so a fragment-route session must not starve them.\n if (elapsedFr > 0) {\n this.downloadSamples.push({ bytes: viaFrag.buf.byteLength, durationMs: elapsedFr });\n if (this.downloadSamples.length > MAX_DOWNLOAD_SAMPLES) this.downloadSamples.shift();\n perf.recordFetch(index, viaFrag.buf.byteLength, elapsedFr);\n }\n // Same staleness guard as the Range path below: mediaUrl can move under us during\n // the await (playlist clip-switch), and appending OLD bytes to the NEW\n // SourceBuffer is Bug 4 from 2026-05-20.\n if (this.disposed || this.mediaUrl !== urlAtStart) {\n debug('buffer', `FETCH-STALE #${index} discarding frag-route bytes (mediaUrl changed)`);\n return;\n }\n if (this.mediaId) playlistFragmentCache.storeFragment(this.mediaId, index, viaFrag.buf);\n if (!this.disposed) this.queueAppend(index, viaFrag.buf);\n return;\n }\n const fetchedFromUrl = this.mediaUrl;\n const res = await fetch(fetchedFromUrl, {\n headers: {\n Range: `bytes=${frag.start_byte}-${frag.end_byte}`,\n 'X-Buffer-Ahead-Ms': String(Math.round(this.getBufferedAheadMs(this.currentPlayheadMs))),\n },\n signal: AbortSignal.any([ac.signal, AbortSignal.timeout(getAdaptiveTimeout(this.getAverageDownloadRate()))]),\n });\n const ttfbMs = Math.round(performance.now() - t0);\n const cfStatus = res.headers.get('cf-cache-status') || res.headers.get('x-cache') || '';\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const expected = frag.end_byte - frag.start_byte + 1;\n // Check the STATUS BEFORE buffering the body. We sent a Range, so\n // anything but 206 means the range was dropped somewhere upstream\n // and the response is the entire file — measured 744MB on prod\n // when Cloudflare collapses concurrent identical Range requests on\n // a cold path (~5% of a 12-way burst). `await res.arrayBuffer()`\n // used to run first, so a viewer would allocate the whole episode\n // in memory before noticing, which is both a huge download and\n // exactly the kind of memory spike that provokes the browser's own\n // MSE quota eviction. Cancel and retry instead.\n if (res.status !== 206) {\n await res.body?.cancel().catch(() => { /* already closed */ });\n logError('buffer', `Fragment #${index} range dropped upstream — got ${res.status}, expected 206`, {\n httpStatus: res.status,\n contentLength: res.headers.get('content-length'),\n cfCacheStatus: cfStatus,\n expectedBytes: expected,\n });\n this.fragmentStates.set(index, 'pending');\n if (attempt < MAX_RETRIES) {\n await new Promise(r => setTimeout(r, getRetryDelay(attempt)));\n continue;\n }\n throw new Error(`fragment expected 206, got ${res.status}`);\n }\n const buf = await res.arrayBuffer();\n if (buf.byteLength !== expected || res.headers.get('content-encoding')) {\n logError('buffer', `Fragment #${index} response anomaly`, {\n httpStatus: res.status, expectedBytes: expected, actualBytes: buf.byteLength,\n });\n }\n // Don't append 0-byte or wrong-size responses — treat as fetch failure and retry\n if (buf.byteLength === 0 || buf.byteLength !== expected) {\n this.fragmentStates.set(index, 'pending');\n if (attempt < MAX_RETRIES) {\n await new Promise(r => setTimeout(r, getRetryDelay(attempt)));\n continue;\n }\n logError('buffer', `Fragment #${index} bad response after ${MAX_RETRIES} retries`, { expected, actual: buf.byteLength });\n this.fragmentStates.set(index, 'error');\n this.errorTimestamps.set(index, Date.now());\n this.fetchErrorCycles.set(index, (this.fetchErrorCycles.get(index) ?? 0) + 1);\n perf.recordFetchError(index);\n return;\n }\n const elapsed = performance.now() - t0;\n if (this.isInInitPhase()) {\n this.recordInitFragEvent(index, { fetchEnd: performance.now(), bytes: buf.byteLength, ttfbMs, cf: cfStatus });\n }\n debug('buffer', `FETCH-OK #${index} [${fragTimeRange}] ${buf.byteLength}B in ${Math.round(elapsed)}ms (ttfb=${ttfbMs}ms cf=${cfStatus || '-'}) (${(buf.byteLength / elapsed * 1000 / 1024).toFixed(0)} KB/s)`);\n if (elapsed > 0) {\n this.downloadSamples.push({ bytes: buf.byteLength, durationMs: elapsed });\n if (this.downloadSamples.length > MAX_DOWNLOAD_SAMPLES) this.downloadSamples.shift();\n perf.recordFetch(index, buf.byteLength, elapsed);\n }\n // Staleness check: this.mediaId / this.mediaUrl may have moved\n // under us while `await res.arrayBuffer()` was in flight (e.g.\n // playlist clip-switch). Writing OLD bytes to the NEW media's\n // cache key — or appending them to the NEW SourceBuffer — would\n // corrupt downstream playback (Bug 4, 2026-05-20). Capture at\n // top of fetchFragment is awkward because the retry loop also\n // re-enters; instead check the URL we just fetched matches the\n // current media URL and bail silently if not.\n if (this.disposed || this.mediaUrl !== fetchedFromUrl) {\n debug('buffer', `FETCH-STALE #${index} discarding (mediaUrl changed)`);\n return;\n }\n if (this.mediaId) playlistFragmentCache.storeFragment(this.mediaId, index, buf);\n if (!this.disposed) this.queueAppend(index, buf);\n return; // success\n } catch (e) {\n // Seek-abort → don't retry\n if ((e as Error).name === 'AbortError' && ac.signal.aborted) {\n debug('buffer', `FETCH-ABORT #${index} [${fragTimeRange}] seek-abort, resetting to pending`);\n this.fragmentStates.set(index, 'pending');\n return;\n }\n // Record timeout as a pessimistic download sample so the adaptive\n // timeout learns about degraded conditions. Without this, timed-out\n // fetches never update downloadSamples, so the timeout stays stuck\n // at its stale low value permanently.\n const isTimeout = (e as Error).name === 'TimeoutError'\n || (e as Error).message?.includes('signal timed out')\n || (e as Error).message?.includes('The operation was aborted');\n if (isTimeout && (e as Error).name !== 'AbortError') {\n const timeoutDuration = getAdaptiveTimeout(this.getAverageDownloadRate());\n this.downloadSamples.push({ bytes: 0, durationMs: timeoutDuration });\n if (this.downloadSamples.length > MAX_DOWNLOAD_SAMPLES) this.downloadSamples.shift();\n perf.recordFetchTimeout(index);\n warn('buffer', `FETCH-ERR #${index} [${fragTimeRange}] attempt=${attempt} timeout=${timeoutDuration}ms avgRate=${Math.round(this.getAverageDownloadRate())}Bps error=${(e as Error).message}`);\n } else {\n warn('buffer', `FETCH-ERR #${index} [${fragTimeRange}] attempt=${attempt} error=${(e as Error).message}`);\n }\n if (this.isInInitPhase()) {\n this.recordInitFragEvent(index, { err: ((e as Error).name === 'TimeoutError' ? 'timeout' : (e as Error).message?.slice(0, 40)) || 'err' });\n }\n this.fragmentStates.set(index, 'pending');\n if (attempt < MAX_RETRIES) {\n await new Promise(r => setTimeout(r, getRetryDelay(attempt)));\n continue;\n }\n logError('buffer', `Fragment #${index} failed after ${MAX_RETRIES} retries`, { error: (e as Error).message });\n this.fragmentStates.set(index, 'error');\n this.errorTimestamps.set(index, Date.now());\n this.fetchErrorCycles.set(index, (this.fetchErrorCycles.get(index) ?? 0) + 1);\n perf.recordFetchError(index);\n throw e;\n }\n }\n } finally {\n this.currentJobs.delete(index);\n // Only remove from pendingFetchIndices if the fragment was NOT queued\n // for append. Successfully queued fragments keep their pending flag\n // until the updateend handler confirms SourceBuffer accepted the data.\n const queuedOrAppending = this.appendQueue.some(item => item.index === index) || this.appendingIndex === index;\n if (!queuedOrAppending) {\n this.pendingFetchIndices.delete(index);\n }\n debug('buffer', `FETCH-DONE #${index} state=${this.fragmentStates.get(index)} queued=${queuedOrAppending} remainingJobs=${this.currentJobs.size} pendingFetches=${JSON.stringify(Array.from(this.pendingFetchIndices).sort((a, b) => a - b))}`);\n this.processAppendQueue();\n }\n })();\n this.currentJobs.set(index, { index, abortController: ac, promise });\n return promise;\n }\n\n /** If the head of the queue is a non-keyframe fragment whose IDR predecessor\n * is in permanent error AND not present in the SourceBuffer, drop every\n * queue item sharing that broken keyframe. MSE would silently discard them\n * (no IDR frame to decode from), creating unbridgeable buffered gaps.\n *\n * Returns true when at least one item was dropped (caller should early-return\n * from processAppendQueue so the next call re-evaluates the queue head).\n * Returns false when the queue head is safe to append.\n *\n * No-op on legacy index.json files that don't carry keyframe flags. */\n private _dropQueueItemsBehindBrokenKeyframe(headIndex: number): boolean {\n if (headIndex <= 0 || this.fragments.length === 0 || this.fragments[0].keyframe === undefined) return false;\n const frag = this.fragments[headIndex];\n if (!frag || frag.keyframe) return false;\n const kfIdx = findKeyframePredecessor(this.fragments, headIndex - 1);\n if (kfIdx < 0 || this.fragmentStates.get(kfIdx) !== 'error') return false;\n const kfFrag = this.fragments[kfIdx];\n const kfInSb = this.sourceBuffer\n ? fragmentOverlapsSB(this.sourceBuffer.buffered, kfFrag.start_ms, kfFrag.end_ms)\n : false;\n if (kfInSb) return false;\n\n const dropped: number[] = [];\n const toKeep: typeof this.appendQueue = [];\n for (const item of this.appendQueue) {\n const itemFrag = this.fragments[item.index];\n if (!itemFrag || itemFrag.keyframe) {\n toKeep.push(item);\n continue;\n }\n const itemKf = findKeyframePredecessor(this.fragments, item.index - 1);\n if (itemKf === kfIdx) {\n dropped.push(item.index);\n this.fragmentStates.set(item.index, 'pending');\n } else {\n toKeep.push(item);\n }\n }\n if (dropped.length === 0) return false;\n this.appendQueue = toKeep;\n warn('buffer', `APPEND-DROP-DEPENDENT [${dropped.join(',')}] — keyframe #${kfIdx} in error, not in SB`, {\n kfIdx, dropped, remainingQueue: this.appendQueue.map(q => q.index),\n });\n return true;\n }\n\n private queueAppend(index: number, buffer: ArrayBuffer): void {\n // DO NOT delete from pendingFetchIndices here. The entry must remain\n // until the updateend handler confirms SourceBuffer accepted the data.\n // Premature deletion allows higher-index fragments to bypass the\n // sequential append hold in processAppendQueue(), causing permanent gaps.\n const pos = this.appendQueue.findIndex(item => item.index > index);\n pos === -1 ? this.appendQueue.push({ index, buffer }) : this.appendQueue.splice(pos, 0, { index, buffer });\n if (this.isInInitPhase()) {\n this.recordInitFragEvent(index, { queued: performance.now() });\n }\n perf.recordAppendQueued(index, this.appendQueue.length);\n const frag = this.fragments[index];\n const kf = frag?.keyframe;\n debug('buffer', `QUEUE-APPEND #${index} ${buffer.byteLength}B pos=${pos === -1 ? this.appendQueue.length - 1 : pos} queueLen=${this.appendQueue.length} keyframe=${kf ?? 'unknown'} pending=${JSON.stringify(Array.from(this.pendingFetchIndices).sort((a, b) => a - b))}`);\n this.processAppendQueue();\n }\n\n /** Bounds the sequential-append hold. Returns true when the hold has lasted\n * longer than APPEND_HOLD_MAX_MS and the blocking fetches have been declared\n * dead, so the caller may proceed with the append.\n *\n * DO NOT weaken this into \"append out of order after a timeout\" — appending\n * past a gap is what creates the permanent SourceBuffer gaps the guard\n * exists to prevent (all browsers). What this does instead is give up on the\n * BLOCKERS: abort their fetches and mark them dead, which is the same\n * \"structural gap, not a blocker\" treatment prefetchAround already applies\n * to fragments past their discard/error budget. The result is a bounded gap\n * that gap-skip and reconcileLoadedStates can act on, instead of an audio\n * queue that never drains again (cedar-ridge 2026-08-25). */\n private _breakStuckAppendHold(headIndex: number, blockers: number[]): boolean {\n const now = Date.now();\n if (this.appendHold?.index !== headIndex) {\n this.appendHold = { index: headIndex, since: now };\n return false;\n }\n const heldMs = now - this.appendHold.since;\n if (heldMs < APPEND_HOLD_MAX_MS) return false;\n\n warn('buffer', `APPEND-HOLD-BREAK #${headIndex} held ${heldMs}ms behind ${blockers.length} pending fetch(es) — declaring them dead so the queue can drain`, {\n headIndex,\n heldMs,\n blockers: blockers.slice().sort((a, b) => a - b),\n queueLen: this.appendQueue.length,\n liveJobs: Array.from(this.currentJobs.keys()).sort((a, b) => a - b),\n });\n for (const p of blockers) {\n this.currentJobs.get(p)?.abortController.abort();\n this.currentJobs.delete(p);\n this.pendingFetchIndices.delete(p);\n this.fragmentStates.set(p, 'error');\n this.errorTimestamps.set(p, now);\n // Mark permanently unservable so prefetchAround treats it as a structural\n // gap rather than immediately re-fetching it and re-taking the hold.\n this.fetchErrorCycles.set(p, MAX_FETCH_ERROR_CYCLES);\n }\n this.appendHold = null;\n return true;\n }\n\n private processAppendQueue(): void {\n if (this.isAppending || this.disposed || !this.sourceBuffer || this.sourceBuffer.updating) return;\n if (this.mediaSource?.readyState !== 'open' || this.appendQueue.length === 0) return;\n const next = this.appendQueue[0];\n if (!next) return;\n if (this.initRange && this.initState !== 'loaded') {\n if (!this.initDigestEmitted) this.initHoldCounts.byInit++;\n debug('buffer', `APPEND-HOLD #${next.index} waiting for init segment (initState=${this.initState})`);\n return;\n }\n // Hold if earlier fragment still in flight or queued — prevents Chrome MSE gaps.\n // Two checks:\n // 1. A fetch for a lower-index fragment is still in progress (not yet queued)\n // 2. A lower-index fragment is currently being appended (appendingIndex)\n // 3. A lower-index fragment is ahead in the queue (shouldn't happen since queue\n // is sorted, but guard defensively)\n if (next.index > 0) {\n // Check pending fetches\n const blockers: number[] = [];\n for (const p of this.pendingFetchIndices) {\n if (p < next.index) blockers.push(p);\n }\n if (blockers.length > 0) {\n if (!this._breakStuckAppendHold(next.index, blockers)) {\n if (!this.initDigestEmitted) this.initHoldCounts.byPendingFetch++;\n debug('buffer', `APPEND-HOLD #${next.index} blocked by pending fetch #${blockers[0]}`, {\n pendingFetchIndices: Array.from(this.pendingFetchIndices).sort((a, b) => a - b),\n appendQueue: this.appendQueue.map(q => q.index),\n });\n return;\n }\n } else {\n this.appendHold = null;\n }\n // Check currently-appending fragment (belt-and-suspenders with isAppending)\n if (this.appendingIndex !== null && this.appendingIndex < next.index) {\n if (!this.initDigestEmitted) this.initHoldCounts.byInProgressAppend++;\n debug('buffer', `APPEND-HOLD #${next.index} blocked by in-progress append #${this.appendingIndex}`);\n return;\n }\n }\n if (this._dropQueueItemsBehindBrokenKeyframe(next.index)) return;\n\n this.appendQueue.shift();\n // isAppending stays true until the updateend event fires (set false there).\n // This prevents re-entrant calls from starting a second appendBuffer()\n // before the browser has updated sourceBuffer.updating, which caused\n // fragment #N's updateend to be swallowed when #N+1 was appended too early.\n this.isAppending = true;\n debug('buffer', `APPEND-SB #${next.index} ${next.buffer.byteLength}B remainingQueue=${JSON.stringify(this.appendQueue.map(q => q.index))}`);\n try {\n // Snapshot SB ranges before append so updateend can detect silent discards\n // by comparing before/after. Geometric overlap alone has false positives\n // for boundary fragments (e.g., fragment [6.0-8.0s] \"overlaps\" SB [0.08-6.13]\n // at the tail, but MSE doesn't actually buffer it).\n const b = this.sourceBuffer.buffered;\n this.preAppendRanges = Array.from({ length: b.length }, (_, i) => ({ start: b.start(i), end: b.end(i) }));\n this.sourceBuffer.appendBuffer(next.buffer);\n this.appendingIndex = next.index;\n } catch (e) {\n logError('buffer', 'Append error', { index: next.index, error: (e as Error).message });\n this.fragmentStates.set(next.index, 'error');\n this.errorTimestamps.set(next.index, Date.now());\n // Only release on error — success releases in updateend handler\n this.isAppending = false;\n }\n }\n\n private evictFurthestJob(urgentMs: number): void {\n let fi = -1, fm = -1;\n for (const [i] of this.currentJobs) { const f = this.fragments[i]; if (f && f.start_ms > fm) { fm = f.start_ms; fi = i; } }\n if (fi < 0 || fm - urgentMs < INITIAL_BUFFER_MS) return;\n const job = this.currentJobs.get(fi);\n if (!job) return;\n job.abortController.abort();\n this.currentJobs.delete(fi);\n this.fragmentStates.set(fi, 'pending');\n }\n\n prefetchAround(currentMs: number): void {\n if (this.disposed || !this.sourceBuffer) return;\n this.currentPlayheadMs = currentMs;\n if (this.currentJobs.size >= MAX_CONCURRENT_JOBS) return;\n\n const cur = findFragmentIndex(this.fragments, currentMs);\n const end = Math.min(this.fragments.length - 1, findFragmentIndex(this.fragments, currentMs + BUFFER_AHEAD_MS));\n\n // Reset error states that have aged past ERROR_RESET_AGE_MS so they become fetchable\n // again. Fragments that have exhausted MAX_FETCH_ERROR_CYCLES rounds of retries are\n // left in error state permanently — they are assumed to be unservable by the server.\n const now = Date.now();\n for (let i = cur; i <= end; i++) {\n if (this.fragmentStates.get(i) === 'error') {\n const age = now - (this.errorTimestamps.get(i) ?? 0);\n if (age >= ERROR_RESET_AGE_MS) {\n if ((this.fetchErrorCycles.get(i) ?? 0) >= MAX_FETCH_ERROR_CYCLES) {\n debug('buffer', `PREFETCH-DEAD #${i} cycles=${this.fetchErrorCycles.get(i)} not resetting`);\n } else {\n this.fragmentStates.set(i, 'pending');\n this.errorTimestamps.delete(i);\n }\n }\n }\n }\n\n // Fetch up to 2 pending fragments, sequentially forward from the playhead.\n // No keyframe diversion — sequential fetching guarantees every preceding\n // keyframe is already loaded or in-flight. Keyframe-aware logic is only\n // needed in _fetchRange (seeks), where we jump to arbitrary positions.\n let launched = 0;\n for (let i = cur; i <= end && launched < MAX_CONCURRENT_JOBS - this.currentJobs.size; i++) {\n const s = this.fragmentStates.get(i);\n if (s !== 'pending' && s !== 'error') continue;\n if ((this.discardCounts.get(i) ?? 0) >= 2) continue;\n if ((this.fetchErrorCycles.get(i) ?? 0) >= 3) continue;\n // Ensure predecessor is loaded or in-flight (sequential ordering).\n // Permanently-dead predecessors (discardCount>=2 or fetchErrorCycles>=3)\n // are treated as structural gaps, not blockers — otherwise a single\n // APPEND-DISCARDED fragment freezes the entire forward buffer and\n // causes PREFETCH-STALL deadlock (rainbow-roost 2026-04-15).\n if (i > cur) {\n const p = this.fragmentStates.get(i - 1);\n const pDead = (this.discardCounts.get(i - 1) ?? 0) >= 2\n || (this.fetchErrorCycles.get(i - 1) ?? 0) >= 3;\n if (p !== 'loaded' && p !== 'loading' && !pDead) break;\n }\n this.fetchFragment(i).catch(() => {});\n launched++;\n }\n\n // Log when prefetch has pending/error fragments ahead but can't launch any.\n // This is the \"stall\" state: buffer will drain if video is playing.\n //\n // Only warn when there are actually fetchable fragments (`pending` or\n // `error`) that aren't progressing. If every fragment in range is already\n // `loaded` or `loading`, there's nothing to launch and the test ran out\n // of work — that's normal end-of-stream behaviour, not a stall. Logging\n // it produced ~120 warns/run on a 98s clip (rainbow-roost repro).\n if (launched === 0 && cur <= end) {\n const stateCounts: Record<string, number> = {};\n for (let i = cur; i <= end; i++) {\n const s = this.fragmentStates.get(i) ?? 'pending';\n stateCounts[s] = (stateCounts[s] ?? 0) + 1;\n }\n const hasLaunchable = (stateCounts.pending ?? 0) > 0 || (stateCounts.error ?? 0) > 0;\n if (hasLaunchable) {\n warn('buffer', 'PREFETCH-STALL no new fetches launched', {\n stateMap: stateCounts, jobs: this.currentJobs.size, maxJobs: MAX_CONCURRENT_JOBS,\n playheadMs: currentMs, aheadFragments: end - cur + 1,\n });\n }\n }\n }\n\n /** Public eviction entry point used by MediaPlayer / prefetch. Delegates\n * to the strategy which owns the isEvicting state + range choice logic. */\n evictBefore(currentMs: number): void {\n // The seek is over when the playhead is actually there. Clearing here rather\n // than when the fetch resolved is the whole point — see pendingSeekTargetMs.\n if (this.pendingSeekTargetMs !== null\n && Math.abs(currentMs - this.pendingSeekTargetMs) <= SEEK_LANDED_TOLERANCE_MS) {\n this.pendingSeekTargetMs = null;\n }\n this.eviction.evictBefore(currentMs);\n }\n\n // Public entry point for socket reconnect scenarios. Clears stale in-flight\n // fetches and their pendingFetchIndices entries so the queue is unblocked on\n // the new socket connection. Safe to call at any time — same as ensureBuffered()\n // calls it on seek, but without the seek target.\n abortStaleJobs(reason = 'reconnect'): void {\n // On reconnect the network is back — dead fragments are retryable again.\n this.fetchErrorCycles.clear();\n this.abortAllJobs(reason);\n }\n\n private generationAbort = new AbortController();\n\n /** Aborted by the NEXT abortAllJobs, including one that finds nothing to\n * kill. Read it right after starting a load and that load can learn that a\n * later caller took the FragmentManager over — see the audio swap in\n * MediaPlayer, whose fire-and-forget load otherwise carried on fetching a\n * position the room had already seeked away from. */\n get supersededSignal(): AbortSignal { return this.generationAbort.signal; }\n\n private abortAllJobs(reason?: string): void {\n this.generationAbort.abort();\n this.generationAbort = new AbortController();\n if (this.currentJobs.size === 0 && this.appendQueue.length === 0) return;\n const jobIndices = Array.from(this.currentJobs.keys()).sort((a, b) => a - b);\n const jobStates = jobIndices.map(i => `#${i}=${this.fragmentStates.get(i) || '?'}`);\n const queueIndices = this.appendQueue.map(item => item.index);\n const pendingIndices = Array.from(this.pendingFetchIndices).sort((a, b) => a - b);\n const waiterIndices = this.appendWaiters.map(w => w.index);\n warn('buffer', `ABORT-ALL [${reason || 'unknown'}] ${JSON.stringify({\n killedJobs: jobStates, queuedAppends: queueIndices,\n pendingFetches: pendingIndices, waiters: waiterIndices,\n })}`);\n // Un-mark the killed jobs SYNCHRONOUSLY. `abort()` only signals; each\n // job's own abort path calls resetFragmentState() a microtask later (or\n // never, if it never resumes), so without this the fragments stay\n // 'loading' with no job behind them. _fetchRange skips anything marked\n // 'loading' when building toFetch — so the very next line of a seek would\n // exclude its own target and then wait on it forever. That is the\n // cedar-ridge 2026-09-08 freeze; a seek must leave nothing falsely\n // in-flight behind it.\n for (const [i, j] of this.currentJobs) {\n j.abortController.abort();\n if (this.fragmentStates.get(i) === 'loading') this.fragmentStates.set(i, 'pending');\n }\n this.currentJobs.clear();\n this.pendingFetchIndices.clear();\n this.appendHold = null;\n // Reset discard counts — SB state may change after seek/reload, making\n // previously-discarded fragments decodable with the new buffer layout.\n this.discardCounts.clear();\n for (const item of this.appendQueue)\n if (this.fragmentStates.get(item.index) === 'loading') this.fragmentStates.set(item.index, 'pending');\n this.appendQueue = [];\n // Reject only append waiters here — buffered waiters may still resolve\n // from the pre-abort state and should not be failed on a seek. Dispose\n // rejects both via rejectAllWaiters() in its own path.\n const err = new Error('Seek aborted pending append');\n for (const w of this.appendWaiters) w.reject(err);\n this.appendWaiters = [];\n }\n\n /** True when fragment `index` will still arrive on its own — an in-flight\n * fetch, a queued append, or an append already committed to the\n * SourceBuffer. False means NOTHING is going to resolve\n * `waitForAppend(index)`, so a caller waiting on it would block until its\n * timeout with this FragmentManager completely idle.\n *\n * Exists because `fetchFragment`'s abort path returns NORMALLY — it calls\n * `resetFragmentState(index)` and returns rather than throwing — so a\n * caller that awaited that job's promise cannot tell a successful fetch\n * from an aborted one. See the `'loading'` route in\n * `seekCoordinator._smartBuffer`, which is what this guards. */\n isFragmentInFlight(index: number): boolean {\n if (this.fragmentStates.get(index) === 'loaded') return true;\n if (this.currentJobs.has(index) || this.pendingFetchIndices.has(index)) return true;\n if (this.appendingIndex === index) return true;\n return this.appendQueue.some(item => item.index === index);\n }\n\n getLoadedCount(): number {\n let c = 0; for (const s of this.fragmentStates.values()) if (s === 'loaded') c++; return c;\n }\n\n /** Number of in-flight fragment fetches. Used by usePlaylistPreBuffer to detect idle state. */\n getPendingCount(): number { return this.currentJobs.size; }\n\n /** Fragments queued for append but not yet confirmed in SourceBuffer. Used by E2E specs. */\n getPendingAppendCount(): number { return this.pendingFetchIndices.size; }\n\n /** Emit a single warn-level digest of the load's init phase: time-to-gate,\n * init-segment timing, per-fragment fetch/queue/append timings for the\n * first 10 fragments, APPEND-HOLD reason counts, and snapshot of state at\n * emission. Always reported (warn level), so multi-viewer asymmetric\n * startup can be diagnosed without wp:reportLogs=true.\n *\n * Idempotent: subsequent calls are no-ops once initDigestEmitted=true.\n * Caller should pass `label` ('video', 'audio:1', etc.) so logs from\n * multiple FMs sharing one MediaSource can be told apart. */\n emitInitDigest(label: string, reason: string): void {\n if (this.initDigestEmitted) return;\n this.initDigestEmitted = true;\n const now = performance.now();\n const loadMs = this.loadStartTs ? Math.round(now - this.loadStartTs) : null;\n const initMs = (this.initFetchStart && this.initAppendDone)\n ? {\n fetch: Math.round(this.initFetchEnd - this.initFetchStart),\n append: Math.round(this.initAppendDone - this.initFetchEnd),\n ...(this.initTtfbMs >= 0 ? { ttfb: this.initTtfbMs } : {}),\n ...(this.initCfStatus ? { cf: this.initCfStatus } : {}),\n }\n : null;\n // Compact per-fragment timing entries: relative to loadStartTs in ms,\n // only deltas are kept to fit MAX_DATA_SIZE=1024.\n const frags: FragInitDigestEntry[] = [];\n const sorted = Array.from(this.fragInitTimings.entries()).sort((a, b) => a[0] - b[0]);\n for (const [i, t] of sorted) {\n const f: FragInitDigestEntry = { i, src: t.src ?? '?' };\n if (t.fetchStart) f.fs = Math.round(t.fetchStart - this.loadStartTs);\n if (t.fetchStart && t.fetchEnd) f.fM = Math.round(t.fetchEnd - t.fetchStart);\n if (t.queued && t.fetchEnd) f.qD = Math.round(t.queued - t.fetchEnd);\n if (t.queued && t.appendDone) f.aM = Math.round(t.appendDone - t.queued);\n if (t.bytes) f.B = t.bytes;\n if (t.ttfbMs !== undefined) f.tt = t.ttfbMs;\n if (t.cf) f.cf = t.cf;\n if (t.err) f.e = t.err;\n frags.push(f);\n }\n const loaded = this.getLoadedCount();\n warn('buffer', `INIT-DIGEST [${label}] reason=${reason} ${JSON.stringify({\n loadMs, initMs, loaded,\n gateBufAheadMs: Math.round(this.getBufferedAheadMs(this.currentPlayheadMs)),\n holds: this.initHoldCounts,\n pending: Array.from(this.pendingFetchIndices).sort((a, b) => a - b),\n queue: this.appendQueue.map(q => q.index),\n frags,\n })}`);\n }\n\n dumpBufferDiagnostics(playheadMs: number, reason: string): void {\n const idx = findFragmentIndex(this.fragments, playheadMs);\n const lo = Math.max(0, idx - 3), hi = Math.min(this.fragments.length - 1, idx + 6);\n const frags = [];\n for (let i = lo; i <= hi; i++) {\n const f = this.fragments[i], s = this.fragmentStates.get(i) || 'unknown';\n frags.push(`#${i}[${(f.start_ms / 1000).toFixed(1)}-${(f.end_ms / 1000).toFixed(1)}s]=${s}${this.currentJobs.has(i) ? '+job' : ''}${i === idx ? ' <<PLAYHEAD' : ''}`);\n }\n warn('buffer', `BUFFER-DIAG [${reason}] ${JSON.stringify({\n playheadMs: Math.round(playheadMs), aheadMs: Math.round(this.getBufferedAheadMs(playheadMs)),\n isBuffered: this.isBuffered(playheadMs), sbRanges: formatBufferedRanges(this.sourceBuffer?.buffered),\n fragments: frags, activeJobs: Array.from(this.currentJobs.keys()),\n appendQueue: this.appendQueue.map(q => q.index),\n pendingFetchIndices: Array.from(this.pendingFetchIndices).sort((a, b) => a - b),\n isAppending: this.isAppending, appendingIndex: this.appendingIndex,\n })}`);\n }\n\n /** Compact per-track geometry for stall diagnostics, cheap enough to attach\n * to every SILENT-STALL / stall-onset warn.\n *\n * This exists because the fields those warns DID carry could not tell an\n * audio starvation apart from a video hole. `reportedBufferedAheadMs` is the\n * video FM's, and it read a healthy 30s through the entire cedar-ridge\n * 2026-08-25 freeze; the actual fault was that the audio SourceBuffer's\n * leading edge had stopped advancing, which appears in NO field of any warn\n * or health report. `bufferedEndMs` is the number that identifies it: when\n * the playhead equals an audio track's buffered end, that track is the\n * starved one, and `queueLen`/`pendingFetches`/`heldBehind` say whether the\n * append pipeline is merely slow or wedged. */\n getTrackDiag(playheadMs: number): {\n bufferedEndMs: number; bufferedAheadMs: number; sbRanges: string[];\n queueLen: number; pendingFetches: number[]; appendingIndex: number | null;\n heldBehindMs: number; loaded: number;\n } {\n const b = this.sourceBuffer?.buffered;\n return {\n bufferedEndMs: Math.round(this.getBufferedEnd(playheadMs)),\n bufferedAheadMs: Math.round(this.getBufferedAheadMs(playheadMs)),\n sbRanges: b ? formatBufferedRanges(b) : [],\n queueLen: this.appendQueue.length,\n pendingFetches: Array.from(this.pendingFetchIndices).sort((a, b2) => a - b2),\n appendingIndex: this.appendingIndex,\n heldBehindMs: this.appendHold ? Date.now() - this.appendHold.since : 0,\n loaded: this.getLoadedCount(),\n };\n }\n\n getBufferState(): { ranges: string[]; gaps: boolean; fragmentStates: Record<string, number> } {\n const r = formatBufferedRanges(this.sourceBuffer?.buffered);\n const c = { pending: 0, loading: 0, loaded: 0, error: 0 };\n for (const s of this.fragmentStates.values()) c[s]++;\n return { ranges: r, gaps: r.length > 1, fragmentStates: c };\n }\n\n getAverageDownloadRate(): number { return getAvgDownloadRate(this.downloadSamples); }\n\n /** Nominal media end time (ms) = last fragment's end. 0 if no index loaded.\n * Reported in health so the leader's status card can tell \"near end-of-stream,\n * little buffer ahead is expected\" from a genuine low-buffer stall. */\n getMediaEndMs(): number {\n const n = this.fragments.length;\n return n > 0 ? this.fragments[n - 1].end_ms : 0;\n }\n\n /** If the playhead is in a gap and a buffered range starts within maxGapMs ahead,\n * returns that range's start time in ms. Used by stall recovery to skip over\n * structural gaps caused by late-IDR fragments (where the IDR is deep inside the\n * fragment's mdat so MSE only buffers from the IDR timestamp, not the nominal start). */\n findNextRangeStart(timeMs: number, maxGapMs: number): number | null {\n if (!this.sourceBuffer?.buffered) return null;\n if (this.mediaSource?.readyState !== 'open') return null;\n const sec = timeMs / 1000;\n const b = this.sourceBuffer.buffered;\n // Do NOT short-circuit on isBuffered(): findContainingRangeIndex uses an\n // inclusive `timeSec <= end(i)`, so a playhead stalled *exactly at a range's\n // trailing edge* counts as \"buffered\" even though it has zero data ahead and\n // is sitting on the lip of an unfillable gap. That blind spot froze the\n // 2026-06-22 ember-cove user (stall at 602.60 = end of [582.58–602.60], gap\n // to [604.60–…] never skipped → leader stall-paused the room in a loop).\n // Instead: bail only when the playhead is comfortably INSIDE a range (real\n // data ahead). Measure the gap from the containing range's END (not the raw\n // playhead) so the test sees the true structural gap — a single missing\n // fragment is ~2002ms (24000 timescale / 23.976fps), so the caller's\n // maxGapMs must exceed one fragment (see GAP_SKIP_MAX_MS).\n const idx = findContainingRangeIndex(b, sec, SB_TOLERANCE.RANGE);\n if (idx >= 0 && (b.end(idx) - sec) > GAP_SKIP_EDGE_SEC) return null;\n const from = idx >= 0 ? Math.max(sec, b.end(idx)) : sec;\n for (let i = 0; i < b.length; i++) {\n const rangeStart = b.start(i);\n if (rangeStart > from && (rangeStart - from) * 1000 <= maxGapMs) {\n return rangeStart * 1000;\n }\n }\n return null;\n }\n\n /** Diagnostic mirror of {@link findNextRangeStart}: describes the forward\n * buffer geometry at a stall so a *declined* gap-skip is never silent again.\n * The 2026-06-22 ember-cove freeze was invisible precisely because gap-skip\n * returned null with no log — we had to deduce \"playhead at the trailing edge\n * of a range whose gap (2002ms) exceeded the 2000ms window\". This surfaces\n * exactly that: `atEdge` (playhead on a range's lip), `aheadInRangeMs` (real\n * data ahead), the `nextRangeStartMs`, and the `gapMs` to it — measured the\n * same way the skip decision measures it (from the containing range's end).\n * Returns null only when there is no usable SourceBuffer. */\n getForwardGapDiag(timeMs: number): {\n atEdge: boolean;\n aheadInRangeMs: number;\n nextRangeStartMs: number | null;\n gapMs: number | null;\n } | null {\n if (!this.sourceBuffer?.buffered) return null;\n if (this.mediaSource?.readyState !== 'open') return null;\n const sec = timeMs / 1000;\n const b = this.sourceBuffer.buffered;\n const idx = findContainingRangeIndex(b, sec, SB_TOLERANCE.RANGE);\n const aheadInRange = idx >= 0 ? Math.max(0, b.end(idx) - sec) : 0;\n const from = idx >= 0 ? Math.max(sec, b.end(idx)) : sec;\n let nextStart: number | null = null;\n for (let i = 0; i < b.length; i++) {\n if (b.start(i) > from) { nextStart = b.start(i); break; }\n }\n return {\n atEdge: idx >= 0 && aheadInRange <= GAP_SKIP_EDGE_SEC,\n aheadInRangeMs: aheadInRange * 1000,\n nextRangeStartMs: nextStart !== null ? nextStart * 1000 : null,\n gapMs: nextStart !== null ? (nextStart - from) * 1000 : null,\n };\n }\n\n /** Best-effort attempt to signal end of stream on the MediaSource.\n *\n * MediaSource.endOfStream() throws InvalidStateError if any SourceBuffer\n * attached to the MediaSource is currently updating. Under v3-fragmented-\n * audio we have video + audio SBs on the same MediaSource — the video\n * FM observes its last fragment landing, but the audio FM may still be\n * appending its own last fragment at that exact moment. Instead of\n * throwing and leaving the MediaSource stuck at state 'open' (which\n * prevents the video element's 'ended' event and drains the buffer\n * indefinitely), we set a pendingEndOfStream flag and retry on a short\n * timer until every SB is idle. Capped retries so a genuinely stuck\n * SB doesn't burn CPU. */\n private tryEndOfStream(): void {\n if (!this.pendingEndOfStream) return;\n const ms = this.mediaSource;\n if (!ms || ms.readyState !== 'open') {\n this.pendingEndOfStream = false;\n return;\n }\n // Any SB on the MS still updating? Retry shortly.\n for (let i = 0; i < ms.sourceBuffers.length; i++) {\n if (ms.sourceBuffers[i].updating) {\n if (this.eosRetryTimer) return; // timer already pending\n this.eosRetryTimer = setTimeout(() => {\n this.eosRetryTimer = null;\n this.tryEndOfStream();\n }, END_OF_STREAM_RETRY_INTERVAL_MS);\n return;\n }\n }\n try {\n ms.endOfStream();\n this.pendingEndOfStream = false;\n debug('buffer', 'END-OF-STREAM signalled');\n } catch (e) {\n // Defense in depth: if endOfStream still throws (e.g., MS transitioned\n // to 'ended' concurrently via another path), clear the flag so we\n // don't loop forever. The video element should still fire 'ended'\n // naturally once playback reaches the end of the buffered range.\n warn('buffer', 'endOfStream call failed', { error: (e as Error).message });\n this.pendingEndOfStream = false;\n }\n }\n\n dispose(): void {\n this.disposed = true;\n if (this.eosRetryTimer) {\n clearTimeout(this.eosRetryTimer);\n this.eosRetryTimer = null;\n }\n this.pendingEndOfStream = false;\n this.abortAllJobs('dispose');\n this.rejectAllWaiters(new Error('FragmentManager disposed'));\n this.appendQueue = [];\n this.isAppending = false;\n this.eviction.reset();\n this.appendingIndex = null;\n this.fragmentStates.clear();\n this.errorTimestamps.clear();\n this.discardCounts.clear();\n this.fetchErrorCycles.clear();\n this.downloadSamples = [];\n this.preAppendRanges = [];\n this.sourceBuffer = null;\n this.mediaSource = null;\n this.initState = 'pending';\n this.initRange = null;\n }\n}\n","/**\n * Pure subtitle-enablement policy, shared by MediaPlayer and unit-tested here.\n *\n * Problem it solves (cedar-ridge 2026-07-09, \"subtitles weren't tracked\"):\n * subtitle display was gated by a single persisted global boolean. Turning CC\n * off during English-dubbed trailers silently suppressed subtitles for a later\n * Japanese-audio episode that shipped an English subtitle. A single global can't\n * express \"on for subbed content, off for dubbed.\"\n */\n\n/**\n * Whether subtitles should default ON for this media based on its content:\n * foreign (non-English) audio that ships an English subtitle track.\n */\nexport function autoSubtitleDefault(\n selectedAudioLang: string | undefined,\n hasEngSub: boolean,\n): boolean {\n return !!selectedAudioLang && selectedAudioLang !== 'eng' && hasEngSub;\n}\n\n/**\n * Effective subtitle enablement for the current media.\n * - `override` is the user's explicit per-media toggle (null = untouched). It\n * always wins so the user can force on/off for what they're watching.\n * - Otherwise subtitles are on if the persisted global pref is on OR the content\n * warrants them (foreign audio + English sub).\n * The override is expected to reset on every media change so each media\n * re-derives its content-based default.\n */\nexport function resolveSubtitlesEnabled(opts: {\n override: boolean | null;\n globalEnabled: boolean;\n selectedAudioLang: string | undefined;\n hasEngSub: boolean;\n}): boolean {\n if (opts.override !== null) return opts.override;\n return opts.globalEnabled || autoSubtitleDefault(opts.selectedAudioLang, opts.hasEngSub);\n}\n","import { useAppStore } from './store';\nimport { beaconLog } from './debug';\n\n/**\n * One-shot page-reload recovery, rate-limited to avoid churn.\n *\n * A fresh document is the only thing that reliably clears browser-MSE-layer\n * phantom-buffer corruption (see the 2026-06-08 incident), so both the phantom\n * path (MediaPlayer) and the stuck-client watchdog (useStallRecovery) reload as\n * a last resort. But on a chronically-bad connection a single recovery reset the\n * budget, producing a reload LOOP (2026-06-16: one viewer reloaded ~30× in 30min,\n * once every ~60s — kept her in the show but churned). A reload cannot fix a bad\n * link, so after MAX_RELOADS within WINDOW_MS we give up and tell the user.\n */\nconst HISTORY_KEY = 'wp_reload_history'; // JSON array of reload timestamps (ms), for the rate limiter\nconst FLAG_KEY = 'wp_phantom_reload'; // breadcrumb: set on reload, read as POST-AUTO-RELOAD, cleared on successful init\nconst WINDOW_MS = 5 * 60_000;\nconst MAX_RELOADS = 3;\n\nfunction recentReloads(): number[] {\n try {\n const raw = sessionStorage.getItem(HISTORY_KEY);\n const arr: number[] = raw ? JSON.parse(raw) : [];\n return arr.filter(t => Date.now() - t < WINDOW_MS);\n } catch {\n return [];\n }\n}\n\n/**\n * Attempt a recovery reload. Beacons the trigger reason FIRST (so it survives the\n * reload), then either reloads or — if we've already burned the budget — gives up\n * with a persistent toast. Returns true if a reload was triggered.\n */\nexport function attemptAutoReload(reason: string, data?: Record<string, unknown>): boolean {\n const recent = recentReloads();\n if (recent.length >= MAX_RELOADS) {\n beaconLog('error', 'buffer',\n `AUTO-RELOAD SUPPRESSED reason=${reason} — ${recent.length} reloads in ${WINDOW_MS / 60000}m, giving up (connection looks unstable)`,\n data);\n try {\n useAppStore.getState().pushToast('error',\n 'Playback keeps dropping — your connection looks unstable. Try reloading the page or rejoining.');\n } catch { /* noop */ }\n return false;\n }\n try {\n recent.push(Date.now());\n sessionStorage.setItem(HISTORY_KEY, JSON.stringify(recent));\n sessionStorage.setItem(FLAG_KEY, String(Date.now()));\n } catch { /* sessionStorage unavailable */ }\n // Synchronous beacon — guaranteed to reach the server before reload tears down\n // the page, unlike the async log batch (which used to be lost here).\n beaconLog('error', 'buffer', `AUTO-RELOAD reason=${reason} attempt=${recent.length}/${MAX_RELOADS}`, data);\n try { window.location.reload(); } catch { /* noop */ }\n return true;\n}\n\n/** Breadcrumb timestamp if this load follows an auto-reload (else null). */\nexport function reloadFlagTs(): number | null {\n try {\n const v = sessionStorage.getItem(FLAG_KEY);\n return v ? Number(v) : null;\n } catch {\n return null;\n }\n}\n\n/** Clear the breadcrumb after a successful init (recovery confirmed). */\nexport function clearReloadFlag(): void {\n try { sessionStorage.removeItem(FLAG_KEY); } catch { /* noop */ }\n}\n","/**\n * Autoplay-policy-aware playback (\"riding\" the browser policy).\n *\n * Browsers grant audible autoplay per-origin via the Media Engagement Index\n * (MEI) — it rises as the user watches WITH SOUND on this origin, and once it\n * crosses a threshold the browser autoplays with sound, no gesture. That's how\n * X/TikTok \"autoplay with sound\": they're not bypassing the policy, their users\n * have earned MEI. We can't force it, but we ride it:\n *\n * - allowed / unknown → play AUDIBLE (free sound for engaged users)\n * - allowed-muted → play MUTED as a brief BRIDGE, then unmute on the\n * first interaction. Audible playback resumes ASAP, so\n * we keep building MEI (never leave it muted — that's\n * what sabotages MEI, see audioUnlock.ts).\n * - disallowed → can't autoplay at all; retry the whole play on the\n * next interaction.\n *\n * The user's explicit mute (mediaPrefs.muted) is sacred: if they chose mute we\n * never auto-unmute.\n */\nimport { onNextInteraction } from './audioUnlock';\nimport { warn, debug } from './debug';\n\ntype AutoplayPolicy = 'allowed' | 'allowed-muted' | 'disallowed' | undefined;\ntype AutoplayPlan = 'audible' | 'muted-bridge' | 'user-muted' | 'deferred';\n\n/** Query the browser's autoplay policy for media elements (undefined on older\n * browsers without the API — callers then optimistically try audible). */\nfunction getMediaAutoplayPolicy(): AutoplayPolicy {\n const nav = navigator as Navigator & { getAutoplayPolicy?: (t: string) => AutoplayPolicy };\n try {\n return typeof nav.getAutoplayPolicy === 'function' ? nav.getAutoplayPolicy('mediaelement') : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Pure decision: given the policy + the user's explicit mute pref, what should\n * we do? No DOM access — unit-testable.\n */\nexport function decideAutoplayPlan(policy: AutoplayPolicy, userWantsMuted: boolean): AutoplayPlan {\n if (policy === 'disallowed') return 'deferred'; // even muted autoplay is blocked → wait for a gesture\n if (userWantsMuted) return 'user-muted'; // honor explicit mute (muted play is allowed otherwise)\n if (policy === 'allowed-muted') return 'muted-bridge';\n return 'audible'; // 'allowed' or undefined (optimistic)\n}\n\ninterface PlayRidingOpts {\n /** The user's explicit mute pref (mediaPrefs.muted). Never overridden. */\n userWantsMuted: boolean;\n /** Called when we enter the muted bridge (UI: show a \"tap for sound\" hint). */\n onMutedBridge?: () => void;\n /** Called when the bridge unmutes on first interaction (UI: clear the hint). */\n onUnmuted?: () => void;\n /** Log label (e.g. 'seek-coord' or 'video'). */\n label: string;\n}\n\n/**\n * Play `v`, riding the autoplay policy. See module doc. Idempotent-ish (play()\n * is safe to call repeatedly). Returns the plan it chose (for logging/tests).\n */\nexport function playRidingAutoplay(v: HTMLVideoElement, opts: PlayRidingOpts): AutoplayPlan {\n const policy = getMediaAutoplayPolicy();\n const plan = decideAutoplayPlan(policy, opts.userWantsMuted);\n debug(opts.label, 'autoplay plan', { policy: policy ?? 'unknown', plan, userWantsMuted: opts.userWantsMuted });\n\n const enterMutedBridge = () => {\n v.muted = true;\n v.play().then(() => {\n opts.onMutedBridge?.();\n // Unmute on the first user interaction so audible playback resumes and\n // keeps building MEI — unless the user explicitly chose mute meanwhile.\n onNextInteraction(() => {\n if (!opts.userWantsMuted) {\n v.muted = false;\n opts.onUnmuted?.();\n debug(opts.label, 'autoplay bridge unmuted on interaction');\n }\n });\n }).catch((e: { name?: string }) => warn(opts.label, 'muted-bridge play failed', { error: e?.name }));\n };\n\n if (plan === 'deferred') {\n onNextInteraction(() => { v.play().catch((e: { name?: string }) => warn(opts.label, 'deferred play retry failed', { error: e?.name })); });\n return plan;\n }\n if (plan === 'muted-bridge') {\n enterMutedBridge();\n return plan;\n }\n\n // 'audible' or 'user-muted': set mute per intent and attempt play. If the\n // browser blocks an audible attempt anyway (policy was 'allowed'/unknown but\n // it still rejected), fall back to the muted bridge.\n v.muted = opts.userWantsMuted;\n v.play().catch((err: { name?: string; message?: string }) => {\n if (err?.name === 'NotAllowedError' && !opts.userWantsMuted) {\n enterMutedBridge();\n } else if (err?.name !== 'AbortError') {\n warn(opts.label, 'video.play() failed', { error: err?.name, message: err?.message });\n }\n });\n return plan;\n}\n","/**\n * SeekCoordinator — Serializes seek operations in MediaPlayer.\n *\n * Uses AbortController identity (not generation numbers) to guarantee\n * exactly one owner clears the seek-in-flight flag. When a new seek\n * arrives while one is in flight, the running seek is preempted (latest wins).\n */\n\nimport { type FragmentManager } from './fragmentManager';\nimport { debug, warn } from './debug';\nimport { useAppStore } from './store';\nimport { playRidingAutoplay } from './autoplay';\nimport { perf } from './perfCollector';\n\n// ── Priority levels ──────────────────────────────────────────────────\n\nexport const SeekPriority = {\n /** Viewer join-time correction — can be preempted by any sync seek. */\n VIEWER_CORRECTION: 2,\n /** Room-level seeks from snapshot changes — highest priority. */\n SEEK_EFFECT: 3,\n} as const;\n\ntype SeekPriorityLevel = (typeof SeekPriority)[keyof typeof SeekPriority];\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface SeekRequest {\n /** Target playhead position in milliseconds. */\n targetMs: number;\n /** Determines preemption behaviour. */\n priority: SeekPriorityLevel;\n /** Diagnostic label for logging. */\n caller: string;\n /** Whether to pause the video before seeking and resume after. Default: true. */\n pauseSeekResume?: boolean;\n /** Fires after the seek completes. Receives actual video.currentTime in ms. */\n onComplete?: (actualMs: number) => void;\n /** Fires when the seek is preempted (err=null) or fails. */\n onAborted?: (err: Error | null) => void;\n}\n\ninterface SeekCoordinatorDeps {\n videoRef: React.RefObject<HTMLVideoElement | null>;\n fragmentManagerRef: React.RefObject<FragmentManager | null>;\n /** MSE audio: up to one FragmentManager for the currently-selected\n * audio track, sharing the single MediaSource on the video element.\n * Length 0 or 1 after the single-SB-swap refactor. The coordinator\n * runs ensureBuffered / prefetchAround across it in parallel with the\n * video FM so audio doesn't trail after a seek. */\n audioFragmentManagersRef: React.RefObject<FragmentManager[]>;\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\n/** Wait for the video element to finish its internal seek (fires 'seeked'). */\nexport function waitForSeeked(video: HTMLVideoElement, timeoutMs = 3000): Promise<void> {\n return new Promise((resolve) => {\n if (!video.seeking) { resolve(); return; }\n const timer = setTimeout(() => {\n video.removeEventListener('seeked', onSeeked);\n perf.recordWaitForSeekedTimeout();\n warn('seek-coord', 'waitForSeeked timed out', {\n currentTime: video.currentTime.toFixed(2),\n readyState: video.readyState,\n timeoutMs,\n });\n resolve();\n }, timeoutMs);\n function onSeeked() {\n clearTimeout(timer);\n video.removeEventListener('seeked', onSeeked);\n resolve();\n }\n video.addEventListener('seeked', onSeeked, { once: true });\n });\n}\n\n// ── SeekCoordinator ──────────────────────────────────────────────────\n\nexport class SeekCoordinator {\n // ── State ──\n private _seekInFlight = false;\n /** Route taken by the in-flight seek (buffered/loading/nearby-pending/far) —\n * captured in _smartBuffer, surfaced in the SEEK-DIGEST completion log. */\n private _lastSeekRoute = '';\n private _currentAbort: AbortController | null = null;\n private _currentPriority: SeekPriorityLevel | null = null;\n private _currentCaller: string | null = null;\n private _currentTargetMs: number | null = null;\n private _disposed = false;\n\n private deps: SeekCoordinatorDeps;\n\n constructor(deps: SeekCoordinatorDeps) {\n this.deps = deps;\n }\n\n // ── Public API ──\n\n /** Is a seek currently in progress? Replaces seekInFlightRef. */\n get seekInFlight(): boolean {\n return this._seekInFlight;\n }\n\n /** Where the in-flight seek is going, or null. For work that must land where\n * the playhead WILL be — the element's currentTime does not move until the\n * seek's data is buffered. */\n get inFlightTargetMs(): number | null {\n return this._seekInFlight ? this._currentTargetMs : null;\n }\n\n /**\n * Submit a seek request.\n *\n * - If no seek is in flight, starts immediately.\n * - If a higher-or-equal priority request arrives, the running seek is aborted.\n * - A lower-priority request is rejected immediately.\n */\n submit(request: SeekRequest): Promise<{ actualMs: number } | null> {\n if (this._disposed) return Promise.resolve(null);\n\n const v = this.deps.videoRef.current;\n\n // Priority gate: lower priority is rejected, same-or-higher preempts\n if (this._seekInFlight && this._currentPriority !== null) {\n if (request.priority < this._currentPriority) {\n debug('seek-coord', `SUBMIT REJECTED [${request.caller}] — active [${this._currentCaller}] has higher priority`, {\n targetMs: Math.round(request.targetMs),\n videoTime: v ? Math.round(v.currentTime * 1000) : null,\n videoPaused: v?.paused,\n });\n request.onAborted?.(null);\n return Promise.resolve(null);\n }\n }\n\n // Preempt current seek (latest wins)\n if (this._currentAbort) {\n warn('seek-coord', `SUBMIT PREEMPT [${this._currentCaller}] → [${request.caller}]`, {\n newTargetMs: Math.round(request.targetMs),\n });\n this._currentAbort.abort();\n this._currentAbort = null;\n } else {\n debug('seek-coord', `SUBMIT [${request.caller}]`, {\n targetMs: Math.round(request.targetMs),\n videoTime: v ? Math.round(v.currentTime * 1000) : null,\n videoPaused: v?.paused,\n videoReady: v?.readyState,\n });\n }\n\n return this._execute(request);\n }\n\n /** Cancel any in-flight seek. Used on unmount or media change. */\n cancel(): void {\n if (this._currentAbort) {\n this._currentAbort.abort();\n this._currentAbort = null;\n }\n this._seekInFlight = false;\n this._currentPriority = null;\n this._currentCaller = null;\n this._currentTargetMs = null;\n }\n\n /** Dispose the coordinator; all future submits become no-ops. */\n dispose(): void {\n this.cancel();\n this._disposed = true;\n }\n\n // ── Internal ──\n\n private async _execute(\n request: SeekRequest,\n ): Promise<{ actualMs: number } | null> {\n const abort = new AbortController();\n this._currentAbort = abort;\n this._seekInFlight = true;\n this._currentPriority = request.priority;\n this._currentCaller = request.caller;\n this._currentTargetMs = request.targetMs;\n\n const { targetMs, caller } = request;\n const pauseSeekResume = request.pauseSeekResume ?? true;\n\n const v = this.deps.videoRef.current;\n const fm = this.deps.fragmentManagerRef.current;\n\n if (!v || !fm) {\n warn('seek-coord', `EXECUTE bail — missing ${!v ? 'video' : 'fragmentManager'}`, { caller });\n this._clear(abort);\n return null;\n }\n\n const executeStart = performance.now();\n const wasPlaying = !v.paused;\n\n try {\n // 1. Optionally pause\n if (pauseSeekResume && wasPlaying) {\n this._pause(v);\n }\n this._checkAborted(abort);\n\n // 2. Ensure target is buffered on video + all audio FMs (smart policy).\n // Running both in parallel prevents audio from lagging behind after\n // the seek completes.\n const audioFms = this.deps.audioFragmentManagersRef.current ?? [];\n const needsBuffer = !fm.isBuffered(targetMs) || audioFms.some(afm => !afm.isBuffered(targetMs));\n if (needsBuffer) {\n const bufferStart = performance.now();\n debug('seek-coord', `BUFFERING [${caller}] target=${Math.round(targetMs)}ms`, {\n videoTime: Math.round(v.currentTime * 1000),\n videoReady: v.readyState,\n videoPaused: v.paused,\n audioFmCount: audioFms.length,\n });\n await this._smartBufferAll(fm, audioFms, targetMs, caller, abort);\n this._checkAborted(abort);\n debug('seek-coord', `BUFFERED [${caller}] took ${Math.round(performance.now() - bufferStart)}ms`);\n } else {\n debug('seek-coord', `ALREADY BUFFERED [${caller}] target=${Math.round(targetMs)}ms`);\n }\n\n // 3. Seek video. Audio rides on the same MediaSource.\n const seekSec = targetMs / 1000;\n v.currentTime = seekSec;\n await waitForSeeked(v);\n this._checkAborted(abort);\n\n const actualMs = v.currentTime * 1000;\n const totalMs = Math.round(performance.now() - executeStart);\n\n // SEEK-DIGEST: one warn per completed seek (forwarded in prod, unlike the\n // debug-level seek trace) so seeking smoothness is visible in party logs —\n // route shows whether it was instant (buffered), awaited a fetch (loading/\n // nearby), or a full re-buffer (far); totalMs is the user-felt latency.\n warn('seek-coord', `SEEK-DIGEST [${caller}] route=${this._lastSeekRoute} ${totalMs}ms`, {\n targetMs: Math.round(targetMs),\n actualMs: Math.round(actualMs),\n delta: Math.round(actualMs - targetMs),\n route: this._lastSeekRoute,\n totalMs,\n videoReady: v.readyState,\n });\n perf.recordSeekCompleted(caller, targetMs, totalMs);\n\n // 4. Resume if should be playing\n const shouldPlay = useAppStore.getState().snapshot.playing;\n if (shouldPlay && (pauseSeekResume || v.paused)) {\n this._resume(v);\n }\n\n request.onComplete?.(actualMs);\n\n // Kick prefetch immediately so the next fragments start downloading\n // before the 2s interval fires. Without this, seeks into already-\n // buffered regions can stall when the playhead outruns the buffer\n // edge within the 2s window. Audio FMs get the same kick — otherwise\n // audio can run out mid-seek while video stays buffered.\n const currentFm = this.deps.fragmentManagerRef.current;\n if (currentFm) currentFm.prefetchAround(actualMs);\n const currentAudioFms = this.deps.audioFragmentManagersRef.current ?? [];\n for (const afm of currentAudioFms) afm.prefetchAround(actualMs);\n\n return { actualMs };\n } catch (e) {\n const elapsedMs = Math.round(performance.now() - executeStart);\n if ((e as Error).name === 'AbortError') {\n debug('seek-coord', `PREEMPTED [${caller}] after ${elapsedMs}ms`, {\n targetMs: Math.round(targetMs),\n videoTime: Math.round(v.currentTime * 1000),\n });\n request.onAborted?.(null);\n return null;\n }\n\n warn('seek-coord', `FAILED [${caller}] after ${elapsedMs}ms: ${(e as Error)?.message}`, {\n targetMs: Math.round(targetMs),\n videoTime: Math.round(v.currentTime * 1000),\n videoReady: v.readyState,\n videoPaused: v.paused,\n });\n request.onAborted?.(e as Error);\n\n // On failure, try to resume if we paused. Use snapshot.playing rather than\n // wasPlaying — a prior seek may have left the video paused (wasPlaying=false)\n // even though the room is still playing, and we must resume it.\n if (pauseSeekResume) {\n const shouldPlay = useAppStore.getState().snapshot.playing;\n if (shouldPlay && v.paused) {\n this._resume(v);\n }\n }\n return null;\n } finally {\n this._clear(abort);\n }\n }\n\n /**\n * Clear seek state, but ONLY if the given AbortController is still\n * the active one. This is the key fix: identity check, not generation\n * counter. A stale finally from a preempted seek cannot clear state.\n */\n private _clear(abort: AbortController): void {\n if (this._currentAbort === abort) {\n this._seekInFlight = false;\n this._currentPriority = null;\n this._currentCaller = null;\n this._currentTargetMs = null;\n this._currentAbort = null;\n }\n }\n\n private _checkAborted(abort: AbortController): void {\n if (abort.signal.aborted) {\n const err = new DOMException('Seek preempted', 'AbortError');\n throw err;\n }\n }\n\n /**\n * Smart buffer policy using resolveTarget():\n * - buffered → no-op (caller already checks, but handle gracefully)\n * - loading → await existing download, then wait for append\n * - nearby → fetchNearby (no abort, parallel with existing jobs)\n * - far → ensureBuffered (aborts in-progress downloads to prioritize target)\n */\n private async _smartBuffer(\n fm: FragmentManager,\n targetMs: number,\n caller: string,\n abort: AbortController,\n ): Promise<void> {\n const resolution = fm.resolveTarget(targetMs);\n this._lastSeekRoute = resolution.status;\n const bufState = fm.getBufferState();\n\n switch (resolution.status) {\n case 'buffered':\n debug('seek-coord', `SMART-BUFFER [${caller}] BUFFERED (no-op)`, {\n targetMs: Math.round(targetMs),\n });\n return;\n\n case 'loading':\n // A seek is a PRIORITY action: terminate whatever is running, clear\n // the queue, and fetch the target now.\n //\n // This case used to piggyback instead — the target already had an\n // in-flight fetch (normally prefetchAround's), so it awaited that\n // job's promise and then waitForAppend(). It never issued a fetch of\n // its own and never aborted anything. That saved re-downloading ~1MB\n // and cost a 30s freeze when the borrowed job went away:\n // `fetchFragment`'s abort path returns NORMALLY (resetFragmentState +\n // return, not throw), so the resolved promise was indistinguishable\n // from success, and nothing re-issued the fragment.\n //\n // The window looks narrow — you have to seek onto the exact fragment\n // prefetch is fetching — but a watch party is synchronized BY\n // CONSTRUCTION: every viewer holds the same buffer and prefetches the\n // same fragment, so the race does not hit one viewer, it hits all of\n // them at once. cedar-ridge 2026-09-08: four for four, 29.9s each.\n //\n // Re-fetching one fragment costs ~200-500ms through the edge cache.\n // That is the entire price of deleting this class of deadlock.\n debug('seek-coord', `SMART-BUFFER [${caller}] LOADING → ensureBuffered (re-issuing #${resolution.fragmentIndex})`, {\n targetMs: Math.round(targetMs),\n });\n await fm.ensureBuffered(targetMs, caller, abort.signal);\n return;\n\n case 'nearby-pending':\n debug('seek-coord', `SMART-BUFFER [${caller}] NEARBY → fetchNearby`, {\n targetMs: Math.round(targetMs),\n loaded: bufState.fragmentStates.loaded,\n ranges: bufState.ranges.join(', '),\n });\n await fm.fetchNearby(targetMs, abort.signal);\n return;\n\n case 'far':\n debug('seek-coord', `SMART-BUFFER [${caller}] FAR → ensureBuffered (will abort ${bufState.fragmentStates.loading} jobs)`, {\n targetMs: Math.round(targetMs),\n loaded: bufState.fragmentStates.loaded,\n loading: bufState.fragmentStates.loading,\n ranges: bufState.ranges.join(', '),\n });\n await fm.ensureBuffered(targetMs, caller, abort.signal);\n return;\n }\n }\n\n /** Buffer video + all MSE audio FMs in parallel. Used by _execute so a\n * seek doesn't complete with audio still empty. Audio FM failures are\n * non-fatal: we log and move on so the video doesn't stall on an audio\n * hiccup. */\n private async _smartBufferAll(\n videoFm: FragmentManager,\n audioFms: FragmentManager[],\n targetMs: number,\n caller: string,\n abort: AbortController,\n ): Promise<void> {\n const videoNeedsBuffer = !videoFm.isBuffered(targetMs);\n const tasks: Promise<void>[] = [];\n if (videoNeedsBuffer) {\n tasks.push(this._smartBuffer(videoFm, targetMs, caller, abort));\n }\n for (const afm of audioFms) {\n if (afm.isBuffered(targetMs)) continue;\n tasks.push(\n this._smartBuffer(afm, targetMs, `${caller}-audio`, abort).catch(e => {\n if ((e as Error).name !== 'AbortError') {\n warn('seek-coord', `Audio FM buffer failed during ${caller}`, {\n error: (e as Error)?.message,\n });\n }\n }),\n );\n }\n if (tasks.length === 0) return;\n await Promise.all(tasks);\n }\n\n // ── Playback helpers ──\n\n private _pause(v: HTMLVideoElement): void {\n v.pause();\n }\n\n private _resume(v: HTMLVideoElement): void {\n // MSE audio rides on the <video> element. Play while \"riding\" the autoplay\n // policy: audible when the browser allows it (earned MEI = free sound, no\n // click), else a brief muted BRIDGE that unmutes on the first interaction\n // (keeps building MEI; never leaves it muted). The user's explicit mute\n // pref is sacred. See lib/autoplay.ts.\n const st = useAppStore.getState();\n playRidingAutoplay(v, {\n userWantsMuted: st.mediaPrefs.muted,\n onMutedBridge: () => useAppStore.getState().setMutedForAutoplay(true),\n onUnmuted: () => useAppStore.getState().setMutedForAutoplay(false),\n label: 'seek-coord',\n });\n }\n}\n","import { DEFAULT_HEALTH_BROADCAST_INTERVAL_MS, CORE_EVENTS } from '@watchparty/shared';\nimport { currentDeliveryArm } from './fragmentDelivery';\nimport { getSocket, currentPongAgeMs } from './socket';\nimport { useAppStore } from './store';\nimport { debug } from './debug';\n\nlet reportInterval: number | null = null;\n\n/**\n * Start periodic health reporting to the server (every 3s). Idempotent.\n */\nexport function startHealthReporting(): void {\n if (reportInterval !== null) return;\n debug('health', 'Starting health reporting');\n reportHealth();\n reportInterval = window.setInterval(reportHealth, DEFAULT_HEALTH_BROADCAST_INTERVAL_MS);\n}\n\n/**\n * Stop health reporting. Call on unmount or media unload.\n */\nexport function stopHealthReporting(): void {\n if (reportInterval === null) return;\n debug('health', 'Stopping health reporting');\n window.clearInterval(reportInterval);\n reportInterval = null;\n}\n\nfunction reportHealth(): void {\n const socket = getSocket();\n if (!socket.connected) return;\n\n const store = useAppStore.getState();\n if (!store.snapshot.mediaId) return;\n\n const fm = window.__FRAGMENT_MANAGER__;\n if (!fm) return;\n\n // Stale-media guard. During a media swap there is a window (tens to\n // hundreds of ms) where store.snapshot.mediaId has already been updated\n // from the server broadcast but the old FragmentManager is still the one\n // on __FRAGMENT_MANAGER__ — the new FM has not been created yet, or has\n // been created but setIndex() has not run. Reporting during that window\n // would carry the OLD media's bufferedAheadMs / fragments_loaded tagged\n // implicitly with the NEW media ID; the leader's health tab would then\n // display fake \"already buffered\" state for clients that have not\n // fetched a byte of the new media. Skip until the FM's cacheKey (set\n // by setIndex to the mediaId) matches the room's current mediaId.\n if (fm.cacheKey !== store.snapshot.mediaId) {\n debug('health', 'skip report — FM mediaId mismatch', { fmKey: fm.cacheKey, snapMedia: store.snapshot.mediaId });\n return;\n }\n\n // Use actual video currentTime, not snapshot.playheadMs (which only updates on server broadcasts)\n const video = document.querySelector('video');\n const actualPlayheadMs = video ? Math.round(video.currentTime * 1000) : store.snapshot.playheadMs;\n\n const bufferState = fm.getBufferState();\n const bufferedEndMs = fm.getBufferedEnd(actualPlayheadMs);\n\n // Buffer health must account for AUDIO, not just video.\n //\n // Video and audio ride separate SourceBuffers on one MediaSource, and the\n // playhead can only advance where BOTH have data. Until 2026-08-25 this\n // report read only `__FRAGMENT_MANAGER__` (the video FM), so an audio-side\n // starvation was invisible to every consumer of buffered_ahead_ms — the\n // leader's health card, the viewer buffer ring, bufferHealthLevel().\n //\n // cedar-ridge 2026-08-25: two viewers froze at exactly the end of the last\n // audio fragment that landed (thyeggman 1128.95s vs audio edge 1129.002s;\n // littlemiss 152.41s vs 152.405s) while this field reported a healthy ~30s\n // and is_buffering stayed false for the entire freeze. The room saw nothing\n // wrong; both users recovered only by refreshing.\n //\n // Report the MIN so a starved track cannot hide behind a healthy one, and\n // report the audio figure separately so the card can name which track it is.\n const audioFms = window.__AUDIO_FRAGMENT_MANAGERS__;\n let audioAheadMs = -1;\n for (const afm of audioFms ?? []) {\n // Skip an FM with no index yet: it reports 0 ahead simply because it has\n // not been given fragments, which would read as a total audio outage\n // during every normal media load.\n if (!afm.hasIndex) continue;\n const ahead = afm.getBufferedAheadMs(actualPlayheadMs);\n if (audioAheadMs < 0 || ahead < audioAheadMs) audioAheadMs = ahead;\n }\n const videoAheadMs = Math.max(0, bufferedEndMs - actualPlayheadMs);\n const worstAheadMs = audioAheadMs < 0 ? videoAheadMs : Math.min(videoAheadMs, audioAheadMs);\n\n const avgRtt = store.drift.rtts.length > 0\n ? store.drift.rtts.reduce((a, b) => a + b, 0) / store.drift.rtts.length\n : 0;\n\n socket.emit(CORE_EVENTS.HEALTH_REPORT, {\n avg_download_rate_bps: fm.getAverageDownloadRate(),\n buffered_ahead_ms: worstAheadMs,\n audio_buffered_ahead_ms: audioAheadMs < 0 ? -1 : Math.round(audioAheadMs),\n rtt_ms: avgRtt,\n playhead_ms: actualPlayheadMs,\n is_buffering: store.bufferState.isBuffering,\n fragments_loaded: bufferState.fragmentStates.loaded || 0,\n fragments_pending: bufferState.fragmentStates.pending || 0,\n fragments_error: bufferState.fragmentStates.error || 0,\n video_ready_state: video?.readyState ?? -1,\n duration_ms: fm.getMediaEndMs(),\n delivery_arm: currentDeliveryArm(),\n ...pageState(video),\n pong_age_ms: currentPongAgeMs(),\n reported_at: Date.now()\n });\n}\n\ninterface NetworkInformationLike { effectiveType?: string; rtt?: number; downlink?: number }\n\n/**\n * What the viewer's page was doing when it reported. A stall in a hidden tab\n * and a stall on screen are different incidents, and nothing else in the\n * report can tell them apart.\n */\nfunction pageState(video: HTMLVideoElement | null) {\n // Every global is guarded: this runs wherever the player does, including\n // hosts and test environments that supply only part of the DOM.\n const nav = typeof navigator === 'undefined' ? undefined : navigator as Navigator & { connection?: NetworkInformationLike };\n const conn = nav?.connection;\n return {\n page_visible: typeof document.visibilityState === 'string' ? document.visibilityState === 'visible' : undefined,\n page_focused: typeof document.hasFocus === 'function' ? document.hasFocus() : undefined,\n muted: video ? video.muted || video.volume === 0 : undefined,\n online: typeof nav?.onLine === 'boolean' ? nav.onLine : undefined,\n net_type: conn?.effectiveType?.slice(0, 16),\n net_rtt_ms: typeof conn?.rtt === 'number' && conn.rtt >= 0 ? conn.rtt : undefined,\n net_downlink_mbps: typeof conn?.downlink === 'number' && conn.downlink >= 0 ? conn.downlink : undefined,\n };\n}\n","import { useState, useRef, useCallback, useEffect } from 'react';\n\nconst RELEASE_HIDE_DELAY_MS = 3000;\n\nexport function useControlsVisibility() {\n const [controlsVisible, setControlsVisible] = useState(false);\n const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // While held (focus is inside the bar — e.g. a native <select> popup is\n // open), nothing hides the controls. A native popup swallows mousemove, so\n // the idle timer used to fade the bar out from under a still-open menu.\n const heldRef = useRef(false);\n\n const clearHideTimeout = () => {\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n hideTimeoutRef.current = null;\n }\n };\n\n const showControls = useCallback((hideDelay = 3000) => {\n clearHideTimeout();\n setControlsVisible(true);\n if (hideDelay > 0) {\n hideTimeoutRef.current = setTimeout(() => {\n if (!heldRef.current) setControlsVisible(false);\n }, hideDelay);\n }\n }, []);\n\n const hideControls = useCallback(() => {\n if (heldRef.current) return;\n clearHideTimeout();\n setControlsVisible(false);\n }, []);\n\n const setControlsHeld = useCallback((held: boolean) => {\n if (heldRef.current === held) return;\n heldRef.current = held;\n if (held) {\n clearHideTimeout();\n setControlsVisible(true);\n } else {\n showControls(RELEASE_HIDE_DELAY_MS);\n }\n }, [showControls]);\n\n useEffect(() => {\n const handleResize = () => {\n showControls(4000);\n };\n window.addEventListener('resize', handleResize);\n return () => {\n window.removeEventListener('resize', handleResize);\n clearHideTimeout();\n };\n }, [showControls]);\n\n return { controlsVisible, showControls, hideControls, setControlsHeld };\n}\n","import type { Fragment } from './fragmentManager';\n\n/** Audio track definition from index.json — each track is delivered as\n * fragmented MP4 and appended to its own audio SourceBuffer via\n * FragmentManager, same as video. */\ninterface IndexJsonAudioTrack {\n track_id: string;\n label: string;\n lang: string;\n path: string;\n mse_codec: string;\n timescale?: number;\n duration_ms?: number;\n init_segment: { start_byte: number; end_byte: number };\n fragments: Fragment[];\n}\n\nexport interface IndexJson {\n duration_ms: number;\n /** Full SHA-256 of output_frag.mp4. Present only on media processed/backfilled after the\n * edge-cacheable fragment route landed; absent => that route is not addressable for this\n * media and the client stays on Range. */\n content_token?: string;\n fragments: Fragment[];\n mse_codec?: string;\n init_segment?: { start_byte: number; end_byte: number };\n audio_tracks?: IndexJsonAudioTrack[];\n}\n\n/** An index together with the URL it was fetched from. */\nexport interface LoadedIndex { url: string; data: IndexJson }\n\nexport interface IndexView {\n fragments: Fragment[];\n durationMs: number | null;\n mseCodec: string | null;\n initSegment: { start_byte: number; end_byte: number } | undefined;\n audioTracks: IndexJsonAudioTrack[];\n contentToken: string | null;\n}\n\n// Stable empties: a fresh [] per render would re-fire every effect keyed on these.\nconst NO_FRAGMENTS: Fragment[] = [];\nconst NO_TRACKS: IndexJsonAudioTrack[] = [];\nconst NOTHING_LOADED: IndexView = {\n fragments: NO_FRAGMENTS, durationMs: null, mseCodec: null,\n initSegment: undefined, audioTracks: NO_TRACKS, contentToken: null,\n};\n\n/**\n * What a consumer may see for `indexUrl`: the loaded index only if it was fetched FROM that URL,\n * otherwise nothing.\n *\n * Derived in render, never reset in an effect. A reset in an effect lands one render late: in the\n * render where the URL changes, the previous media's map is still the state, and MediaPlayer's\n * MSE effect runs in that same commit — building a FragmentManager with the NEW media path and\n * the OLD byte map, token and audio track list. mistral-bluff 2026-10-20: every media switch sent\n * the new path with the previous episode's token (`FRAG-ROUTE disabled … init HTTP 404`) and its\n * track ids (`::audio::jpn_2` on a Chinese-only episode), then `INIT-FETCH stale on completion`\n * when the next render tore that manager down. The 404s were contained only because it died; its\n * Range fallback was reading the new file at the old offsets.\n */\nexport function indexFor(loaded: LoadedIndex | null, indexUrl: string): IndexView {\n if (!loaded || loaded.url !== indexUrl) return NOTHING_LOADED;\n const d = loaded.data;\n return {\n fragments: d.fragments || NO_FRAGMENTS,\n durationMs: d.duration_ms || null,\n mseCodec: d.mse_codec || null,\n initSegment: d.init_segment || undefined,\n audioTracks: d.audio_tracks || NO_TRACKS,\n contentToken: d.content_token || null,\n };\n}\n","import { useState, useEffect, useMemo } from 'react';\nimport { getRetryDelay } from '../lib/bufferUtils';\nimport { debug, warn } from '../lib/debug';\nimport { indexFor, type IndexJson, type LoadedIndex } from '../lib/loadedIndex';\n\n/** Extract Performance Resource Timing breakdown for a URL (dns/tcp/tls/ttfb/download). */\nfunction getResourceTiming(url: string): Record<string, number> | null {\n try {\n const entries = performance.getEntriesByName(url, 'resource') as PerformanceResourceTiming[];\n if (!entries.length) return null;\n const e = entries[entries.length - 1];\n return {\n dns: Math.round(e.domainLookupEnd - e.domainLookupStart),\n tcp: Math.round(e.connectEnd - e.connectStart),\n tls: e.secureConnectionStart > 0 ? Math.round(e.connectEnd - e.secureConnectionStart) : 0,\n ttfb: Math.round(e.responseStart - e.requestStart),\n download: Math.round(e.responseEnd - e.responseStart),\n total: Math.round(e.duration),\n transferSize: (e).transferSize || 0,\n };\n } catch { return null; }\n}\n\nexport function useIndexJson(indexUrl: string) {\n const [loaded, setLoaded] = useState<LoadedIndex | null>(null);\n\n useEffect(() => {\n const ac = new AbortController();\n\n async function fetchIndex() {\n let attempt = 0;\n // Max backoff between retries: 10s. First retry is immediate, then\n // 500ms, 1s, 2s, 4s... Never give up — index.json is critical.\n const MAX_BACKOFF_MS = 10_000;\n\n while (!ac.signal.aborted) {\n const t0 = performance.now();\n try {\n const res = await fetch(indexUrl, {\n cache: 'no-store',\n signal: ac.signal,\n // High priority: index.json is ~120KB but critical for all\n // playback. Without it, MSE can never create a SourceBuffer.\n priority: 'high',\n });\n const headerMs = Math.round(performance.now() - t0);\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const text = await res.text();\n const downloadMs = Math.round(performance.now() - t0) - headerMs;\n const data: IndexJson = JSON.parse(text);\n const parseMs = Math.round(performance.now() - t0) - headerMs - downloadMs;\n const totalMs = headerMs + downloadMs + parseMs;\n if (ac.signal.aborted) return;\n setLoaded({ url: indexUrl, data });\n const rt = getResourceTiming(indexUrl);\n debug('mse', 'Index loaded', {\n attempt: attempt + 1, headerMs, downloadMs, parseMs, totalMs,\n count: data.fragments?.length, duration: data.duration_ms,\n codec: data.mse_codec, initSegment: data.init_segment,\n audioTrackCount: data.audio_tracks?.length || 0,\n ...rt && { timing: rt },\n });\n return;\n } catch (e) {\n if (ac.signal.aborted) return;\n attempt++;\n const elapsedMs = Math.round(performance.now() - t0);\n const rt = getResourceTiming(indexUrl);\n warn('mse', `Index fetch attempt ${attempt} failed (${elapsedMs}ms)`, {\n error: (e as Error)?.message,\n elapsedMs,\n ...rt && { timing: rt },\n });\n // First retry is immediate; then 500ms, 1s, 2s, 4s... cap 10s\n const delay = attempt <= 1\n ? 0\n : getRetryDelay(attempt - 1, { baseMs: 500, capMs: MAX_BACKOFF_MS, jitter: 0 });\n if (delay > 0) await new Promise(r => setTimeout(r, delay));\n }\n }\n }\n void fetchIndex();\n return () => { ac.abort(); };\n }, [indexUrl]);\n\n // Only the index fetched FOR this URL is ever visible — see indexFor().\n return useMemo(() => indexFor(loaded, indexUrl), [loaded, indexUrl]);\n}\n","import { useState, useEffect, useEffectEvent, useCallback } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { useConfig, getServerOrigin } from '../lib/config';\nimport { debug, warn, error as logError, errorFields } from '../lib/debug';\nimport type { AudioTrack } from '../components/controls/AudioTrackSelector';\nimport type { SubtitleTrack } from '../components/controls/SubtitleSelector';\n\ninterface TracksResponse {\n media_id: string;\n audio: AudioTrack[];\n subtitles: SubtitleTrack[];\n}\n\ninterface TrackState {\n audioTracks: AudioTrack[];\n subtitleTracks: SubtitleTrack[];\n selectedAudioIndex: number;\n selectedSubtitleIndex: number;\n}\n\nconst EMPTY_STATE: TrackState = {\n audioTracks: [],\n subtitleTracks: [],\n selectedAudioIndex: 0,\n selectedSubtitleIndex: 0,\n};\n\nexport function useMediaTracks(mediaId: string) {\n const [trackState, setTrackState] = useState<TrackState>(EMPTY_STATE);\n const config = useConfig();\n\n // Read preferred language internally — auto-select only fires on media change\n const preferredAudioLang = useAppStore(s => s.mediaPrefs.preferredAudioLang);\n\n // Reset as soon as the media changes so stale tracks from the previous media\n // don't bleed into the next.\n const [prevMediaId, setPrevMediaId] = useState(mediaId);\n if (prevMediaId !== mediaId) {\n setPrevMediaId(mediaId);\n if (mediaId) setTrackState(EMPTY_STATE);\n }\n\n // Read when the fetch starts, never a reason to fetch again: a language\n // change must not re-pick the track of media already playing.\n const fetchContext = useEffectEvent(() => ({ origin: getServerOrigin(config), preferredAudioLang }));\n\n useEffect(() => {\n if (!mediaId) return;\n\n const controller = new AbortController();\n const { origin, preferredAudioLang } = fetchContext();\n let res: Response | undefined;\n\n (async () => {\n try {\n res = await fetch(`${origin}/api/media/${encodeURIComponent(mediaId)}/tracks`, { signal: controller.signal });\n if (!res.ok) throw new Error('Failed to fetch tracks');\n const data = await res.json() as TracksResponse;\n const resolveUrl = (url: string) => url.startsWith('/') ? `${origin}${url}` : url;\n const audio = (data.audio || []).map(t => ({ ...t, url: resolveUrl(t.url) }));\n const subs = (data.subtitles || []).map(t => ({ ...t, url: resolveUrl(t.url) }));\n\n // Resolve preferred tracks BEFORE exposing to consumers — single atomic\n // update prevents intermediate renders where tracks and selection disagree\n const preferredIdx = audio.findIndex((t: AudioTrack) => t.lang === preferredAudioLang);\n const audioIdx = preferredIdx >= 0 ? preferredIdx : 0;\n const engSubIdx = subs.findIndex((t: SubtitleTrack) => t.lang === 'eng');\n const subIdx = engSubIdx >= 0 ? engSubIdx : 0;\n\n setTrackState({ audioTracks: audio, subtitleTracks: subs, selectedAudioIndex: audioIdx, selectedSubtitleIndex: subIdx });\n debug('mse', 'Tracks loaded', { audio: audio.length, subs: subs.length });\n } catch (err: unknown) {\n const fields = errorFields(err);\n if (fields.name === 'AbortError') return;\n warn('mse', 'Tracks fetch error', { mediaId, status: res?.status, ...fields });\n setTrackState(EMPTY_STATE);\n }\n })().catch((err: unknown) => logError('mse', 'Tracks load threw', { mediaId, ...errorFields(err) }));\n\n return () => { controller.abort(); };\n }, [mediaId]);\n\n const setSelectedAudioIndex = useCallback((idx: number) => {\n setTrackState(prev => ({ ...prev, selectedAudioIndex: idx }));\n }, []);\n\n const setSelectedSubtitleIndex = useCallback((idx: number) => {\n setTrackState(prev => ({ ...prev, selectedSubtitleIndex: idx }));\n }, []);\n\n return {\n audioTracks: trackState.audioTracks,\n subtitleTracks: trackState.subtitleTracks,\n selectedAudioIndex: trackState.selectedAudioIndex,\n setSelectedAudioIndex,\n selectedSubtitleIndex: trackState.selectedSubtitleIndex,\n setSelectedSubtitleIndex,\n };\n}\n","/**\n * Silent-stall classification.\n *\n * Extracted as a pure function so the decision is unit-testable without a DOM,\n * in the same spirit as `bufferHealth.ts` and `seekBarValue.ts`.\n *\n * Context: every stall-recovery path in `useStallRecovery` used to arm on the\n * video element's `waiting` event. On 2026-08-04 (clover-basin,\n * SparksOfTomorrow) a viewer's playhead froze for 36s while the leader and two\n * other viewers advanced normally, and `waiting` never fired — so nothing armed\n * and nothing recovered. The deadman that replaced that assumption needs a\n * decision rule that does NOT consult the buffer metrics, because during that\n * freeze `getBufferedAheadMs()` reported a healthy 31s the whole time (it falls\n * back to the end of any range starting within 1s ahead — a range the playhead\n * could not reach). The playhead failing to advance while the room is playing\n * is the ground truth.\n */\n\n/**\n * How far the playhead must move between deadman arm and fire to count as\n * \"still advancing\". `timeupdate` fires ~4Hz in Chrome, so over a multi-second\n * window healthy playback moves far more than this; the threshold only exists\n * to tolerate timer jitter and rounding.\n */\nexport const ADVANCE_PROGRESS_MS = 100;\n\ntype AdvanceVerdict =\n /** Room is paused, or the element is paused — not a stall, don't re-arm. */\n | 'not-playing'\n /** A seek is in flight; it suppresses timeupdate and owns its own recovery. */\n | 'seeking'\n /** Playhead moved — `timeupdate` was merely sparse. */\n | 'advancing'\n /** Playing, not seeking, and the playhead has not moved. Recover. */\n | 'stalled';\n\ninterface AdvanceInput {\n /** Server/room playback state (snapshot.playing). */\n roomPlaying: boolean;\n /** The video element's own paused flag. */\n videoPaused: boolean;\n /** SeekCoordinator.seekInFlight. */\n seekInFlight: boolean;\n /** currentTime delta (ms) since the deadman was armed. */\n advancedMs: number;\n}\n\nexport function classifyAdvance(i: AdvanceInput): AdvanceVerdict {\n // Order matters. \"Not playing\" wins over everything: a paused element that\n // isn't advancing is correct behaviour, not a freeze.\n if (!i.roomPlaying || i.videoPaused) return 'not-playing';\n if (i.seekInFlight) return 'seeking';\n if (i.advancedMs > ADVANCE_PROGRESS_MS) return 'advancing';\n return 'stalled';\n}\n","import { useEffect, useEffectEvent, useRef, type RefObject } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { sendControl } from '../lib/controls';\nimport type { FragmentManager } from '../lib/fragmentManager';\nimport type { SeekCoordinator} from '../lib/seekCoordinator';\nimport { SeekPriority } from '../lib/seekCoordinator';\nimport { debug, warn, error as logError, errorFields } from '../lib/debug';\nimport { attemptAutoReload } from '../lib/autoReload';\nimport { perf } from '../lib/perfCollector';\nimport { LEADER_STALL_PAUSE_THRESHOLD_MS } from '@watchparty/shared';\nimport { GAP_SKIP_MAX_MS } from '../lib/bufferUtils';\nimport { classifyAdvance } from '../lib/silentStall';\n\n// Stuck-client watchdog (defense-in-depth for the Mode A meltdown + any other\n// unrecoverable freeze). If a client is genuinely frozen — room playing, but\n// the playhead hasn't advanced and the buffer is empty — for this long, we log\n// a full diagnostic and force a one-shot page reload (the only thing that\n// reliably cleared the phantom-buffer corruption). NOT a periodic timer: it is\n// armed on `waiting` and cleared on `playing`, exactly like the leader-stall\n// pause timer below, so it never sweeps.\nconst STUCK_RELOAD_MS = 40_000;\n\n// Silent-stall deadman (DO NOT convert into a periodic sweep, and DO NOT gate\n// it on `waiting`). Every other recovery path in this file arms on the video's\n// `waiting` event. On 2026-08-04 (clover-basin, SparksOfTomorrow) a viewer's\n// playhead froze at 147.2s for 36s while the leader and two other viewers\n// advanced in lockstep — and `waiting` NEVER FIRED. So nothing armed: no\n// stall-onset diagnostic, no gap-skip, no fetchNearby, no stuck-reload\n// watchdog. The client sat silent, issued zero fetches (its download-rate\n// average was frozen across five consecutive health reports), and recovered\n// only when the leader paused and the resulting resync seek ran reconcile.\n//\n// This is a deadman switch, not a sweep: `timeupdate` (≈4Hz during healthy\n// playback) re-arms it, so it only ever fires when timeupdate has STOPPED —\n// i.e. the playhead is not advancing. Same shape as stallTimerRef and\n// stuckReloadTimerRef, just armed by a different signal, so it composes with\n// them instead of stacking another interval.\nconst ADVANCE_STALL_MS = 3_000;\n\n/**\n * AUDIO STARVATION — the shape to check for FIRST when a viewer freezes while\n * every metric says healthy.\n *\n * Video and audio ride separate SourceBuffers on one MediaSource, and the\n * playhead advances only where BOTH have data. An audio-side hole therefore\n * freezes playback exactly like a video one.\n *\n * The signature is arithmetic: **the freeze position equals the `end_ms` of the\n * last AUDIO fragment that landed**, to the millisecond. Read `bufferedEndMs`\n * per track out of the SILENT-STALL payload — the track whose buffered end\n * equals the playhead is the one that stopped it. Two independent viewers have\n * matched this exactly. It is preceded by runs of `APPEND-DISCARDED … kf=n/a`\n * (n/a = audio) with the audio buffer end advancing exactly one fragment per\n * discard, i.e. each fragment does land, on a later attempt, at ~0.41x\n * realtime. Other viewers on the same episode show zero discards, so it is not\n * media corruption.\n *\n * \"It froze at the same point twice\" is DIAGNOSTIC, not coincidence: the audio\n * SourceBuffer's leading edge is stuck at a fixed timestamp, so every replay\n * walks to the same wall. Only a refresh clears it — that builds a fresh\n * MediaSource.\n *\n * WHY THE TRIGGER IS STILL UNSOLVED: the first append of each audio fragment is\n * refused, the before/after range comparison reports no change, and the same\n * bytes land moments later. Not obvious memory pressure — the audio buffer held\n * ~50 fragments ≈ 2.4MB. Everything below makes the freeze survivable and the\n * next occurrence diagnosable in one pass; none of it addresses the cause.\n *\n * Stall recovery: if the video stalls due to a buffer underrun while the\n * room is playing, resume playback when the browser signals it has enough\n * data. Uses native video events instead of polling.\n *\n * Leader-only behavior: if the leader's video stalls for more than\n * LEADER_STALL_PAUSE_THRESHOLD_MS, the leader emits a pause control to\n * stop the server's stopwatch and pause all viewers. When the leader's video\n * recovers, a play control re-syncs everyone to the leader's position.\n */\nexport function useStallRecovery(\n videoRef: RefObject<HTMLVideoElement | null>,\n fragmentManagerRef: RefObject<FragmentManager | null>,\n coordinatorRef: RefObject<SeekCoordinator | null>,\n mseReady: boolean,\n pausePlayback: (v: HTMLVideoElement) => void,\n /** Audio FragmentManagers, one per audio track. Recovery MUST reach these:\n * video and audio ride separate SourceBuffers and the playhead advances only\n * where both have data, so an audio-side hole freezes playback exactly like a\n * video one — but reconcileLoadedStates / fetchNearby on the video FM cannot\n * see it, let alone fix it. cedar-ridge 2026-08-25: two viewers sat frozen at\n * their audio buffer's leading edge; the chain ran for 40s on the video FM\n * and changed nothing, because the video buffer was healthy the whole time. */\n audioFragmentManagersRef?: RefObject<FragmentManager[]>,\n): void {\n /** Per-track geometry for the stall warns, and the audio-side half of\n * recovery. Kept in one place so the log and the action can never disagree\n * about which track is starving. */\n const trackDiags = useEffectEvent((currentMs: number) => {\n const audio: Record<string, unknown> = {};\n const afms = audioFragmentManagersRef?.current ?? [];\n afms.forEach((afm, i) => {\n if (!afm.hasIndex) return;\n audio[`audio:${i}`] = afm.getTrackDiag(currentMs);\n });\n return audio;\n });\n\n /** Run the same ledger re-sync + re-fetch on every audio track. Cheap, and\n * the only thing that can clear an audio-side hole. */\n const recoverAudio = useEffectEvent((currentMs: number, reason: string) => {\n for (const afm of audioFragmentManagersRef?.current ?? []) {\n if (!afm.hasIndex) continue;\n afm.reconcileLoadedStates();\n afm.fetchNearby(currentMs).catch((e: unknown) => {\n if ((e as Error)?.name !== 'AbortError') {\n warn('sync', 'audio FM recovery fetchNearby failed', {\n reason, currentMs: Math.round(currentMs), error: (e as Error)?.message,\n });\n }\n });\n }\n });\n // The latest pausePlayback, read when a handler fires.\n const pause = useEffectEvent((v: HTMLVideoElement) => pausePlayback(v));\n\n const stallTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const autoRePausedRef = useRef(false);\n const stallStartMsRef = useRef<number | null>(null);\n // Stuck-client watchdog: armed on `waiting`, cleared on `playing`.\n const stuckReloadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const stuckArmCurrentTimeRef = useRef<number>(0);\n // Silent-stall deadman: re-armed by `timeupdate`, so it fires only when the\n // playhead has stopped advancing without the browser emitting `waiting`.\n const advanceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const advanceArmCurrentTimeRef = useRef<number>(0);\n const silentStallStartRef = useRef<number | null>(null);\n\n useEffect(() => {\n const v = videoRef.current;\n if (!v || !mseReady) return;\n\n function clearAdvanceWatchdog() {\n if (advanceTimerRef.current) {\n clearTimeout(advanceTimerRef.current);\n advanceTimerRef.current = null;\n }\n }\n\n /** (Re)arm the deadman. Called from `timeupdate`/`playing` — i.e. proof of life. */\n function armAdvanceWatchdog() {\n clearAdvanceWatchdog();\n const vv = videoRef.current;\n if (!vv) return;\n advanceArmCurrentTimeRef.current = vv.currentTime;\n advanceTimerRef.current = setTimeout(handleAdvanceStalled, ADVANCE_STALL_MS);\n }\n\n /**\n * Fired only when `timeupdate` has gone quiet for ADVANCE_STALL_MS. Either\n * the playhead legitimately isn't running (paused / room paused / seeking —\n * all re-checked here), or we are frozen with no `waiting` event to hang\n * recovery off. The latter is the case this exists for.\n */\n function handleAdvanceStalled() {\n advanceTimerRef.current = null;\n const vv = videoRef.current;\n if (!vv) return;\n\n const st = useAppStore.getState();\n const coord = coordinatorRef.current;\n const verdict = classifyAdvance({\n roomPlaying: st.snapshot.playing,\n videoPaused: vv.paused,\n seekInFlight: !!coord?.seekInFlight,\n advancedMs: (vv.currentTime - advanceArmCurrentTimeRef.current) * 1000,\n });\n if (verdict !== 'stalled') {\n silentStallStartRef.current = null;\n // Only 'not-playing' stands down; `playing` re-arms it. The other\n // benign verdicts keep watching.\n if (verdict !== 'not-playing') armAdvanceWatchdog();\n return;\n }\n\n const currentMs = vv.currentTime * 1000;\n const fm = fragmentManagerRef.current;\n const now = Date.now();\n if (silentStallStartRef.current === null) silentStallStartRef.current = now;\n const frozenMs = now - silentStallStartRef.current;\n\n // Deliberately does NOT consult getBufferedAheadMs to decide whether this\n // is real. That metric reports the END of a range the playhead cannot\n // reach (it falls back to any range starting within 1s ahead), so during\n // the 2026-08-04 freeze it read a healthy 31s the entire time. The\n // playhead not moving while the room plays IS the ground truth here.\n warn('sync', 'SILENT-STALL playhead frozen with no `waiting` event', {\n currentTime: +vv.currentTime.toFixed(2),\n readyState: vv.readyState,\n frozenMs,\n // Reported for correlation only — see comment above; may look healthy.\n reportedBufferedAheadMs: Math.round(fm?.getBufferedAheadMs(currentMs) ?? -1),\n // Per-track geometry. `reportedBufferedAheadMs` above is VIDEO's and read\n // a healthy 30s through the entire cedar-ridge 2026-08-25 freeze; the\n // starved track only shows up here. A track whose `bufferedEndMs` equals\n // the playhead is the one that stopped the playhead.\n video: fm?.getTrackDiag(currentMs),\n ...trackDiags(currentMs),\n });\n\n // Audio first, and unconditionally: a video-healthy freeze is exactly the\n // case where the block below decides there is nothing to do.\n recoverAudio(currentMs, 'silent-stall');\n\n if (fm) {\n // The ledger may claim 'loaded' for fragments the browser silently\n // dropped. Nothing else reconciles during continuous playback, so if we\n // don't do it here the re-fetch below finds nothing to do.\n fm.reconcileLoadedStates();\n\n const gapSkipTarget = fm.findNextRangeStart(currentMs, GAP_SKIP_MAX_MS);\n if (gapSkipTarget !== null && coord) {\n warn('sync', 'SILENT-STALL gap-skip', {\n currentMs: Math.round(currentMs),\n gapSkipTarget: Math.round(gapSkipTarget),\n });\n coord.submit({\n targetMs: gapSkipTarget,\n priority: SeekPriority.VIEWER_CORRECTION,\n caller: 'silent-stall-gap-skip',\n pauseSeekResume: false,\n }).catch((e: unknown) => logError('sync', 'SILENT-STALL gap-skip seek threw', errorFields(e)));\n } else {\n fm.fetchNearby(currentMs).catch((e: unknown) => {\n const fields = errorFields(e);\n if (fields.name !== 'AbortError') {\n warn('sync', 'SILENT-STALL fetchNearby failed', {\n currentMs: Math.round(currentMs),\n error: fields.error,\n });\n }\n });\n }\n }\n\n // Escalate on the same budget/threshold as the `waiting`-armed watchdog,\n // so a silent freeze can't outlive a noisy one.\n if (frozenMs >= STUCK_RELOAD_MS) {\n fm?.dumpBufferDiagnostics(currentMs, 'silent-stall-reload');\n attemptAutoReload('silent-stall', {\n frozenMs,\n currentTime: +vv.currentTime.toFixed(2),\n readyState: vv.readyState,\n loaded: fm?.getLoadedCount?.() ?? -1,\n });\n silentStallStartRef.current = null;\n return;\n }\n\n armAdvanceWatchdog();\n }\n\n function handleTimeUpdate() {\n // Proof of life: the playhead moved, so reset the freeze clock and re-arm.\n silentStallStartRef.current = null;\n armAdvanceWatchdog();\n }\n\n function handleWaiting() {\n const currentMs = v!.currentTime * 1000;\n stallStartMsRef.current = Date.now();\n const st = useAppStore.getState();\n const shouldPlay = st.snapshot.playing;\n const isLeader = !!st.snapshot.leaderId && st.snapshot.leaderId === st.me.clientId;\n // Log server position at stall time so we can compute drift at recovery.\n const serverMsAtStall = st.snapshot.playheadMs + (st.snapshot.playing ? Date.now() - st.snapshotReceivedAt : 0);\n debug('sync', 'VIDEO waiting event', {\n currentTime: v!.currentTime.toFixed(2),\n readyState: v!.readyState,\n playing: shouldPlay,\n isLeader,\n serverMsAtStall: Math.round(serverMsAtStall),\n });\n perf.recordStallStart(currentMs, fragmentManagerRef.current?.getBufferedAheadMs(currentMs) ?? -1);\n\n const fm = fragmentManagerRef.current;\n const coord = coordinatorRef.current;\n // Dump diagnostics only when ahead-buffer is actually starved. The\n // browser emits `waiting` during normal initial-decode startup even\n // when the SourceBuffer has several seconds ready, and logging\n // BUFFER-DIAG for those benign events is noise. A real stall has\n // near-zero ahead-buffer (<1s).\n const aheadAtStall = fm?.getBufferedAheadMs(currentMs) ?? 0;\n if (fm && aheadAtStall < 1000) {\n fm.dumpBufferDiagnostics(currentMs, 'stall-onset');\n }\n // A `waiting` with a healthy VIDEO ahead-buffer is the audio-starvation\n // shape, and the aheadAtStall < 1000 guard above deliberately suppresses\n // the video dump for it. Name the audio geometry instead, so the log says\n // which track ran dry rather than staying silent.\n if (shouldPlay && aheadAtStall >= 1000) {\n const audio = trackDiags(currentMs);\n if (Object.keys(audio).length > 0) {\n warn('sync', 'STALL-TRACKS video buffer healthy at stall — per-track geometry', {\n currentMs: Math.round(currentMs),\n videoAheadMs: Math.round(aheadAtStall),\n ...audio,\n });\n }\n }\n if (shouldPlay && !coord?.seekInFlight && currentMs > 0) {\n recoverAudio(currentMs, 'waiting');\n }\n if (shouldPlay && fm && !coord?.seekInFlight && currentMs > 0) {\n // Re-sync the ledger against the real SourceBuffer before deciding what\n // to fetch. A stall whose fragments are all marked 'loaded' is the\n // browser-quota-eviction signature: without this, gap-skip declines and\n // fetchNearby finds nothing pending, so the stall never resolves.\n fm.reconcileLoadedStates();\n const aheadMs = fm.getBufferedAheadMs(currentMs);\n // Gap-skip: if the playhead fell into a structural SourceBuffer gap (caused\n // by a late-IDR fragment whose MSE-buffered range starts after the nominal\n // fragment start), seek to the start of the next buffered range rather than\n // waiting for data that can never fill the gap.\n const gapSkipTarget = fm.findNextRangeStart(currentMs, GAP_SKIP_MAX_MS);\n if (gapSkipTarget !== null && coord) {\n warn('sync', 'GAP-SKIP seeking over late-IDR gap', {\n currentMs: Math.round(currentMs),\n gapSkipTarget: Math.round(gapSkipTarget),\n });\n coord.submit({\n targetMs: gapSkipTarget,\n priority: SeekPriority.VIEWER_CORRECTION,\n caller: 'gap-skip',\n pauseSeekResume: false,\n }).catch((e: unknown) => logError('sync', 'GAP-SKIP seek threw', errorFields(e)));\n } else if (aheadMs < 500) {\n // Real stall, but gap-skip declined. Log WHY — a declined skip used to\n // be silent, which is what made the 2026-06-22 trailing-edge freeze\n // (gap 2002ms > the old 2000ms window) take deduction to root-cause.\n // If a next range exists ahead but we're not jumping to it, this line\n // shows the geometry (atEdge / gapMs vs window) at warn level.\n const gap = fm.getForwardGapDiag(currentMs);\n if (gap && gap.nextRangeStartMs !== null) {\n warn('sync', 'STALL-GAP not skipped', {\n currentMs: Math.round(currentMs),\n atEdge: gap.atEdge,\n aheadInRangeMs: Math.round(gap.aheadInRangeMs),\n nextRangeStartMs: Math.round(gap.nextRangeStartMs),\n gapMs: gap.gapMs !== null ? Math.round(gap.gapMs) : null,\n windowMs: GAP_SKIP_MAX_MS,\n });\n }\n fm.fetchNearby(currentMs).catch((e: unknown) => {\n const fields = errorFields(e);\n if (fields.name !== 'AbortError') {\n warn('sync', 'Stall recovery fetchNearby failed', {\n currentMs: Math.round(currentMs),\n error: fields.error,\n });\n }\n });\n }\n }\n\n // Leader: if stall persists beyond threshold, pause the whole room so\n // the server stopwatch stops and viewers don't advance past the leader.\n if (isLeader && shouldPlay && !stallTimerRef.current) {\n stallTimerRef.current = setTimeout(() => {\n stallTimerRef.current = null;\n const stNow = useAppStore.getState();\n const stillLeader = !!stNow.snapshot.leaderId && stNow.snapshot.leaderId === stNow.me.clientId;\n // Use video element truth (v.paused) instead of snapshot.playing — the\n // snapshot can lag behind the element during a seek-induced pause.\n // Skip the room pause if a seek is in flight — the seek will pause/resume\n // on its own and a concurrent sendControl('pause') would race with it.\n const seekActive = !!coordinatorRef.current?.seekInFlight;\n if (stillLeader && !v!.paused && v!.readyState < 3 && !seekActive) {\n warn('sync', 'Leader stall threshold reached — pausing room', { stallMs: LEADER_STALL_PAUSE_THRESHOLD_MS });\n autoRePausedRef.current = true;\n sendControl('pause');\n }\n }, LEADER_STALL_PAUSE_THRESHOLD_MS);\n }\n\n // Stuck-client watchdog (all clients): arm a one-shot reload if this\n // freeze persists. Cleared by `playing`. Fires only if genuinely frozen.\n if (shouldPlay && !stuckReloadTimerRef.current) {\n stuckArmCurrentTimeRef.current = v!.currentTime;\n stuckReloadTimerRef.current = setTimeout(() => {\n stuckReloadTimerRef.current = null;\n const vv = videoRef.current;\n if (!vv) return;\n const stNow = useAppStore.getState();\n const playing = stNow.snapshot.playing;\n const fm2 = fragmentManagerRef.current;\n const aheadMs = fm2?.getBufferedAheadMs(vv.currentTime * 1000) ?? -1;\n const advancedSec = vv.currentTime - stuckArmCurrentTimeRef.current;\n const frozen = playing && advancedSec < 0.5 && vv.readyState < 3 && aheadMs < 1000;\n if (!frozen) {\n debug('sync', 'STUCK-CHECK cleared — playback progressed, no reload', {\n advancedSec: +advancedSec.toFixed(2), readyState: vv.readyState,\n bufferedAheadMs: Math.round(aheadMs), playing,\n });\n return;\n }\n // Genuinely frozen — dump full diagnostics, then escalate to a\n // rate-limited auto-reload. attemptAutoReload beacons the STUCK-RELOAD\n // trigger (so it survives the reload, unlike before) and gives up after\n // repeated reloads instead of churning a bad connection (see autoReload.ts).\n fm2?.dumpBufferDiagnostics(vv.currentTime * 1000, 'stuck-reload');\n attemptAutoReload('stuck-watchdog', {\n stuckMs: STUCK_RELOAD_MS, advancedSec: +advancedSec.toFixed(2),\n currentTime: +vv.currentTime.toFixed(2), readyState: vv.readyState,\n bufferedAheadMs: Math.round(aheadMs), loaded: fm2?.getLoadedCount?.() ?? -1,\n });\n }, STUCK_RELOAD_MS);\n }\n }\n\n function handlePlaying() {\n perf.recordStallEnd();\n // Clear any pending stall timer\n if (stallTimerRef.current) {\n clearTimeout(stallTimerRef.current);\n stallTimerRef.current = null;\n }\n // Recovered — disarm the stuck-reload watchdog.\n if (stuckReloadTimerRef.current) {\n clearTimeout(stuckReloadTimerRef.current);\n stuckReloadTimerRef.current = null;\n }\n // Playback is live again: reset the freeze clock and re-arm the deadman.\n silentStallStartRef.current = null;\n armAdvanceWatchdog();\n\n const st = useAppStore.getState();\n const shouldPlay = st.snapshot.playing;\n const isLeader = !!st.snapshot.leaderId && st.snapshot.leaderId === st.me.clientId;\n\n if (!shouldPlay && !v!.paused) {\n pause(v!);\n }\n\n // Leader: if we auto-paused due to a stall, resume now that video recovered.\n if (isLeader && autoRePausedRef.current && !shouldPlay) {\n autoRePausedRef.current = false;\n warn('sync', 'Leader stall recovered — resuming room', { currentTime: v!.currentTime.toFixed(2) });\n sendControl('play');\n }\n\n const stallDurationMs = stallStartMsRef.current !== null ? Date.now() - stallStartMsRef.current : null;\n stallStartMsRef.current = null;\n const st2 = useAppStore.getState();\n const serverMsAtRecovery = st2.snapshot.playheadMs + (st2.snapshot.playing ? Date.now() - st2.snapshotReceivedAt : 0);\n const clientMs = v!.currentTime * 1000;\n debug('sync', 'VIDEO playing event', {\n currentTime: v!.currentTime.toFixed(2),\n shouldPlay,\n stallDurationMs,\n // Positive = viewer is behind server; negative = viewer is ahead.\n driftMs: stallDurationMs !== null ? Math.round(serverMsAtRecovery - clientMs) : null,\n });\n }\n\n function handlePause() {\n // Paused on purpose — stop the deadman so it can't misfire as a freeze.\n silentStallStartRef.current = null;\n clearAdvanceWatchdog();\n }\n\n v.addEventListener('waiting', handleWaiting);\n v.addEventListener('playing', handlePlaying);\n v.addEventListener('timeupdate', handleTimeUpdate);\n v.addEventListener('pause', handlePause);\n // Playback may already be running when this effect (re)binds after an MSE\n // rebuild — arm now rather than waiting for the next `playing`.\n if (!v.paused) armAdvanceWatchdog();\n\n return () => {\n v.removeEventListener('waiting', handleWaiting);\n v.removeEventListener('playing', handlePlaying);\n v.removeEventListener('timeupdate', handleTimeUpdate);\n v.removeEventListener('pause', handlePause);\n if (stallTimerRef.current) {\n clearTimeout(stallTimerRef.current);\n stallTimerRef.current = null;\n }\n if (stuckReloadTimerRef.current) {\n clearTimeout(stuckReloadTimerRef.current);\n stuckReloadTimerRef.current = null;\n }\n clearAdvanceWatchdog();\n silentStallStartRef.current = null;\n };\n }, [mseReady, videoRef, fragmentManagerRef, coordinatorRef]);\n}\n","import { useEffect, useRef, useCallback, type RefObject } from 'react';\nimport type { BufferState } from '../lib/store';\nimport type { FragmentManager } from '../lib/fragmentManager';\nimport type { SeekCoordinator } from '../lib/seekCoordinator';\nimport { DEFAULT_PREFETCH_INTERVAL_MS } from '@watchparty/shared';\n\n/** How late the interval must be before `timeupdate` runs the tick in its place.\n * Above 1 so ordinary timer jitter never doubles the cadence. */\nconst PREFETCH_OVERDUE_FACTOR = 1.5;\n\n/**\n * Prefetch during playback — runs every 2s while mseReady.\n * Uses video.currentTime as the prefetch position. No speculation\n * about \"expected\" positions — initializePlayback already ensures\n * the video is seeked to the correct position before mseReady is set.\n */\nexport function usePrefetch(\n videoRef: RefObject<HTMLVideoElement | null>,\n fragmentManagerRef: RefObject<FragmentManager | null>,\n coordinatorRef: RefObject<SeekCoordinator | null>,\n mseReady: boolean,\n setBufferState: (state: Partial<BufferState>) => void,\n audioFragmentManagersRef?: RefObject<FragmentManager[]>,\n) {\n const prefetchIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const detachTimeUpdateRef = useRef<(() => void) | null>(null);\n\n const stopPrefetch = useCallback(() => {\n if (prefetchIntervalRef.current) {\n clearInterval(prefetchIntervalRef.current);\n prefetchIntervalRef.current = null;\n }\n detachTimeUpdateRef.current?.();\n detachTimeUpdateRef.current = null;\n }, []);\n\n useEffect(() => {\n const v = videoRef.current;\n const fm = fragmentManagerRef.current;\n\n if (!mseReady || !fm) {\n stopPrefetch();\n return;\n }\n\n const tickAudio = (currentMs: number) => {\n const afms = audioFragmentManagersRef?.current;\n if (!afms) return;\n for (const afm of afms) {\n afm.prefetchAround(currentMs);\n afm.evictBefore(currentMs);\n }\n };\n\n let lastTickAt = 0;\n const tick = () => {\n if (v && fm) {\n if (coordinatorRef.current?.seekInFlight) return;\n lastTickAt = performance.now();\n\n const currentMs = v.currentTime * 1000;\n fm.prefetchAround(currentMs);\n fm.evictBefore(currentMs);\n tickAudio(currentMs);\n\n const bufferedEnd = fm.getBufferedEnd(currentMs);\n setBufferState({ bufferedEndMs: bufferedEnd });\n }\n };\n prefetchIntervalRef.current = setInterval(tick, DEFAULT_PREFETCH_INTERVAL_MS);\n\n // The interval is not guaranteed to run. A hidden, silent tab has its timers\n // cut to about one wake-up a minute, while the video keeps playing and\n // `timeupdate` keeps firing — so the buffer drained with nothing refilling it,\n // and the viewer froze once it ran out (mistral-bluff 2026-10-20: health\n // reports, also an interval, stopped at the same moment, and the freeze came\n // one buffer-length later). `timeupdate` stands in only when the interval is\n // overdue, so a page whose timers run sees exactly the old cadence.\n const onTimeUpdate = () => {\n if (performance.now() - lastTickAt >= DEFAULT_PREFETCH_INTERVAL_MS * PREFETCH_OVERDUE_FACTOR) tick();\n };\n if (v) {\n v.addEventListener('timeupdate', onTimeUpdate);\n detachTimeUpdateRef.current = () => v.removeEventListener('timeupdate', onTimeUpdate);\n }\n\n // Initial prefetch kick at current video position\n if (v && !coordinatorRef.current?.seekInFlight) {\n lastTickAt = performance.now();\n const currentMs = v.currentTime * 1000;\n fm.prefetchAround(currentMs);\n tickAudio(currentMs);\n }\n\n return stopPrefetch;\n // The refs and stopPrefetch are stable; listing them changes no run.\n }, [mseReady, setBufferState, videoRef, fragmentManagerRef, coordinatorRef, audioFragmentManagersRef, stopPrefetch]);\n\n return { stopPrefetch };\n}\n","import { useEffect, useEffectEvent, useRef, type MutableRefObject, type RefObject } from 'react';\nimport { useAppStore } from '../lib/store';\nimport type { SeekCoordinator } from '../lib/seekCoordinator';\nimport { SeekPriority } from '../lib/seekCoordinator';\nimport { DEFAULT_DRIFT_SOFT_MS } from '@watchparty/shared';\nimport { debug, error as logError, errorFields } from '../lib/debug';\n\n/**\n * Syncs video playback with room snapshot state:\n * - Seek effect: delegates seeks to the coordinator when snapshot.playheadMs changes\n * - Play/pause effect: syncs video.paused with snapshot.playing\n */\nexport function usePlaybackSync(\n coordinatorRef: RefObject<SeekCoordinator | null>,\n videoRef: RefObject<HTMLVideoElement | null>,\n // Multi-writer ref: also written by initializePlayback in MediaPlayer.tsx\n lastAppliedPlayheadRef: MutableRefObject<number>,\n mseReady: boolean,\n resumePlayback: (v: HTMLVideoElement) => void,\n pausePlayback: (v: HTMLVideoElement) => void,\n): void {\n const snapshot = useAppStore(s => s.snapshot);\n const loadGeneration = useAppStore(s => s.loadGeneration);\n const lastLoadGenRef = useRef<number>(loadGeneration);\n // The latest callbacks, read when the effect runs: their identity is not a\n // reason to re-sync play/pause.\n const resume = useEffectEvent((v: HTMLVideoElement) => resumePlayback(v));\n const pause = useEffectEvent((v: HTMLVideoElement) => pausePlayback(v));\n\n // Handle seek: when snapshot.playheadMs changes significantly, delegate to coordinator.\n // Also fires on loadGeneration change (same-media reload where playheadMs stays 0→0).\n useEffect(() => {\n const coordinator = coordinatorRef.current;\n if (!coordinator || !mseReady) return;\n\n const targetMs = snapshot.playheadMs;\n const lastApplied = lastAppliedPlayheadRef.current;\n const genChanged = loadGeneration !== lastLoadGenRef.current;\n lastLoadGenRef.current = loadGeneration;\n\n if (!genChanged && Math.abs(targetMs - lastApplied) <= DEFAULT_DRIFT_SOFT_MS) return;\n\n debug('buffer', 'Seek triggered', {\n targetMs,\n lastApplied,\n delta: Math.round(targetMs - lastApplied),\n genChanged,\n });\n\n lastAppliedPlayheadRef.current = targetMs;\n\n coordinator.submit({\n targetMs,\n priority: SeekPriority.SEEK_EFFECT,\n caller: 'seek-effect',\n pauseSeekResume: true,\n }).catch((e: unknown) => logError('buffer', 'Seek effect submit threw', errorFields(e)));\n }, [snapshot.playheadMs, mseReady, loadGeneration, coordinatorRef, lastAppliedPlayheadRef]);\n\n // Sync play/pause state (skipped when a seek is in-flight — coordinator owns play/pause then)\n useEffect(() => {\n const v = videoRef.current;\n if (!v || !mseReady) return;\n if (coordinatorRef.current?.seekInFlight) return;\n\n const shouldPlay = snapshot.playing;\n const isPaused = v.paused;\n\n if (shouldPlay && isPaused) {\n debug('sync', 'Play/pause sync', { playing: shouldPlay, videoPaused: isPaused, currentTime: Math.round(v.currentTime * 1000) });\n resume(v);\n } else if (!shouldPlay && !isPaused) {\n debug('sync', 'Play/pause sync', { playing: shouldPlay, videoPaused: isPaused, currentTime: Math.round(v.currentTime * 1000) });\n pause(v);\n }\n }, [snapshot.playing, mseReady, coordinatorRef, videoRef]);\n}\n","import { useEffect, useEffectEvent, type RefObject } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { playlistFragmentCache } from '../lib/PlaylistFragmentCache';\nimport { resolveMediaUrls } from '../lib/mediaUrls';\nimport { findKeyframeIndex, findFragmentIndex, type Fragment } from '../lib/bufferUtils';\nimport { INITIAL_BUFFER_MS, BACKGROUND_FETCH_PRIORITY_MS } from '@watchparty/shared';\n// Progress is debug; only failures are warn (they are forwarded to the server\n// and checked by the E2E warning gate). Every line here was warn until the\n// 10-5-26 party, where progress was 1,770 of its 2,244 client warnings (79%).\nimport { debug, warn, errorFields } from '../lib/debug';\nimport type { FragmentManager } from '../lib/fragmentManager';\n\ninterface IndexJson {\n fragments: Fragment[];\n init_segment?: { start_byte: number; end_byte: number };\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const id = setTimeout(resolve, ms);\n signal.addEventListener('abort', () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });\n });\n}\n\n/**\n * Background pre-buffer hook. When the FM is idle, fetches the init segment +\n * first INITIAL_BUFFER_MS of fragments for every non-active playlist video and\n * stores them in PlaylistFragmentCache. FM will use cache hits transparently on\n * video switch, making playback start near-instantly.\n *\n * Runs only when fm.getPendingCount() === 0 (playback queue idle).\n * Aborts and restarts when activePlaylistIndex changes.\n */\nexport function usePlaylistPreBuffer(fmRef: RefObject<FragmentManager | null>): void {\n const playlist = useAppStore(s => s.playlist);\n const activeIndex = useAppStore(s => s.activePlaylistIndex);\n const currentMediaId = useAppStore(s => s.snapshot.mediaId);\n // The loop restarts only when the playlist's identity changes, not on every\n // playlist update; it reads the playlist as of that (re)start.\n const playlistId = playlist?.id;\n const readPlaylist = useEffectEvent(() => playlist);\n\n useEffect(() => {\n const playlist = readPlaylist();\n if (!playlist) return;\n // Only pre-buffer while the loaded media is actually a clip of this playlist.\n // Guards against a stale playlist (e.g. inherited by a late joiner, or a race\n // before the server's playlist-clear broadcast lands) triggering a cold\n // prebuffer storm that starves a fresh non-playlist episode's init fetch —\n // the cedar-ridge 2026-07-09 \"MediaSource buffer not sufficient\" freeze.\n if (!playlist.videos.some(v => v.mediaId === currentMediaId)) return;\n\n const ac = new AbortController();\n debug('prebuffer', 'Pre-buffer started', { playlistId: playlist.id, activeIndex, videoCount: playlist.videos.length });\n\n (async () => {\n for (let vi = 0; vi < playlist.videos.length; vi++) {\n if (ac.signal.aborted) return;\n if (vi === activeIndex) continue;\n\n const video = playlist.videos[vi];\n const { mediaUrl, indexUrl } = resolveMediaUrls(video.mediaId);\n\n // Helper: wait until FM is idle, then wait an extra backoff to avoid server saturation.\n // Pre-buffer dispatches at most one fetch per FETCH_INTERVAL_MS regardless of FM state.\n const FETCH_INTERVAL_MS = 3000;\n const waitAndYield = async (): Promise<boolean> => {\n while (!ac.signal.aborted) {\n if ((fmRef.current?.getPendingCount() ?? 0) === 0) break;\n try { await sleep(2000, ac.signal); } catch { return false; }\n }\n if (ac.signal.aborted) return false;\n // Mandatory backoff — prevents flooding the server even when FM is momentarily idle\n try { await sleep(FETCH_INTERVAL_MS, ac.signal); } catch { return false; }\n return !ac.signal.aborted;\n };\n\n // Fetch index.json for this video. Gated behind waitAndYield like the\n // media fetches below: an ungated index.json fetch competes with the\n // active FM's init/fragment fetches on the same HTTP/2 connection during\n // the most latency-sensitive window (cedar-ridge 2026-07-09 hardening).\n if (!await waitAndYield()) return;\n let indexData: IndexJson;\n try {\n const res = await fetch(indexUrl, { signal: ac.signal });\n if (!res.ok) { warn('prebuffer', `index.json fetch failed for ${video.mediaId}: HTTP ${res.status}`); continue; }\n indexData = await res.json() as IndexJson;\n } catch (e: unknown) {\n if (ac.signal.aborted) return;\n warn('prebuffer', `index.json fetch error for ${video.mediaId}`, errorFields(e));\n continue;\n }\n\n const { fragments, init_segment } = indexData;\n if (!fragments?.length) continue;\n\n // Determine which fragment indices to pre-buffer (keyframe-aligned, covers INITIAL_BUFFER_MS)\n const startIdx = findKeyframeIndex(fragments, 0);\n const endIdx = findFragmentIndex(fragments, INITIAL_BUFFER_MS);\n\n const initAlreadyCached = playlistFragmentCache.getInit(video.mediaId) !== undefined;\n const mediaAlreadyCached = playlistFragmentCache.getBufferedMs(video.mediaId, fragments) >= INITIAL_BUFFER_MS;\n\n if (initAlreadyCached && mediaAlreadyCached) {\n debug('prebuffer', `${video.mediaId} already fully pre-buffered, skipping`);\n continue;\n }\n\n debug('prebuffer', `Pre-buffering ${video.mediaId} frags ${startIdx}-${endIdx}`);\n\n // Fetch init segment\n if (!initAlreadyCached && init_segment) {\n if (!await waitAndYield()) return;\n try {\n const res = await fetch(mediaUrl, {\n headers: {\n Range: `bytes=${init_segment.start_byte}-${init_segment.end_byte}`,\n 'X-Buffer-Ahead-Ms': String(BACKGROUND_FETCH_PRIORITY_MS), // background fetch — low priority\n },\n signal: ac.signal,\n });\n if (res.ok) {\n const buf = await res.arrayBuffer();\n playlistFragmentCache.storeInit(video.mediaId, buf);\n debug('prebuffer', `Init cached for ${video.mediaId} (${buf.byteLength}B)`);\n }\n } catch (e: unknown) {\n if (ac.signal.aborted) return;\n warn('prebuffer', `Init fetch failed for ${video.mediaId}`, errorFields(e));\n }\n }\n\n // Fetch media fragments — one at a time with mandatory inter-fetch backoff\n for (let i = startIdx; i <= endIdx; i++) {\n if (ac.signal.aborted) return;\n if (playlistFragmentCache.hasFragment(video.mediaId, i)) continue;\n\n if (!await waitAndYield()) return;\n\n const frag = fragments[i];\n try {\n const res = await fetch(mediaUrl, {\n headers: {\n Range: `bytes=${frag.start_byte}-${frag.end_byte}`,\n 'X-Buffer-Ahead-Ms': String(BACKGROUND_FETCH_PRIORITY_MS),\n },\n signal: ac.signal,\n });\n if (res.ok) {\n const buf = await res.arrayBuffer();\n playlistFragmentCache.storeFragment(video.mediaId, i, buf);\n }\n } catch (e: unknown) {\n if (ac.signal.aborted) return;\n warn('prebuffer', `Fragment #${i} fetch failed for ${video.mediaId}`, errorFields(e));\n }\n }\n\n debug('prebuffer', `Pre-buffer complete for ${video.mediaId}`);\n }\n\n debug('prebuffer', `All playlist videos pre-buffered`);\n })().catch(e => {\n if ((e as Error)?.name !== 'AbortError') warn('prebuffer', `Pre-buffer loop error: ${(e as Error)?.message}`);\n });\n\n return () => {\n debug('prebuffer', 'Pre-buffer aborted', { playlistId: playlist.id });\n ac.abort();\n };\n }, [playlistId, activeIndex, currentMediaId, fmRef]);\n}\n","import { useEffect, useEffectEvent, useRef, type RefObject } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { sendControl } from '../lib/controls';\nimport { resolveMediaAsset } from '../lib/mediaUrls';\nimport { debug, warn, errorFields } from '../lib/debug';\nimport type { SeekCoordinator } from '../lib/seekCoordinator';\nimport { SeekPriority } from '../lib/seekCoordinator';\nimport {\n artworkCandidates,\n BG_RESYNC_TOLERANCE_MS,\n mediaDisplayName,\n mediaSessionAction,\n resyncPlan,\n roomPlayheadNow,\n shouldKeepAlive,\n} from '../lib/backgroundPlay';\n\nconst LABEL = 'bg-play';\n\nfunction isLeaderNow(): boolean {\n const st = useAppStore.getState();\n return !!st.snapshot.leaderId && st.snapshot.leaderId === st.me.clientId;\n}\n\n/** First candidate image that actually loads; null when none does. */\nfunction firstLoadingImage(urls: string[]): Promise<string | null> {\n if (typeof Image === 'undefined') return Promise.resolve(null);\n return urls.reduce<Promise<string | null>>(\n (prev, url) => prev.then((hit) => hit ?? new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve(url);\n img.onerror = () => resolve(null);\n img.src = url;\n })),\n Promise.resolve(null),\n );\n}\n\n/**\n * Keeps a viewer in the room with the screen off or the app in the\n * background: lock-screen controls and metadata (Media Session), re-issuing\n * play when the browser pauses a hidden video, landing back on the room's\n * position on return, and holding a screen wake lock while visibly playing.\n * The rules are in `lib/backgroundPlay.ts`.\n */\nexport function useBackgroundPlay(\n videoRef: RefObject<HTMLVideoElement | null>,\n coordinatorRef: RefObject<SeekCoordinator | null>,\n mediaId: string,\n durationMs: number,\n mseReady: boolean,\n resumePlayback: (v: HTMLVideoElement) => void,\n): void {\n const localPauseRef = useRef(false);\n const userPausedAtRef = useRef(0);\n\n // The latest resumePlayback, so its identity never re-wires the listeners.\n const resume = useEffectEvent((v: HTMLVideoElement) => resumePlayback(v));\n\n // Bring the element back to the room: a seek when it drifted, else a resume.\n const resync = useEffectEvent((reason: string) => {\n const v = videoRef.current;\n const coord = coordinatorRef.current;\n if (!v || !mseReady) return;\n const st = useAppStore.getState();\n const elementMs = v.currentTime * 1000;\n const roomMs = roomPlayheadNow(st.snapshot, st.snapshotReceivedAt, Date.now());\n const plan = resyncPlan({\n roomPlaying: st.snapshot.playing,\n seekInFlight: !!coord?.seekInFlight,\n elementPaused: v.paused,\n elementMs,\n roomMs,\n durationMs,\n driftToleranceMs: BG_RESYNC_TOLERANCE_MS,\n });\n const driftMs = Math.round(roomMs - elementMs);\n if (plan.kind === 'none') {\n debug(LABEL, 'resync not needed', { reason, driftMs });\n return;\n }\n // A seek here is a visible stall, so it is logged at warn with the drift\n // that caused it: the measurement a stutter report needs.\n if (plan.kind === 'seek') warn(LABEL, 'BG-RESYNC seek', { reason, driftMs, elementMs: Math.round(elementMs) });\n else debug(LABEL, 'resync', { reason, plan: plan.kind, driftMs });\n if (plan.kind === 'resume') {\n resumePlayback(v);\n } else if (coord) {\n void coord.submit({ targetMs: plan.targetMs, priority: SeekPriority.SEEK_EFFECT, caller: `bg-resync:${reason}` });\n }\n });\n\n // Keep-alive and return-to-page.\n useEffect(() => {\n const v = videoRef.current;\n if (!v || !mseReady) return;\n const onPause = () => {\n const st = useAppStore.getState();\n const keep = shouldKeepAlive({\n hidden: document.visibilityState === 'hidden',\n roomPlaying: st.snapshot.playing,\n seekInFlight: !!coordinatorRef.current?.seekInFlight,\n ended: v.ended,\n localPause: localPauseRef.current,\n sinceUserPauseMs: userPausedAtRef.current ? Date.now() - userPausedAtRef.current : Infinity,\n });\n if (!keep) return;\n debug(LABEL, 'keep-alive: browser paused a hidden video, resuming', { currentMs: Math.round(v.currentTime * 1000) });\n resume(v);\n };\n const onVisibility = () => {\n if (document.visibilityState !== 'visible') return;\n // A viewer has no in-app play button: a lock-screen pause ends here.\n localPauseRef.current = false;\n resync('visible');\n };\n v.addEventListener('pause', onPause);\n document.addEventListener('visibilitychange', onVisibility);\n return () => {\n v.removeEventListener('pause', onPause);\n document.removeEventListener('visibilitychange', onVisibility);\n };\n }, [mseReady, videoRef, coordinatorRef]);\n\n // Media Session: lock-screen metadata, play/pause, position.\n useEffect(() => {\n const v = videoRef.current;\n if (!v || typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;\n const ms = navigator.mediaSession;\n let cancelled = false;\n const title = mediaDisplayName(mediaId);\n const setMeta = (artwork: string | null) => {\n if (cancelled || typeof MediaMetadata === 'undefined') return;\n ms.metadata = new MediaMetadata({\n title,\n artist: 'Watch Party',\n artwork: artwork ? [{ src: artwork }] : [],\n });\n };\n setMeta(null);\n void firstLoadingImage(artworkCandidates(mediaId).map(resolveMediaAsset)).then(setMeta);\n\n const press = (action: 'play' | 'pause') => () => {\n const st = useAppStore.getState();\n const result = mediaSessionAction(action, { isLeader: isLeaderNow(), roomPlaying: st.snapshot.playing });\n debug(LABEL, 'media session press', { action, result: result.kind });\n if (action === 'pause') userPausedAtRef.current = Date.now();\n switch (result.kind) {\n case 'control':\n sendControl(result.control);\n // Pause at once rather than after the broadcast: the socket may be slow to wake.\n if (result.control === 'pause') v.pause();\n break;\n case 'local-pause':\n localPauseRef.current = true;\n v.pause();\n break;\n case 'resync':\n localPauseRef.current = false;\n resync('media-session');\n break;\n case 'none':\n break;\n }\n };\n const handlers: Array<[MediaSessionAction, MediaSessionActionHandler | null]> = [\n ['play', press('play')],\n ['pause', press('pause')],\n // Seeking from the lock screen is not a room control; show no seek buttons.\n ['seekbackward', null],\n ['seekforward', null],\n ['seekto', null],\n ];\n for (const [action, handler] of handlers) {\n try {\n ms.setActionHandler(action, handler);\n } catch (e) {\n debug(LABEL, 'media session action unsupported', { action, ...errorFields(e) });\n }\n }\n const updatePosition = () => {\n const dur = durationMs / 1000;\n if (!(dur > 0) || typeof ms.setPositionState !== 'function') return;\n try {\n ms.setPositionState({ duration: dur, position: Math.min(Math.max(v.currentTime, 0), dur), playbackRate: v.playbackRate || 1 });\n } catch (e) {\n warn(LABEL, 'setPositionState rejected', errorFields(e));\n }\n };\n const onPlay = () => { ms.playbackState = 'playing'; updatePosition(); };\n const onPause = () => { ms.playbackState = 'paused'; updatePosition(); };\n v.addEventListener('play', onPlay);\n v.addEventListener('pause', onPause);\n v.addEventListener('seeked', updatePosition);\n v.addEventListener('ratechange', updatePosition);\n updatePosition();\n return () => {\n cancelled = true;\n v.removeEventListener('play', onPlay);\n v.removeEventListener('pause', onPause);\n v.removeEventListener('seeked', updatePosition);\n v.removeEventListener('ratechange', updatePosition);\n for (const [action] of handlers) {\n try { ms.setActionHandler(action, null); } catch { /* unsupported action: nothing was set */ }\n }\n ms.metadata = null;\n ms.playbackState = 'none';\n };\n }, [mediaId, durationMs, videoRef]);\n\n // Screen wake lock while visibly playing. The browser drops it when the page\n // hides, so it is re-requested on return.\n useEffect(() => {\n const v = videoRef.current;\n if (!v || typeof navigator === 'undefined' || !('wakeLock' in navigator)) return;\n let lock: WakeLockSentinel | null = null;\n let requesting = false;\n let disposed = false;\n const release = () => {\n const l = lock;\n lock = null;\n if (l) void l.release().catch((e: unknown) => debug(LABEL, 'wake lock release failed', errorFields(e)));\n };\n const sync = () => {\n const want = !disposed && !v.paused && !v.ended && document.visibilityState === 'visible';\n if (!want) { release(); return; }\n if (lock || requesting) return;\n requesting = true;\n navigator.wakeLock.request('screen').then((l) => {\n requesting = false;\n lock = l;\n l.addEventListener('release', () => { if (lock === l) lock = null; });\n if (disposed || v.paused || document.visibilityState !== 'visible') release();\n }).catch((e: unknown) => {\n requesting = false;\n debug(LABEL, 'wake lock refused', errorFields(e));\n });\n };\n v.addEventListener('play', sync);\n v.addEventListener('pause', sync);\n v.addEventListener('ended', sync);\n document.addEventListener('visibilitychange', sync);\n sync();\n return () => {\n disposed = true;\n v.removeEventListener('play', sync);\n v.removeEventListener('pause', sync);\n v.removeEventListener('ended', sync);\n document.removeEventListener('visibilitychange', sync);\n release();\n };\n }, [videoRef]);\n}\n","import { useEffect, useEffectEvent, useRef, useState, useCallback, type MouseEvent } from 'react';\nimport { useAppStore } from '../lib/store';\nimport { emitClientReady } from '../lib/socket';\nimport ControlBar from './controls/ControlBar';\nimport MediaTitle from './MediaTitle';\nimport SubtitleOverlay, { type SubtitleStyle } from './SubtitleOverlay';\nimport { FragmentManager } from '../lib/fragmentManager';\nimport { resolveSubtitlesEnabled } from '../lib/subtitlePolicy';\nimport { INITIAL_BUFFER_MS, DEFAULT_BUFFER_GATE_TIMEOUT_MS, FRAGMENT_MP4_FILENAME } from '@watchparty/shared';\nimport { debug, warn, error as logError, errorFields } from '../lib/debug';\nimport { attemptAutoReload, reloadFlagTs, clearReloadFlag } from '../lib/autoReload';\nimport { playRidingAutoplay } from '../lib/autoplay';\nimport { SeekCoordinator, SeekPriority, waitForSeeked } from '../lib/seekCoordinator';\nimport { startHealthReporting, stopHealthReporting } from '../lib/healthReporter';\nimport { useControlsVisibility } from '../hooks/useControlsVisibility';\nimport { useIndexJson } from '../hooks/useIndexJson';\nimport { useMediaTracks } from '../hooks/useMediaTracks';\nimport { useStallRecovery } from '../hooks/useStallRecovery';\nimport { usePrefetch } from '../hooks/usePrefetch';\nimport { usePlaybackSync } from '../hooks/usePlaybackSync';\nimport { usePlaylistPreBuffer } from '../hooks/usePlaylistPreBuffer';\nimport { useBackgroundPlay } from '../hooks/useBackgroundPlay';\nimport { Icon } from '@wolffm/themes'\n\n// Phantom-buffer recovery (Mode A meltdown fix). A fresh load never legitimately\n// reports minutes of buffered-ahead with ZERO fragments loaded; that disagreement\n// is the browser MSE quota-corruption signature that used to spin the init retry\n// loop forever (the 2026-06-08 meltdown that forced an API restart). On detection\n// we dispose + recreate the MediaSource instead of looping against the corrupt one.\nconst PHANTOM_BUFFER_MS = 300_000; // >5min buffered-ahead with 0 loaded = corrupt SourceBuffer\nconst MAX_INIT_RETRIES = 1; // plain retries against the same MediaSource before recreating\nconst MAX_MSE_RECREATES = 1; // in-page MediaSource recreations before escalating to a page reload.\n // The 2026-06-16 retest showed in-page recreates DON'T clear the\n // phantom (it recurs on the fresh MediaSource); only a fresh DOCUMENT\n // (reload) does. So try recreate once, then reload (see autoReload.ts).\n\ndeclare global {\n interface Window {\n /** E2E diagnostics handles, set while a player is mounted. */\n __SEEK_COORDINATOR__?: SeekCoordinator;\n __AUDIO_SELECT__?: ((idx: number) => void) | null;\n }\n}\n\n// initializePlayback reports its own failures; this logs anything that escapes them.\nfunction logInitializeThrow(e: unknown): void {\n logError('buffer', 'initializePlayback threw', errorFields(e));\n}\n\ninterface Props { mediaUrl: string; indexUrl: string; mediaId: string; }\n\nexport default function MediaPlayer({ mediaUrl, indexUrl, mediaId }: Props) {\n const videoRef = useRef<HTMLVideoElement>(null);\n const [mseReady, setMseReady] = useState(false);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const { controlsVisible, showControls, hideControls, setControlsHeld } = useControlsVisibility();\n\n useEffect(() => {\n const h = () => setIsFullscreen(!!document.fullscreenElement);\n document.addEventListener('fullscreenchange', h);\n return () => document.removeEventListener('fullscreenchange', h);\n }, []);\n const {\n fragments, durationMs, mseCodec, initSegment,\n audioTracks: mseAudioTracks, contentToken,\n } = useIndexJson(indexUrl);\n // Fragment-route addressing. mediaUrl minus the /output_frag.mp4 suffix is exactly\n // `<origin>/media/<mediaId>` — the prefix the server route parses. A null token (media not\n // backfilled) yields null here, so those media stay on Range by construction.\n const deliveryFor = useCallback((trackId?: string) => {\n if (!contentToken) return null;\n const baseUrl = mediaUrl.replace(new RegExp(`/${FRAGMENT_MP4_FILENAME.replace(/\\./g, '\\\\.')}$`), '');\n return baseUrl === mediaUrl ? null : { baseUrl, token: contentToken, trackId };\n }, [contentToken, mediaUrl]);\n // useMseAudio gates audio setup until index.json loads (mseCodec is null\n // until then — premature setup would use a placeholder codec).\n const indexLoaded = mseCodec !== null;\n const useMseAudio = indexLoaded && mseAudioTracks.length > 0;\n const readiness = useAppStore(s => s.readiness);\n const emittedRef = useRef(false); // Guards duplicate client_ready emits\n const initSucceededRef = useRef(false); // Guards retry — only true after setMseReady(true)\n const sourceBufferRef = useRef<SourceBuffer | null>(null);\n const mediaSourceRef = useRef<MediaSource | null>(null);\n const fragmentManagerRef = useRef<FragmentManager | null>(null);\n // Phantom-buffer recovery state (Mode A fix — see PHANTOM_BUFFER_MS above).\n const initRetryRef = useRef(0); // consecutive init failures against the current MediaSource\n const mseRecreateRef = useRef(0); // MediaSource recreations for the current media\n const mseRecreateMediaRef = useRef<string | null>(null);\n const [mseRecreateNonce, setMseRecreateNonce] = useState(0);\n // MSE audio (v3+): exactly ONE FragmentManager + SourceBuffer at a time,\n // matching the currently-selected audio track. We can't keep N audio SBs\n // around and flip video.audioTracks[i].enabled because Chrome stable does\n // NOT expose video.audioTracks (the property is `undefined` without the\n // experimental-web-platform-features flag, which end users don't have).\n // So track switching means: dispose old FM, removeSourceBuffer the old SB,\n // addSourceBuffer for the new track's codec, create new FM, re-buffer from\n // the current playhead. Incurs a brief audio gap (~200-2000ms depending on\n // network). Audio output during normal playback is whatever Chrome routes\n // from the single audio SourceBuffer attached to the MediaSource.\n // Array form preserved (length 0 or 1) so seekCoordinator/usePrefetch don't\n // need their multi-FM signatures changed.\n const audioFmsRef = useRef<FragmentManager[]>([]);\n const audioSbsRef = useRef<SourceBuffer[]>([]);\n // Index of the audio track currently attached to the MSE pipeline. Compared\n // against selectedAudioIndex to detect track-swap requests.\n const activeAudioTrackIdxRef = useRef<number>(-1);\n const audioSwapInFlightRef = useRef<boolean>(false);\n const coordinatorRef = useRef<SeekCoordinator | null>(null);\n // Multi-writer ref: also written by usePlaybackSync hook\n const lastAppliedPlayheadRef = useRef<number>(0);\n const stopPrefetchRef = useRef<(() => void) | null>(null);\n // Bumped per MSE pipeline; tags FMs so a stale effect never re-indexes a new one.\n const mseEpochRef = useRef(0);\n\n // Buffer state from store\n const bufferState = useAppStore(s => s.bufferState);\n const setBufferState = useAppStore(s => s.setBufferState);\n\n // Track state\n const {\n audioTracks, subtitleTracks,\n selectedAudioIndex, setSelectedAudioIndex,\n selectedSubtitleIndex, setSelectedSubtitleIndex,\n } = useMediaTracks(mediaId);\n\n // Media preferences from store (persisted to localStorage)\n const mediaPrefs = useAppStore(s => s.mediaPrefs);\n const mutedForAutoplay = useAppStore(s => s.mutedForAutoplay);\n const setMutedForAutoplay = useAppStore(s => s.setMutedForAutoplay);\n const setSubtitlesEnabled = useAppStore(s => s.setSubtitlesEnabled);\n const setSubtitleStyleStore = useAppStore(s => s.setSubtitleStyle);\n const setPreferredAudioLang = useAppStore(s => s.setPreferredAudioLang);\n\n // Use persisted values\n const subtitlesEnabled = mediaPrefs.subtitlesEnabled;\n const subtitleStyle = mediaPrefs.subtitleStyle as SubtitleStyle;\n\n // Per-media subtitle default. For foreign-audio content that ships an English\n // subtitle, default subtitles ON even when the global pref is off — otherwise\n // toggling CC off during (English-dubbed) trailers silently suppressed subs\n // for a later Japanese-audio episode (cedar-ridge 2026-07-09, \"subtitles\n // weren't tracked\"). `subtitleOverride` lets the user still toggle for the\n // current media and resets on media change so each media re-derives its\n // default; toggling also updates the persisted global pref (baseline).\n const selectedAudioLang = audioTracks[selectedAudioIndex]?.lang;\n const hasEngSub = subtitleTracks.some((t) => t.lang === 'eng');\n const [subtitleOverride, setSubtitleOverride] = useState<boolean | null>(null);\n const effectiveSubtitlesEnabled = resolveSubtitlesEnabled({\n override: subtitleOverride, globalEnabled: subtitlesEnabled, selectedAudioLang, hasEngSub,\n });\n const toggleSubtitles = useCallback(() => {\n const next = !effectiveSubtitlesEnabled;\n setSubtitleOverride(next);\n setSubtitlesEnabled(next);\n }, [effectiveSubtitlesEnabled, setSubtitlesEnabled]);\n\n const resumePlayback = useCallback((v: HTMLVideoElement) => {\n // Ride the autoplay policy: audible when the browser allows it, else a\n // muted bridge that unmutes on first interaction. See lib/autoplay.ts.\n const st = useAppStore.getState();\n playRidingAutoplay(v, {\n userWantsMuted: st.mediaPrefs.muted,\n onMutedBridge: () => st.setMutedForAutoplay(true),\n onUnmuted: () => st.setMutedForAutoplay(false),\n label: 'video',\n });\n }, []);\n const pausePlayback = useCallback((v: HTMLVideoElement) => { v.pause(); }, []);\n\n // Live playhead from video element (updates via timeupdate ~4x/sec)\n const [displayPlayheadMs, setDisplayPlayheadMs] = useState(0);\n\n // Reset per-media state on every media change: the subtitle override (see\n // above) and the displayed playhead. displayPlayheadMs (not\n // snapshot.playheadMs, which the seek bar's fallback never reads) drives\n // the seek bar; it's only updated by `timeupdate`. On a playlist switch —\n // especially a PAUSED playlist_select where no timeupdate ever fires — it\n // would otherwise strand the bar at the previous clip's position over the new\n // clip's duration (cedar-ridge 2026-07-09). Keyed on mediaId since MediaPlayer\n // is not remounted per clip.\n const [prevMediaId, setPrevMediaId] = useState(mediaId);\n if (prevMediaId !== mediaId) {\n setPrevMediaId(mediaId);\n setSubtitleOverride(null);\n setDisplayPlayheadMs(0);\n }\n\n useEffect(() => {\n const v = videoRef.current;\n if (!v) return;\n\n const handleTimeUpdate = () => {\n setDisplayPlayheadMs(Math.round(v.currentTime * 1000));\n };\n\n v.addEventListener('timeupdate', handleTimeUpdate);\n return () => v.removeEventListener('timeupdate', handleTimeUpdate);\n }, []);\n\n // Initialize FragmentManager when buffer state changes\n const handleBufferingChange = useCallback((isBuffering: boolean) => {\n setBufferState({ isBuffering });\n }, [setBufferState]);\n\n // Initialize playback by loading initial fragments\n // Dispose the corrupt MediaSource and rebuild from scratch (Mode A recovery).\n // Bumping the nonce re-runs the MSE setup effect: cleanup disposes the FM +\n // revokes the MediaSource, the re-run creates a fresh MediaSource/SourceBuffer\n // and re-inits. Same effect the operator's API restart had — automatic and\n // per-client — instead of retrying forever against a buffer that can't append.\n const recreateMediaSource = useCallback((reason: string) => {\n if (mseRecreateRef.current >= MAX_MSE_RECREATES) {\n // In-page MediaSource recreation does NOT clear browser-MSE-layer phantom-\n // buffer corruption — it recurs on the fresh MediaSource (2026-06-16 retest:\n // 3/10 clients stayed stuck after recreates). A fresh DOCUMENT does clear it.\n // Escalate to a rate-limited auto-reload (beacons the trigger, gives up after\n // repeated reloads so a bad connection doesn't churn — see autoReload.ts).\n attemptAutoReload('phantom-recreate-exhausted', { reason, recreates: mseRecreateRef.current, mediaId });\n return;\n }\n mseRecreateRef.current += 1;\n initRetryRef.current = 0;\n warn('buffer', 'MSE-RECREATE — rebuilding MediaSource to clear corrupt buffer', {\n reason, attempt: mseRecreateRef.current,\n });\n setMseRecreateNonce(n => n + 1);\n }, [mediaId]);\n\n async function initializePlayback() {\n const fm = fragmentManagerRef.current;\n const v = videoRef.current;\n if (!fm || !fragments.length) return;\n // Already fully initialized — don't re-run\n if (initSucceededRef.current) return;\n\n // Buffer from the snapshot's playhead position so viewers joining\n // mid-playback don't waste time buffering from the start\n const snap = useAppStore.getState().snapshot;\n const startMs = snap.playheadMs || 0;\n debug('buffer', 'Initializing playback', { mediaId, startMs, playing: snap.playing });\n\n // Tell FragmentManager our playhead so X-Buffer-Ahead-Ms header is correct\n fm.setPlayhead(startMs);\n\n const initStart = performance.now();\n try {\n // Kick MSE audio FMs in parallel with the video FM — audio fragments are\n // small (~40KB at 192 kbps/2s) and cheap to fetch. Running them in\n // parallel means audio is buffered by the time the video buffer gate\n // passes, avoiding a perceptual \"video starts, audio joins a beat late\"\n // pattern. Errors on audio FMs are non-fatal: audio falls behind but\n // video plays.\n const audioFms = audioFmsRef.current;\n const audioInitPromises = audioFms.map(afm =>\n afm.ensureBuffered(startMs, 'initial-audio').catch(e => {\n // Disposed = the media changed before this load finished (a clip\n // switch). That is our own teardown cancelling it, not a failure:\n // at warn it was 12 false alarms in one trailer party (10-5-26).\n const log = afm.isDisposed ? debug : warn;\n log('buffer', afm.isDisposed\n ? 'Audio FM initial load cancelled by media change'\n : 'Audio FM initial ensureBuffered failed', {\n error: (e as Error)?.message,\n });\n }),\n );\n\n await fm.ensureBuffered(startMs, 'initial');\n if (fm.isDisposed) return;\n\n // Pre-play buffer gate: wait until we have buffer ahead before starting\n // playback. ensureBuffered only awaits the target fragment's append —\n // ahead fragments may still be in the append queue.\n // Use getBufferedAheadMs instead of isBuffered because fMP4 container\n // overhead can push buffered.start(0) up to ~0.6s past the fragment's\n // nominal start time. getBufferedAheadMs has a 1s-ahead fallback that\n // handles this case without a 20s timeout.\n const GATE_TIMEOUT = DEFAULT_BUFFER_GATE_TIMEOUT_MS;\n // Wait for audio FMs alongside the video buffer gate. Each audio gate\n // resolves independently; we proceed when all are done (or timed out).\n await Promise.allSettled(audioInitPromises);\n const audioGatePromises = audioFms.map(afm =>\n afm.waitForBuffered(startMs + INITIAL_BUFFER_MS, GATE_TIMEOUT),\n );\n const reached = await fm.waitForBuffered(startMs + INITIAL_BUFFER_MS, GATE_TIMEOUT);\n await Promise.allSettled(audioGatePromises);\n if (!reached) {\n warn('buffer', 'Buffer gate timeout — proceeding with partial buffer', {\n startMs, aheadMs: Math.round(fm.getBufferedAheadMs(startMs)), targetMs: INITIAL_BUFFER_MS,\n });\n }\n\n if (fm.isDisposed) return;\n\n // ── Phantom-buffer guard (Mode A meltdown fix) ──────────────\n // MSE quota corruption can leave the SourceBuffer reporting a buffered\n // range spanning the whole media while ZERO fragments are actually loaded.\n // The gate above then \"passes\" on that phantom range; without this guard\n // we'd emit client_ready and every real append throws \"SourceBuffer is\n // full\" forever. Recreate the MediaSource instead of trusting the phantom.\n if (fm.getBufferedAheadMs(startMs) > PHANTOM_BUFFER_MS && fm.getLoadedCount() === 0) {\n // Capture the full SourceBuffer geometry + fragment ledger so we can\n // root-cause WHY a fresh SB reports a whole-media buffered range with\n // zero fragments loaded. This is the smoking-gun diagnostic for the\n // phantom; without it we only know \"it happened\", not its shape.\n fm.dumpBufferDiagnostics(startMs, 'phantom-detected');\n warn('buffer', 'PHANTOM-BUFFER detected at gate — recreating MediaSource', {\n gateAheadMs: Math.round(fm.getBufferedAheadMs(startMs)),\n loaded: fm.getLoadedCount(),\n startMs: Math.round(startMs),\n recreateAttempt: mseRecreateRef.current,\n mediaId,\n });\n recreateMediaSource('phantom-buffer');\n return;\n }\n\n const initialBufferMs = Math.round(performance.now() - initStart);\n debug('buffer', 'Buffer gate passed', {\n aheadMs: Math.round(fm.getBufferedAheadMs(startMs)),\n totalMs: initialBufferMs,\n });\n // Single warn-level digest of the load's init phase per FM. See\n // FragmentManager.emitInitDigest() for what's captured and why warn.\n const digestReason = reached ? 'gate-pass' : 'gate-timeout';\n fm.emitInitDigest('video', digestReason);\n audioFms.forEach((afm, i) => afm.emitInitDigest(`audio:${i}`, digestReason));\n\n // Emit client_ready exactly once per media load (separate from retry guard).\n // Uses emitClientReady() so socket.ts can re-emit on reconnect if the\n // socket disconnects after init but before the server registers readiness.\n if (!emittedRef.current) {\n emittedRef.current = true;\n emitClientReady(mediaId);\n }\n\n // ── Mark initialized EARLY ──────────────────────────────────\n // Set mseReady + initSucceeded now, BEFORE viewer correction.\n // This unblocks prefetch and stall recovery — if the correction\n // below fails, those mechanisms fill gaps independently instead\n // of the retry loop aborting all progress every 3s.\n let seekMs = startMs;\n if (v && seekMs > 0) {\n // Snap to actual buffer start if startMs lands before it.\n // fMP4 container overhead / late-IDR can push buffered.start()\n // 0.3-0.6s past the fragment's nominal start time. Seeking\n // before the buffer leaves Chrome at readyState=1 indefinitely\n // because there's no decodable data at that position.\n const b = v.buffered;\n if (b.length > 0) {\n const seekSec = seekMs / 1000;\n const bufStart = b.start(0);\n if (seekSec < bufStart && bufStart - seekSec < 1.0) {\n seekMs = bufStart * 1000;\n }\n }\n v.currentTime = seekMs / 1000;\n await waitForSeeked(v, 3000);\n }\n lastAppliedPlayheadRef.current = seekMs;\n setMseReady(true);\n initSucceededRef.current = true;\n initRetryRef.current = 0; // clean slate after a successful init\n if (reloadFlagTs()) warn('buffer', 'RECOVERY-SUCCESS — playback initialized after an auto-reload', { mediaId });\n clearReloadFlag();\n debug('buffer', 'initializePlayback complete', {\n mediaId, startMs, seekMs, initDurationMs: Math.round(performance.now() - initStart),\n });\n\n // Start playback immediately if the room is playing\n const latestSnap = useAppStore.getState().snapshot;\n if (v && latestSnap.playing && v.paused) {\n debug('buffer', 'initializePlayback: calling play()', { playing: latestSnap.playing });\n resumePlayback(v);\n }\n\n // ── Best-effort viewer correction (non-fatal) ───────────────\n // Correct for time elapsed during buffering (viewers only).\n // If this fails, the video is already playing at startMs and\n // prefetch/stall-recovery will handle gap-filling.\n if (fm.isDisposed) return;\n const state = useAppStore.getState();\n const amLeader = !!state.snapshot.leaderId && state.snapshot.leaderId === state.me.clientId;\n if (!amLeader && state.snapshot.playing) {\n try {\n const elapsed = Date.now() - state.snapshotReceivedAt;\n const correctedMs = state.snapshot.playheadMs + elapsed;\n debug('buffer', 'Viewer correction computed', {\n startMs,\n playheadMs: state.snapshot.playheadMs,\n elapsed,\n correctedMs,\n deltaSec: ((correctedMs - startMs) / 1000).toFixed(1),\n initialBufferMs,\n });\n if (correctedMs > startMs) {\n const coordinator = coordinatorRef.current;\n if (v && coordinator) {\n // Recompute correction target to account for any time spent in the\n // buffer gate and ensureBuffered above. Use the freshest position.\n const lateState = useAppStore.getState();\n const lateElapsed = Date.now() - lateState.snapshotReceivedAt;\n const lateCorrectedMs = lateState.snapshot.playheadMs + lateElapsed;\n const seekTarget = lateCorrectedMs > correctedMs ? lateCorrectedMs : correctedMs;\n\n // Route through coordinator (VIEWER_CORRECTION priority).\n // SEEK_EFFECT can preempt it if a room sync fires during correction.\n // The coordinator handles buffering, snap-to-buffered, and audio re-sync.\n await coordinator.submit({\n targetMs: seekTarget,\n priority: SeekPriority.VIEWER_CORRECTION,\n caller: 'viewer-correction',\n pauseSeekResume: true,\n onComplete: (actualMs) => {\n lastAppliedPlayheadRef.current = actualMs;\n debug('buffer', 'Viewer correction applied', {\n startMs, correctedMs, seekTarget, actualMs,\n totalInitMs: Math.round(performance.now() - initStart),\n });\n },\n onAborted: (err) => {\n if (err === null) {\n warn('buffer', 'Viewer correction preempted by sync seek — OK');\n } else {\n warn('buffer', 'Viewer correction aborted', { error: (err as Error)?.message });\n }\n },\n });\n }\n }\n } catch (corrErr) {\n warn('buffer', 'Viewer correction failed (non-fatal), prefetch will fill gaps', {\n error: (corrErr as Error)?.message,\n });\n }\n\n }\n } catch (e) {\n // ensureBuffered itself failed. If we got fragment 0 loaded,\n // set mseReady anyway so prefetch and stall recovery can take over.\n if (!initSucceededRef.current && fm.getLoadedCount() > 0) {\n warn('buffer', 'ensureBuffered failed but have buffer — setting mseReady for recovery', {\n loadedCount: fm.getLoadedCount(),\n error: (e as Error)?.message,\n });\n setMseReady(true);\n initSucceededRef.current = true;\n\n const latestSnap = useAppStore.getState().snapshot;\n if (v && latestSnap.playing && v.paused) {\n resumePlayback(v);\n }\n } else {\n const errMsg = (e as Error)?.message || '';\n // Disposal is a CANCELLATION, not a failure. `dispose()` rejects every\n // pending waiter with \"FragmentManager disposed\", so any init still in\n // flight when the media changes (or the player unmounts) lands here\n // through no fault of its own. The retry below already refuses to run\n // for a disposed FM, and the incoming media brings its own FM and its\n // own initializePlayback — so there is nothing to recover and nothing\n // to report. Logging it via logError made every deliberate media switch\n // emit an error-level line, which is indistinguishable in the session\n // logs from a real init failure and is exactly what\n // `media-switch-firefox.spec.ts` watches for as an MSE fault signature.\n if (fm.isDisposed || errMsg.includes('FragmentManager disposed')) {\n debug('buffer', 'initializePlayback aborted — FragmentManager disposed (media switch or unmount)', {\n mediaId,\n });\n return;\n }\n // \"SourceBuffer is full\" means the buffer is corrupt — retrying against\n // the same MediaSource loops forever (the Monday meltdown). Recreate\n // immediately. Other failures get MAX_INIT_RETRIES plain retries first\n // (transient network), then escalate to a recreate.\n const sbFull = errMsg.includes('SourceBuffer is full');\n // A persistent init-segment failure (FragmentManager gave up after\n // MAX_INIT_FETCH_RETRIES and rejected its waiters) won't be fixed by\n // plain retries against the same MediaSource — recreate immediately,\n // same as sbFull. Fast path that avoids the ~65s freeze seen on\n // cedar-ridge 2026-06-25 (DemonLord s01e04).\n const initFailed = errMsg.includes('init segment failed');\n initRetryRef.current += 1;\n if (sbFull || initFailed || initRetryRef.current > MAX_INIT_RETRIES) {\n logError('buffer', 'Initialize playback failed — recreating MediaSource', {\n error: errMsg, mediaId, attempt: initRetryRef.current, sbFull, initFailed,\n });\n recreateMediaSource(sbFull ? 'sourcebuffer-full' : initFailed ? 'init-failed' : 'init-retry-cap');\n } else {\n logError('buffer', 'Initialize playback error — will retry in 3s', {\n error: errMsg, mediaId, attempt: initRetryRef.current,\n });\n setTimeout(() => {\n if (fragmentManagerRef.current && !fm.isDisposed && !initSucceededRef.current) {\n debug('buffer', 'Retrying initializePlayback');\n initializePlayback().catch(logInitializeThrow);\n }\n }, 3000);\n }\n }\n }\n }\n\n // Inputs the sourceopen handler uses but that must not rebuild the pipeline.\n // Read once when the effect runs, as of the render that (re)built it; later\n // index and audio-track changes reach the pipeline through the effects below.\n const readPipelineInputs = useEffectEvent(() => ({\n useMseAudio, selectedAudioIndex, mseAudioTracks, fragments, deliveryFor, initializePlayback,\n }));\n\n // Initialize MSE and FragmentManager\n // Wait for mseCodec from index.json before creating MediaSource to avoid\n // creating a SourceBuffer with a fallback codec that gets torn down when\n // the real codec arrives (causes SourceBuffer errors on Chromium).\n useEffect(() => {\n if (!mseCodec) return; // Wait for index.json to load with the real codec\n const {\n useMseAudio, selectedAudioIndex, mseAudioTracks, fragments, deliveryFor, initializePlayback,\n } = readPipelineInputs();\n\n // Reset the phantom-recovery budget when the MEDIA changes — but NOT when\n // this effect re-runs because we ourselves bumped mseRecreateNonce to\n // recover (same mediaId). Otherwise the recreate cap would reset every\n // recreation and could loop indefinitely.\n if (mseRecreateMediaRef.current !== mediaId) {\n mseRecreateMediaRef.current = mediaId;\n mseRecreateRef.current = 0;\n initRetryRef.current = 0;\n }\n\n // Recovery breadcrumb across the reload boundary. window.location.reload()\n // ends this socket's session log and starts a fresh one — without this the\n // post-reload session looks like a normal join. If the guard flag is set,\n // this load IS a recovery attempt; log it so tonight's logs correlate the\n // pre-reload freeze with the post-reload outcome (RECOVERY-SUCCESS on init,\n // or another PHANTOM-BUFFER if the reload didn't clear it).\n const reloadTs = reloadFlagTs();\n if (reloadTs) {\n warn('buffer', 'POST-AUTO-RELOAD — this load follows an auto-reload recovery', {\n reloadAtMs: reloadTs, sinceReloadMs: Date.now() - reloadTs, mediaId,\n });\n }\n\n let objectUrl: string | null = null;\n const ms = new MediaSource();\n mediaSourceRef.current = ms;\n\n // Create FragmentManager\n const fm = new FragmentManager(handleBufferingChange);\n fragmentManagerRef.current = fm;\n const epoch = ++mseEpochRef.current;\n // Expose for E2E test diagnostics\n window.__FRAGMENT_MANAGER__ = fm;\n // Audio FMs ref (array, ordered matching mseAudioTracks). Tests read this\n // to observe per-track buffer state independent of video. The array is\n // stable — we expose the ref so tests see live updates as tracks are added.\n window.__AUDIO_FRAGMENT_MANAGERS__ = audioFmsRef.current;\n\n // Create SeekCoordinator — single owner of all seek state\n const coordinator = new SeekCoordinator({\n videoRef,\n fragmentManagerRef,\n audioFragmentManagersRef: audioFmsRef,\n });\n coordinatorRef.current = coordinator;\n // Expose for E2E test diagnostics\n window.__SEEK_COORDINATOR__ = coordinator;\n\n startHealthReporting();\n\n const v = videoRef.current;\n\n // Video element error handler — surfaces playback failures to the user\n const handleVideoError = () => {\n const mediaError = v?.error;\n if (mediaError) {\n logError('video', `Media error code=${mediaError.code}: ${mediaError.message}`, {\n code: mediaError.code,\n networkState: v!.networkState,\n readyState: v!.readyState\n });\n useAppStore.getState().pushToast('error', 'Video playback error. Try reloading.');\n }\n };\n\n // Base URL for audio fragment Range requests, derived from mediaUrl by\n // stripping the video output filename: /media/.../foo/output_frag.mp4\n // → /media/.../foo/ → append `audio/audio_<lang>_<idx>.m4a`\n const audioBase = mediaUrl.replace(new RegExp(`/${FRAGMENT_MP4_FILENAME.replace(/\\./g, '\\\\.')}$`), '');\n\n const onOpen = () => {\n try {\n const mimeType = `video/mp4; codecs=\"${mseCodec}\"`;\n if (!MediaSource.isTypeSupported(mimeType)) {\n warn('mse', `Codec not supported by browser MSE: ${mimeType}`);\n useAppStore.getState().pushToast('error', 'Video codec not supported by your browser.');\n }\n\n debug('mse', `addSourceBuffer: ${mimeType}`);\n const sb = ms.addSourceBuffer(mimeType);\n sourceBufferRef.current = sb;\n sb.addEventListener('error', () => {\n const videoError = videoRef.current?.error;\n logError('mse', 'SourceBuffer error', {\n mediaError: videoError ? { code: videoError.code, message: videoError.message } : null,\n readyState: ms.readyState,\n updating: sb.updating,\n });\n });\n\n // Attach SourceBuffer to FragmentManager\n fm.setSourceBuffer(sb, ms);\n\n // MSE audio (v3+): create ONE audio SourceBuffer + FragmentManager\n // for the currently-selected track. Track swap is handled by a\n // separate effect that disposes + recreates the SB. All Range\n // requests for audio fragments go through the same /media endpoint\n // as video, sharing the priority scheduler and fragment cache.\n if (useMseAudio) {\n const initIdx = Math.max(0, Math.min(selectedAudioIndex, mseAudioTracks.length - 1));\n const track = mseAudioTracks[initIdx];\n const audioMime = track.mse_codec;\n if (!MediaSource.isTypeSupported(audioMime)) {\n warn('mse', `Audio codec not supported: ${audioMime}, track=${track.track_id}`);\n } else {\n try {\n const audioSb = ms.addSourceBuffer(audioMime);\n audioSb.addEventListener('error', () => {\n logError('mse', 'Audio SourceBuffer error', {\n trackId: track.track_id,\n readyState: ms.readyState,\n updating: audioSb.updating,\n });\n });\n const audioFm = new FragmentManager(\n handleBufferingChange,\n { suppressEndOfStream: true, label: `audio:${track.track_id}` },\n );\n audioFm.setSourceBuffer(audioSb, ms);\n audioFmsRef.current.push(audioFm);\n audioSbsRef.current.push(audioSb);\n activeAudioTrackIdxRef.current = initIdx;\n debug('mse', `Audio SB added: codec=${audioMime} track=${track.track_id} lang=${track.lang} idx=${initIdx}`);\n } catch (e) {\n warn('mse', `Audio SB creation failed for track ${track.track_id}`, { error: (e as Error)?.message });\n }\n }\n // <video> element drives audio output in MSE path — unmute so the\n // user hears it. Volume + muted preferences are still enforced by\n // VolumeSlider (which writes to both video + audio elements).\n const v = videoRef.current;\n if (v) v.muted = false;\n }\n\n // Initialize index if already loaded\n if (fragments.length) {\n fm.setIndex(fragments, mediaUrl, initSegment, mediaId, deliveryFor());\n fm.epoch = epoch;\n // Attach the active audio track index to its FM (only one FM exists\n // at a time per the single-SB design). Cache mediaId includes\n // trackId so swapping tracks doesn't collide in PlaylistFragmentCache.\n if (useMseAudio) {\n const idx = activeAudioTrackIdxRef.current;\n const audioFm = audioFmsRef.current[0];\n const track = idx >= 0 ? mseAudioTracks[idx] : undefined;\n if (audioFm && track) {\n const audioUrl = `${audioBase}/${track.path}`.replace(/\\\\/g, '/');\n audioFm.setIndex(\n track.fragments,\n audioUrl,\n track.init_segment,\n `${mediaId}::audio::${track.track_id}`,\n deliveryFor(track.track_id),\n );\n audioFm.epoch = epoch;\n }\n }\n initializePlayback().catch(logInitializeThrow);\n }\n } catch (e) {\n warn('buffer', 'MSE source error', { error: (e as Error)?.message });\n }\n };\n\n if (v) {\n // Register sourceopen BEFORE setting src — setting src triggers\n // sourceopen asynchronously, and under CPU contention (many tabs)\n // the event can fire before a listener registered afterward.\n ms.addEventListener('sourceopen', onOpen);\n objectUrl = URL.createObjectURL(ms);\n v.src = objectUrl;\n v.addEventListener('error', handleVideoError);\n }\n\n return () => {\n // Pause the video element so a stale pipeline doesn't keep playing\n // while the new media initializes. MSE audio rides on the same\n // <video> — the pause stops both streams.\n v?.pause();\n\n if (v) v.removeEventListener('error', handleVideoError);\n ms.removeEventListener('sourceopen', onOpen);\n stopHealthReporting();\n coordinator.dispose();\n coordinatorRef.current = null;\n fm.dispose();\n fragmentManagerRef.current = null;\n window.__FRAGMENT_MANAGER__ = null;\n window.__AUDIO_FRAGMENT_MANAGERS__ = null;\n // Dispose MSE audio FMs first (stops any further appends) — video FM owns\n // endOfStream; audio FMs just hold fetch state that needs to abort.\n for (const afm of audioFmsRef.current) afm.dispose();\n audioFmsRef.current = [];\n\n // Explicitly release browser-MSE memory before the NEXT MediaSource is\n // created. URL.revokeObjectURL alone does NOT free the old SourceBuffers'\n // buffered data or detach the MediaSource from the <video> element — the\n // browser holds that memory until GC. On a media SWITCH (and on in-page\n // MSE recreate) the new MediaSource is built on the SAME <video> element\n // while the old buffers are still resident; the new init-segment append\n // then throws \"MediaSource buffer not sufficient\" (Chrome). That\n // corruption SURVIVES an in-page recreate — only a fresh document cleared\n // it — so switching from a heavily-buffered episode froze on \"Waiting for\n // sync\" until the user reloaded (cedar-ridge 2026-06-29: a paused,\n // ~80s-buffered WitchHat → NeedyGirl switch; 3× init-append fails, the\n // recreate failed identically, recovered only on manual reload).\n // Aborting + removing each SourceBuffer while the MS is still 'open', then\n // resetting the element, forces the browser to drop the old buffers\n // synchronously so the next allocation starts clean. (DO NOT REMOVE.)\n if (ms.readyState === 'open') {\n for (const sb of [sourceBufferRef.current, ...audioSbsRef.current]) {\n if (!sb) continue;\n try {\n if (sb.updating) sb.abort();\n ms.removeSourceBuffer(sb);\n } catch {\n // Already detached/removed, or MS state changed mid-teardown.\n }\n }\n }\n audioSbsRef.current = [];\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n // Detach the old MediaSource and reset the element's media pipeline so the\n // browser releases decode/buffer resources before the new src is set.\n if (v) {\n try { v.removeAttribute('src'); v.load(); } catch { /* element gone */ }\n }\n\n // Stop prefetch interval (owned by usePrefetch hook)\n stopPrefetchRef.current?.();\n // Reset init guards so the next media triggers initializePlayback\n initSucceededRef.current = false;\n emittedRef.current = false;\n sourceBufferRef.current = null;\n setMseReady(false);\n };\n }, [mediaUrl, mediaId, mseCodec, initSegment, handleBufferingChange, mseRecreateNonce]);\n\n // When fragments arrive after MSE open, initialize FragmentManager.\n // Guard: only call setIndex if it's the SAME FragmentManager that the MSE\n // effect created — prevents the double-init-segment race where this effect\n // fires for a stale FM during rapid media reloads. New fragments, URL, init\n // segment or audio tracks run it; the rest (media id, delivery, audio mode,\n // initializePlayback) is read as it stands then.\n const indexFragments = useEffectEvent(() => {\n const fm = fragmentManagerRef.current;\n const epoch = mseEpochRef.current;\n if (mediaSourceRef.current?.readyState === 'open' && sourceBufferRef.current && fragments.length && fm) {\n // If the MSE effect already called setIndex (same epoch), skip the\n // duplicate call that would reset initState mid-flight.\n if (fm.hasIndex && fm.epoch === epoch) return;\n fm.setIndex(fragments, mediaUrl, initSegment, mediaId, deliveryFor());\n fm.epoch = epoch;\n // MSE audio: setIndex on the single audio FM (the one matching the\n // currently active track index). Same epoch guard.\n if (useMseAudio) {\n const audioBase = mediaUrl.replace(new RegExp(`/${FRAGMENT_MP4_FILENAME.replace(/\\./g, '\\\\.')}$`), '');\n const idx = activeAudioTrackIdxRef.current;\n const audioFm = audioFmsRef.current[0];\n const track = idx >= 0 ? mseAudioTracks[idx] : undefined;\n if (audioFm && track && !(audioFm.hasIndex && audioFm.epoch === epoch)) {\n const audioUrl = `${audioBase}/${track.path}`.replace(/\\\\/g, '/');\n audioFm.setIndex(\n track.fragments,\n audioUrl,\n track.init_segment,\n `${mediaId}::audio::${track.track_id}`,\n deliveryFor(track.track_id),\n );\n audioFm.epoch = epoch;\n }\n }\n if (!initSucceededRef.current) {\n initializePlayback().catch(logInitializeThrow);\n }\n }\n });\n useEffect(() => { indexFragments(); }, [fragments, mediaUrl, initSegment, mseAudioTracks]);\n\n // Listen for socket reconnect events to abort stale FM jobs.\n // socket.ts dispatches this event instead of reaching into window.__FRAGMENT_MANAGER__\n // directly, keeping the socket layer decoupled from the FM instance.\n useEffect(() => {\n const handler = () => {\n fragmentManagerRef.current?.abortStaleJobs('socket-reconnect');\n for (const afm of audioFmsRef.current) afm.abortStaleJobs('socket-reconnect');\n };\n window.addEventListener('watchparty:socket-reconnect', handler);\n return () => window.removeEventListener('watchparty:socket-reconnect', handler);\n }, []);\n\n // Test hook: expose the audio-select callback so e2e scripts can invoke\n // a swap without fighting ControlBar's hover-visibility. Matches the\n // exact path the UI uses (sets selectedAudioIndex + preferred lang).\n useEffect(() => {\n window.__AUDIO_SELECT__ = (idx: number) => {\n setSelectedAudioIndex(idx);\n const track = audioTracks[idx];\n if (track?.lang) setPreferredAudioLang(track.lang);\n };\n return () => { window.__AUDIO_SELECT__ = null; };\n }, [audioTracks, setSelectedAudioIndex, setPreferredAudioLang]);\n\n // MSE audio track swap: when selectedAudioIndex changes, dispose the\n // current audio FM/SourceBuffer and create new ones for the target track.\n // Chrome stable does NOT expose video.audioTracks, so we can't flip an\n // .enabled flag — we have to physically swap the SourceBuffer feeding\n // audio to the MediaSource. Brief audio gap (~200-2000ms) during swap.\n useEffect(() => {\n if (!useMseAudio || !mseReady) return;\n if (selectedAudioIndex < 0 || selectedAudioIndex >= mseAudioTracks.length) return;\n if (selectedAudioIndex === activeAudioTrackIdxRef.current) return;\n if (audioSwapInFlightRef.current) {\n // Latest-wins: a prior swap is mid-flight. The state will reach the\n // user's intent once the in-flight swap completes — re-running this\n // effect at that point would race. Skip; selectedAudioIndex still\n // points at the target so when the in-flight swap finishes the\n // activeAudioTrackIdxRef will differ from selectedAudioIndex and we\n // re-fire on the next render. (React's effect ordering ensures this.)\n debug('audio', 'MSE audio swap requested while another in flight — deferring');\n return;\n }\n const ms = mediaSourceRef.current;\n if (!ms || ms.readyState !== 'open') {\n warn('audio', `MSE audio swap skipped — MediaSource readyState=${ms?.readyState}`);\n return;\n }\n const v = videoRef.current;\n if (!v) return;\n\n const targetIdx = selectedAudioIndex;\n const targetTrack = mseAudioTracks[targetIdx];\n const currentEpoch = mseEpochRef.current;\n audioSwapInFlightRef.current = true;\n const playheadMs = Math.round(v.currentTime * 1000);\n debug('audio', `MSE audio swap START idx=${activeAudioTrackIdxRef.current}→${targetIdx} lang=${targetTrack.lang} playheadMs=${playheadMs}`);\n\n (async () => {\n try {\n // We REUSE the same SourceBuffer instead of remove+add. Chrome\n // enforces a per-MediaSource quota on SourceBuffers (1 video + 1\n // audio, effectively) and does NOT fully free the slot after\n // removeSourceBuffer, so addSourceBuffer throws QuotaExceededError.\n // Instead: abort + clear the existing SB, then feed it a new init\n // segment + fragments for the new track. Same codec (AAC-LC) for\n // all v3 audio tracks, so reusing the SB is legal.\n const oldFm = audioFmsRef.current[0];\n const sb = audioSbsRef.current[0];\n if (!sb) {\n warn('audio', 'swap skipped — no audio SourceBuffer ref');\n return;\n }\n if (oldFm) oldFm.dispose();\n\n // Abort any in-flight append, then clear all buffered audio data.\n // SB ownership now moves to the new FM; we intentionally don't\n // dispose the SB itself.\n try {\n if (sb.updating) sb.abort();\n // sb.abort() puts sb in a valid state; now clear all content\n if (ms.readyState === 'open') {\n const waitRemove = new Promise<void>(resolve => {\n const onEnd = () => { sb.removeEventListener('updateend', onEnd); resolve(); };\n sb.addEventListener('updateend', onEnd, { once: true });\n setTimeout(resolve, 2000);\n });\n sb.remove(0, Infinity);\n await waitRemove;\n }\n } catch (e) {\n warn('audio', 'SB clear failed during swap', { error: (e as Error)?.message });\n }\n\n if (ms.readyState !== 'open') {\n warn('audio', 'MSE audio swap aborted mid-flight — MS no longer open');\n return;\n }\n\n // Attach a fresh FragmentManager to the same SourceBuffer and point\n // it at the new track. fetchInit will append the new track's init\n // segment; ensureBuffered pulls fragments around the playhead.\n const newFm = new FragmentManager(handleBufferingChange,\n { suppressEndOfStream: true, label: `audio:${targetTrack.track_id}` });\n newFm.setSourceBuffer(sb, ms);\n const audioBase = mediaUrl.replace(new RegExp(`/${FRAGMENT_MP4_FILENAME.replace(/\\./g, '\\\\.')}$`), '');\n const audioUrl = `${audioBase}/${targetTrack.path}`.replace(/\\\\/g, '/');\n newFm.setIndex(\n targetTrack.fragments,\n audioUrl,\n targetTrack.init_segment,\n `${mediaId}::audio::${targetTrack.track_id}`,\n deliveryFor(targetTrack.track_id),\n );\n newFm.epoch = currentEpoch;\n // Mutate in-place: audioFmsRef.current array is the SAME reference\n // published on window.__AUDIO_FRAGMENT_MANAGERS__, so reassigning\n // would orphan the window pointer and tests would read a stale FM.\n audioFmsRef.current.length = 0;\n audioFmsRef.current.push(newFm);\n // audioSbsRef.current unchanged (same SB)\n activeAudioTrackIdxRef.current = targetIdx;\n\n // A room seek arriving while this load waits on the new track's init\n // segment takes the FM over and buffers its own target; this load must\n // then stop, not resume and fetch the position the room left. Without\n // that, a swap 80ms before a seek left a 2.01-4.01s island beside the\n // seek's 298.79s range for ~37s (audio-swap-seek.spec), or — when the\n // seek's eviction removed what had landed — waited out a 60s timeout\n // and reported a failed swap while the audio was fine (DO fleet,\n // 2026-10-20). The signal must be linked AFTER ensureBuffered starts:\n // the load's own abort runs synchronously and would otherwise trip it.\n //\n // Load where the playhead is NOW, not where it was when the swap began:\n // clearing the old buffer above is an await, and a seek landing in it\n // has already moved the room (its audio work went to the FM this swap\n // disposed). A seek still in flight has not moved currentTime yet, so\n // its target wins.\n const loadMs = coordinatorRef.current?.inFlightTargetMs ?? Math.round(v.currentTime * 1000);\n const swapAbort = new AbortController();\n newFm.setPlayhead(loadMs);\n const swapLoad = newFm.ensureBuffered(loadMs, 'audio-swap', swapAbort.signal);\n newFm.supersededSignal.addEventListener('abort', () => swapAbort.abort(), { once: true });\n swapLoad.catch(e => {\n const cancelled = newFm.isDisposed || swapAbort.signal.aborted;\n (cancelled ? debug : warn)('audio', cancelled\n ? 'audio-swap load superseded by a seek or a later swap'\n : 'audio-swap ensureBuffered failed', { error: (e as Error)?.message });\n });\n debug('audio', `MSE audio swap DONE idx=${targetIdx} track=${targetTrack.track_id}`);\n } finally {\n audioSwapInFlightRef.current = false;\n }\n })().catch((e: unknown) => logError('audio', 'MSE audio swap failed', errorFields(e)));\n }, [useMseAudio, mseReady, selectedAudioIndex, mseAudioTracks, mediaId, mediaUrl, handleBufferingChange, deliveryFor]);\n\n usePlaybackSync(coordinatorRef, videoRef, lastAppliedPlayheadRef, mseReady, resumePlayback, pausePlayback);\n useStallRecovery(videoRef, fragmentManagerRef, coordinatorRef, mseReady, pausePlayback, audioFmsRef);\n useBackgroundPlay(videoRef, coordinatorRef, mediaId, durationMs ?? 0, mseReady, resumePlayback);\n const { stopPrefetch } = usePrefetch(videoRef, fragmentManagerRef, coordinatorRef, mseReady, setBufferState, audioFmsRef);\n usePlaylistPreBuffer(fragmentManagerRef);\n useEffect(() => { stopPrefetchRef.current = stopPrefetch; }, [stopPrefetch]);\n\n // Double-click for fullscreen\n const handleDoubleClick = useCallback(() => {\n const container = videoRef.current?.parentElement;\n if (!container) return;\n if (document.fullscreenElement) {\n document.exitFullscreen().catch((e: unknown) => warn('ui', 'exitFullscreen failed', errorFields(e)));\n } else {\n container.requestFullscreen().catch((e: unknown) => warn('ui', 'requestFullscreen failed', errorFields(e)));\n }\n }, []);\n\n // Prevent context menu on video\n const handleContextMenu = useCallback((e: MouseEvent) => {\n e.preventDefault();\n }, []);\n\n // Get current subtitle URL\n const currentSubtitleUrl = effectiveSubtitlesEnabled && subtitleTracks[selectedSubtitleIndex]\n ? subtitleTracks[selectedSubtitleIndex].url\n : null;\n\n return (\n <div\n className={`relative w-full h-full bg-black ${isFullscreen && !controlsVisible ? 'cursor-none' : ''}`}\n onMouseEnter={() => showControls(3000)}\n onMouseMove={() => showControls(3000)}\n onMouseLeave={hideControls}\n >\n <video\n ref={videoRef}\n className=\"w-full h-full\"\n playsInline\n onDoubleClick={handleDoubleClick}\n onContextMenu={handleContextMenu}\n />\n\n {/* Subtitle Overlay */}\n <SubtitleOverlay\n subtitleUrl={currentSubtitleUrl}\n videoRef={videoRef}\n style={subtitleStyle}\n enabled={effectiveSubtitlesEnabled}\n />\n \n {/* Media Title */}\n <MediaTitle mediaId={mediaId} visible={controlsVisible} />\n \n {/* Control Bar */}\n <ControlBar\n videoRef={videoRef}\n mediaId={mediaId}\n durationMs={durationMs || undefined}\n visible={controlsVisible}\n displayPlayheadMs={displayPlayheadMs}\n audioTracks={audioTracks}\n selectedAudioIndex={selectedAudioIndex}\n onAudioSelect={(idx) => {\n setSelectedAudioIndex(idx);\n // Save preferred language when user manually selects a track\n const track = audioTracks[idx];\n if (track?.lang) setPreferredAudioLang(track.lang);\n }}\n subtitleTracks={subtitleTracks}\n selectedSubtitleIndex={selectedSubtitleIndex}\n onSubtitleSelect={setSelectedSubtitleIndex}\n subtitlesEnabled={effectiveSubtitlesEnabled}\n onSubtitlesToggle={toggleSubtitles}\n subtitleStyle={subtitleStyle}\n onSubtitleStyleChange={setSubtitleStyleStore}\n onChatActivity={() => showControls(5000)}\n onMenuHeld={setControlsHeld}\n />\n \n {/* Autoplay muted bridge: the browser blocked audible autoplay, so we're\n playing muted + synced. Any interaction auto-unmutes (audioUnlock);\n this badge just makes it discoverable. */}\n {mutedForAutoplay && (\n <button\n type=\"button\"\n onClick={() => { const v = videoRef.current; if (v) v.muted = false; setMutedForAutoplay(false); }}\n className=\"autoplay-sound-badge\"\n >\n <Icon name=\"volume-high\" /> Tap for sound\n </button>\n )}\n\n {/* Buffering Overlay */}\n {bufferState.isBuffering && readiness.ready && (\n <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none\">\n <div className=\"flex flex-col items-center gap-2\">\n <div className=\"w-8 h-8 border-2 border-text border-t-transparent rounded-full animate-spin\" />\n <div className=\"px-4 py-2 rounded bg-wp-surface-2/80 text-xs font-medium\">\n Buffering…\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n","import { useMemo } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport type { GameBundleModule } from '@wolffm/rooms-protocol';\nimport MediaPlayer from '../components/MediaPlayer';\nimport StarfieldCanvas from '../components/StarfieldCanvas';\nimport { ErrorBoundary } from '../components/ErrorBoundary';\nimport { ConfigContext, getRuntimeConfig } from './config';\nimport { resolveMediaUrls } from './mediaUrls';\nimport { useAppStore } from './store';\n\n/**\n * The media player as a game bundle: the room's default activity, mounted by\n * `GameBundleSlot` through the same `mount()` as any game. Built in, not\n * served by URL, because it plays from the app's own socket and store — that\n * is what keeps it in sync — so it ignores the context. No shadow root: the\n * player's root is the fullscreen element, and `FullscreenPortal` moves the\n * room's overlays into it, where they need the app's stylesheet.\n */\nfunction PlayerActivity() {\n const mediaId = useAppStore((s) => s.snapshot.mediaId);\n const media = useMemo(() => (mediaId ? { mediaId, ...resolveMediaUrls(mediaId) } : null), [mediaId]);\n if (!media) return <StarfieldCanvas />;\n return (\n <ErrorBoundary>\n <MediaPlayer mediaUrl={media.mediaUrl} indexUrl={media.indexUrl} mediaId={media.mediaId} />\n </ErrorBoundary>\n );\n}\n\nexport const mount: GameBundleModule['mount'] = (el) => {\n const root = createRoot(el);\n root.render(\n <ConfigContext.Provider value={getRuntimeConfig()}>\n <PlayerActivity />\n </ConfigContext.Provider>,\n );\n return {\n update() {},\n // The host unmounts from a React effect cleanup, where unmounting another\n // root synchronously is a race React reports. A microtask later the\n // host's commit has finished.\n unmount() {\n queueMicrotask(() => root.unmount());\n },\n };\n};\n","import * as mediaPlayer from './mediaPlayerBundle';\nimport type { BundleActivity } from './gameBundleHost';\n\n/**\n * The room's built-in catalog entry: what the activity slot holds underneath\n * any game the leader starts. It is the media player — a `GameBundleModule`\n * like every game (`mediaPlayerBundle.tsx`), but compiled into the app rather\n * than imported by URL, so it is never missing after a deploy the way a\n * stale lazy chunk can be.\n */\nexport const DEFAULT_ACTIVITY: BundleActivity = {\n id: 'player',\n name: 'Media player',\n load: () => Promise.resolve(mediaPlayer),\n};\n","import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { OPEN_MEDIA_DRAWER_EVENT } from '../lib/openMediaDrawer';\nimport { useAppStore } from '../lib/store';\nimport { ErrorBoundary } from '../components/ErrorBoundary';\nimport { lazyWithRetry } from '../lib/lazyWithRetry';\n\nconst MediaDrawer = lazyWithRetry(() => import('../components/MediaDrawer'));\n\nimport ChatPanel from '../components/ChatPanel';\nimport GameBundleSlot from '../components/GameBundleSlot';\nimport { DrawerLoading } from '../components/DrawerShell';\nimport FloatingChat from '../components/FloatingChat';\nimport ClientStatusOverlay from '../components/ClientStatusOverlay';\nimport TierlistModal from '../components/TierlistModal';\nimport FullscreenPortal from '../components/FullscreenPortal';\nimport RoomCornerControls from '../components/RoomCornerControls';\nimport { connectAndJoin, sendAvatarChange } from '../lib/socket';\nimport { useParams } from 'react-router-dom';\nimport { debug, warn } from '../lib/debug';\nimport { loadDisplayNames } from '../lib/characters';\nimport { syncFromServerIfFirstLoad } from '../lib/preferences';\nimport { fetchRequestedSprite } from '../lib/avatar';\nimport { useAvatarChanges } from '../hooks/useAvatarChanges';\nimport { initAudioUnlock } from '../lib/audioUnlock';\nimport { probeRoomKeyType } from '../lib/roomKeyProbe';\nimport { useActiveGame } from '../hooks/useStoreSelectors';\nimport { DEFAULT_ACTIVITY } from '../lib/defaultActivity';\nimport { moduleActivity } from '../lib/gameBundleHost';\n\nexport default function Room() {\n const snapshot = useAppStore(s => s.snapshot);\n const ensureAutoIdentity = useAppStore(s => s.ensureAutoIdentity);\n const activeGame = useActiveGame();\n const activeBundle = useMemo(\n () => (activeGame ? moduleActivity(activeGame, window.location.origin) : null),\n [activeGame],\n );\n const params = useParams<{ roomKey?: string }>();\n const joinedRef = useRef(false);\n const [drawerOpen, setDrawerOpen] = useState(false);\n\n // Get roomKey from URL params\n const roomKey = params.roomKey || null;\n\n // A character picked in the fleet settings popout reaches the room live.\n useAvatarChanges();\n\n // Unlock audio context on first user interaction so socket-triggered audio.play()\n // works without a NotAllowedError (autoplay policy).\n useEffect(() => initAudioUnlock(), []);\n\n // Confirm the room key type against the server, in parallel with the\n // pre-join wait below. Until it answers, useLeaderAffordance reads the key's\n // SHAPE straight off the URL, so the presenter buttons are in the first\n // frame; this answer then supersedes the shape in both directions, and the\n // socket supersedes both (lib/leaderAffordance.ts).\n // Fire-and-forget on purpose: it must not be cancellable by this effect's\n // teardown, and nothing blocks on it.\n useEffect(() => {\n if (!roomKey) return;\n void probeRoomKeyType(roomKey);\n }, [roomKey]);\n\n // Listen for \"Select Media\" (ChatPanel's toolbar or ControlBar's fallback).\n // Using a CustomEvent keeps drawer state local to Room without lifting it into\n // Zustand for one button in a sibling component. Games no longer have a\n // button or a drawer of their own: they are the drawer's \"Play\" tab.\n useEffect(() => {\n const onMedia = () => setDrawerOpen(true);\n window.addEventListener(OPEN_MEDIA_DRAWER_EVENT, onMedia);\n return () => window.removeEventListener(OPEN_MEDIA_DRAWER_EVENT, onMedia);\n }, []);\n\n // Join room once we have a roomKey. We deliberately await three boot-time\n // promises BEFORE calling ensureAutoIdentity + connectAndJoin so the\n // first presence broadcast carries the right identity:\n //\n // loadDisplayNames() — without it, ensureAutoIdentity's fallback\n // writes \"Knight\"/\"Cleric\" (title-cased\n // sprite IDs) to localStorage and the\n // user is stuck with that name forever\n // (Bug 1b from the 2026-05-20 retro).\n // syncFromServerIfFirstLoad — pulls cross-device prefs BEFORE the join\n // payload is built (Bug 1 / 1a from the\n // same retro).\n // fetchRequestedSprite — the character the person chose in the\n // fleet settings popout (prefs-api,\n // lib/avatar.ts). A signed-out visitor\n // gets none and is auto-assigned. Late\n // (past the cap) it goes out as an\n // identity_update (sendAvatarChange).\n //\n // The first two are already in flight (entry.tsx kicks them off at boot); on\n // subsequent device loads they resolve synchronously. First-device cold\n // load adds ≤500ms. Hard timeout caps the wait so a broken prefs server\n // can't permanently block joining.\n useEffect(() => {\n if (!roomKey || joinedRef.current) return;\n\n let cancelled = false;\n const BOOT_WAIT_TIMEOUT_MS = 1500;\n void (async () => {\n try {\n await Promise.race([\n Promise.all([\n loadDisplayNames(),\n syncFromServerIfFirstLoad(),\n fetchRequestedSprite().then(sendAvatarChange),\n ]),\n new Promise<void>(r => setTimeout(r, BOOT_WAIT_TIMEOUT_MS)),\n ]);\n } catch (e) {\n warn('room', 'pre-join wait threw, proceeding anyway', { err: (e as Error)?.message });\n }\n // StrictMode double-mounts in dev: this closure's cleanup fires\n // before the await resolves on the first pass. Bail out so we\n // don't join, but DON'T toggle joinedRef — the second pass will\n // do the actual join on its own clean closure.\n if (cancelled) return;\n // Re-read store after the wait — the requested sprite and\n // rehydrateFromPrefs (entry.tsx's .then chain) may both have landed.\n const fresh = useAppStore.getState();\n debug('room', 'Joining room', { roomKey, name: fresh.name, sprite: fresh.sprite });\n ensureAutoIdentity();\n const post = useAppStore.getState();\n connectAndJoin({\n room_key: roomKey,\n name: post.name || undefined,\n sprite: post.sprite || undefined,\n });\n // Claim AFTER the successful connectAndJoin so a torn-down StrictMode\n // pre-mount can't lock out the real mount's join.\n joinedRef.current = true;\n })();\n\n return () => { cancelled = true; };\n }, [roomKey, ensureAutoIdentity]);\n\n function closeMedia() {\n debug('room', 'Closing media drawer');\n setDrawerOpen(false);\n }\n\n // If no room key, show error\n if (!roomKey) {\n warn('room', 'No room key provided');\n return (\n <div className=\"flex-1 flex items-center justify-center p-6\">\n <div className=\"text-text-secondary\">No room key provided. Go to the <a href=\"/\" className=\"underline\">home page</a> to select a room.</div>\n </div>\n );\n }\n\n return (\n <div className=\"relative flex-1 flex overflow-hidden min-h-0\">\n {/* Main content area: two layers of game bundles. Base is the room's\n default activity (lib/defaultActivity.ts); overlay is the active game,\n a GAMES `module` entry. A\n game started mid-episode leaves the episode loaded underneath (it\n pauses via leader control, not by us). A bundle is told the room's\n ID, never the URL `roomKey` (the leader's is a credential), so a\n game mounts only once the snapshot has named it. The base mounts at\n once, so the empty room shows its starfield from the first frame;\n it is our own code, and gets the room's id through update(). */}\n <div className=\"flex-1 relative bg-black overflow-hidden min-w-0\">\n <GameBundleSlot activity={DEFAULT_ACTIVITY} roomKey={snapshot.roomId ?? ''} layer=\"base\" />\n {activeBundle && snapshot.roomId && (\n <ErrorBoundary>\n <GameBundleSlot key={activeBundle.id} activity={activeBundle} roomKey={snapshot.roomId} layer=\"overlay\" />\n </ErrorBoundary>\n )}\n <FullscreenPortal>\n <FloatingChat />\n <ClientStatusOverlay />\n {/* Above a game (z-30): Stop game, Show chat, and Select Media when\n the game hides ControlBar's. */}\n <RoomCornerControls />\n {/* Portals to <body>, so it is unaffected by the chat panel's own\n visibility rules and paints over fullscreen. */}\n <TierlistModal />\n </FullscreenPortal>\n </div>\n\n {/* Chat sidebar */}\n <ChatPanel />\n\n {/* Drawers (lazy-loaded).\n\n The drawer fallbacks are gated on the drawer's own open flag: while\n the chunk is in flight React renders the fallback whether or not the\n drawer is open, and an unconditional one would throw a backdrop over\n the room at every page load. Gated, it only appears for someone who\n actually clicked — who would otherwise get no response at all until\n the chunk landed. DrawerLoading is imported statically on purpose; a\n fallback bundled into the chunk it covers can never render. */}\n <React.Suspense fallback={<DrawerLoading open={drawerOpen} onClose={closeMedia} />}>\n <MediaDrawer open={drawerOpen} onClose={closeMedia} />\n </React.Suspense>\n </div>\n );\n}\n"],"names":["ownsObjectUrl","p","ChatImageUploadError","message","uploadErrorMessage","reason","messageForUploadFailure","err","CdnError","preflightMessage","file","type","kindOfType","MAX_BYTES","imageFilesFromClipboard","dt","filesFromClipboard","imageFilesFromDrop","filesFromDrop","measureImage","dims","measureImageBytes","uploadChatImage","opts","result","getCdnClient","isChatImageKey","uploadAll","pending","out","key","uploadErrorFields","useClientRef","client","ref","useRef","useEffect","useCatalogueSearch","basePath","query","options","limit","debounceMs","SEARCH_DEBOUNCE_MS","clientRef","clientKey","items","setItems","useState","cursor","setCursor","loading","setLoading","error","setError","page","setPage","abortRef","cancelled","isFirstPage","run","controller","prev","timer","loadMore","useCallback","PREVIEW_PX","PREVIEW_GAP","PREVIEW_CHROME_PX","PREVIEW_MAX_IMG_PX","placePreview","tile","panel","viewport","left","boxHeight","top","MEME_CATALOGUE_PATH","HOVER_INTENT_MS","MemePickerPanel","onClose","onPick","onUnavailable","setQuery","panelRef","inputRef","hasMore","rows","useMemo","seen","item","preview","setPreview","hoverTimer","cancelHover","hidePreview","canHover","showPreview","immediate","place","onKey","e","onDown","pick","isEmpty","jsxs","jsx","src","resolveChatImageUrl","MemePicker","open","rest","isGameBundleModule","value","isMountedGameBundle","v","OPEN_MEDIA_DRAWER_EVENT","openMediaDrawer","source","debug","ErrorBoundary","Component","info","logError","useIsFullscreen","isFullscreen","setIsFullscreen","sync","isChatPanelHidden","s","selectMediaAffordance","chatToggleSite","pictureForAuthor","roster","author","u","InjectCosmeticsStyles","createContext","useCosmeticsStyles","inject","useContext","ensureCosmeticsStyles","wornKeyFor","useAuthorCosmetics","useAppStore","baseColour","colour","CosmeticHtml","React","html","as","className","style","a","b","useIdleWhenHidden","enabled","el","io","entry","ChatAvatar","sprite","url","alt","worn","AvatarImage","decorHtml","shouldOpenLightbox","PortalTargetContext","usePortalTarget","ChatImageLightbox","title","scoped","target","createPortal","MAX_H_PX","MAX_W_PX","Attachment","image","expanded","setExpanded","hasDims","scale","Fragment","ChatImageAttachments","images","im","i","isOwnMessage","me","formatTime","ts","ChatMessage","myClientId","myAuthor","isOwn","pictureUrl","nameColor","usePersonColor","row","textHtml","messageHtml","nameHtml","ChatLog","chat","containerRef","userScrolledUpRef","handleScroll","container","scrollHeight","scrollTop","clientHeight","distanceFromBottom","msg","showDateBreak","SLASH_COMMANDS","SLASH","getPickerQuery","text","filterCommands","commands","q","scored","cmd","name","applyCompletion","wrapIndex","index","len","resolveSubmission","trimmed","body","spaceAt","word","args","command","c","commandBlockedReason","ctx","SlashCommandPicker","activeIndex","hasMedia","onHover","listboxId","blocked","active","ChatAttachmentStrip","uploading","onRemove","useChatImageAttachments","setPending","setUploading","pushToast","liveUrls","releasePreview","addFiles","files","accepted","rejection","w","h","previewUrl","room","DEFAULT_CHAT_MAX_IMAGES","addMeme","remove","id","hit","clear","onPaste","onDrop","onDragOver","uploadPending","warn","MAX_LENGTH","DEFAULT_CHAT_MAX_LENGTH","COOLDOWN_MS","DEFAULT_CHAT_COOLDOWN_MS","ChatInput","setText","cooldown","setCooldown","selection","setSelection","dismissed","setDismissed","useId","memeOpen","setMemeOpen","memeUnavailable","setMemeUnavailable","attach","mediaId","setTierlistOpen","matches","pickerOpen","setActiveIndex","runCommand","emitRecapRequest","warmTierlistPosters","complete","handleSubmit","resolved","sendChat","handleKeyDown","onFocusRequest","remaining","showCounter","activeCmd","Icon","Roster","users","RosterRow","color","RosterButton","userCount","setOpen","handleClickOutside","resolveLeaderAffordance","leaderId","clientId","keyType","LEADER_KEY_SUFFIX_RE","guessRoomKeyType","roomKey","ROOM_PATH_RE","roomKeyFromPath","pathname","m","effectiveKeyType","probed","useIsLeader","useActiveGame","activeGameId","games","useGameCatalog","g","useLeaderAffordance","useReadiness","useSnapshot","READINESS_OVERRIDE_MS","useReadinessOverride","readiness","overrideReady","setOverrideReady","gate","setGate","t","GameSettingsSection","draw","AdminPanel","lazyWithRetry","PlaylistPanel","ChatPanel","chatCollapsed","sidebarTab","setSidebarTab","clientCount","playlistLoaded","isLeader","leaderAffordance","gameSettings","gameOverlay","activeTab","Link","leaveRoom","HadokuThemePicker","ConnectedSettings","renamed","isTheme","THEMES","resolveChromeTheme","explicit","ambient","saved","resolveGameBundleUrl","pageOrigin","isSameOriginPath","moduleActivity","href","describe","mountGameBundle","latest","mounted","disposed","failed","openSettings","settingsError","cause","closeSettings","releaseSettings","bundle","teardown","fail","update","mod","mountedCtx","raw","GameBundleSlot","activity","layer","hostRef","handleRef","ambientTheme","useHadokuThemeOptional","theme","loadPreferences","getUserId","getUserType","readCtx","useEffectEvent","reportError","reportSettingsError","load","setGameSettings","drawSettings","handle","available","current","shown","DRAWER_BTN","DrawerShell","header","footer","children","DrawerLoading","LIFETIME_MS","FADE_DURATION_MS","CC_STYLE_CLASSES","FloatingMessage","age","ccStyle","fadeStart","opacity","ccClass","FloatingChat","floatingChatEnabled","subtitleStyle","now","setNow","checkFullscreen","hasChat","shouldShow","recentMessages","cutoff","READY_STATE_CANNOT_ADVANCE","isNearEnd","LOW_BUFFER_MS","bufferHealthCanAdvance","rs","bufferHealthLevel","CRITICAL_BUFFER_MS","BUFFER_HEALTH_LABEL","_ctx","getCtx","AC","ensureRunning","envelope","t0","peak","durSec","playTone","freq","durationMs","gain","when","osc","playFM","carrierHz","modHz","modIndex","carrier","modDepth","playReadyCheckStartSound","playReadyToggleOnSound","playReadyToggleOffSound","playAllReadySound","registerDebugGlobal","NONE","diffReadyToggles","next","wasActive","isActive","before","after","on","off","RING_SIZE","RING_STROKE","RING_RADIUS","CIRCUMFERENCE","SPRITE_SIZE","isBufferingSufficient","INITIAL_BUFFER_MS","ringColor","progress","BufferRing","fading","onFadeEnd","offset","borderColor","ReadyAvatar","isReady","isSelf","onToggle","badgeFamily","badge","EMPTY_TRACK","advanceTrack","status","completed","below","entries","ClientStatusOverlay","bufferStatus","readyCheck","isPlaying","track","setTrack","handleFadeEnd","readySet","iAmReady","allReady","wasActiveRef","wasAllReadyRef","prevReadyRef","prevActiveRef","showReadyCheck","showBufferLoading","showStartButton","emitReadyCheckToggle","emitReadyCheckStart","TIER_VAR","UNRANKED","markLoaded","Card","draggable","picked","onDragStart","resolveCardImage","VoterFace","VoterCard","anchor","itemId","snap","pos","setPos","r","voterId","tier","TierlistModal","draft","saving","move","setPicked","dragOver","setDragOver","hover","setHover","savedAt","setSavedAt","tab","setTab","refreshTierlist","byZone","placed","TIER_ORDER","it","dropIndex","clientX","draggedId","cards","zone","z","cur","onSave","saveTierlist","dirty","hasVoted","showing","label","FullscreenPortal","fsEl","setFsEl","StarIcon","PlayIcon","PauseIcon","VolumeMutedIcon","VolumeLowIcon","VolumeHighIcon","FullscreenIcon","ExitFullscreenIcon","ChevronLeftIcon","ChevronRightIcon","SidebarCloseIcon","SidebarOpenIcon","CloseIcon","EjectIcon","FilmIcon","SelectMediaButton","site","useChatToggle","toggleChatCollapsed","panelHidden","ChatToggleButton","toggle","RoomCornerControls","activeGame","showSelectMedia","showStop","showChatToggle","emitGameSelect","useAvatarChanges","listenForAvatarChanges","settings","requestedSpriteOf","sendAvatarChange","onNextInteraction","cb","drain","cbs","errorFields","registered","initAudioUnlock","RETRY_DELAY_MS","parseType","attempt","origin","getRuntimeServerOrigin","res","data","probeRoomKeyType","sendControl","kind","extra","client_seq","payload","getSocket","CORE_EVENTS","sendLoad","sendHome","PlayPauseButton","snapshot","canOverride","disabled","UnloadButton","handleClick","seekDisabled","o","resolveSeekValue","seekProgress","valueMs","ms","totalSecs","hours","mins","secs","SeekBar","propDurationMs","displayPlayheadMs","hoverPosition","setHoverPosition","hoverTimeMs","setHoverTimeMs","isDragging","setIsDragging","localValue","setLocalValue","lastSentRef","prevMediaId","setPrevMediaId","actualPlayheadMs","val","localValueSetAt","handleMouseMove","rect","x","ratio","timeMs","handleMouseLeave","to_ms","VolumeSlider","videoRef","volume","muted","setVolumeStore","setMutedStore","video","handleVolumeChange","newVolume","toggleMute","volumeIcon","currentVolume","volumePercent","AudioTrackSelector","tracks","selectedIndex","onSelect","single","SubtitleSelector","STYLE_OPTIONS","SubtitleStyleSelector","onStyleChange","opt","FullscreenButton","handleFullscreenChange","toggleFullscreen","ControlBar","parentVisible","audioTracks","selectedAudioIndex","onAudioSelect","subtitleTracks","selectedSubtitleIndex","onSubtitleSelect","subtitlesEnabled","onSubtitlesToggle","onSubtitleStyleChange","onChatActivity","onMenuHeld","scrollRef","chatInputRef","canScrollLeft","setCanScrollLeft","canScrollRight","setCanScrollRight","chatText","setChatText","chatCooldown","setChatCooldown","handleChatKeyDown","visible","updateScrollState","scrollLeft","scrollWidth","clientWidth","resizeObserver","scrollBy","direction","amount","barClasses","showScrollButtons","USER_PAUSE_WINDOW_MS","shouldKeepAlive","mediaSessionAction","action","BG_RESYNC_TOLERANCE_MS","resyncPlan","roomPlayheadNow","receivedAt","mediaDisplayName","parts","artworkCandidates","MediaTitle","MAX_RETRIES","RETRY_CAP_MS","SB_TOLERANCE","GAP_SKIP_EDGE_SEC","GAP_SKIP_MAX_MS","getRetryDelay","baseMs","capMs","jitter","base","jitterMs","NEARBY_FRAGMENT_THRESHOLD","PENDING_SEEK_PROTECT_MS","SEEK_LANDED_TOLERANCE_MS","MAX_DOWNLOAD_SAMPLES","findFragmentIndex","fragments","lo","hi","mid","findKeyframeIndex","idx","findContainingRangeIndex","buffered","timeSec","tolerance","fragmentOverlapsSB","startMs","endMs","startSec","endSec","formatBufferedRanges","_","getAvgDownloadRate","samples","d","REFERENCE_FRAGMENT_BYTES","TIMEOUT_SAFETY_FACTOR","getAdaptiveTimeout","avgRateBytesPerSec","estimatedMs","findKeyframePredecessor","gapOverlapsPendingFetch","gapStartSec","gapEndSec","inFlightIndices","gapStartMs","gapEndMs","f","parseVtt","vttText","cues","lines","line","timeParts","startTime","parseTimestamp","rightSide","endTime","linePercent","positionPercent","align","si","setting","pct","textLines","minutes","seconds","cleanSubtitleText","STYLE_CLASSES","SubtitleOverlay","subtitleUrl","setCues","activeCues","setActiveCues","bottomOffset","setBottomOffset","fontSize","setFontSize","prevSubtitleUrl","setPrevSubtitleUrl","MAX_RETRY","parsed","retryMs","resolve","updateActiveCues","currentTime","cue","handleTimeUpdate","hasActiveCues","calculateOffset","containerHeight","containerWidth","videoWidth","videoHeight","displayedHeight","letterbox","baseFontPx","margin","bucketFor","horiz","buckets","renderGroup","group","alignClass","textAlignClass","renderRow","leftCues","centerCues","rightCues","verticalStyle","centerRef","MAX_SAMPLES","MAX_TIMEOUT_RECORDS","MAX_SEEK_RECORDS","MAX_STALL_RECORDS","MAX_EVICT_RECORDS","HIGH_AUDIO_DRIFT_MS","sorted","makeStats","values","sum","acc","cap","arr","max","PerfCollector","bytes","_index","queueDepth","start","timeoutMs","caller","targetMs","driftMs","currentMs","aheadMs","end","fragmentCount","survivingRanges","fetchDurations","fetchThroughputs","appendLatencies","appendDepths","seekDurations","stallDurations","callerMap","seekByCaller","durations","perf","PlaylistFragmentCache","mc","buf","startIdx","maxMs","playlistFragmentCache","BEHIND_KEEP_SEC","EVICT_STUCK_MS","EvictionStrategy","deps","heldMs","sb","targetSec","rangeEnd","forwardThreshold","BUFFER_AHEAD_MS","rangeStart","prevEnd","sec","pendingMs","pendingSec","trimEnd","isBehind","isFarAhead","gapAfter","isOrphanedGap","isForwardOrphan","st","FragmentManager","INIT_DIGEST_TRACK_COUNT","onBufferingChange","expectedBytes","signal","isFragmentDeliveryEnabled","buildFragmentUrl","ttfbMs","mediaUrl","initSegment","delivery","patch","existing","appendedIdx","wasEvictionUpdateend","start_byte","end_byte","expectedMediaId","expectedMediaUrl","expectedSb","stale","cached","viaFrag","reject","onEnd","onErr","MAX_INIT_FETCH_RETRIES","INIT_RETRY_DELAY_MS","frag","inSb","pre","ri","lastFragEndSec","mediaDurationSec","lastRangeWidth","minWidth","dc","kf","unfillableGaps","playheadSec","gapStart","gapEnd","signature","STABLE_GAP_DWELL_MS","still","evicted","job","minimal","abort","ti","winEnd","kfIdx","kfFrag","kfInSb","toFetch","skipped","MAX_CONCURRENT_JOBS","indices","concurrency","batch","promises","ac","fragTimeRange","fragBytes","promise","expectedLen","urlAtStart","elapsedFr","fetchedFromUrl","cfStatus","expected","elapsed","timeoutDuration","queuedOrAppending","headIndex","dropped","toKeep","itemFrag","buffer","blockers","APPEND_HOLD_MAX_MS","MAX_FETCH_ERROR_CYCLES","urgentMs","fi","fm","ERROR_RESET_AGE_MS","launched","pDead","stateCounts","jobStates","queueIndices","pendingIndices","waiterIndices","j","loadMs","initMs","frags","loaded","playheadMs","b2","n","maxGapMs","from","aheadInRange","nextStart","END_OF_STREAM_RETRY_INTERVAL_MS","autoSubtitleDefault","selectedAudioLang","hasEngSub","resolveSubtitlesEnabled","HISTORY_KEY","FLAG_KEY","WINDOW_MS","MAX_RELOADS","recentReloads","attemptAutoReload","recent","beaconLog","reloadFlagTs","clearReloadFlag","getMediaAutoplayPolicy","nav","decideAutoplayPlan","policy","userWantsMuted","playRidingAutoplay","plan","enterMutedBridge","SeekPriority","waitForSeeked","onSeeked","SeekCoordinator","request","pauseSeekResume","executeStart","wasPlaying","audioFms","afm","bufferStart","seekSec","actualMs","totalMs","currentFm","currentAudioFms","elapsedMs","resolution","bufState","videoFm","videoNeedsBuffer","tasks","reportInterval","startHealthReporting","reportHealth","DEFAULT_HEALTH_BROADCAST_INTERVAL_MS","stopHealthReporting","socket","store","bufferState","bufferedEndMs","audioAheadMs","ahead","videoAheadMs","worstAheadMs","avgRtt","currentDeliveryArm","pageState","currentPongAgeMs","conn","RELEASE_HIDE_DELAY_MS","useControlsVisibility","controlsVisible","setControlsVisible","hideTimeoutRef","heldRef","clearHideTimeout","showControls","hideDelay","hideControls","setControlsHeld","held","handleResize","NO_FRAGMENTS","NO_TRACKS","NOTHING_LOADED","indexFor","indexUrl","getResourceTiming","useIndexJson","setLoaded","fetchIndex","MAX_BACKOFF_MS","headerMs","downloadMs","parseMs","rt","delay","EMPTY_STATE","useMediaTracks","trackState","setTrackState","config","useConfig","preferredAudioLang","fetchContext","getServerOrigin","resolveUrl","audio","subs","preferredIdx","audioIdx","engSubIdx","subIdx","fields","setSelectedAudioIndex","setSelectedSubtitleIndex","ADVANCE_PROGRESS_MS","classifyAdvance","STUCK_RELOAD_MS","ADVANCE_STALL_MS","useStallRecovery","fragmentManagerRef","coordinatorRef","mseReady","pausePlayback","audioFragmentManagersRef","trackDiags","recoverAudio","pause","stallTimerRef","autoRePausedRef","stallStartMsRef","stuckReloadTimerRef","stuckArmCurrentTimeRef","advanceTimerRef","advanceArmCurrentTimeRef","silentStallStartRef","clearAdvanceWatchdog","armAdvanceWatchdog","vv","handleAdvanceStalled","coord","verdict","frozenMs","gapSkipTarget","handleWaiting","shouldPlay","serverMsAtStall","aheadAtStall","gap","stNow","stillLeader","seekActive","LEADER_STALL_PAUSE_THRESHOLD_MS","playing","fm2","advancedSec","handlePlaying","stallDurationMs","st2","serverMsAtRecovery","clientMs","handlePause","PREFETCH_OVERDUE_FACTOR","usePrefetch","setBufferState","prefetchIntervalRef","detachTimeUpdateRef","stopPrefetch","tickAudio","afms","lastTickAt","tick","bufferedEnd","DEFAULT_PREFETCH_INTERVAL_MS","onTimeUpdate","usePlaybackSync","lastAppliedPlayheadRef","resumePlayback","loadGeneration","lastLoadGenRef","resume","coordinator","lastApplied","genChanged","DEFAULT_DRIFT_SOFT_MS","isPaused","sleep","usePlaylistPreBuffer","fmRef","playlist","currentMediaId","playlistId","readPlaylist","vi","resolveMediaUrls","FETCH_INTERVAL_MS","waitAndYield","indexData","init_segment","endIdx","initAlreadyCached","mediaAlreadyCached","BACKGROUND_FETCH_PRIORITY_MS","LABEL","isLeaderNow","firstLoadingImage","urls","img","useBackgroundPlay","localPauseRef","userPausedAtRef","resync","elementMs","roomMs","onPause","onVisibility","setMeta","artwork","resolveMediaAsset","press","handlers","handler","updatePosition","dur","onPlay","lock","requesting","release","l","PHANTOM_BUFFER_MS","MAX_INIT_RETRIES","MAX_MSE_RECREATES","logInitializeThrow","MediaPlayer","setMseReady","mseCodec","mseAudioTracks","contentToken","deliveryFor","trackId","baseUrl","FRAGMENT_MP4_FILENAME","useMseAudio","emittedRef","initSucceededRef","sourceBufferRef","mediaSourceRef","initRetryRef","mseRecreateRef","mseRecreateMediaRef","mseRecreateNonce","setMseRecreateNonce","audioFmsRef","audioSbsRef","activeAudioTrackIdxRef","audioSwapInFlightRef","stopPrefetchRef","mseEpochRef","mediaPrefs","mutedForAutoplay","setMutedForAutoplay","setSubtitlesEnabled","setSubtitleStyleStore","setPreferredAudioLang","subtitleOverride","setSubtitleOverride","effectiveSubtitlesEnabled","toggleSubtitles","setDisplayPlayheadMs","handleBufferingChange","isBuffering","recreateMediaSource","initializePlayback","initStart","audioInitPromises","GATE_TIMEOUT","DEFAULT_BUFFER_GATE_TIMEOUT_MS","audioGatePromises","reached","initialBufferMs","digestReason","emitClientReady","seekMs","bufStart","latestSnap","state","correctedMs","lateState","lateElapsed","lateCorrectedMs","seekTarget","corrErr","errMsg","sbFull","initFailed","readPipelineInputs","reloadTs","objectUrl","epoch","handleVideoError","mediaError","audioBase","onOpen","mimeType","videoError","initIdx","audioMime","audioSb","audioFm","audioUrl","indexFragments","targetIdx","targetTrack","currentEpoch","oldFm","waitRemove","newFm","swapAbort","swapLoad","handleDoubleClick","handleContextMenu","currentSubtitleUrl","PlayerActivity","media","StarfieldCanvas","mount","root","createRoot","ConfigContext","getRuntimeConfig","DEFAULT_ACTIVITY","mediaPlayer","MediaDrawer","Room","ensureAutoIdentity","activeBundle","params","useParams","joinedRef","drawerOpen","setDrawerOpen","onMedia","BOOT_WAIT_TIMEOUT_MS","loadDisplayNames","syncFromServerIfFirstLoad","fetchRequestedSprite","fresh","post","connectAndJoin","closeMedia"],"mappings":"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","x_google_ignoreList":[1]}