From 569edfc3aff1680ad8c61865a798657543717ef1 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 6 Feb 2026 00:10:16 -0800 Subject: [PATCH] fix(video-player): hls fallback was cooked two bugs: - chrome without av1 and version >=142 would use hls native. - hls polyfill was never being used because of a bundling mistake. for chrome, they made `video.canPlayType("application/x-mpegURL")` return "maybe", which is total propaganda because chrome does not actually support this type of stream. they support some insane subset that isn't real world use cases. since i was testing windows 7, decided to also fix some IE9 bugs. --- src/file-viewer/backend.tsx | 1 - src/global.css | 14 ++- .../history-of-japan-reanimated.client.ts | 1 - src/pages/history-of-japan-reanimated.marko | 34 ++++---- src/pages/index.css | 2 +- src/q+a/views/permalink.marko | 1 - src/tags/clover-video.client.ts | 87 ++++++++++++++----- 7 files changed, 92 insertions(+), 48 deletions(-) diff --git a/src/file-viewer/backend.tsx b/src/file-viewer/backend.tsx index d744127ecd7194d8fffa276b878772dabef17ec8..5e1a17ecacd5f450bd366b39acaff25e9dd81a71 100644 --- a/src/file-viewer/backend.tsx +++ b/src/file-viewer/backend.tsx @@ -62,7 +62,6 @@ app.get("/file/*", async (c, next) => { // The permissions system is currently binary, using the friend auth. const permissions = FilePermissions.getByPrefix(rawFilePath); if (permissions !== 0) { - console.log(rawFilePath, getForFile(rawFilePath)); const friendAuthChallenge = requireFriendAuth(c, getForFile(rawFilePath)); if (friendAuthChallenge) return friendAuthChallenge; } diff --git a/src/global.css b/src/global.css index 95cfa646e7c3830b7d9132e6f9bc7829dd973e1b..820be21ef2c35f45730d4075e35eec89ba575a32 100644 --- a/src/global.css +++ b/src/global.css @@ -66,8 +66,10 @@ body { } main { + display: block; margin: auto; - max-width: 80ch; + width: auto; + max-width: 768px; padding: 1em; } @@ -86,7 +88,7 @@ figure, ul, ol { margin-top: 1em; - margin-bottom: 1rem; + margin-bottom: 1em; } ul, @@ -100,11 +102,15 @@ li { a { text-decoration: none; + color: var(--primary); +} +a.custom { + color: inherit; } a:where([href]:not(.custom)) { color: var(--primary); - margin: -0.2rem -0.3rem; - padding: 0.2rem 0.3rem; + margin: -0.2em -0.3em; + padding: 0.2em 0.3em; border-radius: 4px; &:hover { text-decoration: underline; diff --git a/src/pages/history-of-japan-reanimated.client.ts b/src/pages/history-of-japan-reanimated.client.ts index 2d053ec6dd77136c34766e0529c91d8beda2f2a2..d9d1f3d0f3fe93365ac7ef0ccadfb443d98f0064 100644 --- a/src/pages/history-of-japan-reanimated.client.ts +++ b/src/pages/history-of-japan-reanimated.client.ts @@ -124,7 +124,6 @@ function updateCredits(video) { } globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => { - console.log(1, id, video); if (id === "main-player") { updateCredits(video); video.addEventListener("timeupdate", () => { diff --git a/src/pages/history-of-japan-reanimated.marko b/src/pages/history-of-japan-reanimated.marko index 4a860cadfb02c35520a19948b14436074f915d9d..863671af05effeabb78a1b6b6b00b751f8099108 100644 --- a/src/pages/history-of-japan-reanimated.marko +++ b/src/pages/history-of-japan-reanimated.marko @@ -26,7 +26,7 @@ export const theme = { me and 16 other artists (music, narration, and animation) have been spending the past couple months on a recreation of bill wurtz's legendary "history of japan" to celebrate the 10 year anniversary of his release. i - contributed two animation sections. the canonical release is on + contributed two animation sections. the canonical release is on LunaPogi's YouTube page, but i am providing this ad-free video player.

