feat(video): server-generated poster thumbnails + HTTP Range streaming

- Dockerfile: add ffmpeg (Alpine).
- static.js: new /thumbs/<id> — ffmpeg extracts the first frame (0.5s), caches it next to
  the file, serves as the video poster (cosmetic; 404s gracefully if ffmpeg unavailable).
- static.js: /files now supports HTTP Range (206 Partial Content) + Accept-Ranges, which
  iOS requires to stream/seek video reliably (fixes the buffer-before-play / multi-tap);
  media (image/video/audio) now served inline, other files still download. Shared
  attachment auth refactored into one helper used by /files and /thumbs.
- home.html: video poster points at /thumbs/<id>. build batch157.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 22:09:32 +05:30
parent 92d41e6324
commit fd15628393
3 changed files with 59 additions and 13 deletions
+2 -2
View File
@@ -1162,7 +1162,7 @@
</head>
<body>
<script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-22-batch156';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-22-batch157';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
//
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
@@ -2010,7 +2010,7 @@ function bubbleHTML(m){
const A = m.attachment;
const att = A ? (
A.isImage ? '<img class="att-img" src="/files/'+pEsc(A.id)+'" data-img="/files/'+pEsc(A.id)+'" alt="'+pEsc(A.name)+'" title="Click to view">'
: A.isVideo ? '<video class="att-vid" src="/files/'+pEsc(A.id)+'#t=0.1" controls playsinline preload="metadata" title="'+pEsc(A.name)+'"></video>'
: A.isVideo ? '<video class="att-vid" src="/files/'+pEsc(A.id)+'#t=0.1" poster="/thumbs/'+pEsc(A.id)+'" controls playsinline preload="metadata" title="'+pEsc(A.name)+'"></video>'
: '<a class="att-file" href="/files/'+pEsc(A.id)+'" download="'+pEsc(A.name)+'">'+ic('file',15)+' <span>'+pEsc(A.name)+'</span> <span class="att-sz">'+fmtSize(A.size)+'</span></a>') : '';
const mentionsMe=convoIsGroup && !mine && Array.isArray(m.mentions) && (m.mentions.includes(ME.id)||m.mentions.includes('everyone'));
// DM ticks: sent (1 grey) → delivered (2 grey) → read (2 yellow).