fix(docs): academy videos never loaded on first visit (moov atom at end of file) - #5989
fix(docs): academy videos never loaded on first visit (moov atom at end of file)#5989waleedlatif1 wants to merge 1 commit into
Conversation
The Academy lesson videos were exported without `-movflags +faststart`, so the `moov` atom sat at the end of each file. A browser cannot decode a frame until it has read `moov`, so the `<video>` element had to download the whole 45-90 MB file before painting anything: the first visit to a lesson showed a black player, and a second visit played instantly off the HTTP disk cache. `scripts/academy-videos.ts` discovers every video referenced from the Academy MDX and offers three operations: - `--check` range-reads a few KB per file and walks the ISO-BMFF box list to report atom order. Needs no credentials; exits non-zero if any file would stall, so it can guard future uploads. - `--apply` remuxes the broken files with a lossless stream copy. - `--compress` re-encodes with x264 at `--crf` (default 23), after copying the live object aside to `academy/originals/`. CRF 23 at native resolution was chosen by measurement, not feel: on a 1440p lesson it scored VMAF 95.34 mean / 90.66 worst frame for a 3.0x size cut, while CRF 20 bought only +0.77 VMAF for 64% more bytes and a 1080p downscale dropped to 92.14 while permanently discarding resolution. Post-write verification forces revalidation with `cache-control: no-cache`. The blob CDN keys purely on pathname — a query-string buster is ignored — and will serve the previous body for a few seconds after an overwrite, which otherwise reads as a bogus "still not faststart".
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryLow Risk Overview
Adds Reviewed by Cursor Bugbot for commit a906b5d. Configure here. |
|
Closing — this PR contains no fix, and production is already correct. The reported bug (Academy videos never loading on first visit) was caused by the What's left here is repair/audit tooling for a one-off operation that's now complete. The audit script was never wired into CI, so it would only help when someone remembered to run it — which is the same failure mode it claimed to prevent. The durable fix is upstream, in the export settings, not in a repair script. Whoever records the lessons should enable web-optimized / streaming export (ffmpeg: Root cause and the operational gotchas are captured in the commit on |
Greptile SummaryAdds a Bun maintenance script and npm scripts to audit/fix Academy lesson MP4 faststart layout and optionally re-encode them on Vercel Blob.
Confidence Score: 4/5Safe to merge for product runtime; the PR only adds maintenance scripts, with non-blocking issues in compress skip handling and post-upload verification. No application runtime path changes. Remaining issues are operator-script correctness: insufficient-compression treated as hard failure (breaks idempotent --compress) and post-write confirmation that can accept size match without proving moov-first. Files Needing Attention: scripts/academy-videos.ts
|
| Filename | Overview |
|---|---|
| scripts/academy-videos.ts | Solid operator tooling for faststart audit/repair and compress-with-backup; skip-as-failure and post-write size short-circuit weaken reliability of reported success. |
| package.json | Wires academy video check/apply/compress scripts and adds @vercel/blob dev dependency. |
| bun.lock | Lockfile updates for @vercel/blob and related transitive packages. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
MDX[Academy MDX src MP4 URLs] --> Probe[probeRemote range GET]
Probe -->|moov first| OK[Report faststart]
Probe -->|mdat first / unknown| Bad[Flag bad]
Bad -->|--check| Exit1[exit 1]
Bad -->|--apply| Repair[download ffmpeg copy faststart put]
Probe -->|--compress all| Comp[backup encode put]
Repair --> Confirm[confirmRemoteFaststart]
Comp --> Confirm
Reviews (1): Last reviewed commit: "fix(docs): add academy video faststart a..." | Re-trigger Greptile
| const encodedSize = (await stat(encoded)).size | ||
| const ratio = sourceSize / encodedSize | ||
| if (ratio < MIN_COMPRESSION_RATIO) { | ||
| throw new Error( | ||
| `only ${ratio.toFixed(2)}x smaller — not worth a lossy overwrite (source already efficient?)` | ||
| ) | ||
| } |
There was a problem hiding this comment.
Compress ratio treated as failure
When --compress encodes a file that is already under 1.2× smaller (including a second full run on already re-encoded live objects), compressOne throws after the encode and main records that as a failure and exits 1, so intentional “leave alone” cases look like hard errors and make the job non-idempotent.
| const encodedSize = (await stat(encoded)).size | |
| const ratio = sourceSize / encodedSize | |
| if (ratio < MIN_COMPRESSION_RATIO) { | |
| throw new Error( | |
| `only ${ratio.toFixed(2)}x smaller — not worth a lossy overwrite (source already efficient?)` | |
| ) | |
| } | |
| const encodedSize = (await stat(encoded)).size | |
| const ratio = sourceSize / encodedSize | |
| if (ratio < MIN_COMPRESSION_RATIO) { | |
| console.log( | |
| ` skipping overwrite: only ${ratio.toFixed(2)}x smaller (below ${MIN_COMPRESSION_RATIO}x threshold)` | |
| ) | |
| return | |
| } |
| const { faststart, size } = await probeRemote(video.url, true) | ||
| if (faststart === true) return | ||
|
|
||
| if (size === expectedSize) { | ||
| console.log(' (edge still serving the previous body; object metadata already matches)') | ||
| return | ||
| } |
There was a problem hiding this comment.
confirmRemoteFaststart returns success when reported size equals the upload even if the range probe never sees moov first, so a post-write run can report repair/compress success while the body still does not lead with moov.
| const { faststart, size } = await probeRemote(video.url, true) | |
| if (faststart === true) return | |
| if (size === expectedSize) { | |
| console.log(' (edge still serving the previous body; object metadata already matches)') | |
| return | |
| } | |
| const { faststart, size } = await probeRemote(video.url, true) | |
| if (faststart === true) return | |
| if (size === expectedSize) { | |
| console.log(' (edge still serving the previous body; object metadata already matches)') | |
| // Size alone is not proof of layout; keep retrying until moov leads or attempts exhaust. | |
| continue | |
| } |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a906b5d. Configure here.
| throw new Error( | ||
| `remote object still does not lead with a moov atom after ${REMOTE_CONFIRM_ATTEMPTS} attempts` | ||
| ) | ||
| } |
There was a problem hiding this comment.
Confirm accepts non-faststart on size match
Medium Severity
The confirmRemoteFaststart function can prematurely report success. It exits the retry loop if the remote object's size matches the expected size, even when the faststart probe indicates the moov atom is still not at the beginning. This can lead to the script incorrectly confirming a video as faststart.
Reviewed by Cursor Bugbot for commit a906b5d. Configure here.


