aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-07 16:16:28 -0700
committerAdam Soltys <[email protected]>2026-10-07 16:16:28 -0700
commit738bace91bcd17c0ab8ba920fe2f0765f6504fc8 (patch)
treecfc620c8a759ae3fc924711956b4b5c7a0218d30 /src/features/messages.js
parent3ffe565b3d50539470a05b1f7a65f5fed419daf5 (diff)
downloadcoinosv3-738bace91bcd17c0ab8ba920fe2f0765f6504fc8.tar.gz
coinosv3-738bace91bcd17c0ab8ba920fe2f0765f6504fc8.zip
Videos: posters for clips that came without one, and an H.264 copy for a clip this browser can't picture (an iPhone's HEVC on Chrome for Linux played sound over black) — vmedia container on cs (bun + ffmpeg, /vmedia/), switch on zero-width metadata or an HEVC header from the probe, a line saying why if the copy fails too
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js68
1 files changed, 59 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 4f12812..12d928e 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -5847,7 +5847,10 @@ export function messagesFeature(ctx) {
const tw = u32(end - 8) >>> 16, th = u32(end - 4) >>> 16;
if (tw && th) { w = tw; h = th; }
}
- return { faststart, ...(moovEnd ? { moovEnd } : {}), ...(w ? { w, h } : {}) };
+ // an iPhone's HEVC (hvc1/hev1 in the sample description)
+ let hevc = false;
+ for (let j = 0; j + 4 <= b.length && !hevc; j++) if (b[j] === 0x68 && (type(j) === 'hvc1' || type(j) === 'hev1')) hevc = true;
+ return { faststart, ...(moovEnd ? { moovEnd } : {}), ...(w ? { w, h } : {}), ...(hevc ? { hevc } : {}) };
}
const videoProbes = new Map();
let videoProbing = 0;
@@ -5901,7 +5904,7 @@ export function messagesFeature(ctx) {
let o = 0;
for (const p of parts) { if (o >= buf.length) break; buf.set(p.subarray(0, buf.length - o), o); o += p.length; }
const head = readMp4Head(buf);
- const d = { probed: true, ranges, faststart: head.faststart === true, failedAt: undefined, ...(head.moovEnd ? { moovEnd: head.moovEnd } : {}) };
+ const d = { probed: true, ranges, faststart: head.faststart === true, failedAt: undefined, ...(head.moovEnd ? { moovEnd: head.moovEnd } : {}), ...(head.hevc ? { hevc: true } : {}) };
if (total > 0) d.size = total;
else {
// a server that hides Content-Range from scripts (Primal's) still
@@ -6738,21 +6741,67 @@ export function messagesFeature(ctx) {
cover.remove();
onPlay && onPlay();
const v = box.querySelector('video');
- if (v) { if (!v.getAttribute('src')) v.setAttribute('src', url); v.preload = 'auto'; v.play().catch(() => {}); }
+ if (v) { if (!v.getAttribute('src')) v.setAttribute('src', playSrc(url)); v.preload = 'auto'; v.play().catch(() => {}); }
} }, h('span', { class: 'vid-heavy-play', 'aria-hidden': 'true' }, '\u25b6'), h('span', {}, heavyLabel(url)));
box.append(cover);
}
+ // ---- clips this browser can't picture, posters a post didn't give -------
+ // The server (vmedia/server.js) makes a still for a clip that came without
+ // one — a clip waiting for a tap was a black box — and an H.264 copy of
+ // one whose picture this browser can't decode: an iPhone's HEVC on Chrome
+ // for Linux plays its sound over black.
+ const VMEDIA = APP_BASE + '/vmedia/';
+ const vmediaOk = (url) => /^https:\/\//i.test(url || '');
+ const serverThumb = (url) => (vmediaOk(url) ? VMEDIA + 'thumb.jpg?u=' + encodeURIComponent(url) : undefined);
+ const h264Of = (url) => VMEDIA + 'h264.mp4?u=' + encodeURIComponent(url);
+ const noPicture = new Set(), h264Failed = new Set();
+ let hevcPlays;
+ const canHevc = () => hevcPlays ??= (() => {
+ try { return !!document.createElement('video').canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"'); } catch { return true; }
+ })();
+ // what a player for this clip loads: the copy, once the original is known
+ // not to show here
+ const playSrc = (url) => vmediaOk(url) && !h264Failed.has(url)
+ && (noPicture.has(url) || ((videoInfo.get(url) || {}).hevc && !canHevc())) ? h264Of(url) : url;
+ // A player whose clip turns out to have no picture it can show (its
+ // metadata says zero width) switches to the copy; if the copy fails too,
+ // the original's sound plays under a line saying why, with the file.
+ function guardPicture(v, url) {
+ if (!vmediaOk(url)) return;
+ const onCopy = () => (v.getAttribute('src') || '').startsWith(VMEDIA);
+ v.addEventListener('loadedmetadata', () => {
+ if (v.videoWidth || onCopy() || h264Failed.has(url)) return;
+ noPicture.add(url);
+ const want = !v.paused;
+ v.setAttribute('src', h264Of(url));
+ if (want) v.play().catch(() => {});
+ });
+ v.addEventListener('error', () => {
+ if (!onCopy()) return;
+ h264Failed.add(url);
+ const want = !v.paused;
+ v.setAttribute('src', url);
+ if (want) v.play().catch(() => {});
+ const box = v.parentElement;
+ if (box && !box.querySelector('.vid-nopic')) {
+ box._skipMorph = true;
+ box.append(h('div', { class: 'vid-nopic' }, h('span', {}, t('videoNoPicture')),
+ h('a', { href: url, target: '_blank', rel: 'noopener noreferrer', onClick: (e) => e.stopPropagation() }, t('videoOpenFile'))));
+ }
+ });
+ }
function videoNode(url, { stable = false } = {}) {
const loud = unmutedClips.has(url);
// Nothing is fetched until the clip is about to play (preload none): a
// window of rows used to pull every clip's metadata, which for a file
// with its index at the end can be most of the file.
- const v = h('video', { src: url, class: 'note-video', controls: true, poster: videoThumbs.get(url) || (videoInfo.get(url) || {}).image || undefined,
+ const v = h('video', { src: playSrc(url), class: 'note-video', controls: true, poster: videoThumbs.get(url) || (videoInfo.get(url) || {}).image || serverThumb(url),
preload: 'none', playsinline: true, muted: loud ? undefined : true,
style: stable ? videoBoxStyle(feedMedia(url)) : undefined,
// learned now, used by the next paint of any row with this clip
onLoadedmetadata: (e) => rememberVideo(url, e.target.videoWidth, e.target.videoHeight),
- onError: (e) => { if (!stable) { const b = e.target.parentElement; if (b) b.style.display = 'none'; } } });
+ onError: (e) => { if (!stable && !(e.target.getAttribute('src') || '').startsWith(VMEDIA)) { const b = e.target.parentElement; if (b) b.style.display = 'none'; } } });
+ guardPicture(v, url);
v.muted = !loud; // the property, not just the attribute: a script-made element autoplays only muted
const box = h('div', { class: 'note-video-box' }, v);
if (loud) box._skipMorph = true;
@@ -6782,7 +6831,7 @@ export function messagesFeature(ctx) {
box._inView = true;
clearTimeout(box._release);
if (!v.getAttribute('src') && !v.getAttribute('data-lazy-src')) {
- v.setAttribute('src', url);
+ v.setAttribute('src', playSrc(url));
v.addEventListener('loadedmetadata', () => { v.style.minHeight = ''; }, { once: true });
}
// a clip over the budget waits for a tap, with what it costs on it
@@ -10627,7 +10676,7 @@ export function messagesFeature(ctx) {
const video = pager.kind === 'videos';
const media = video
? h('video', { class: 'mp-media', 'data-src': url, loop: true, playsinline: true, preload: 'none',
- poster: videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || undefined })
+ poster: videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || serverThumb(url) })
: h('img', { class: 'mp-media', 'data-src': url, alt: '', draggable: 'false' });
const caption = mediaCaption(ev);
const stop = (fn) => (e) => { e.stopPropagation(); fn(e); };
@@ -10659,6 +10708,7 @@ export function messagesFeature(ctx) {
}) }, h('span', { class: 'i', html: I_ZAP }), h('span', { class: 'n' })))));
slide._ev = ev;
if (video) {
+ guardPicture(media, url);
media.muted = !pagerLoud;
media.addEventListener('click', () => {}); // a tap is the pager's (overlay), not the player's
}
@@ -10666,7 +10716,7 @@ export function messagesFeature(ctx) {
}
function pagerAttach(slide) {
const m = slide.querySelector('.mp-media');
- if (m && !m.getAttribute('src')) m.setAttribute('src', m.getAttribute('data-src'));
+ if (m && !m.getAttribute('src')) m.setAttribute('src', m.tagName === 'VIDEO' ? playSrc(m.getAttribute('data-src')) : m.getAttribute('data-src'));
}
function pagerRelease(slide) {
const m = slide.querySelector('video.mp-media');
@@ -10741,7 +10791,7 @@ export function messagesFeature(ctx) {
}
function startSlide(slide) {
const v = slide.querySelector('video.mp-media');
- if (/#t=[\d.]+$/.test(v.getAttribute('src') || '')) v.setAttribute('src', v.getAttribute('data-src')); // the still's src: from the start now
+ if (/#t=[\d.]+$/.test(v.getAttribute('src') || '')) v.setAttribute('src', playSrc(v.getAttribute('data-src'))); // the still's src: from the start now
v.preload = 'auto';
pagerAttach(slide);
v.muted = !pagerLoud;