perf(video): stream a capped, faststart rendition instead of the raw upload
THE ANSWER to "why does an already-downloaded video still buffer?" — it was never the download, and it was not the server. Probing the real uploads on the box: d0e49e58… 1920x1080 19.4 Mbps 75 MB / 31 s ad929d0b… 1920x1080 19.0 Mbps 27 MB / 11 s 9f4e0865… 720x1584 3.6 Mbps 14 MB / 31 s To play a 19 Mbps file the client has to SUSTAIN a 19 Mbps download for the whole clip. No mobile link does, so the <video> buffer drains every few seconds: buffers, plays, buffers, plays. Server-side disk read was instant and load was 1.7 on 20 cores throughout — the bottleneck is the media itself, not the delivery path. Second, independent defect: phone MP4s store `moov` AFTER `mdat` (verified on two uploads), so the player must fetch the file's tail before it can start at all. Fix — keep the original bytes untouched (that is what the download button serves, full quality) and build <id>.web.mp4 beside it: longest side capped at 1280, ~2.5 Mbps ceiling, +faststart. Measured on the 19 Mbps file: 27.3 MB @ 19.0 Mbps -> 2.55 MB @ 1.78 Mbps (10.7x less bandwidth) transcode took 2.4 s for an 11.5 s clip - server/media.js (new): probe, decide, 2-at-a-time background queue. Already light + correctly sized + faststart => no rendition at all. Light but wrong atom order => remux -c copy (seconds, no re-encode). Otherwise re-encode. A rendition that lands bigger than the original is discarded. MP4 box-walker for the faststart test is unit-checked against known fast/slow files, both directions. - /stream/<id> serves the rendition, falling back to the original while it is still transcoding, so a video is never unplayable. /files/<id> is unchanged and still serves the pristine original for download. - Renditions are queued at upload, and backfilled 15 s after boot for the videos that predate this. Range serving is now one shared helper for both routes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1165,7 +1165,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=6"></script>
|
||||
<script>window.__BUILD='2026-07-22-batch159';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-23-batch160';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
|
||||
@@ -2013,7 +2013,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 ? '<div class="att-vid"><video class="att-vid-v" src="/files/'+pEsc(A.id)+'#t=0.1" poster="/thumbs/'+pEsc(A.id)+'" controls playsinline preload="none" title="'+pEsc(A.name)+'"></video><span class="att-vid-spin" aria-hidden="true"></span></div>'
|
||||
: A.isVideo ? '<div class="att-vid"><video class="att-vid-v" src="/stream/'+pEsc(A.id)+'#t=0.1" poster="/thumbs/'+pEsc(A.id)+'" controls playsinline preload="none" title="'+pEsc(A.name)+'"></video><span class="att-vid-spin" aria-hidden="true"></span></div>'
|
||||
: '<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).
|
||||
|
||||
Reference in New Issue
Block a user