diff --git a/bun.lock b/bun.lock index d9b5304..ac06424 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "myanimelist-ui", "dependencies": { + "hls.js": "^1.6.16", "htmx.org": "1.9.12", }, "devDependencies": { @@ -185,6 +186,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hls.js": ["hls.js@1.6.16", "", {}, "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA=="], + "htmx.org": ["htmx.org@1.9.12", "", {}, "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], diff --git a/package.json b/package.json index 1457ec4..fb1a5ce 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint:go": "golangci-lint run ./..." }, "dependencies": { + "hls.js": "^1.6.16", "htmx.org": "1.9.12" }, "devDependencies": { diff --git a/static/player/video.ts b/static/player/video.ts index 5544806..91d7a0e 100644 --- a/static/player/video.ts +++ b/static/player/video.ts @@ -1,25 +1,54 @@ +import Hls from "hls.js"; import { state } from "./state"; import { absoluteTimeFromDisplay, displayTimeFromAbsolute, invalidateBounds } from "./timeline"; +let hls: Hls | null = null; + +const destroyHLS = (): void => { + hls?.destroy(); + hls = null; +}; + +export const destroyVideoSource = (): void => { + destroyHLS(); + state.video.pause(); + state.video.removeAttribute("src"); + state.video.load(); +}; + +const shouldUseHLS = (type: string | undefined, url: string): boolean => { + if (type === "m3u8") return true; + try { + const parsed = new URL(url, window.location.href); + return parsed.pathname.toLowerCase().endsWith(".m3u8"); + } catch { + return url.toLowerCase().includes(".m3u8"); + } +}; + /** * Force-loads a new video source and preserves playback position. * * Some browsers can be flaky when switching between HLS URLs while playing. * Clearing `src` first ensures the media element fully resets before the new URL is set. */ -export const loadVideoSource = (url: string): void => { +export const loadVideoSource = (url: string, type?: string): void => { if (!url) return; const wasPlaying = !state.video.paused; const prevDisplayTime = displayTimeFromAbsolute(state.video.currentTime); // Fully reset the element before setting a new source. - state.video.pause(); - state.video.removeAttribute("src"); - state.video.load(); + destroyVideoSource(); - state.video.src = url; - state.video.load(); + if (shouldUseHLS(type, url) && Hls.isSupported()) { + hls = new Hls(); + hls.loadSource(url); + hls.attachMedia(state.video); + } else { + state.video.src = url; + state.video.load(); + } // Try an eager seek; if metadata isn't ready yet, main.ts will restore via pendingSeekTime. state.pendingSeekTime = prevDisplayTime;