{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9818-7e33-bf50-62ff28d3df59","file":"B9SyWrH4.mjs","mappings":"qTAqCO,SAASA,EAAeC,EAAwBC,EAAgC,CACtF,IAAIC,EAAOD,EACX,MAAME,EAAY,IAAMD,EAAK,WAAa,KAE1C,IAAIE,EAAwB,UACxBC,EAAQ,GACRC,EAA8C,KAElD,SAASC,EAAKC,EAAsBC,EAA+B,CAC9DD,IAASJ,IACbA,EAAQI,EACRN,EAAK,QAAQM,EAAMC,CAAM,EAC1B,CAEA,SAASC,GAAa,CACjBJ,IAAU,OACb,aAAaA,CAAK,EAClBA,EAAQ,KAEV,CAGA,SAASK,GAAW,CACnBD,EAAA,EACI,CAAAL,IACJC,EAAQ,WAAW,IAAM,CACxB,GAAI,CAAAD,EAMJ,IAAIL,EAAK,YAAc,EAAuB,CAC7CY,EAAA,EACA,MACD,CACAL,EAAK,QAAS,SAAS,EACxB,EAAGJ,GAAW,EACf,CAEA,SAASS,GAAU,CAClBP,EAAQ,GACRK,EAAA,EACAH,EAAK,OAAO,CACb,CAEA,SAASM,GAAU,CAClBH,EAAA,EACAH,EAAK,QAAS,OAAO,CACtB,CAIA,SAASO,GAAU,CACbT,GAAOM,EAAA,CACb,CAGA,SAASI,GAAc,CACtBV,EAAQ,GACRE,EAAK,SAAS,EACdI,EAAA,CACD,CAEA,OAAAX,EAAK,iBAAiB,YAAae,CAAW,EAC9Cf,EAAK,iBAAiB,UAAWY,CAAO,EACxCZ,EAAK,iBAAiB,iBAAkBY,CAAO,EAC/CZ,EAAK,iBAAiB,aAAcY,CAAO,EAC3CZ,EAAK,iBAAiB,UAAWY,CAAO,EACxCZ,EAAK,iBAAiB,QAASa,CAAO,EACtCb,EAAK,iBAAiB,UAAWc,CAAO,EACxCd,EAAK,iBAAiB,UAAWc,CAAO,EACxCd,EAAK,iBAAiB,UAAWc,CAAO,EAGpCd,EAAK,MAAOa,EAAA,EACPb,EAAK,YAAc,EAA0BY,EAAA,EACjDD,EAAA,EAEE,CACN,OAAOH,EAA6B,CACnCN,EAAOM,CACR,EACA,SAAU,CACTE,EAAA,EACAV,EAAK,oBAAoB,YAAae,CAAW,EACjDf,EAAK,oBAAoB,UAAWY,CAAO,EAC3CZ,EAAK,oBAAoB,iBAAkBY,CAAO,EAClDZ,EAAK,oBAAoB,aAAcY,CAAO,EAC9CZ,EAAK,oBAAoB,UAAWY,CAAO,EAC3CZ,EAAK,oBAAoB,QAASa,CAAO,EACzCb,EAAK,oBAAoB,UAAWc,CAAO,EAC3Cd,EAAK,oBAAoB,UAAWc,CAAO,EAC3Cd,EAAK,oBAAoB,UAAWc,CAAO,CAC5C,CAAA,CAEF","names":["videoLoadGuard","node","options","opts","timeoutMs","state","ready","timer","emit","next","reason","clearTimer","armTimer","onReady","onError","onStall","onLoadStart"],"sources":["../../../../../../src/lib/actions/video-load-guard.ts"],"sourcesContent":["/**\n * RAT: guard against the \"spins forever\" video failure.\n *\n * Both in-app lightbox players (chat `MediaLightbox`, paid-album viewer) used to\n * react only to the `<video>` `error` event, which fires on a hard load error\n * but NOT when a source stalls mid-fetch or a signed URL 403s silently — so a\n * dead / stalled clip just sat on the native buffering spinner with no error and\n * no way to retry. Two session recordings (chat + a paid album preview) caught\n * exactly this.\n *\n * This action watches the pre-ready phase of a `<video>` and derives one of\n * three states — `loading`, `ready`, `error` — reporting each transition through\n * `onState`. It flips to `error` on either a hard `error` event OR a load timeout\n * (the clip never reached a playable state within `timeoutMs`), which is the case\n * the bare `error` listener missed. `stalled` / `suspend` / `waiting` while still\n * loading re-arm the timeout so a stall that never recovers eventually surfaces\n * as an error instead of an endless spinner.\n *\n * Once the clip has played, native buffering handles rebuffers — the guard only\n * covers getting to first-playable, so a brief mid-playback rebuffer doesn't get\n * misreported as a failure. Calling `node.load()` (or swapping `src`, which fires\n * `loadstart`) resets the guard back to `loading` — that's how the retry button\n * re-arms it.\n */\n\nexport type VideoLoadState = 'loading' | 'ready' | 'error';\n\n/** Why the guard flipped to `error` — surfaced so callers can instrument it. */\nexport type VideoLoadErrorReason = 'error' | 'timeout';\n\nexport interface VideoLoadGuardOptions {\n\t/** Fired on every state transition. `reason` is set only for `error`. */\n\tonState: (state: VideoLoadState, reason?: VideoLoadErrorReason) => void;\n\t/** ms to wait for a playable state before treating the load as dead. */\n\ttimeoutMs?: number;\n}\n\nexport function videoLoadGuard(node: HTMLVideoElement, options: VideoLoadGuardOptions) {\n\tlet opts = options;\n\tconst timeoutMs = () => opts.timeoutMs ?? 15000;\n\n\tlet state: VideoLoadState = 'loading';\n\tlet ready = false;\n\tlet timer: ReturnType<typeof setTimeout> | null = null;\n\n\tfunction emit(next: VideoLoadState, reason?: VideoLoadErrorReason) {\n\t\tif (next === state) return;\n\t\tstate = next;\n\t\topts.onState(next, reason);\n\t}\n\n\tfunction clearTimer() {\n\t\tif (timer !== null) {\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = null;\n\t\t}\n\t}\n\n\t// Only guard the pre-ready phase; once playable we defer to native buffering.\n\tfunction armTimer() {\n\t\tclearTimer();\n\t\tif (ready) return;\n\t\ttimer = setTimeout(() => {\n\t\t\tif (ready) return;\n\t\t\t// iOS Safari Low Power Mode blocks muted autoplay and demotes preload\n\t\t\t// to metadata — none of the playable events fire on a perfectly good\n\t\t\t// clip. If metadata arrived, the URL and network are fine: report\n\t\t\t// ready (poster + controls are up, playback starts on tap) instead of\n\t\t\t// slamming a false error overlay over a working player.\n\t\t\tif (node.readyState >= 1 /* HAVE_METADATA */) {\n\t\t\t\tonReady();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\temit('error', 'timeout');\n\t\t}, timeoutMs());\n\t}\n\n\tfunction onReady() {\n\t\tready = true;\n\t\tclearTimer();\n\t\temit('ready');\n\t}\n\n\tfunction onError() {\n\t\tclearTimer();\n\t\temit('error', 'error');\n\t}\n\n\t// Buffering / stall events restart the countdown so a source that stalls\n\t// before first-playable flips to error instead of spinning forever.\n\tfunction onStall() {\n\t\tif (!ready) armTimer();\n\t}\n\n\t// A fresh load (src swap or explicit node.load()) resets us to loading.\n\tfunction onLoadStart() {\n\t\tready = false;\n\t\temit('loading');\n\t\tarmTimer();\n\t}\n\n\tnode.addEventListener('loadstart', onLoadStart);\n\tnode.addEventListener('canplay', onReady);\n\tnode.addEventListener('canplaythrough', onReady);\n\tnode.addEventListener('loadeddata', onReady);\n\tnode.addEventListener('playing', onReady);\n\tnode.addEventListener('error', onError);\n\tnode.addEventListener('stalled', onStall);\n\tnode.addEventListener('suspend', onStall);\n\tnode.addEventListener('waiting', onStall);\n\n\t// The src may already be set (and even resolved) before the action runs.\n\tif (node.error) onError();\n\telse if (node.readyState >= 3 /* HAVE_FUTURE_DATA */) onReady();\n\telse armTimer();\n\n\treturn {\n\t\tupdate(next: VideoLoadGuardOptions) {\n\t\t\topts = next;\n\t\t},\n\t\tdestroy() {\n\t\t\tclearTimer();\n\t\t\tnode.removeEventListener('loadstart', onLoadStart);\n\t\t\tnode.removeEventListener('canplay', onReady);\n\t\t\tnode.removeEventListener('canplaythrough', onReady);\n\t\t\tnode.removeEventListener('loadeddata', onReady);\n\t\t\tnode.removeEventListener('playing', onReady);\n\t\t\tnode.removeEventListener('error', onError);\n\t\t\tnode.removeEventListener('stalled', onStall);\n\t\t\tnode.removeEventListener('suspend', onStall);\n\t\t\tnode.removeEventListener('waiting', onStall);\n\t\t}\n\t};\n}\n"],"version":3}