Skip to content

fix: self-host episode audio via jsDelivr, GitHub Releases can't serve inline media#94

Open
Samurai33 wants to merge 4 commits into
mainfrom
feat/self-host-episode-audio
Open

fix: self-host episode audio via jsDelivr, GitHub Releases can't serve inline media#94
Samurai33 wants to merge 4 commits into
mainfrom
feat/self-host-episode-audio

Conversation

@Samurai33

Copy link
Copy Markdown
Owner

Summary

Root cause of the live "Playback failed: NotSupportedError: Failed to load because no supported source was found" console error: GitHub Release assets are always served with Content-Type: application/octet-stream + Content-Disposition: attachment + X-Content-Type-Options: nosniff — browsers correctly refuse to play that inline as <audio>, regardless of what the CSP media-src allows. This is fixed GitHub behavior, not something we can configure around.

Downloaded the 3 existing episode audio files, committed them under media/audio/, and switched audioUrl to jsDelivr's GitHub CDN, which serves the same bytes with a correct Content-Type: audio/mp4 and no attachment disposition.

Why jsDelivr over the alternatives

  • Git LFS: 10GB/month free bandwidth cap — real traffic streaming full episodes would exceed it fast (and this exact tradeoff was the reason we didn't go with LFS earlier this session either).
  • Cloudflare R2 / Vercel Blob: both would work well, but need new service setup (R2 wants a payment method on file even for the free tier) — this fix ships now without waiting on that.
  • jsDelivr: free, unmetered, verified all 3 files (largest 17.9MB) serve correctly with the right Content-Type before wiring it in.

Test plan

  • Verified via curl that all 3 audio files serve as 200 audio/mp4 (no Content-Disposition) from jsDelivr before touching any code
  • lint + type-check + test (27/27) + build all clean
  • Updated CLAUDE.md and the performance skill so a future session doesn't move audio back to GitHub Releases or LFS without knowing why those were rejected

…ve in CSP

Reported directly from the browser console on the live site:

- "Uncaught Error: Minified React error #418" - Background.tsx's particle
  positions use Math.random() in a useState initializer that runs during
  both the server and client's shared render pass, so they necessarily
  differ. That's intentional (each pageview gets its own scatter), but
  without suppressHydrationWarning React treated the mismatch as an error
  instead of the expected divergence it is. Tried moving generation into a
  useEffect first, but that trips the React Compiler's
  react-hooks/set-state-in-effect lint rule (setState synchronously in an
  effect) - suppressHydrationWarning on the affected divs is the officially
  documented fix for exactly this "value legitimately differs per render"
  case and needs no lint exception.

- CSP blocked https://vercel.live/_next-live/feedback/feedback.js - that's
  Vercel's Preview Comments/feedback toolbar, injected on preview (branch)
  deployments only. Added vercel.live to script-src/connect-src/frame-src.

Verified with lint (0 errors) + type-check + test (27/27) + build.
…e inline media

Root cause of "Playback failed: NotSupportedError: Failed to load because no
supported source was found" reported live: GitHub Release assets are always
served with Content-Type: application/octet-stream, Content-Disposition:
attachment, and X-Content-Type-Options: nosniff - browsers correctly refuse
to play that inline as <audio>, no matter what the CSP media-src allows.
This isn't configurable on GitHub's end; Releases are built for downloads,
not inline streaming.

Downloaded the 3 existing episode audio files, committed them under
media/audio/, and switched audioUrl in data/episodes.ts to jsDelivr's GitHub
CDN (cdn.jsdelivr.net/gh/.../media/audio/...), which serves the same bytes
with a correct Content-Type: audio/mp4 and no attachment disposition -
verified via curl for all 3 files (largest is 17.9MB, well under jsDelivr's
per-file limit) before wiring it into the app. Free, unmetered bandwidth,
and keeps the ~44MB of audio out of Vercel's deployed function/build output
entirely (nothing in code imports these files directly, only references the
CDN URL string).

Considered and rejected Git LFS for this (10GB/month free bandwidth cap that
real traffic streaming full episodes would exceed) and Cloudflare
R2/Vercel Blob (both need new service setup before they could be used).

Updated next.config.mjs CSP media-src, CLAUDE.md, and the performance skill
to describe the new hosting instead of GitHub Releases.

Verified with lint + type-check + test (27/27) + build.
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v0-cyber-justica Ready Ready Preview, Comment, Open in v0 Jul 12, 2026 5:11pm

The audio files (fragmented MP4/AAC-LC) still failed with the same
MEDIA_ERR_SRC_NOT_SUPPORTED / "Format error" after moving hosting to
jsDelivr - confirmed via a direct new Audio() + .load() test that this was
never a hosting/CSP problem, it's the browser's progressive-src demuxer
rejecting this specific fragmented-MP4 layout. Confirmed via
MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"') that the same
browser CAN decode this exact codec - it's a demuxer gap for plain
`audio.src = url`, not a missing codec, and MSE is the correct fix rather
than a workaround.

contexts/AudioContext.tsx now fetches the audio, feeds it through a
MediaSource + SourceBuffer, and only falls back to a plain src assignment
on browsers without MediaSource support at all. Updated next.config.mjs CSP:
connect-src needs cdn.jsdelivr.net (the fetch() pulling the bytes) and
media-src needs blob: (the MediaSource object URL) in addition to
cdn.jsdelivr.net (kept for the fallback path).

Verified with lint + type-check + test (27/27) + build.
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