authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-06 00:10:16-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-06 00:14:48-08:00
log569edfc3aff1680ad8c61865a798657543717ef1
tree833d911d4d84358192ceba9fc6c7cfd8e48338be
parenta5bd9c290e07a3d724b254b0306bba8ea76268d2
signature Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

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.

7 files changed, 92 insertions(+), 48 deletions(-)

src/file-viewer/backend.tsx-1
......@@ -62,7 +62,6 @@ app.get("/file/*", async (c, next) => {
6262 // The permissions system is currently binary, using the friend auth.
6363 const permissions = FilePermissions.getByPrefix(rawFilePath);
6464 if (permissions !== 0) {
65 console.log(rawFilePath, getForFile(rawFilePath));
6665 const friendAuthChallenge = requireFriendAuth(c, getForFile(rawFilePath));
6766 if (friendAuthChallenge) return friendAuthChallenge;
6867 }
src/global.css+10-4
......@@ -66,8 +66,10 @@ body {
6666}
6767
6868main {
69 display: block;
6970 margin: auto;
70 max-width: 80ch;
71 width: auto;
72 max-width: 768px;
7173 padding: 1em;
7274}
7375
......@@ -86,7 +88,7 @@ figure,
8688ul,
8789ol {
8890 margin-top: 1em;
89 margin-bottom: 1rem;
91 margin-bottom: 1em;
9092}
9193
9294ul,
......@@ -100,11 +102,15 @@ li {
100102
101103a {
102104 text-decoration: none;
105 color: var(--primary);
106}
107a.custom {
108 color: inherit;
103109}
104110a:where([href]:not(.custom)) {
105111 color: var(--primary);
106 margin: -0.2rem -0.3rem;
107 padding: 0.2rem 0.3rem;
112 margin: -0.2em -0.3em;
113 padding: 0.2em 0.3em;
108114 border-radius: 4px;
109115 &:hover {
110116 text-decoration: underline;
src/pages/history-of-japan-reanimated.client.ts-1
......@@ -124,7 +124,6 @@ function updateCredits(video) {
124124}
125125
126126globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => {
127 console.log(1, id, video);
128127 if (id === "main-player") {
129128 updateCredits(video);
130129 video.addEventListener("timeupdate", () => {
src/pages/history-of-japan-reanimated.marko+17-17
......@@ -26,7 +26,7 @@ export const theme = {
2626 me and 16 other artists (music, narration, and animation) have been
2727 spending the past couple months on a recreation of bill wurtz's legendary
2828 "<a href="https://billwurtz.com/history-of-japan.html">history of japan</a>" to celebrate the 10 year anniversary of his release. i
29 contributed two animation sections. the canonical release is on
29 contributed two animation sections. the canonical release is on
3030 <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i
3131 am providing this ad-free video player.
3232 </p>
......@@ -49,22 +49,22 @@ export const theme = {
4949 </p>
5050 <ul>
5151 <li><a href="https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg">Kappacap</a> - thumbnail artist</li>
52 <li><a href="https://www.tumblr.com/atamai-m8">atamai</a> - animation <span style="color:#fff9">(0:03-1:02)</></li>
53 <li><a href="https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow">moxley</a> - music and narration <span style="color:#fff9">(0:03-3:10)</>, animation <span style="color:#fff9">(3:51-4:03)</></li>
54 <li><a href="https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw">TheJustinator</a> - animation <span style="color:#fff9">(1:03-1:33, 2:02-2:33)</>, music <span style="color:#fff9">(3:11-6:04)</></li>
55 <li><a href="https://chaosyumi.net">chaosyumi</a> - animation <span style="color:#fff9">(1:34-2:01, 5:34-6:05)</></li>
56 <li><a href="https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ">LunaPogi</a> - animation <span style="color:#fff9">(3:36-3:51, 8:01-9:10)</>, music <span style="color:#fff9">(6:05-9:10)</>, narration <span style="color:#fff9">(8:01-9:10)</></li>
57 <li><a href="https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ">zeccet (bgongo)</a> - animation <span style="color:#fff9">(2:23-3:10)</></li>
58 <li><a href="https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg">TheRealTinyWorld</a> - animation <span style="color:#fff9">(3:11-3:35)</></li>
59 <li><a href="https://www.castingcall.club/fredfrenchiii">Fred French VA</a> - narration <span style="color:#fff9">(3:11-4:35)</></li>
60 <li><a href="https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ">rqfirqfo</a> - animation <span style="color:#fff9">(4:04-4:35)</></li>
61 <li><a href="https://www.youtube.com/@krispykarim">krispykarim</a> - animation <span style="color:#fff9">(4:36 - 5:04)</>, narration <span style="color:#fff9">(4:36-8:01)</></li>
62 <li><a href="https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg">SimplyDoodled Studios</a> - animation <span style="color:#fff9">(5:05-5:33)</></li>
63 <li><span style="color:#ff7de9">paper clover (me)</span> - animation <span style="color:#fff9">(6:05-6:38)</>, credits animation <span style="color:#fff9">(9:11-9:35)</></li>
64 <li><a href="https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg">Coffee</a> - transcription <span style="color:#fff9">(6:05-9:10)</></li>
65 <li><a href="https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw">harjjw</a> - animation <span style="color:#fff9">(6:38-7:03)</></li>
66 <li><a href="https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ">yellowmarkers</a> - animation <span style="color:#fff9">(7:04-7:36)</></li>
67 <li><a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg">TheNothingGuy07</a> - animation <span style="color:#fff9">(7:37-7:59)</></li>
52 <li><a href="https://www.tumblr.com/atamai-m8">atamai</a> - animation <span style="color:#B4A7AD">(0:03-1:02)</></li>
53 <li><a href="https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow">moxley</a> - music and narration <span style="color:#B4A7AD">(0:03-3:10)</>, animation <span style="color:#B4A7AD">(3:51-4:03)</></li>
54 <li><a href="https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw">TheJustinator</a> - animation <span style="color:#B4A7AD">(1:03-1:33, 2:02-2:33)</>, music <span style="color:#B4A7AD">(3:11-6:04)</></li>
55 <li><a href="https://chaosyumi.net">chaosyumi</a> - animation <span style="color:#B4A7AD">(1:34-2:01, 5:34-6:05)</></li>
56 <li><a href="https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ">LunaPogi</a> - animation <span style="color:#B4A7AD">(3:36-3:51, 8:01-9:10)</>, music <span style="color:#B4A7AD">(6:05-9:10)</>, narration <span style="color:#B4A7AD">(8:01-9:10)</></li>
57 <li><a href="https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ">zeccet (bgongo)</a> - animation <span style="color:#B4A7AD">(2:23-3:10)</></li>
58 <li><a href="https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg">TheRealTinyWorld</a> - animation <span style="color:#B4A7AD">(3:11-3:35)</></li>
59 <li><a href="https://www.castingcall.club/fredfrenchiii">Fred French VA</a> - narration <span style="color:#B4A7AD">(3:11-4:35)</></li>
60 <li><a href="https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ">rqfirqfo</a> - animation <span style="color:#B4A7AD">(4:04-4:35)</></li>
61 <li><a href="https://www.youtube.com/@krispykarim">krispykarim</a> - animation <span style="color:#B4A7AD">(4:36 - 5:04)</>, narration <span style="color:#B4A7AD">(4:36-8:01)</></li>
62 <li><a href="https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg">SimplyDoodled Studios</a> - animation <span style="color:#B4A7AD">(5:05-5:33)</></li>
63 <li><span style="color:#ff7de9">paper clover (me)</span> - animation <span style="color:#B4A7AD">(6:05-6:38)</>, credits animation <span style="color:#B4A7AD">(9:11-9:35)</></li>
64 <li><a href="https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg">Coffee</a> - transcription <span style="color:#B4A7AD">(6:05-9:10)</></li>
65 <li><a href="https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw">harjjw</a> - animation <span style="color:#B4A7AD">(6:38-7:03)</></li>
66 <li><a href="https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ">yellowmarkers</a> - animation <span style="color:#B4A7AD">(7:04-7:36)</></li>
67 <li><a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg">TheNothingGuy07</a> - animation <span style="color:#B4A7AD">(7:37-7:59)</></li>
6868 </ul>
6969
7070 <p>
src/pages/index.css+1-1
......@@ -4,7 +4,7 @@ main {
44 flex-direction: column;
55 justify-content: center;
66 align-items: center;
7 height: 100vh;
7 min-height: 100vh;
88 & > div {
99 width: 450px;
1010 margin: auto;
src/q+a/views/permalink.marko-1
......@@ -4,7 +4,6 @@ export interface Input {
44}
55
66server export function meta({ context, question }) {
7 console.log(context.req.header("user-agent"));
87 const isDiscord = context.req.header("user-agent")
98 ?.toLowerCase()
109 .includes("discord");
src/tags/clover-video.client.ts+64-23
......@@ -2,6 +2,12 @@
22
33// Polyfill for old browsers.
44type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none";
5declare global {
6 function hydrateVideo(container: HTMLElement): void;
7 var ManagedMediaSource: typeof MediaSource | null;
8 var onCloverVideoInit: null | ((id: string, video: HTMLVideoElement) => void);
9}
10
511const forceMode = location.search.match(/(?:\?|&)force-player=(.*)/)?.[1] as
612 | PlaybackMode
713 | undefined;
......@@ -16,7 +22,6 @@ Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) =>
1622 (reason) => ({ status: "rejected" as const, reason }),
1723 )));
1824
19declare function hydrateVideo(container: HTMLElement): void;
2025globalThis.hydrateVideo = function hydrateVideo(container) {
2126 mode ??= decidePlaybackMode();
2227 mode.then((mode) => hydrateVideoInner(container, mode));
......@@ -27,9 +32,6 @@ Array.from(
2732 (container) => hydrateVideo(container),
2833);
2934
30// TODO: be less stupid about this
31const onCloverVideoInit = window.onCloverVideoInit;
32
3335function decidePlaybackMode(): Promise<PlaybackMode> {
3436 if (forceMode) {
3537 if (forceMode === "hls-polyfill") {
......@@ -49,27 +51,41 @@ function decidePlaybackMode(): Promise<PlaybackMode> {
4951 // Software decoding is too slow to use, unfortunately.
5052 // Consider a device like a 2014 two core laptop.
5153 console.warn(`implements non-accelerated av1+opus`);
54
55 // but on my slow Windows 7 laptop on Chrome, the HLS polyfill lags
56 // even on stable internet, so the av1 encoder ends up being better.
57 if (navigator.platform === "Win32") {
58 const chromeVersion = /Chrome\/(\d+)/.exec(navigator.userAgent)?.[1];
59 if (chromeVersion && Number(chromeVersion) < 110) {
60 console.warn("using av1 since win32 chrome runs hls.js badly");
61 return "none";
62 }
63 }
5264 } else {
5365 console.warn(`does not implement av1+opus`);
5466 }
5567
56 // Native HLS support (Safari v6)
57 const video = document.querySelector("video")!;
58 if (video.canPlayType("application/x-mpegURL")) {
59 return "hls-native";
60 }
61
62 // @ts-expect-error
63 return (hls ??= import("/js/scripts/vendor/hls.js"))
64 .then((hls): PlaybackMode => {
65 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)
66 // TODO: ES modules have a minimum of Chrome 63 or Firefox 60
67 if (hls.default.isSupported()) return "hls-polyfill";
68 // Other browsers
69 return "none";
70 });
68 return canNativelyPlayHls().then((nativeHls): PlaybackMode => {
69 if (nativeHls) return "hls-native";
70 // @ts-expect-error
71 return (hls ??= import("/js/scripts/vendor/hls.js"))
72 .then((hls): PlaybackMode => {
73 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)
74 // TODO: ES modules have a minimum of Chrome 63 or Firefox 60
75 if (hls?.default?.isSupported?.() || hls?.isSupported()) {
76 return "hls-polyfill";
77 }
78 console.warn("does not support hls.js");
79 // Other browsers
80 return "none";
81 });
82 });
83 })
84 .catch((e): PlaybackMode => {
85 console.warn(e);
86 console.warn("falling back to default player");
87 return "none";
7188 })
72 .catch((): PlaybackMode => "none")
7389 .then((mode) => {
7490 console.info(`selected playback mode: ${mode}`);
7591 return mode;
......@@ -115,7 +131,8 @@ function hydrateVideoInner(
115131 enableSafariAirplay();
116132 player.initialize(video, dashFile, false);
117133 video.controls = true;
118 onCloverVideoInit?.(id, video);
134 // TODO: be less stupid about this
135 window.onCloverVideoInit?.(id, video);
119136 return;
120137 }
121138
......@@ -219,7 +236,6 @@ function hydrateVideoInner(
219236 onCloverVideoInit?.(id, video);
220237 } else if (mode === "hls-polyfill") {
221238 hls!.then((hls) => {
222 console.log(hls);
223239 // @ts-expect-error suspicious
224240 const player = new (hls.default ?? hls.Hls)();
225241 player.loadSource(hlsFile);
......@@ -234,7 +250,6 @@ function hydrateVideoInner(
234250 }
235251}
236252
237declare var ManagedMediaSource: typeof MediaSource | null;
238253function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
239254 const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource;
240255 if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) {
......@@ -268,3 +283,29 @@ function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
268283 }));
269284 }
270285}
286
287function canNativelyPlayHls() {
288 const video = document.createElement("video");
289 const canPlay = video.canPlayType("application/x-mpegURL");
290
291 if (!canPlay) return Promise.resolve(false);
292
293 return new Promise((resolve) => {
294 video.src = "data:application/x-mpegURL,#EXTM3U";
295 video.load();
296
297 function cleanup() {
298 clearTimeout(timeout);
299 video.removeEventListener("error", onError);
300 video.removeEventListener("loadedmetadata", onLoad);
301 video.src = "";
302 }
303
304 const onError = () => (cleanup(), resolve(false));
305 const onLoad = () => (cleanup(), resolve(true));
306 const timeout = setTimeout(onError, 100);
307
308 video.addEventListener("error", onError);
309 video.addEventListener("loadedmetadata", onLoad);
310 });
311}