Skip to content

fix(docs): academy videos never loaded on first visit (moov atom at end of file) - #5989

Closed
waleedlatif1 wants to merge 1 commit into
stagingfrom
worktree-fix-academy-video-load
Closed

fix(docs): academy videos never loaded on first visit (moov atom at end of file)#5989
waleedlatif1 wants to merge 1 commit into
stagingfrom
worktree-fix-academy-video-load

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

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 the moov atom (the index a decoder needs before it can render anything) sat at the end of each file. For tables-operations.mp4 that 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:

  • First visit → black player while ~56 MB streams in
  • Back to the page → served from the HTTP disk cache (max-age=2592000), moov available immediately → plays instantly

Which 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:

original    moov atom not found -> Invalid data found when processing input
faststart   width=2560  height=1440  duration=111.637000

What shipped to the blob store

Two passes over all 21 videos, already applied:

  1. Faststart remux — lossless stream copy, moov moved to the front. Verified 21/21.
  2. Re-encode at CRF 23, native resolution — after copying each live object aside to academy/originals/.

What's in this diff

scripts/academy-videos.ts, which discovers every video referenced from the Academy MDX and offers:

Command Does
bun run academy:videos:check Range-reads a few KB per file, walks the ISO-BMFF box list, reports atom order. No credentials. Exits non-zero if any file would stall.
bun run academy:videos:faststart Lossless remux of anything broken.
bun run academy:videos:compress Re-encode at --crf (default 23), backing up first.

The check mode 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 libvmaf on a full 1440p lesson, not chosen by feel:

Encode Size Reduction VMAF mean Worst frame
native 1440p, CRF 23 19.7 MB 3.0x 95.34 90.66
native 1440p, CRF 20 32.3 MB 1.8x 96.11 92.45
1080p, CRF 23 10.7 MB 5.6x 92.14 86.86

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

  • Each object is copied server-side to academy/originals/<name>.mp4 before it is overwritten. Existing backups are skipped, never clobbered, so a second run can't overwrite a pristine backup with an already-encoded file.
  • Output must be at least 1.2x smaller or the file is left alone rather than overwritten for a negligible win.
  • Duration must survive within 0.5s; the encode must lead with 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 the no-cache header the verification reports a false "still not faststart" on writes that actually succeeded.

Follow-up not in this PR

The academy:videos:check script is not yet wired into CI. Worth adding if Academy recordings become routine.

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".
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner July 27, 2026 23:21
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Building Building Preview, Comment Jul 27, 2026 11:21pm

Request Review

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Dev-only maintenance script and lockfile changes; no runtime app or docs UI changes, though apply/compress can overwrite production blob objects when run with a token.

Overview
Adds scripts/academy-videos.ts and root academy:videos:* commands so Academy lesson MP4s on Vercel Blob can be audited and fixed when the moov index sits at the end of the file (first-visit playback stalls until the full ~45–90 MB download).

academy:videos:check discovers URLs from Academy MDX, range-reads the file head, and exits non-zero if any asset is not faststart—no credentials. academy:videos:faststart losslessly remuxes bad files with ffmpeg -movflags +faststart and overwrites in place. academy:videos:compress optionally re-encodes at --crf with server-side backup to academy/originals/, duration/size guards, and post-upload verification (including cache-control: no-cache so CDN stale ranges do not false-fail).

Adds @vercel/blob as a dev dependency for put/copy/head during apply/compress runs.

Reviewed by Cursor Bugbot for commit a906b5d. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

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 moov atom sitting at the end of each MP4, so a browser had to download the whole 45–90 MB file before painting a frame. That was fixed by remuxing all 21 videos in the blob store with -movflags +faststart; every one now leads with moov. No repo change was needed for that, and none of this PR's code ships to apps/docs.

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: -movflags +faststart), so the index is written at the front and this can't recur.

Root cause and the operational gotchas are captured in the commit on worktree-fix-academy-video-load if anyone needs them later.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a Bun maintenance script and npm scripts to audit/fix Academy lesson MP4 faststart layout and optionally re-encode them on Vercel Blob.

  • New scripts/academy-videos.ts discovers Academy MDX .mp4 URLs, probes ISO-BMFF atom order via range GETs, and supports --check / --apply (lossless faststart remux) / --compress (x264 CRF + backup).
  • Root package.json adds academy:videos:* scripts and @vercel/blob@2.6.1; lockfile updates follow.

Confidence Score: 4/5

Safe 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

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "fix(docs): add academy video faststart a..." | Re-trigger Greptile

Comment thread scripts/academy-videos.ts
Comment on lines +411 to +417
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?)`
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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
}

Comment thread scripts/academy-videos.ts
Comment on lines +297 to +303
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Size match skips moov proof

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.

Suggested change
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
}

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread scripts/academy-videos.ts
throw new Error(
`remote object still does not lead with a moov atom after ${REMOTE_CONFIRM_ATTEMPTS} attempts`
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a906b5d. Configure here.

@waleedlatif1
waleedlatif1 deleted the worktree-fix-academy-video-load branch July 28, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant