No description
This repository has been archived on 2026-05-16. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • JavaScript 77.6%
  • Python 12.1%
  • HTML 10.3%
Find a file
Phillippe Pelzer 71dd61bbf4
All checks were successful
Deploy Pages / deploy (push) Successful in 5s
ci: use Git host (forgejo.phillippepelzer.me) not Pages host
The previous PUBLIC_HOST pointed at the Pages domain which doesn't
serve the Git endpoint, producing 404s on clone/push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:15:39 +02:00
.forgejo/workflows ci: use Git host (forgejo.phillippepelzer.me) not Pages host 2026-05-12 01:15:39 +02:00
server feat: integrate NoMercy player with synthetic HLS for all qualities 2026-05-07 05:38:32 +02:00
.gitignore Initial commit: yt-dlp.wasm 2026-05-07 05:24:03 +02:00
index.html feat: direct CDN fetch for media, proxy reduced to metadata-only 2026-05-12 01:10:59 +02:00
LICENSE.md feat: add LICENSE and README files with project details and usage instructions 2026-05-08 03:16:14 +02:00
main.js feat: direct CDN fetch for media, proxy reduced to metadata-only 2026-05-12 01:10:59 +02:00
network_patch.py feat: direct CDN fetch for media, proxy reduced to metadata-only 2026-05-12 01:10:59 +02:00
README.md feat: direct CDN fetch for media, proxy reduced to metadata-only 2026-05-12 01:10:59 +02:00
ui-plugin.js feat: subtitles, 1080p+ via audio-group HLS, controls plugin, wheel cache, ffmpeg.wasm muxing 2026-05-07 05:54:25 +02:00
worker.js feat: direct CDN fetch for media, proxy reduced to metadata-only 2026-05-12 01:10:59 +02:00

yt-dlp.wasm

A browser-based YouTube (and more) downloader and player, powered by yt-dlp running in WebAssembly via Pyodide. All heavy lifting — video, audio and subtitle byte transfer — happens directly between the visitor's browser and the upstream CDN (googlevideo for YouTube). Only the tiny metadata-extraction step uses a CORS proxy.

Why a proxy at all?

YouTube's watch HTML, youtubei API and signature JavaScript do not return Access-Control-Allow-Origin headers, so browser code cannot fetch them cross-origin. yt-dlp inside Pyodide hits those endpoints during the extraction step. The proxy is a thin pass-through that forwards just those metadata calls and rewrites headers. It is not in the path for actual video/audio downloads or playback — that's all direct fetch from the visitor's browser to googlevideo.com, which does serve CORS headers.

Concretely:

  • Metadata extraction (~1050 KB / video): visitor's browser → proxy → youtube.com
  • HLS playback, all variants, all segments: visitor's browser ↔ googlevideo.com directly
  • Subtitle (.vtt) fetch: visitor's browser ↔ origin directly
  • Single-file and DASH (video-only + audio-only) downloads: visitor's browser ↔ googlevideo.com directly. DASH muxing into a single .mp4 happens locally via ffmpeg.wasm.

Features

  • Plays and downloads videos from YouTube and many other sites supported by yt-dlp.
  • Quality selection: progressive, DASH (1080p+, includes 4K), audio-only.
  • DASH downloads are muxed to MP4 client-side with ffmpeg.wasm (no re-encode).
  • Subtitle extraction and track selection.
  • NoMercy video player with a custom controls plugin.
  • yt-dlp wheel is cached in IndexedDB after first load.

Project Structure

index.html         # Web UI + proxy configuration
main.js            # UI, player integration, direct CDN fetch, DASH mux orchestration
worker.js          # Pyodide worker: extract() + mux() RPCs
ui-plugin.js       # Custom video player controls plugin
network_patch.py   # urllib monkey-patch routing through the metadata proxy
.forgejo/workflows/deploy.yaml  # Forgejo Pages deploy workflow
server/
  proxy.js         # Minimal Node.js CORS proxy (metadata only)
  package.json

How It Works

  1. The user enters a video URL.
  2. main.js asks the worker to extract(url).
  3. The worker (Pyodide + yt-dlp) calls youtube.com through the proxy and returns the full info dict (formats, subtitles, etc.).
  4. main.js builds an HLS master playlist whose variant manifests point at the raw googlevideo URLs. The NoMercy player loads playback directly from googlevideo via the visitor's connection.
  5. For downloads, main.js fetches the format URL(s) directly with fetch() and (for DASH) ships the bytes to the worker for ffmpeg.wasm muxing.

Prerequisites

  • Node.js 18+ (for the metadata proxy)
  • A modern browser
  • Internet (yt-dlp wheel is cached after first run)

Setup & Usage

1. Install proxy dependencies

cd server
npm install

2. Start the metadata proxy

npm start

Listens on http://localhost:8181 by default.

3. Serve the static files

Open index.html via a local web server (recommended) or directly. Module imports and Workers need the proper origin, so a static server such as python -m http.server works well.

4. Use it

  • Paste a video URL.
  • Click Load video. Metadata extraction starts.
  • Pick a quality and click Save to download — bytes flow straight from the CDN to your browser, no server round-trip.

Configuring the proxy URL

The proxy URL is read from window.YT_DLP_PROXY in index.html:

<script>
  // Empty string falls back to http://localhost:8181.
  // Set to your own public proxy if you deploy the UI to a static host.
  window.YT_DLP_PROXY = "";
</script>

If you publish the UI somewhere public (Forgejo Pages, GitHub Pages, …), deploy server/proxy.js somewhere reachable (a small VPS, Fly.io, etc.) and point window.YT_DLP_PROXY at it. The proxy only sees small metadata requests; it is never in the path of actual video bytes.

Deploying to Forgejo Pages

The .forgejo/workflows/deploy.yaml workflow pushes a static copy of the UI to the pages branch on every push to main. It clones via the public hostname (configurable via PUBLIC_HOST in the workflow file) because the internal Docker name forgejo is not resolvable inside the workflow's container network.

Security & Privacy

  • Video bytes go directly between the visitor's browser and the upstream CDN.
  • The proxy only relays metadata calls and never sees video content; it doesn't log or persist anything.

Troubleshooting

  • ModuleNotFoundError: No module named 'ssl' — older builds. The current worker explicitly loads the ssl Pyodide package on boot.
  • Network error on extraction — the proxy isn't running, isn't reachable, or window.YT_DLP_PROXY points at the wrong URL.
  • CORS error on playback — the format URL has expired (yt-dlp's URLs are short-lived signed URLs). Re-extract.

License

MIT. See LICENSE for details.