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) => {...@@ -62,7 +62,6 @@ app.get("/file/*", async (c, next) => {
62 // The permissions system is currently binary, using the friend auth.62 // The permissions system is currently binary, using the friend auth.
63 const permissions = FilePermissions.getByPrefix(rawFilePath);63 const permissions = FilePermissions.getByPrefix(rawFilePath);
64 if (permissions !== 0) {64 if (permissions !== 0) {
65 console.log(rawFilePath, getForFile(rawFilePath));
66 const friendAuthChallenge = requireFriendAuth(c, getForFile(rawFilePath));65 const friendAuthChallenge = requireFriendAuth(c, getForFile(rawFilePath));
67 if (friendAuthChallenge) return friendAuthChallenge;66 if (friendAuthChallenge) return friendAuthChallenge;
68 }67 }
src/global.css+10-4
...@@ -66,8 +66,10 @@ body {...@@ -66,8 +66,10 @@ body {
66}66}
6767
68main {68main {
69 display: block;
69 margin: auto;70 margin: auto;
70 max-width: 80ch;71 width: auto;
72 max-width: 768px;
71 padding: 1em;73 padding: 1em;
72}74}
7375
...@@ -86,7 +88,7 @@ figure,...@@ -86,7 +88,7 @@ figure,
86ul,88ul,
87ol {89ol {
88 margin-top: 1em;90 margin-top: 1em;
89 margin-bottom: 1rem;91 margin-bottom: 1em;
90}92}
9193
92ul,94ul,
...@@ -100,11 +102,15 @@ li {...@@ -100,11 +102,15 @@ li {
100102
101a {103a {
102 text-decoration: none;104 text-decoration: none;
105 color: var(--primary);
106}
107a.custom {
108 color: inherit;
103}109}
104a:where([href]:not(.custom)) {110a:where([href]:not(.custom)) {
105 color: var(--primary);111 color: var(--primary);
106 margin: -0.2rem -0.3rem;112 margin: -0.2em -0.3em;
107 padding: 0.2rem 0.3rem;113 padding: 0.2em 0.3em;
108 border-radius: 4px;114 border-radius: 4px;
109 &:hover {115 &:hover {
110 text-decoration: underline;116 text-decoration: underline;
src/pages/history-of-japan-reanimated.client.ts-1
...@@ -124,7 +124,6 @@ function updateCredits(video) {...@@ -124,7 +124,6 @@ function updateCredits(video) {
124}124}
125125
126globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => {126globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => {
127 console.log(1, id, video);
128 if (id === "main-player") {127 if (id === "main-player") {
129 updateCredits(video);128 updateCredits(video);
130 video.addEventListener("timeupdate", () => {129 video.addEventListener("timeupdate", () => {
src/pages/history-of-japan-reanimated.marko+17-17
...@@ -26,7 +26,7 @@ export const theme = {...@@ -26,7 +26,7 @@ export const theme = {
26 me and 16 other artists (music, narration, and animation) have been26 me and 16 other artists (music, narration, and animation) have been
27 spending the past couple months on a recreation of bill wurtz's legendary27 spending the past couple months on a recreation of bill wurtz's legendary
28 "<a href="https://billwurtz.com/history-of-japan.html">history of japan</a>" to celebrate the 10 year anniversary of his release. i28 "<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
30 <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i30 <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i
31 am providing this ad-free video player.31 am providing this ad-free video player.
32 </p>32 </p>
...@@ -49,22 +49,22 @@ export const theme = {...@@ -49,22 +49,22 @@ export const theme = {
49 </p>49 </p>
50 <ul>50 <ul>
51 <li><a href="https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg">Kappacap</a> - thumbnail artist</li>51 <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>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:#fff9">(0:03-3:10)</>, animation <span style="color:#fff9">(3:51-4:03)</></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:#fff9">(1:03-1:33, 2:02-2:33)</>, music <span style="color:#fff9">(3:11-6:04)</></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:#fff9">(1:34-2:01, 5:34-6:05)</></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:#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>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:#fff9">(2:23-3: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:#fff9">(3:11-3:35)</></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:#fff9">(3:11-4: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:#fff9">(4:04-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:#fff9">(4:36 - 5:04)</>, narration <span style="color:#fff9">(4:36-8:01)</></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:#fff9">(5:05-5:33)</></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:#fff9">(6:05-6:38)</>, credits animation <span style="color:#fff9">(9:11-9:35)</></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:#fff9">(6:05-9:10)</></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:#fff9">(6:38-7:03)</></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:#fff9">(7:04-7:36)</></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:#fff9">(7:37-7:59)</></li>67 <li><a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg">TheNothingGuy07</a> - animation <span style="color:#B4A7AD">(7:37-7:59)</></li>
68 </ul>68 </ul>
6969
70 <p>70 <p>
src/pages/index.css+1-1
...@@ -4,7 +4,7 @@ main {...@@ -4,7 +4,7 @@ main {
4 flex-direction: column;4 flex-direction: column;
5 justify-content: center;5 justify-content: center;
6 align-items: center;6 align-items: center;
7 height: 100vh;7 min-height: 100vh;
8 & > div {8 & > div {
9 width: 450px;9 width: 450px;
10 margin: auto;10 margin: auto;
src/q+a/views/permalink.marko-1
...@@ -4,7 +4,6 @@ export interface Input {...@@ -4,7 +4,6 @@ export interface Input {
4}4}
55
6server export function meta({ context, question }) {6server export function meta({ context, question }) {
7 console.log(context.req.header("user-agent"));
8 const isDiscord = context.req.header("user-agent")7 const isDiscord = context.req.header("user-agent")
9 ?.toLowerCase()8 ?.toLowerCase()
10 .includes("discord");9 .includes("discord");
src/tags/clover-video.client.ts+64-23
...@@ -2,6 +2,12 @@...@@ -2,6 +2,12 @@
22
3// Polyfill for old browsers.3// Polyfill for old browsers.
4type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none";4type 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
5const forceMode = location.search.match(/(?:\?|&)force-player=(.*)/)?.[1] as11const forceMode = location.search.match(/(?:\?|&)force-player=(.*)/)?.[1] as
6 | PlaybackMode12 | PlaybackMode
7 | undefined;13 | undefined;
...@@ -16,7 +22,6 @@ Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) =>...@@ -16,7 +22,6 @@ Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) =>
16 (reason) => ({ status: "rejected" as const, reason }),22 (reason) => ({ status: "rejected" as const, reason }),
17 )));23 )));
1824
19declare function hydrateVideo(container: HTMLElement): void;
20globalThis.hydrateVideo = function hydrateVideo(container) {25globalThis.hydrateVideo = function hydrateVideo(container) {
21 mode ??= decidePlaybackMode();26 mode ??= decidePlaybackMode();
22 mode.then((mode) => hydrateVideoInner(container, mode));27 mode.then((mode) => hydrateVideoInner(container, mode));
...@@ -27,9 +32,6 @@ Array.from(...@@ -27,9 +32,6 @@ Array.from(
27 (container) => hydrateVideo(container),32 (container) => hydrateVideo(container),
28);33);
2934
30// TODO: be less stupid about this
31const onCloverVideoInit = window.onCloverVideoInit;
32
33function decidePlaybackMode(): Promise<PlaybackMode> {35function decidePlaybackMode(): Promise<PlaybackMode> {
34 if (forceMode) {36 if (forceMode) {
35 if (forceMode === "hls-polyfill") {37 if (forceMode === "hls-polyfill") {
...@@ -49,27 +51,41 @@ function decidePlaybackMode(): Promise<PlaybackMode> {...@@ -49,27 +51,41 @@ function decidePlaybackMode(): Promise<PlaybackMode> {
49 // Software decoding is too slow to use, unfortunately.51 // Software decoding is too slow to use, unfortunately.
50 // Consider a device like a 2014 two core laptop.52 // Consider a device like a 2014 two core laptop.
51 console.warn(`implements non-accelerated av1+opus`);53 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 }
52 } else {64 } else {
53 console.warn(`does not implement av1+opus`);65 console.warn(`does not implement av1+opus`);
54 }66 }
5567
56 // Native HLS support (Safari v6)68 return canNativelyPlayHls().then((nativeHls): PlaybackMode => {
57 const video = document.querySelector("video")!;69 if (nativeHls) return "hls-native";
58 if (video.canPlayType("application/x-mpegURL")) {70 // @ts-expect-error
59 return "hls-native";71 return (hls ??= import("/js/scripts/vendor/hls.js"))
60 }72 .then((hls): PlaybackMode => {
6173 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)
62 // @ts-expect-error74 // TODO: ES modules have a minimum of Chrome 63 or Firefox 60
63 return (hls ??= import("/js/scripts/vendor/hls.js"))75 if (hls?.default?.isSupported?.() || hls?.isSupported()) {
64 .then((hls): PlaybackMode => {76 return "hls-polyfill";
65 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)77 }
66 // TODO: ES modules have a minimum of Chrome 63 or Firefox 6078 console.warn("does not support hls.js");
67 if (hls.default.isSupported()) return "hls-polyfill";79 // Other browsers
68 // Other browsers80 return "none";
69 return "none";81 });
70 });82 });
83 })
84 .catch((e): PlaybackMode => {
85 console.warn(e);
86 console.warn("falling back to default player");
87 return "none";
71 })88 })
72 .catch((): PlaybackMode => "none")
73 .then((mode) => {89 .then((mode) => {
74 console.info(`selected playback mode: ${mode}`);90 console.info(`selected playback mode: ${mode}`);
75 return mode;91 return mode;
...@@ -115,7 +131,8 @@ function hydrateVideoInner(...@@ -115,7 +131,8 @@ function hydrateVideoInner(
115 enableSafariAirplay();131 enableSafariAirplay();
116 player.initialize(video, dashFile, false);132 player.initialize(video, dashFile, false);
117 video.controls = true;133 video.controls = true;
118 onCloverVideoInit?.(id, video);134 // TODO: be less stupid about this
135 window.onCloverVideoInit?.(id, video);
119 return;136 return;
120 }137 }
121138
...@@ -219,7 +236,6 @@ function hydrateVideoInner(...@@ -219,7 +236,6 @@ function hydrateVideoInner(
219 onCloverVideoInit?.(id, video);236 onCloverVideoInit?.(id, video);
220 } else if (mode === "hls-polyfill") {237 } else if (mode === "hls-polyfill") {
221 hls!.then((hls) => {238 hls!.then((hls) => {
222 console.log(hls);
223 // @ts-expect-error suspicious239 // @ts-expect-error suspicious
224 const player = new (hls.default ?? hls.Hls)();240 const player = new (hls.default ?? hls.Hls)();
225 player.loadSource(hlsFile);241 player.loadSource(hlsFile);
...@@ -234,7 +250,6 @@ function hydrateVideoInner(...@@ -234,7 +250,6 @@ function hydrateVideoInner(
234 }250 }
235}251}
236252
237declare var ManagedMediaSource: typeof MediaSource | null;
238function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {253function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
239 const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource;254 const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource;
240 if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) {255 if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) {
...@@ -268,3 +283,29 @@ function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {...@@ -268,3 +283,29 @@ function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
268 }));283 }));
269 }284 }
270}285}
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}