diff options
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 61 |
1 files changed, 57 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index bb2d788..ef427eb 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -6152,6 +6152,7 @@ export function messagesFeature(ctx) { const READY_MS = 8000; const mediaReady = new Map(); // URL -> decoded dimensions const mediaFailed = new Set(); // URLs whose picture refused to load (not merely slow) + const corsRefused = new Set(); // pictures whose host wouldn't hand over the bytes (this session) const mediaWarming = new Map(); // A post's photo is often the camera's original — 3024px wide, twelve // megapixels — painted into a column a sixth of that. Chrome decodes it @@ -6185,7 +6186,7 @@ export function messagesFeature(ctx) { if (!res.ok) return null; blob = await res.blob(); } catch (e) { - if (e && e.corsRefused) noteNoCors(url); // refused, as corsFetch confirmed + if (e && e.corsRefused) { noteNoCors(url); corsRefused.add(url); } // refused, as corsFetch confirmed return null; } const result = await makeImageThumbnails(blob, { @@ -6216,7 +6217,10 @@ export function messagesFeature(ctx) { if (mediaWarming.has(url)) return mediaWarming.get(url); if (thumb && /^https?:/i.test(url)) { const task = (async () => { - const got = (await storedMediaThumb(url)) || (await makeMediaThumb(url).catch(() => null)); + let got = (await storedMediaThumb(url)) || (await makeMediaThumb(url).catch(() => null)); + // no copy to be made here (the host keeps its bytes, or no worker to + // make it): the server makes one + if (!got && (noCorsHost(url) || corsRefused.has(url) || typeof OffscreenCanvas === 'undefined')) got = await serverMediaThumb(url).catch(() => null); if (got) { // no copy (small already, or a GIF): the original is what paints, // so it's the one decoded ahead — the row mustn't decode it on sight @@ -7383,6 +7387,47 @@ export function messagesFeature(ctx) { const h264Of = (url) => VMEDIA + 'h264.mp4?u=' + encodeURIComponent(url); const noPicture = new Set(), h264Failed = new Set(); const serverThumbFailed = new Set(); // clips vmedia had no picture for (this session) + // A picture from a host that won't hand over its bytes (no CORS: primal's + // blossom, nostrcheck, most personal sites) can't have its column copy made + // here, so the server makes it (pic.webp: 1100px wide, or a 360px square + // for the grid; libvips, cached for everyone at the CDN). Without this the + // <img> painted the original, and a 9528×6328 camera file froze a wheel + // scroll for 400 ms on its texture upload. The copy is kept in the same + // cache as the ones made here, so the next visit paints it at once. + const serverPic = (url, shape) => (vmediaOk(url) && !/\.gif(\?|$)/i.test(url) ? VMEDIA + 'pic.webp?s=' + shape + '&u=' + encodeURIComponent(url) : null); + const serverPicFailed = new Set(); // pictures the server had no copy for (this session) + const picDims = (src) => new Promise((resolve) => { + const img = new Image(); + img.onload = () => (img.decode ? img.decode() : Promise.resolve()).then(() => resolve({ width: img.naturalWidth, height: img.naturalHeight }), () => resolve(null)); + img.onerror = () => resolve(null); + img.src = src; + }); + async function serverPicBlob(url, shape) { + const src = serverPic(url, shape); + if (!src || serverPicFailed.has(shape + '|' + url)) return null; + let res; + try { res = await fetch(src, { signal: AbortSignal.timeout(READY_MS) }); } catch { return null; } + if (!res.ok) { + if (res.status !== 429 && res.status !== 503) serverPicFailed.add(shape + '|' + url); // busy is tried again + return null; + } + const blob = await res.blob(); + return /^image\//.test(blob.type) && blob.size ? blob : null; + } + async function serverMediaThumb(url) { + const blob = await serverPicBlob(url, 'col'); + if (!blob) return null; + const thumb = URL.createObjectURL(blob); + const dims = await picDims(thumb); + if (!dims || !dims.width) { URL.revokeObjectURL(thumb); serverPicFailed.add('col|' + url); return null; } + try { + const store = await thumbStore(); + await store.put(url, new Response(blob, { headers: { 'x-dims': JSON.stringify(dims), 'x-original': '0' } })); + const keys = await store.keys(); + for (const k of keys.slice(0, Math.max(0, keys.length - MEDIA_THUMB_MAX))) store.delete(k); + } catch {} + return { ...dims, thumb }; + } let hevcPlays; const canHevc = () => hevcPlays ??= (() => { try { return !!document.createElement('video').canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"'); } catch { return true; } @@ -11388,13 +11433,21 @@ export function messagesFeature(ctx) { if (typeof createImageBitmap === 'undefined' || typeof OffscreenCanvas === 'undefined' || typeof caches === 'undefined') return; // the column copy is already here and smaller than the original const kept = mediaReady.get(url)?.thumb; - if (!kept && noCorsHost(url)) return; + if (!kept && (noCorsHost(url) || corsRefused.has(url))) { + // the host keeps its bytes: the server cuts the tile + const tile = await serverPicBlob(url, 'tile'); + if (tile) await saveTileThumb(url, tile); + return; + } let blob; try { const res = kept ? await fetch(kept) : await corsFetch(url, AbortSignal.timeout(READY_MS)); if (!res.ok) return; blob = await res.blob(); - } catch { return; } + } catch (e) { + if (e && e.corsRefused) { corsRefused.add(url); const tile = await serverPicBlob(url, 'tile'); if (tile) await saveTileThumb(url, tile); } + return; + } if (/gif/i.test(blob.type)) return; // animated: the tile shows the original const result = await makeImageThumbnails(blob, { tileSize: TILE_PX }); if (result?.tile) await saveTileThumb(url, result.tile); |
