@ssgc/hls-player 0.1.1
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.
- package/README.md +169 -0
- package/dist/HlsPlayer.d.ts +96 -0
- package/dist/components/GoLiveButton.d.ts +15 -0
- package/dist/components/PlaybackControls.d.ts +11 -0
- package/dist/components/SpeedMenu.d.ts +14 -0
- package/dist/hooks/useClickOutside.d.ts +2 -0
- package/dist/hooks/useHlsEngine.d.ts +23 -0
- package/dist/hooks/useLiveEdge.d.ts +18 -0
- package/dist/hooks/usePlaybackSpeed.d.ts +23 -0
- package/dist/hooks/useRecordingSearch.d.ts +22 -0
- package/dist/hooks/useVideoPlaybackState.d.ts +9 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.es.js +936 -0
- package/dist/index.es.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/utils/hlsPlayerUtils.d.ts +30 -0
- package/dist/utils/hlsPlaylist.d.ts +37 -0
- package/package.json +73 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/components/GoLiveButton.tsx","../src/utils/hlsPlayerUtils.ts","../src/components/PlaybackControls.tsx","../src/components/SpeedMenu.tsx","../src/hooks/useHlsEngine.ts","../src/hooks/useLiveEdge.ts","../src/hooks/useClickOutside.ts","../src/hooks/usePlaybackSpeed.ts","../src/utils/hlsPlaylist.ts","../src/hooks/useRecordingSearch.ts","../src/hooks/useVideoPlaybackState.ts","../src/HlsPlayer.tsx"],"sourcesContent":["// The Go Live button.\n//\n// It knows nothing about how going live works. The player decides whether the\n// button should be enabled and what the hover text says; this just draws it.\n\ninterface GoLiveButtonProps {\n enabled: boolean;\n label: string;\n /** Hover text explaining the current state. */\n hint: string;\n /**\n * True when the stream is already live and this button is the way back to\n * the recording. It is drawn plainly then, because red with a dot reads as\n * \"you are live\" rather than as a way out of it.\n */\n live?: boolean;\n onGoLive: () => void;\n}\n\nexport default function GoLiveButton({\n enabled,\n label,\n hint,\n live = false,\n onGoLive,\n}: Readonly<GoLiveButtonProps>) {\n const accented = enabled && !live;\n return (\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation();\n onGoLive();\n }}\n onMouseDown={(event) => event.stopPropagation()}\n disabled={!enabled}\n title={hint}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"5px 10px\",\n background: accented ? \"rgba(211,47,47,0.85)\" : \"rgba(0,0,0,0.55)\",\n color: enabled ? \"#fff\" : \"rgba(255,255,255,0.45)\",\n border: \"1px solid rgba(255,255,255,0.22)\",\n borderRadius: 6,\n cursor: enabled ? \"pointer\" : \"default\",\n fontSize: 12,\n fontWeight: 600,\n letterSpacing: \"0.02em\",\n whiteSpace: \"nowrap\",\n backdropFilter: \"blur(4px)\",\n WebkitBackdropFilter: \"blur(4px)\",\n }}\n >\n {!live && (\n <span\n style={{\n width: 7,\n height: 7,\n borderRadius: \"50%\",\n background: enabled ? \"#fff\" : \"rgba(255,255,255,0.45)\",\n }}\n />\n )}\n {label}\n </button>\n );\n}\n","// Small helper functions used by the player.\n//\n// Nothing here touches React. These are plain calculations and checks: how\n// much video to keep buffered at each speed, how far ahead the browser has\n// already downloaded, how to jump to the newest moment of a live stream, and\n// how to tell whether the browser is really Safari.\n\n/** The speeds the menu offers. */\nexport const SPEED_OPTIONS = [1, 2, 3, 5, 10];\n\n// Buffer scaling\n// At Nx speed the player consumes buffered video N times faster than real\n// time, so the network+decode pipeline has 1/N as long to keep the buffer\n// fed. Scaling the buffer target by the selected rate keeps the same\n// real-time lookahead window instead of it shrinking to base/N. Capped to\n// avoid unbounded memory use.\nconst BASE_BUFFER_SECONDS = 30;\nconst MAX_SCALED_BUFFER_SECONDS = 600;\n\nexport function getScaledBufferSeconds(rate: number): number {\n return Math.min(BASE_BUFFER_SECONDS * rate, MAX_SCALED_BUFFER_SECONDS);\n}\n\n// HLS.js also caps buffering by total byte size (maxBufferSize, default\n// 60MB) *independently* of maxBufferLength — whichever limit is hit first\n// stops fragment loading. At high playback rates the time-based target\n// scales up, but the byte-size cap does not, so it becomes the real\n// bottleneck for higher-bitrate streams. Scale it the same way.\nconst BASE_MAX_BUFFER_SIZE_BYTES = 60 * 1000 * 1000; // hls.js default\nconst MAX_SCALED_BUFFER_SIZE_BYTES = 600 * 1000 * 1000; // memory tradeoff cap\n\nexport function getScaledBufferSizeBytes(rate: number): number {\n return Math.min(BASE_MAX_BUFFER_SIZE_BYTES * rate, MAX_SCALED_BUFFER_SIZE_BYTES);\n}\n\n// ── Jump-scan (fast-forward simulation for Chromium/HLS.js)\n// `video.playbackRate` doesn't make the decoder skip frames — it still\n// decodes every frame, just tries to present them faster. At 5x/10x that\n// means sustaining 5-10x the stream's native decode rate, which routinely\n// outruns Chromium's MSE decode pipeline (hardware or software), so\n// playback stalls even though segments keep downloading fine. Safari's\n// native AVPlayer path doesn't hit this — it isn't used there.\n//\n// Fix: play continuously at a rate the decoder CAN sustain, and periodically\n// hard-seek forward to make up the rest of the target speed. Each seek\n// resumes decode fresh from the nearest keyframe instead of grinding\n// through a backlog, sidestepping the stall. Same technique DVR/VMS players\n// use for fast-forward beyond native decode limits.\nexport const JUMP_SCAN_THRESHOLD = 5; // speeds at/above this use jump-scan on HLS.js\nexport const JUMP_SCAN_BASE_RATE = 2; // continuous rate confirmed stable on Chromium\nexport const JUMP_SCAN_INTERVAL_MS = 250;\n\n/** How many seconds of video after the current position are already loaded. */\nexport function getBufferedAhead(video: HTMLVideoElement): number {\n const at = video.currentTime;\n for (let i = 0; i < video.buffered.length; i += 1) {\n if (video.buffered.start(i) <= at + 0.01 && at <= video.buffered.end(i)) {\n return Math.max(0, video.buffered.end(i) - at);\n }\n }\n return 0;\n}\n\n/** The newest moment the stream can play, in seconds. Null if not known yet. */\nexport function getLiveEdge(video: HTMLVideoElement): number | null {\n if (video.seekable.length === 0) {\n return null;\n }\n return video.seekable.end(video.seekable.length - 1);\n}\n\n/**\n * How far behind the newest moment the viewer currently is, in seconds.\n * Null when the stream has not reported a seekable range yet.\n */\nexport function getSecondsBehindLive(video: HTMLVideoElement): number | null {\n const edge = getLiveEdge(video);\n return edge === null ? null : Math.max(0, edge - video.currentTime);\n}\n\n/** Only call for confirmed live streams — never for a recorded clip. */\nexport function jumpToLiveEdge(video: HTMLVideoElement): void {\n const edge = getLiveEdge(video);\n if (edge !== null) {\n video.currentTime = Math.max(0, edge - 1);\n }\n}\n\n// Genuine native HLS (.m3u8) decode only exists on real Safari/WebKit —\n// Chromium-based browsers never support it, regardless of what\n// canPlayType('application/vnd.apple.mpegurl') reports. That check alone\n// can't be trusted: some Chrome extensions/enterprise policies patch it to\n// report support that isn't backed by a real decoder, which sends playback\n// down the native branch only to fail on every attempt (MediaError code 4)\n// until the repeated-error fallback finally switches to HLS.js — a real,\n// user-visible multi-second freeze. Gating on a real Safari check avoids\n// ever taking that broken path in the first place.\nexport function isSafariBrowser(): boolean {\n return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n}\n\n/** Turn seconds into m:ss for the control bar. */\nexport function formatTime(seconds: number): string {\n if (!seconds || !isFinite(seconds) || seconds <= 0) {\n return \"0:00\";\n }\n const whole = Math.floor(seconds);\n const minutes = Math.floor(whole / 60);\n const rest = whole % 60;\n return `${minutes}:${rest.toString().padStart(2, \"0\")}`;\n}\n\n/**\n * Add a throwaway fragment to a URL so re-setting it counts as a change.\n *\n * Reloading after a failure means feeding the player the same address again,\n * which React would otherwise treat as \"nothing changed\" and skip.\n */\nexport function withReloadMarker(url: string): string {\n return `${url.replace(/#reload\\d*$/, \"\")}#reload${Date.now()}`;\n}\n\n/** Strip the marker above before handing the URL to the player. */\nexport function withoutReloadMarker(url: string): string {\n return url.replace(/#reload\\d*$/, \"\");\n}\n","// The bar along the bottom of the video: play/pause, a seek bar and the time.\n// The browser's own controls are switched off\n\nimport { Pause, Play } from \"lucide-react\";\nimport {\n useEffect,\n useRef,\n useState,\n type ReactNode,\n type RefObject,\n} from \"react\";\n\nimport { formatTime } from \"../utils/hlsPlayerUtils\";\n\n// Row widths, in pixels, below which the clock gives up parts of itself. A\n// player dropped into a pane can be far narrower than a full-page one, and\n// everything else in the row is either a control or the seek bar.\nconst HIDE_DURATION_BELOW = 300;\nconst HIDE_TIME_BELOW = 210;\n\ninterface PlaybackControlsProps {\n videoRef: RefObject<HTMLVideoElement | null>;\n isPlaying: boolean;\n currentTime: number;\n duration: number;\n isLive: boolean;\n goLive?: ReactNode;\n}\n\nexport default function PlaybackControls({\n videoRef,\n isPlaying,\n currentTime,\n duration,\n isLive,\n goLive,\n}: Readonly<PlaybackControlsProps>) {\n const rowRef = useRef<HTMLDivElement | null>(null);\n const [rowWidth, setRowWidth] = useState(0);\n\n // Measure the row, not the window: the player is often one pane among\n // several, so its width has little to do with the viewport's.\n useEffect(() => {\n const row = rowRef.current;\n if (!row || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const observer = new ResizeObserver(([entry]) => {\n setRowWidth(entry.contentRect.width);\n });\n observer.observe(row);\n return () => observer.disconnect();\n }, []);\n\n const seekable = duration > 0;\n // Zero means \"not measured yet\" - assume there is room, so the first paint\n // matches the common case instead of flashing the compact layout.\n const measured = rowWidth > 0;\n const showDuration = !measured || rowWidth >= HIDE_DURATION_BELOW;\n const showTime = !measured || rowWidth >= HIDE_TIME_BELOW;\n\n const togglePlay = () => {\n const video = videoRef.current;\n if (!video) {\n return;\n }\n if (video.paused) {\n video.play().catch(() => {});\n } else {\n video.pause();\n }\n };\n\n const seekTo = (fraction: number) => {\n const video = videoRef.current;\n if (!video || !isFinite(video.duration)) {\n return;\n }\n video.currentTime = Math.max(0, Math.min(1, fraction)) * video.duration;\n };\n\n return (\n <div\n ref={rowRef}\n style={{\n position: \"absolute\",\n left: 12,\n right: 12,\n bottom: 12,\n display: \"flex\",\n alignItems: \"center\",\n gap: measured && rowWidth < HIDE_DURATION_BELOW ? 8 : 12,\n minWidth: 0,\n pointerEvents: \"auto\",\n }}\n >\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation();\n togglePlay();\n }}\n onMouseDown={(event) => event.stopPropagation()}\n style={{\n display: \"flex\",\n flex: \"0 0 auto\",\n background: \"rgba(0,0,0,0.6)\",\n color: \"#fff\",\n border: \"none\",\n padding: \"6px 10px\",\n borderRadius: 6,\n cursor: \"pointer\",\n }}\n aria-label={isPlaying ? \"Pause\" : \"Play\"}\n title={isPlaying ? \"Pause\" : \"Play\"}\n >\n {isPlaying ? <Pause size={16} /> : <Play size={16} />}\n </button>\n\n {seekable ? (\n <div\n onClick={(event) => {\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n seekTo((event.clientX - rect.left) / rect.width);\n }}\n onMouseDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => {\n const video = videoRef.current;\n if (!video || !isFinite(video.duration)) {\n return;\n }\n const step = video.duration > 0 ? video.duration * 0.02 : 5;\n if (event.key === \"ArrowRight\") {\n event.stopPropagation();\n video.currentTime = Math.min(\n video.duration,\n video.currentTime + step,\n );\n } else if (event.key === \"ArrowLeft\") {\n event.stopPropagation();\n video.currentTime = Math.max(0, video.currentTime - step);\n }\n }}\n role=\"slider\"\n tabIndex={0}\n aria-label=\"Seek\"\n aria-valuemin={0}\n aria-valuemax={duration}\n aria-valuenow={currentTime}\n style={{\n // Basis of zero with no minimum, so the bar gives up its width to\n // the buttons and the clock instead of pushing them off the edge.\n flex: \"1 1 0\",\n minWidth: 0,\n height: 8,\n background: \"rgba(255,255,255,0.12)\",\n borderRadius: 6,\n position: \"relative\",\n overflow: \"hidden\",\n cursor: \"pointer\",\n }}\n title=\"Seek\"\n >\n <div\n style={{\n position: \"absolute\",\n left: 0,\n top: 0,\n bottom: 0,\n // Live streams can report a position a shade past the duration;\n // clamped so the fill never runs past its track.\n width: `${duration > 0 ? Math.min(100, Math.max(0, (currentTime / duration) * 100)) : 0}%`,\n background: \"rgba(66,153,225,0.85)\",\n borderRadius: 6,\n }}\n />\n </div>\n ) : (\n <span style={{ flex: \"1 1 0\", minWidth: 0 }} />\n )}\n\n {/* \"Live\" stays whatever the width, since it is the only thing telling\n you what you are watching; the clock is the part that gives way. */}\n {(showTime || (!seekable && isLive)) && (\n <div\n style={{\n flex: \"0 0 auto\",\n color: \"#fff\",\n fontSize: 12,\n fontVariantNumeric: \"tabular-nums\",\n whiteSpace: \"nowrap\",\n }}\n >\n {seekable\n ? showDuration\n ? `${formatTime(currentTime)}`\n : formatTime(currentTime)\n : isLive\n ? \"Live\"\n : formatTime(currentTime)}\n </div>\n )}\n\n {goLive && (\n <div style={{ display: \"flex\", flex: \"0 0 auto\" }}>{goLive}</div>\n )}\n </div>\n );\n}\n","// The speed button and its drop-down\n\nimport { SPEED_OPTIONS } from \"../utils/hlsPlayerUtils\";\n\nfunction SpeedIcon() {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M5 16a7 7 0 0 1 14 0\" />\n <line x1=\"12\" y1=\"16\" x2=\"9\" y2=\"9\" />\n <circle cx=\"12\" cy=\"16\" r=\"1.5\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n );\n}\n\ninterface SpeedMenuProps {\n speed: number;\n setSpeed: (speed: number) => void;\n showSpeedMenu: boolean;\n setShowSpeedMenu: React.Dispatch<React.SetStateAction<boolean>>;\n speedMenuRef: React.RefObject<HTMLDivElement | null>;\n options?: number[];\n /** Greyed out and unclickable while a live stream is playing. */\n disabled?: boolean;\n /** Hover text explaining why it is greyed out. */\n disabledHint?: string;\n}\n\nexport default function SpeedMenu({\n speed,\n setSpeed,\n showSpeedMenu,\n setShowSpeedMenu,\n speedMenuRef,\n options = SPEED_OPTIONS,\n disabled = false,\n disabledHint = \"Speed is not available on a live stream\",\n}: Readonly<SpeedMenuProps>) {\n return (\n <div\n ref={speedMenuRef}\n style={{ position: \"absolute\", top: 10, right: 10, zIndex: 2 }}\n onMouseDown={(event) => event.stopPropagation()}\n >\n <button\n type=\"button\"\n onClick={(event) => {\n event.stopPropagation();\n setShowSpeedMenu((open) => !open);\n }}\n disabled={disabled}\n title={disabled ? disabledHint : \"Playback speed\"}\n aria-haspopup=\"menu\"\n aria-expanded={showSpeedMenu && !disabled}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 5,\n padding: \"5px 10px\",\n background: \"rgba(0,0,0,0.60)\",\n color: disabled ? \"rgba(255,255,255,0.40)\" : \"#fff\",\n border: \"1px solid rgba(255,255,255,0.22)\",\n borderRadius: 6,\n cursor: disabled ? \"default\" : \"pointer\",\n fontSize: 13,\n fontWeight: 600,\n backdropFilter: \"blur(4px)\",\n WebkitBackdropFilter: \"blur(4px)\",\n }}\n >\n <SpeedIcon />\n {speed}×\n </button>\n\n {showSpeedMenu && !disabled && (\n <div\n role=\"menu\"\n style={{\n position: \"absolute\",\n top: \"calc(100% + 6px)\",\n right: 0,\n background: \"rgba(18,18,18,0.92)\",\n border: \"1px solid rgba(255,255,255,0.14)\",\n borderRadius: 8,\n padding: \"4px 0\",\n minWidth: 90,\n backdropFilter: \"blur(10px)\",\n WebkitBackdropFilter: \"blur(10px)\",\n boxShadow: \"0 4px 16px rgba(0,0,0,0.45)\",\n }}\n >\n {options.map((option) => (\n <button\n key={option}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={option === speed}\n onClick={(event) => {\n event.stopPropagation();\n setSpeed(option);\n setShowSpeedMenu(false);\n }}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n width: \"100%\",\n padding: \"7px 14px\",\n background:\n option === speed ? \"rgba(26,115,232,0.55)\" : \"transparent\",\n border: \"none\",\n color: option === speed ? \"#fff\" : \"rgba(255,255,255,0.75)\",\n fontWeight: option === speed ? 700 : 400,\n fontSize: 13,\n cursor: \"pointer\",\n textAlign: \"left\",\n }}\n >\n <span style={{ width: 12, fontSize: 10, color: \"#4caf50\" }}>\n {option === speed ? \"✓\" : \"\"}\n </span>\n {option}×\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n","// Loads the video stream and keeps it playing.\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\n\nimport Hls from \"hls.js\";\nimport type { ErrorData, HlsConfig, LevelLoadedData } from \"hls.js\";\n\nimport {\n getScaledBufferSeconds,\n getScaledBufferSizeBytes,\n isSafariBrowser,\n jumpToLiveEdge,\n withoutReloadMarker,\n withReloadMarker,\n} from \"../utils/hlsPlayerUtils\";\nimport type { PlaybackEngine, PlayerError } from \"../types\";\n\ninterface UseHlsEngineArgs {\n src?: string;\n videoRef: RefObject<HTMLVideoElement | null>;\n hlsRef: RefObject<Hls | null>;\n engineRef: RefObject<PlaybackEngine | null>;\n speedRef: RefObject<number>;\n applyPlaybackSpeed: (video: HTMLVideoElement, targetSpeed: number) => void;\n clearJumpScan: () => void;\n autoPlay: boolean;\n onError?: (error: PlayerError) => void;\n /** Print what the player is doing to the console. Off by default. */\n debug?: boolean;\n}\n\nexport interface HlsEngineApi {\n url: string;\n isLive: boolean | null;\n engine: PlaybackEngine | null;\n}\n\nexport function useHlsEngine({\n src,\n videoRef,\n hlsRef,\n engineRef,\n speedRef,\n applyPlaybackSpeed,\n clearJumpScan,\n autoPlay,\n onError,\n debug = false,\n}: UseHlsEngineArgs): HlsEngineApi {\n const debugRef = useRef(debug);\n debugRef.current = debug;\n\n const warn = (...args: unknown[]) => {\n if (debugRef.current) {\n console.warn(...args);\n }\n };\n const logError = (...args: unknown[]) => {\n if (debugRef.current) {\n console.error(...args);\n }\n };\n\n const [url, setUrl] = useState(src ?? \"\");\n const [isLive, setIsLive] = useState<boolean | null>(null);\n const [engine, setEngine] = useState<PlaybackEngine | null>(null);\n\n const isLiveRef = useRef<boolean | null>(null);\n\n // Set once Native HLS has failed repeatedly\n const forceHlsJsRef = useRef(false);\n\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n useEffect(() => {\n setUrl(src ?? \"\");\n }, [src]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video || !url) {\n return undefined;\n }\n\n const markLive = (live: boolean | null) => {\n isLiveRef.current = live;\n setIsLive(live);\n };\n\n const selectEngine = (next: PlaybackEngine) => {\n engineRef.current = next;\n setEngine(next);\n };\n\n markLive(null);\n engineRef.current = null;\n setEngine(null);\n\n clearJumpScan();\n\n if (hlsRef.current) {\n hlsRef.current.destroy();\n hlsRef.current = null;\n }\n\n const onLoadedMetadata = () => {\n // For native HLS a duration of Infinity reliably signals a live stream.\n // For HLS.js the answer comes from LEVEL_LOADED instead.\n if (engineRef.current === \"Native HLS\" && isLiveRef.current === null) {\n markLive(!isFinite(video.duration));\n }\n // Restore the selected speed after every metadata load, which covers\n // the reload case too.\n applyPlaybackSpeed(video, speedRef.current);\n };\n\n video.addEventListener(\"loadedmetadata\", onLoadedMetadata);\n\n const removeSharedListeners = () => {\n video.removeEventListener(\"loadedmetadata\", onLoadedMetadata);\n };\n\n const cleanUrl = withoutReloadMarker(url);\n\n // 1. Native HLS (Safari / iOS)\n // Tried first to avoid CORS pre-flight issues on CDN streams. Requires a\n // real Safari user agent on top of canPlayType, since canPlayType alone\n // can be patched to lie about native support on Chrome.\n if (\n !forceHlsJsRef.current &&\n isSafariBrowser() &&\n video.canPlayType(\"application/vnd.apple.mpegurl\")\n ) {\n selectEngine(\"Native HLS\");\n\n video.src = cleanUrl;\n video.playbackRate = speedRef.current;\n video.play().catch(() => {});\n\n let lastTime = 0;\n let stallCounter = 0;\n let nativeErrorCount = 0;\n\n // Check every 3s for a stall: the time not moving while not paused.\n const interval = setInterval(() => {\n if (video.paused) {\n return;\n }\n if (video.currentTime === lastTime && video.readyState >= 2) {\n stallCounter += 1;\n if (stallCounter >= 2) {\n if (isLiveRef.current === true) {\n warn(\"[HlsPlayer/Native] Stall — jumping to live edge\");\n jumpToLiveEdge(video);\n } else {\n warn(\"[HlsPlayer/Native] Stall — resuming recorded playback\");\n }\n video.playbackRate = speedRef.current;\n video.play().catch(() => {});\n stallCounter = 0;\n }\n } else {\n stallCounter = 0;\n }\n lastTime = video.currentTime;\n }, 3000);\n\n const onNativeError = () => {\n // Chromium can fire a spurious 'error' on a <video> tag for the\n // brief window between React committing it (no src yet — src is only\n // ever assigned imperatively, above) and this effect's src assignment\n // taking effect. It carries MEDIA_ERR_SRC_NOT_SUPPORTED with the\n // message below and does not reflect a real failure; reacting to it\n // forces a needless pause and reload.\n if (\n video.error?.code === 4 &&\n /empty src/i.test(video.error?.message || \"\")\n ) {\n warn(\"[HlsPlayer/Native] Ignoring spurious empty-src error\", video.error);\n return;\n }\n\n nativeErrorCount += 1;\n warn(\"[HlsPlayer/Native] Media error\", video.error);\n onErrorRef.current?.(video.error);\n\n if (nativeErrorCount > 5) {\n warn(\n \"[HlsPlayer/Native] Giving up after repeated errors — canPlayType \" +\n \"claimed HLS support but playback never worked. Falling back to HLS.js.\",\n );\n forceHlsJsRef.current = true;\n clearInterval(interval);\n video.removeEventListener(\"error\", onNativeError);\n setUrl((previous) => withReloadMarker(previous));\n return;\n }\n\n const savedUrl = video.src;\n video.src = \"\";\n setTimeout(() => {\n video.src = savedUrl;\n // Only jump forward on a live stream; a recorded clip should resume\n // where it left off.\n if (isLiveRef.current === true) {\n jumpToLiveEdge(video);\n }\n video.playbackRate = speedRef.current;\n video.play().catch(() => {});\n }, 1500);\n };\n\n video.addEventListener(\"error\", onNativeError);\n\n return () => {\n clearInterval(interval);\n video.removeEventListener(\"error\", onNativeError);\n removeSharedListeners();\n video.src = \"\";\n };\n }\n\n // 2. HLS.js (Chrome / Firefox / Edge)\n if (Hls.isSupported()) {\n selectEngine(\"HLS.js\");\n\n let mediaErrorRecoveries = 0;\n let hardReloadTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Destroys hls.js and re-triggers this effect through setUrl.\n const hardReload = (reason: string) => {\n if (hardReloadTimer) {\n return;\n }\n warn(\"[HlsPlayer/HLS.js] Hard reload:\", reason);\n hardReloadTimer = setTimeout(() => {\n hardReloadTimer = null;\n setUrl((previous) => withReloadMarker(previous));\n }, 1500);\n };\n\n // Buffer targets are scaled by the selected speed so playback at Nx\n // keeps roughly the same real-time lookahead window.\n const scaledBufferSeconds = getScaledBufferSeconds(speedRef.current);\n\n const hlsConfig: Partial<HlsConfig> = {\n enableWorker: true,\n lowLatencyMode: false,\n liveSyncDurationCount: 3,\n liveMaxLatencyDurationCount: 6,\n fragLoadingMaxRetry: 2,\n fragLoadingRetryDelay: 500,\n fragLoadingMaxRetryTimeout: 4000,\n manifestLoadingMaxRetry: 3,\n levelLoadingMaxRetry: 3,\n maxBufferLength: scaledBufferSeconds,\n maxMaxBufferLength: scaledBufferSeconds,\n maxBufferSize: getScaledBufferSizeBytes(speedRef.current),\n };\n\n const hls = new Hls(hlsConfig);\n hlsRef.current = hls;\n\n hls.loadSource(cleanUrl);\n hls.attachMedia(video);\n\n // LEVEL_LOADED is the reliable live/recorded signal for hls.js.\n hls.on(Hls.Events.LEVEL_LOADED, (_event, data: LevelLoadedData) => {\n markLive(data.details.live);\n });\n\n hls.on(Hls.Events.MANIFEST_PARSED, () => {\n applyPlaybackSpeed(video, speedRef.current);\n // The native `autoplay` attribute is not reliable here: hls.js\n // attaches its MediaSource to video.src programmatically well after\n // mount, and browsers do not consistently re-run autoplay selection\n // for that later, script-driven assignment. Calling play() here is\n // what actually starts playback.\n if (autoPlay) {\n video.play().catch(() => {});\n }\n });\n\n hls.on(Hls.Events.ERROR, (_event, data: ErrorData) => {\n const isFragError =\n data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR ||\n data.details === Hls.ErrorDetails.FRAG_LOAD_TIMEOUT;\n\n // A fragment error that hls.js has already retried past. The manifest\n // has been refreshed in the background by now, so jumping to the live\n // edge lands on the nearest playable position.\n if (isFragError && !data.fatal) {\n setTimeout(() => {\n if (isLiveRef.current === true) {\n jumpToLiveEdge(video);\n }\n applyPlaybackSpeed(video, speedRef.current);\n video.play().catch(() => {});\n }, 500);\n return;\n }\n\n if (!data.fatal) {\n return;\n }\n\n logError(\"[HlsPlayer/HLS.js] Fatal error\", data.type, data.details);\n onErrorRef.current?.(data);\n\n switch (data.type) {\n case Hls.ErrorTypes.MEDIA_ERROR:\n if (mediaErrorRecoveries < 3) {\n mediaErrorRecoveries += 1;\n hls.recoverMediaError();\n } else {\n hardReload(\"repeated media errors\");\n }\n break;\n\n case Hls.ErrorTypes.NETWORK_ERROR:\n if (\n data.details === Hls.ErrorDetails.MANIFEST_LOAD_ERROR ||\n data.details === Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT\n ) {\n hardReload(\"manifest unreachable\");\n } else {\n setTimeout(() => hls.startLoad(), 2000);\n }\n break;\n\n default:\n hardReload(\"unrecoverable error\");\n break;\n }\n });\n\n return () => {\n if (hardReloadTimer) {\n clearTimeout(hardReloadTimer);\n }\n clearJumpScan();\n removeSharedListeners();\n hls.destroy();\n hlsRef.current = null;\n };\n }\n\n // Neither path is available\n warn(\"[HlsPlayer] HLS is not supported in this browser\");\n onErrorRef.current?.(new Error(\"HLS playback is not supported in this browser.\"));\n return () => {\n removeSharedListeners();\n };\n // Only a change of address rebuilds the player. Speed changes must not.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url]);\n\n return { url, isLive, engine };\n}\n","// Works out how far behind \"now\" the viewer is, and provides the jump back.\n//\n// A live stream keeps growing at the end. The newest moment it can play is\n// called the live edge. Watching anything earlier than that means you are\n// behind it, which is when the Go Live button is worth showing.\n//\n// It checks on a timer rather than only when the video reports progress,\n// because the live edge keeps moving forward even while the video is paused.\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\n\nimport { getSecondsBehindLive, jumpToLiveEdge } from \"../utils/hlsPlayerUtils\";\n\n/**\n * How far behind counts as behind, in seconds.\n *\n * An HLS player sits a few segments back from the newest moment by design,\n * so treating any gap at all as \"behind\" leaves the button flickering on and\n * off while the viewer is, for all practical purposes, live.\n */\nexport const DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 5;\n\n/** How often to re-check, in milliseconds. */\nconst POLL_MS = 1000;\n\nexport interface LiveEdgeApi {\n /** Seconds between the viewer and the newest moment. Null while unknown. */\n secondsBehind: number | null;\n /** True when the viewer is far enough back for Go Live to be worth offering. */\n behindLiveEdge: boolean;\n /** Jump to the newest moment and resume playing. */\n goLive: () => void;\n}\n\nexport function useLiveEdge(\n videoRef: RefObject<HTMLVideoElement | null>,\n isLive: boolean | null,\n toleranceSeconds: number = DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS,\n): LiveEdgeApi {\n const [secondsBehind, setSecondsBehind] = useState<number | null>(null);\n\n useEffect(() => {\n if (isLive !== true) {\n setSecondsBehind(null);\n return undefined;\n }\n\n const check = () => {\n const video = videoRef.current;\n setSecondsBehind(video ? getSecondsBehindLive(video) : null);\n };\n\n check();\n const interval = setInterval(check, POLL_MS);\n return () => clearInterval(interval);\n }, [isLive, videoRef]);\n\n const goLive = useCallback(() => {\n const video = videoRef.current;\n if (!video) {\n return;\n }\n jumpToLiveEdge(video);\n video.play().catch(() => {});\n setSecondsBehind(getSecondsBehindLive(video));\n }, [videoRef]);\n\n return {\n secondsBehind,\n behindLiveEdge: secondsBehind !== null && secondsBehind > toleranceSeconds,\n goLive,\n };\n}\n","// Closes when the user clicks anywhere else on the page.\n// Used by the speed menu\n\nimport { useEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\n\nexport function useClickOutside(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n onOutside: () => void,\n): void {\n const onOutsideRef = useRef(onOutside);\n useEffect(() => {\n onOutsideRef.current = onOutside;\n });\n\n useEffect(() => {\n if (!active) {\n return undefined;\n }\n\n const onMouseDown = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n onOutsideRef.current();\n }\n };\n\n document.addEventListener(\"mousedown\", onMouseDown);\n return () => document.removeEventListener(\"mousedown\", onMouseDown);\n }, [active, ref]);\n}\n","// Handles how fast the video plays.\n//\n// Setting the speed is usually just one line, but the browser cannot really\n// decode video at 5x or 10x. So above a threshold this plays at a speed the\n// browser can manage and quietly skips forward on a timer to make up the\n// rest. It also opens and closes the little speed menu.\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport type Hls from \"hls.js\";\n\nimport {\n getBufferedAhead,\n getScaledBufferSeconds,\n getScaledBufferSizeBytes,\n JUMP_SCAN_BASE_RATE,\n JUMP_SCAN_INTERVAL_MS,\n JUMP_SCAN_THRESHOLD,\n} from \"../utils/hlsPlayerUtils\";\nimport type { PlaybackEngine } from \"../types\";\nimport { useClickOutside } from \"./useClickOutside\";\n\ninterface UsePlaybackSpeedArgs {\n videoRef: RefObject<HTMLVideoElement | null>;\n hlsRef: RefObject<Hls | null>;\n engineRef: RefObject<PlaybackEngine | null>;\n /** Starting speed. */\n initialSpeed?: number;\n onSpeedChange?: (speed: number) => void;\n}\n\nexport interface PlaybackSpeedApi {\n speed: number;\n setSpeed: (speed: number) => void;\n showSpeedMenu: boolean;\n setShowSpeedMenu: React.Dispatch<React.SetStateAction<boolean>>;\n speedMenuRef: RefObject<HTMLDivElement | null>;\n speedRef: RefObject<number>;\n applyPlaybackSpeed: (video: HTMLVideoElement, targetSpeed: number) => void;\n clearJumpScan: () => void;\n}\n\nexport function usePlaybackSpeed({\n videoRef,\n hlsRef,\n engineRef,\n initialSpeed = 1,\n onSpeedChange,\n}: UsePlaybackSpeedArgs): PlaybackSpeedApi {\n const [speed, setSpeedState] = useState(initialSpeed);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n\n const speedRef = useRef(initialSpeed);\n const speedMenuRef = useRef<HTMLDivElement | null>(null);\n const jumpScanIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const onSpeedChangeRef = useRef(onSpeedChange);\n onSpeedChangeRef.current = onSpeedChange;\n\n const clearJumpScan = useCallback(() => {\n if (jumpScanIntervalRef.current) {\n clearInterval(jumpScanIntervalRef.current);\n jumpScanIntervalRef.current = null;\n }\n }, []);\n\n // Sets the effective playback speed for `video`, choosing jump-scan mode\n // over a direct playbackRate assignment when the engine/rate combination\n // is known to stall (see the jump-scan notes in hlsPlayerUtils).\n const applyPlaybackSpeed = useCallback(\n (video: HTMLVideoElement, targetSpeed: number) => {\n clearJumpScan();\n\n if (engineRef.current !== \"HLS.js\" || targetSpeed < JUMP_SCAN_THRESHOLD) {\n video.playbackRate = targetSpeed;\n return;\n }\n\n video.playbackRate = JUMP_SCAN_BASE_RATE;\n const targetExtraSeconds =\n (targetSpeed - JUMP_SCAN_BASE_RATE) * (JUMP_SCAN_INTERVAL_MS / 1000);\n\n jumpScanIntervalRef.current = setInterval(() => {\n if (video.paused || video.seeking) {\n return;\n }\n\n // Only jump as far as content that's actually already buffered —\n // jumping past the buffered edge forces a fresh fetch+decode at the\n // landing spot, which stalls exactly like continuous high-rate\n // decode does on slower connections/machines. Skip the jump instead\n // of forcing it and fall back to plain 2x for this tick; speed\n // recovers automatically once the buffer catches back up.\n const safetyMarginSeconds = 2;\n const maxSafeJump = Math.max(0, getBufferedAhead(video) - safetyMarginSeconds);\n const jumpSeconds = Math.min(targetExtraSeconds, maxSafeJump);\n if (jumpSeconds <= 0) {\n return;\n }\n\n const seekableEnd =\n video.seekable.length > 0\n ? video.seekable.end(video.seekable.length - 1)\n : video.duration;\n const target = video.currentTime + jumpSeconds;\n if (isFinite(seekableEnd) && target >= seekableEnd - 0.5) {\n clearJumpScan();\n return;\n }\n video.currentTime = target;\n }, JUMP_SCAN_INTERVAL_MS);\n },\n [clearJumpScan, engineRef],\n );\n\n const setSpeed = useCallback((next: number) => {\n setSpeedState(next);\n }, []);\n\n // Keep speedRef in sync without triggering the engine effect.\n useEffect(() => {\n speedRef.current = speed;\n onSpeedChangeRef.current?.(speed);\n }, [speed]);\n\n // Apply speed to the video element. Separate effect so speed changes never\n // rebuild the player instance.\n useEffect(() => {\n const video = videoRef.current;\n if (!video) {\n return undefined;\n }\n applyPlaybackSpeed(video, speed);\n\n // Rescale the HLS.js buffer target live so a higher rate gets more\n // pre-fetched runway without tearing down and rebuilding the player.\n const hls = hlsRef.current;\n if (hls) {\n const scaledSeconds = getScaledBufferSeconds(speed);\n hls.config.maxBufferLength = scaledSeconds;\n hls.config.maxMaxBufferLength = scaledSeconds;\n hls.config.maxBufferSize = getScaledBufferSizeBytes(speed);\n }\n\n return () => clearJumpScan();\n }, [speed, applyPlaybackSpeed, clearJumpScan, hlsRef, videoRef]);\n\n useClickOutside(speedMenuRef, showSpeedMenu, () => setShowSpeedMenu(false));\n\n return {\n speed,\n setSpeed,\n showSpeedMenu,\n setShowSpeedMenu,\n speedMenuRef,\n speedRef,\n applyPlaybackSpeed,\n clearJumpScan,\n };\n}\n","// Rebuilding playable playlists out of a recorded feed.\n//\n// The recordings API hands back one m3u8 per camera whose entries are .ts\n// segments. Playing the whole thing at once is unwieldy, so the text is sliced\n// into fixed-length chunks and each chunk is reassembled into a standalone\n// playlist, handed to the player as a blob address.\n//\n// Ported from SeniorLivingUI's hlsPlaylistChunking.js. That version leans on\n// dayjs; this one uses Date so the package stays dependency-free.\n\nimport type { ParsedPlaylist, PlaylistSegment, RecordingChunk, RecordingSearchResult } from \"../types\";\n\n/** Seconds of footage per rebuilt chunk. */\nexport const CHUNK_DURATION_SECONDS = 120;\n\nconst EXTINF_RE = /^#EXTINF:([\\d.]+),?/;\n// e.g. seg_20260824_044356_109074.ts\nconst SEGMENT_TIMESTAMP_RE = /seg_(\\d{8})_(\\d{6})_/;\n\n/** Epoch milliseconds from a segment filename, or null when it carries none. */\nexport function parseSegmentTimestamp(uri: string): number | null {\n const match = SEGMENT_TIMESTAMP_RE.exec(uri);\n if (!match) {\n return null;\n }\n\n const [, date, time] = match;\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(4, 6));\n const day = Number(date.slice(6, 8));\n const hour = Number(time.slice(0, 2));\n const minute = Number(time.slice(2, 4));\n const second = Number(time.slice(4, 6));\n\n const parsed = new Date(year, month - 1, day, hour, minute, second);\n if (Number.isNaN(parsed.getTime())) {\n return null;\n }\n // Date rolls impossible values over (month 13 becomes January) where dayjs's\n // strict parse rejects them. Reject them here too, so a malformed name is\n // reported as \"no timestamp\" rather than as a plausible wrong one.\n if (\n parsed.getFullYear() !== year ||\n parsed.getMonth() !== month - 1 ||\n parsed.getDate() !== day ||\n parsed.getHours() !== hour ||\n parsed.getMinutes() !== minute ||\n parsed.getSeconds() !== second\n ) {\n return null;\n }\n\n return parsed.getTime();\n}\n\n/**\n * Splits a raw m3u8 playlist into its header lines (everything before the\n * first #EXTINF) and its ordered list of segments.\n */\nexport function parseM3u8(feedText: string | null | undefined): ParsedPlaylist {\n const lines = (feedText ?? \"\")\n .split(/\\r?\\n/)\n .filter((line) => line.trim() !== \"\");\n const headerLines: string[] = [];\n const segments: PlaylistSegment[] = [];\n\n let pendingDuration: number | null = null;\n let sawFirstExtinf = false;\n\n for (const line of lines) {\n const extinfMatch = EXTINF_RE.exec(line);\n if (extinfMatch) {\n sawFirstExtinf = true;\n pendingDuration = Number(extinfMatch[1]);\n continue;\n }\n if (line.startsWith(\"#\")) {\n // Trailing tags (e.g. #EXT-X-ENDLIST) are dropped —\n // buildChunkPlaylistText appends its own closing tag per chunk.\n if (!sawFirstExtinf) {\n headerLines.push(line);\n }\n continue;\n }\n if (pendingDuration !== null) {\n segments.push({\n durationSec: pendingDuration,\n uri: line,\n timestamp: parseSegmentTimestamp(line),\n });\n pendingDuration = null;\n }\n }\n\n return { headerLines, segments };\n}\n\n/**\n * Groups consecutive segments so each group's cumulative duration reaches\n * ~chunkSeconds. The final group may be shorter.\n */\nexport function chunkSegments(\n segments: PlaylistSegment[],\n chunkSeconds: number = CHUNK_DURATION_SECONDS,\n): PlaylistSegment[][] {\n const chunks: PlaylistSegment[][] = [];\n let current: PlaylistSegment[] = [];\n let currentDuration = 0;\n\n for (const segment of segments) {\n current.push(segment);\n currentDuration += segment.durationSec;\n if (currentDuration >= chunkSeconds) {\n chunks.push(current);\n current = [];\n currentDuration = 0;\n }\n }\n\n if (current.length > 0) {\n chunks.push(current);\n }\n\n return chunks;\n}\n\n/** Reassembles a standalone, playable m3u8 string for one chunk of segments. */\nexport function buildChunkPlaylistText(\n headerLines: string[],\n chunkSegmentList: PlaylistSegment[],\n): string {\n const lines = [...headerLines];\n for (const segment of chunkSegmentList) {\n lines.push(`#EXTINF:${segment.durationSec},`, segment.uri);\n }\n lines.push(\"#EXT-X-ENDLIST\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nconst clockLabel = (epochMs: number | null): string =>\n epochMs === null ? \"--:--\" : new Date(epochMs).toTimeString().slice(0, 5);\n\n/**\n * Builds the flat, chronologically-sorted list of playable chunk descriptors\n * from a recordings search response.\n */\nexport function buildRecordingChunks(\n results: RecordingSearchResult[] | null | undefined,\n chunkSeconds: number = CHUNK_DURATION_SECONDS,\n): RecordingChunk[] {\n const chunks = (results ?? []).flatMap((result, resultIndex) => {\n const { spaceId, cameraId, feed } = result;\n const { headerLines, segments } = parseM3u8(feed);\n if (segments.length === 0) {\n return [];\n }\n\n return chunkSegments(segments, chunkSeconds).map(\n (chunkSegmentList, chunkIndex) => {\n const startTime = chunkSegmentList[0].timestamp;\n // Real feeds have been seen with every segment in a batch sharing one\n // filename timestamp (only the trailing sequence number increments),\n // so deriving the end from the last segment's own timestamp\n // under-counts. The summed EXTINF durations are the reliable source.\n const durationSec = chunkSegmentList.reduce(\n (sum, segment) => sum + segment.durationSec,\n 0,\n );\n const endTime =\n startTime === null ? null : startTime + durationSec * 1000;\n\n return {\n // The result index is part of the id because one camera can appear\n // in the response more than once, over different time ranges — and\n // then space + camera + chunk index alone is not unique.\n id: `${spaceId}-${cameraId}-${resultIndex}-${chunkIndex}`,\n spaceId,\n cameraId,\n startTime,\n endTime,\n durationSec,\n startLabel: clockLabel(startTime),\n endLabel: clockLabel(endTime),\n segmentCount: chunkSegmentList.length,\n playlistText: buildChunkPlaylistText(headerLines, chunkSegmentList),\n };\n },\n );\n });\n\n return chunks.sort((a, b) => {\n if (a.startTime === null || b.startTime === null) {\n return 0;\n }\n return a.startTime - b.startTime;\n });\n}\n\n/**\n * Turns raw m3u8 text into an address the player can load.\n *\n * The caller owns the result: release it with `URL.revokeObjectURL` once the\n * player is done with it, or the blob is held in memory for the life of the\n * document.\n */\nexport function makeBlobUrl(playlistText: string): string {\n const blob = new Blob([new TextEncoder().encode(playlistText)], {\n type: \"application/vnd.apple.mpegurl\",\n });\n return URL.createObjectURL(blob);\n}\n\n/**\n * The chunk covering `epochMs`, or the first chunk when nothing covers it.\n *\n * Chunks whose filenames carried no timestamp cannot be matched by time, so\n * they only ever come back as the first-chunk fallback.\n */\nexport function chunkAt(\n chunks: RecordingChunk[],\n epochMs?: number | null,\n): RecordingChunk | null {\n if (chunks.length === 0) {\n return null;\n }\n if (epochMs === undefined || epochMs === null) {\n return chunks[0];\n }\n const covering = chunks.find(\n (chunk) =>\n chunk.startTime !== null &&\n chunk.endTime !== null &&\n epochMs >= chunk.startTime &&\n epochMs < chunk.endTime,\n );\n return covering ?? chunks[0];\n}\n","// Turns the host's recordings search into something the player can load.\n//\n// The package has no API client of its own: the host passes its own call in as\n// `search`, this runs it, rebuilds the playlists (see hlsPlaylist), and hands\n// back a blob address for the chunk being watched.\n//\n// Blob addresses are released as soon as the chunk changes or the player goes\n// away, so a long session does not accumulate playlists in memory.\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n buildRecordingChunks,\n chunkAt,\n makeBlobUrl,\n CHUNK_DURATION_SECONDS,\n} from \"../utils/hlsPlaylist\";\nimport type {\n PlayerError,\n RecordingChunk,\n RecordingSearch,\n RecordingSearchResult,\n} from \"../types\";\n\ninterface UseRecordingSearchArgs {\n /** The host's call. Leave it out and the hook stays idle. */\n search?: RecordingSearch;\n /** Epoch ms of the moment to watch; picks the chunk covering it. */\n at?: number | null;\n /** Seconds of footage per chunk. */\n chunkSeconds?: number;\n onError?: (error: PlayerError) => void;\n}\n\nexport interface RecordingSearchApi {\n /** Every chunk built from the response, oldest first. */\n chunks: RecordingChunk[];\n /** The chunk being watched. */\n chunk: RecordingChunk | null;\n /** Address for `chunk`, or null when there is nothing to play. */\n url: string | null;\n loading: boolean;\n error: PlayerError | null;\n}\n\nexport function useRecordingSearch({\n search,\n at,\n chunkSeconds = CHUNK_DURATION_SECONDS,\n onError,\n}: UseRecordingSearchArgs): RecordingSearchApi {\n const [results, setResults] = useState<RecordingSearchResult[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<PlayerError | null>(null);\n const [url, setUrl] = useState<string | null>(null);\n\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (!search) {\n setResults([]);\n setLoading(false);\n setError(null);\n return undefined;\n }\n\n // The call cannot be cancelled — it is the host's, and it may not accept a\n // signal — so a stale response is dropped on arrival instead.\n let active = true;\n setLoading(true);\n setError(null);\n\n search()\n .then((response) => {\n if (!active) {\n return;\n }\n setResults(Array.isArray(response) ? response : []);\n })\n .catch((searchError: PlayerError) => {\n if (!active) {\n return;\n }\n setResults([]);\n setError(searchError);\n onErrorRef.current?.(searchError);\n })\n .finally(() => {\n if (active) {\n setLoading(false);\n }\n });\n\n return () => {\n active = false;\n };\n }, [search]);\n\n const chunks = useMemo(\n () => buildRecordingChunks(results, chunkSeconds),\n [results, chunkSeconds],\n );\n\n const chunk = useMemo(() => chunkAt(chunks, at), [chunks, at]);\n\n // Created in an effect rather than a memo so there is a cleanup to revoke on.\n const playlistText = chunk?.playlistText ?? null;\n useEffect(() => {\n if (playlistText === null) {\n setUrl(null);\n return undefined;\n }\n const next = makeBlobUrl(playlistText);\n setUrl(next);\n return () => {\n URL.revokeObjectURL(next);\n setUrl(null);\n };\n }, [playlistText]);\n\n return { chunks, chunk, url, loading, error };\n}\n","// Keeps track of whether the video is playing, where it has got to, and how\n// long it is.\n//\n// The browser's own controls are switched off, so these values feed the\n// player's own control bar.\n\nimport { useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\n\nexport interface VideoPlaybackState {\n isPlaying: boolean;\n /** Seconds from the start of the stream. */\n currentTime: number;\n /** Seconds. Zero for a live stream, which has no fixed length. */\n duration: number;\n}\n\nexport function useVideoPlaybackState(\n videoRef: RefObject<HTMLVideoElement | null>,\n url: string,\n autoPlay: boolean,\n): VideoPlaybackState {\n const [isPlaying, setIsPlaying] = useState(autoPlay);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) {\n return undefined;\n }\n\n const onTime = () => setCurrentTime(video.currentTime || 0);\n const onLoaded = () => setDuration(isFinite(video.duration) ? video.duration : 0);\n // 'play' fires the instant play() is *called* — including the brief,\n // immediately-reverted play() that React's development-mode double mount\n // triggers, which showed up as a Play/Pause flicker on every load.\n // 'playing' only fires once frames are actually advancing, so it skips\n // that transient window entirely.\n const onPlaying = () => setIsPlaying(true);\n const onPause = () => setIsPlaying(false);\n\n video.addEventListener(\"timeupdate\", onTime);\n video.addEventListener(\"loadedmetadata\", onLoaded);\n video.addEventListener(\"playing\", onPlaying);\n video.addEventListener(\"pause\", onPause);\n\n // Seed the timings only. `isPlaying` is left to the autoPlay prop.\n setCurrentTime(video.currentTime || 0);\n setDuration(isFinite(video.duration) ? video.duration : 0);\n\n return () => {\n video.removeEventListener(\"timeupdate\", onTime);\n video.removeEventListener(\"loadedmetadata\", onLoaded);\n video.removeEventListener(\"playing\", onPlaying);\n video.removeEventListener(\"pause\", onPause);\n };\n // videoRef is a stable ref object, not a reactive value — only a change\n // of address should re-bind these listeners.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url]);\n\n return { isPlaying, currentTime, duration };\n}\n","// The player itself.\n//\n// Give it the address of an HLS stream and it plays it, live or recorded. It\n// draws its own play/pause bar, a speed menu, and when you switch it on a\n// Go Live button that jumps to the newest moment of a live stream.\n\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type Hls from \"hls.js\";\n\nimport GoLiveButton from \"./components/GoLiveButton\";\nimport PlaybackControls from \"./components/PlaybackControls\";\nimport SpeedMenu from \"./components/SpeedMenu\";\nimport { useHlsEngine } from \"./hooks/useHlsEngine\";\nimport {\n useLiveEdge,\n DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS,\n} from \"./hooks/useLiveEdge\";\nimport { usePlaybackSpeed } from \"./hooks/usePlaybackSpeed\";\nimport { useRecordingSearch } from \"./hooks/useRecordingSearch\";\nimport { useVideoPlaybackState } from \"./hooks/useVideoPlaybackState\";\nimport type {\n PlaybackEngine,\n PlayerError,\n RecordingChunk,\n RecordingSearch,\n} from \"./types\";\n\nexport interface HlsPlayerProps {\n /**\n * Address of the stream to play. Leave it out, or pass an empty string,\n * and the player shows `emptyMessage` in place of the video.\n */\n src?: string;\n /** Shown instead of the video while there is no `src`. */\n emptyMessage?: ReactNode;\n /**\n * The host's recordings search. Called when there is no `src`, and whatever\n * it returns is rebuilt into a playable playlist — see `useRecordingSearch`.\n * This package has no API client of its own, so the call is injected.\n */\n recordingSearch?: RecordingSearch;\n /**\n * Epoch ms of the moment being investigated. The chunk covering it is the\n * one played; without it playback starts at the earliest chunk.\n */\n recordingAt?: number | null;\n /** Seconds of footage per rebuilt chunk. Defaults to 120. */\n recordingChunkSeconds?: number;\n /** Handed every chunk built from the search response, oldest first. */\n onRecordingChunks?: (chunks: RecordingChunk[]) => void;\n /** Shown while the recordings search is in flight. */\n loadingMessage?: ReactNode;\n autoPlay?: boolean;\n muted?: boolean;\n /** Height of the video in pixels */\n height?: number | string;\n className?: string;\n\n /** Hide the play/pause and seek bar. */\n showControls?: boolean;\n /** Hide the speed menu. */\n showSpeedMenu?: boolean;\n /** Speeds the menu offers. */\n speedOptions?: number[];\n /** Speed to start at. */\n initialSpeed?: number;\n\n /** Offer a Go Live button. */\n showGoLive?: boolean;\n /**\n * What going live means for this stream.\n *\n * \"source\" is for a camera system that serves live footage and recordings\n * at two different addresses. The button shows while a recording is\n * playing, calls `onGoLive`, and the app is expected to fetch the live\n * address and hand it back as a new `src`. The button then goes away,\n * because the stream is live.\n *\n * \"seek\" is for a single stream holding both, where going live only means\n * jumping to its newest moment.\n */\n goLiveMode?: \"source\" | \"seek\";\n /** Wording on the button. */\n goLiveLabel?: string;\n /**\n * Wording once the stream is live and the button offers the way back.\n * Only used by \"source\" mode, and only when `onExitLive` is given.\n */\n exitLiveLabel?: string;\n /**\n * Called when the viewer wants the recording back. The app fetches the\n * recorded address and hands it over as a new `src`, the mirror of\n * `onGoLive`. Leave it out and the button simply disappears once live.\n */\n onExitLive?: () => void;\n /**\n * Let the speed menu stay usable on a live stream. Off by default: there is\n * nothing ahead of live to fast-forward into, so a higher speed only runs\n * the player into the end of the stream and stalls.\n */\n allowSpeedWhenLive?: boolean;\n /**\n * Only used by \"seek\" mode. How many seconds behind the newest moment\n * counts as behind. A player sits a few seconds back by design, so a very\n * small number makes the button flicker on and off while the viewer is\n * effectively live.\n */\n liveEdgeToleranceSeconds?: number;\n\n /**\n * Called when Go Live is pressed. In \"source\" mode this is where the app\n * fetches the live address and updates `src`.\n */\n onGoLive?: () => void;\n onSpeedChange?: (speed: number) => void;\n onError?: (error: PlayerError) => void;\n /** Print what the player is doing to the console. Off by default. */\n debug?: boolean;\n /** Called once the player knows whether this is live or a recorded clip. */\n onLiveChange?: (isLive: boolean | null) => void;\n /** Called with the engine that ended up being used. */\n onEngineChange?: (engine: PlaybackEngine | null) => void;\n}\n\nexport default function HlsPlayer({\n src,\n emptyMessage = \"No video to display\",\n recordingSearch,\n recordingAt,\n recordingChunkSeconds,\n onRecordingChunks,\n loadingMessage = \"Loading recording…\",\n autoPlay = true,\n muted = true,\n height = 480,\n className,\n showControls = true,\n showSpeedMenu = true,\n speedOptions,\n initialSpeed = 1,\n showGoLive = false,\n goLiveMode = \"source\",\n goLiveLabel = \"Go Live\",\n exitLiveLabel = \"Back to recording\",\n onExitLive,\n allowSpeedWhenLive = false,\n liveEdgeToleranceSeconds = DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS,\n onGoLive,\n onSpeedChange,\n onError,\n onLiveChange,\n onEngineChange,\n debug = false,\n}: Readonly<HlsPlayerProps>) {\n const videoRef = useRef<HTMLVideoElement | null>(null);\n\n\n const hlsRef = useRef<Hls | null>(null);\n const engineRef = useRef<PlaybackEngine | null>(null);\n\n // An address given outright wins, and stops the search from being run at\n // all: there is nothing for it to contribute once the stream is known.\n const hasExplicitSrc = typeof src === \"string\" && src.trim() !== \"\";\n\n const {\n chunks: recordingChunks,\n url: recordingUrl,\n loading: recordingLoading,\n } = useRecordingSearch({\n search: hasExplicitSrc ? undefined : recordingSearch,\n at: recordingAt,\n chunkSeconds: recordingChunkSeconds,\n onError,\n });\n\n const onRecordingChunksRef = useRef(onRecordingChunks);\n onRecordingChunksRef.current = onRecordingChunks;\n useEffect(() => {\n onRecordingChunksRef.current?.(recordingChunks);\n }, [recordingChunks]);\n\n const effectiveSrc = hasExplicitSrc ? src : (recordingUrl ?? undefined);\n\n const {\n speed,\n setSpeed,\n showSpeedMenu: menuOpen,\n setShowSpeedMenu,\n speedMenuRef,\n speedRef,\n applyPlaybackSpeed,\n clearJumpScan,\n } = usePlaybackSpeed({\n videoRef,\n hlsRef,\n engineRef,\n initialSpeed,\n onSpeedChange,\n });\n\n const { url, isLive, engine } = useHlsEngine({\n src: effectiveSrc,\n videoRef,\n hlsRef,\n engineRef,\n speedRef,\n applyPlaybackSpeed,\n clearJumpScan,\n autoPlay,\n onError,\n debug,\n });\n\n const { isPlaying, currentTime, duration } = useVideoPlaybackState(\n videoRef,\n url,\n autoPlay,\n );\n\n const { secondsBehind, behindLiveEdge, goLive } = useLiveEdge(\n videoRef,\n isLive,\n liveEdgeToleranceSeconds,\n );\n\n const onLiveChangeRef = useRef(onLiveChange);\n onLiveChangeRef.current = onLiveChange;\n useEffect(() => {\n onLiveChangeRef.current?.(isLive);\n }, [isLive]);\n\n const onEngineChangeRef = useRef(onEngineChange);\n onEngineChangeRef.current = onEngineChange;\n useEffect(() => {\n onEngineChangeRef.current?.(engine);\n }, [engine]);\n\n // When the button is shown, and whether it can be pressed.\n //\n // The two modes are opposites. In \"source\" mode you press Go Live *while\n // watching a recording*, so the button belongs on a stream that is not\n // live and disappears once it is. In \"seek\" mode you are already on the\n // live stream and only need it once you have drifted behind the newest\n // moment.\n // While live, the button changes into the way back to the recording, so one\n // control carries the viewer in both directions.\n const showingExit = goLiveMode === \"source\" && isLive === true;\n\n let offerGoLive = false;\n let goLiveEnabled = false;\n let goLiveHint = goLiveLabel;\n let goLiveText = goLiveLabel;\n\n if (showGoLive) {\n if (goLiveMode === \"source\") {\n if (showingExit) {\n // Only offered when the app said what \"back\" means.\n offerGoLive = Boolean(onExitLive);\n goLiveEnabled = true;\n goLiveText = exitLiveLabel;\n goLiveHint = `${exitLiveLabel} — leave the live stream`;\n } else {\n offerGoLive = true;\n goLiveEnabled = isLive === false;\n goLiveHint =\n isLive === null\n ? \"Checking the stream…\"\n : `${goLiveLabel} — switch to the live stream`;\n }\n } else {\n offerGoLive = isLive === true;\n goLiveEnabled = behindLiveEdge;\n goLiveHint =\n secondsBehind === null\n ? goLiveLabel\n : `${goLiveLabel} — ${Math.round(secondsBehind)}s behind`;\n }\n }\n\n const handleGoLive = () => {\n if (showingExit) {\n onExitLive?.();\n return;\n }\n // \"source\" mode has nothing to seek to: the live footage lives at another\n // address, which only the app can fetch.\n if (goLiveMode === \"seek\") {\n goLive();\n }\n onGoLive?.();\n };\n\n // Fast-forward past live is not a thing — there are no frames there yet, so\n // a higher rate just runs the player into the end of the stream and stalls.\n const speedLocked = !allowSpeedWhenLive && isLive === true;\n\n // Drop back to normal speed on reaching a live stream, so a rate left over\n // from a recording does not carry across.\n useEffect(() => {\n if (speedLocked && speedRef.current !== 1) {\n setSpeed(1);\n }\n }, [speedLocked, setSpeed, speedRef]);\n\n const hasSource =\n typeof effectiveSrc === \"string\" && effectiveSrc.trim() !== \"\";\n\n if (!hasSource) {\n return (\n <div\n className={className}\n style={{ position: \"relative\", width: \"100%\" }}\n data-testid=\"hls-player\"\n >\n <div\n data-testid={\n recordingLoading ? \"hls-player-loading\" : \"hls-player-empty\"\n }\n style={{\n width: \"100%\",\n height,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: \"#000\",\n color: \"#94a3b8\",\n fontSize: 14,\n textAlign: \"center\",\n padding: 16,\n boxSizing: \"border-box\",\n }}\n >\n {recordingLoading ? loadingMessage : emptyMessage}\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={className}\n style={{ position: \"relative\", width: \"100%\" }}\n data-testid=\"hls-player\"\n >\n <video\n ref={videoRef}\n controls={false}\n autoPlay={autoPlay}\n muted={muted}\n playsInline\n // Keep the browser's own menu out of the way - it offers a competing\n // playback-speed setting and a download entry.\n onContextMenu={(event) => event.preventDefault()}\n controlsList=\"nodownload nofullscreen noremoteplayback noplaybackrate\"\n disablePictureInPicture\n disableRemotePlayback\n style={{ width: \"100%\", height, display: \"block\", background: \"#000\" }}\n >\n <track kind=\"captions\" />\n </video>\n\n {showControls && (\n <PlaybackControls\n videoRef={videoRef}\n isPlaying={isPlaying}\n currentTime={currentTime}\n duration={duration}\n isLive={isLive === true}\n goLive={\n offerGoLive ? (\n <GoLiveButton\n enabled={goLiveEnabled}\n label={goLiveText}\n hint={goLiveHint}\n live={showingExit}\n onGoLive={handleGoLive}\n />\n ) : undefined\n }\n />\n )}\n\n {showSpeedMenu && (\n <SpeedMenu\n speed={speed}\n setSpeed={setSpeed}\n showSpeedMenu={menuOpen}\n setShowSpeedMenu={setShowSpeedMenu}\n speedMenuRef={speedMenuRef}\n options={speedOptions}\n disabled={speedLocked}\n />\n )}\n </div>\n );\n}\n"],"names":["GoLiveButton","enabled","label","hint","live","onGoLive","accented","jsxs","event","jsx","SPEED_OPTIONS","BASE_BUFFER_SECONDS","MAX_SCALED_BUFFER_SECONDS","getScaledBufferSeconds","rate","BASE_MAX_BUFFER_SIZE_BYTES","MAX_SCALED_BUFFER_SIZE_BYTES","getScaledBufferSizeBytes","JUMP_SCAN_THRESHOLD","JUMP_SCAN_BASE_RATE","JUMP_SCAN_INTERVAL_MS","getBufferedAhead","video","at","i","getLiveEdge","getSecondsBehindLive","edge","jumpToLiveEdge","isSafariBrowser","formatTime","seconds","whole","minutes","rest","withReloadMarker","url","withoutReloadMarker","HIDE_DURATION_BELOW","HIDE_TIME_BELOW","PlaybackControls","videoRef","isPlaying","currentTime","duration","isLive","goLive","rowRef","useRef","rowWidth","setRowWidth","useState","useEffect","row","observer","entry","seekable","measured","showDuration","showTime","togglePlay","seekTo","fraction","Pause","Play","rect","step","SpeedIcon","SpeedMenu","speed","setSpeed","showSpeedMenu","setShowSpeedMenu","speedMenuRef","options","disabled","disabledHint","open","option","useHlsEngine","src","hlsRef","engineRef","speedRef","applyPlaybackSpeed","clearJumpScan","autoPlay","onError","debug","debugRef","warn","args","logError","setUrl","setIsLive","engine","setEngine","isLiveRef","forceHlsJsRef","onErrorRef","markLive","selectEngine","next","onLoadedMetadata","removeSharedListeners","cleanUrl","lastTime","stallCounter","nativeErrorCount","interval","onNativeError","previous","savedUrl","Hls","mediaErrorRecoveries","hardReloadTimer","hardReload","reason","scaledBufferSeconds","hlsConfig","hls","_event","data","DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS","POLL_MS","useLiveEdge","toleranceSeconds","secondsBehind","setSecondsBehind","check","useCallback","useClickOutside","ref","active","onOutside","onOutsideRef","onMouseDown","usePlaybackSpeed","initialSpeed","onSpeedChange","setSpeedState","jumpScanIntervalRef","onSpeedChangeRef","targetSpeed","targetExtraSeconds","maxSafeJump","jumpSeconds","seekableEnd","target","scaledSeconds","CHUNK_DURATION_SECONDS","EXTINF_RE","SEGMENT_TIMESTAMP_RE","parseSegmentTimestamp","uri","match","date","time","year","month","day","hour","minute","second","parsed","parseM3u8","feedText","lines","line","headerLines","segments","pendingDuration","sawFirstExtinf","extinfMatch","chunkSegments","chunkSeconds","chunks","current","currentDuration","segment","buildChunkPlaylistText","chunkSegmentList","clockLabel","epochMs","buildRecordingChunks","results","result","resultIndex","spaceId","cameraId","feed","chunkIndex","startTime","durationSec","sum","endTime","a","b","makeBlobUrl","playlistText","blob","chunkAt","chunk","useRecordingSearch","search","setResults","loading","setLoading","error","setError","response","searchError","useMemo","useVideoPlaybackState","setIsPlaying","setCurrentTime","setDuration","onTime","onLoaded","onPlaying","onPause","HlsPlayer","emptyMessage","recordingSearch","recordingAt","recordingChunkSeconds","onRecordingChunks","loadingMessage","muted","height","className","showControls","speedOptions","showGoLive","goLiveMode","goLiveLabel","exitLiveLabel","onExitLive","allowSpeedWhenLive","liveEdgeToleranceSeconds","onLiveChange","onEngineChange","hasExplicitSrc","recordingChunks","recordingUrl","recordingLoading","onRecordingChunksRef","effectiveSrc","menuOpen","behindLiveEdge","onLiveChangeRef","onEngineChangeRef","showingExit","offerGoLive","goLiveEnabled","goLiveHint","goLiveText","handleGoLive","speedLocked"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { default as HlsPlayer } from './HlsPlayer';
|
|
2
|
+
export type { HlsPlayerProps } from './HlsPlayer';
|
|
3
|
+
export { default as PlaybackControls } from './components/PlaybackControls';
|
|
4
|
+
export { default as SpeedMenu } from './components/SpeedMenu';
|
|
5
|
+
export { default as GoLiveButton } from './components/GoLiveButton';
|
|
6
|
+
export { useHlsEngine } from './hooks/useHlsEngine';
|
|
7
|
+
export type { HlsEngineApi } from './hooks/useHlsEngine';
|
|
8
|
+
export { usePlaybackSpeed } from './hooks/usePlaybackSpeed';
|
|
9
|
+
export type { PlaybackSpeedApi } from './hooks/usePlaybackSpeed';
|
|
10
|
+
export { useVideoPlaybackState } from './hooks/useVideoPlaybackState';
|
|
11
|
+
export type { VideoPlaybackState } from './hooks/useVideoPlaybackState';
|
|
12
|
+
export { useLiveEdge, DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS, } from './hooks/useLiveEdge';
|
|
13
|
+
export type { LiveEdgeApi } from './hooks/useLiveEdge';
|
|
14
|
+
export { useClickOutside } from './hooks/useClickOutside';
|
|
15
|
+
export { useRecordingSearch } from './hooks/useRecordingSearch';
|
|
16
|
+
export type { RecordingSearchApi } from './hooks/useRecordingSearch';
|
|
17
|
+
export { buildChunkPlaylistText, buildRecordingChunks, chunkAt, chunkSegments, makeBlobUrl, parseM3u8, parseSegmentTimestamp, CHUNK_DURATION_SECONDS, } from './utils/hlsPlaylist';
|
|
18
|
+
export { formatTime, getBufferedAhead, getLiveEdge, getScaledBufferSeconds, getScaledBufferSizeBytes, getSecondsBehindLive, isSafariBrowser, jumpToLiveEdge, withReloadMarker, withoutReloadMarker, JUMP_SCAN_BASE_RATE, JUMP_SCAN_INTERVAL_MS, JUMP_SCAN_THRESHOLD, SPEED_OPTIONS, } from './utils/hlsPlayerUtils';
|
|
19
|
+
export type { ParsedPlaylist, PlaybackEngine, PlayerError, PlaylistSegment, RecordingChunk, RecordingSearch, RecordingSearchResult, SharedPlayerRefs, } from './types';
|