The bug
Reported by Justin: "The first time I navigate to an academy page the video doesn't load, if I navigate away then back to the page it loads."
Not a code bug — the video files. All 21 Academy lessons were exported by ffmpeg without
-movflags +faststart, so themoovatom (the index a decoder needs before it can render anything) sat at the end of each file. Fortables-operations.mp4that was byte 59,219,342 of 59,397,998 — the last 0.3% of a 56 MB file.A browser therefore had to download essentially the entire file before painting a single frame:
max-age=2592000),moovavailable immediately → plays instantlyWhich is exactly the reported symptom, including why it only happened the first time.
Before/after on the same file, reading only the first 1 MB:
What shipped to the blob store
Two passes over all 21 videos, already applied:
moovmoved to the front. Verified 21/21.academy/originals/.What's in this diff
scripts/academy-videos.ts, which discovers every video referenced from the Academy MDX and offers:bun run academy:videos:checkbun run academy:videos:faststartbun run academy:videos:compress--crf(default 23), backing up first.The
checkmode is the point of keeping this: it's a guard so the next batch of recordings can't silently reintroduce the bug.Why CRF 23 at native resolution
Measured with
libvmafon a full 1440p lesson, not chosen by feel:CRF 20 costs 64% more bytes for +0.77 VMAF. The 1080p downscale saves more but drops 3.2 VMAF and permanently discards resolution — and VMAF's default model is trained for 1080p video at typical viewing distance, so it understates the penalty for text-heavy screen capture inspected at 1:1. Native resolution keeps Academy UI text legible when a viewer maximizes the player.
Safety on the lossy path
academy/originals/<name>.mp4before it is overwritten. Existing backups are skipped, never clobbered, so a second run can't overwrite a pristine backup with an already-encoded file.moov; the uploaded URL must match the source URL.One gotcha worth knowing
Post-write verification forces revalidation with
cache-control: no-cache. The blob CDN keys purely on pathname — a query-string cache-buster is ignored — and will serve the previous body for a few seconds after an overwrite. Without theno-cacheheader the verification reports a false "still not faststart" on writes that actually succeeded.Follow-up not in this PR
The
academy:videos:checkscript is not yet wired into CI. Worth adding if Academy recordings become routine.