@@ -49,22 +49,22 @@ export const theme = {

diff --git a/src/pages/index.css b/src/pages/index.css index c9e4176e75d230808f12088236a0e974f1bc8cf9..bd1994a8235c8cc4aa772fa4ad748c2c10339405 100644 --- a/src/pages/index.css +++ b/src/pages/index.css @@ -4,7 +4,7 @@ main { flex-direction: column; justify-content: center; align-items: center; - height: 100vh; + min-height: 100vh; & > div { width: 450px; margin: auto; diff --git a/src/q+a/views/permalink.marko b/src/q+a/views/permalink.marko index b9e2d3d602e239c3170547124181a437b15de461..64500a56967d3c20419c02f2fd63a22134673a53 100644 --- a/src/q+a/views/permalink.marko +++ b/src/q+a/views/permalink.marko @@ -4,7 +4,6 @@ export interface Input { } server export function meta({ context, question }) { - console.log(context.req.header("user-agent")); const isDiscord = context.req.header("user-agent") ?.toLowerCase() .includes("discord"); diff --git a/src/tags/clover-video.client.ts b/src/tags/clover-video.client.ts index 0c33bd467afa2e79298b8e0456d6db6d753a7721..8337506fd8f89f75ade682d2b0c058fe8fb37ab6 100644 --- a/src/tags/clover-video.client.ts +++ b/src/tags/clover-video.client.ts @@ -2,6 +2,12 @@ // Polyfill for old browsers. type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none"; +declare global { + function hydrateVideo(container: HTMLElement): void; + var ManagedMediaSource: typeof MediaSource | null; + var onCloverVideoInit: null | ((id: string, video: HTMLVideoElement) => void); +} + const forceMode = location.search.match(/(?:\?|&)force-player=(.*)/)?.[1] as | PlaybackMode | undefined; @@ -16,7 +22,6 @@ Promise.allSettled ??= (promises: Iterable>) => (reason) => ({ status: "rejected" as const, reason }), ))); -declare function hydrateVideo(container: HTMLElement): void; globalThis.hydrateVideo = function hydrateVideo(container) { mode ??= decidePlaybackMode(); mode.then((mode) => hydrateVideoInner(container, mode)); @@ -27,9 +32,6 @@ Array.from( (container) => hydrateVideo(container), ); -// TODO: be less stupid about this -const onCloverVideoInit = window.onCloverVideoInit; - function decidePlaybackMode(): Promise { if (forceMode) { if (forceMode === "hls-polyfill") { @@ -49,27 +51,41 @@ function decidePlaybackMode(): Promise { // Software decoding is too slow to use, unfortunately. // Consider a device like a 2014 two core laptop. console.warn(`implements non-accelerated av1+opus`); + + // but on my slow Windows 7 laptop on Chrome, the HLS polyfill lags + // even on stable internet, so the av1 encoder ends up being better. + if (navigator.platform === "Win32") { + const chromeVersion = /Chrome\/(\d+)/.exec(navigator.userAgent)?.[1]; + if (chromeVersion && Number(chromeVersion) < 110) { + console.warn("using av1 since win32 chrome runs hls.js badly"); + return "none"; + } + } } else { console.warn(`does not implement av1+opus`); } - // Native HLS support (Safari v6) - const video = document.querySelector("video")!; - if (video.canPlayType("application/x-mpegURL")) { - return "hls-native"; - } - - // @ts-expect-error - return (hls ??= import("/js/scripts/vendor/hls.js")) - .then((hls): PlaybackMode => { - // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8) - // TODO: ES modules have a minimum of Chrome 63 or Firefox 60 - if (hls.default.isSupported()) return "hls-polyfill"; - // Other browsers - return "none"; - }); + return canNativelyPlayHls().then((nativeHls): PlaybackMode => { + if (nativeHls) return "hls-native"; + // @ts-expect-error + return (hls ??= import("/js/scripts/vendor/hls.js")) + .then((hls): PlaybackMode => { + // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8) + // TODO: ES modules have a minimum of Chrome 63 or Firefox 60 + if (hls?.default?.isSupported?.() || hls?.isSupported()) { + return "hls-polyfill"; + } + console.warn("does not support hls.js"); + // Other browsers + return "none"; + }); + }); + }) + .catch((e): PlaybackMode => { + console.warn(e); + console.warn("falling back to default player"); + return "none"; }) - .catch((): PlaybackMode => "none") .then((mode) => { console.info(`selected playback mode: ${mode}`); return mode; @@ -115,7 +131,8 @@ function hydrateVideoInner( enableSafariAirplay(); player.initialize(video, dashFile, false); video.controls = true; - onCloverVideoInit?.(id, video); + // TODO: be less stupid about this + window.onCloverVideoInit?.(id, video); return; } @@ -219,7 +236,6 @@ function hydrateVideoInner( onCloverVideoInit?.(id, video); } else if (mode === "hls-polyfill") { hls!.then((hls) => { - console.log(hls); // @ts-expect-error suspicious const player = new (hls.default ?? hls.Hls)(); player.loadSource(hlsFile); @@ -234,7 +250,6 @@ function hydrateVideoInner( } } -declare var ManagedMediaSource: typeof MediaSource | null; function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> { const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource; if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) { @@ -268,3 +283,29 @@ function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> { })); } } + +function canNativelyPlayHls() { + const video = document.createElement("video"); + const canPlay = video.canPlayType("application/x-mpegURL"); + + if (!canPlay) return Promise.resolve(false); + + return new Promise((resolve) => { + video.src = "data:application/x-mpegURL,#EXTM3U"; + video.load(); + + function cleanup() { + clearTimeout(timeout); + video.removeEventListener("error", onError); + video.removeEventListener("loadedmetadata", onLoad); + video.src = ""; + } + + const onError = () => (cleanup(), resolve(false)); + const onLoad = () => (cleanup(), resolve(true)); + const timeout = setTimeout(onError, 100); + + video.addEventListener("error", onError); + video.addEventListener("loadedmetadata", onLoad); + }); +} -- 2.54.0