VLC 4's playback engine compiled to WebAssembly, served as an offline-capable PWA on GitHub Pages. Play the media formats browsers can't — MKV with any codec, FLAC/ALAC, WMV, RealMedia, DVD images with menus, tracker music… entirely in your browser: no uploads, no server-side processing, works offline after the first visit.
- Engine:
wasm/libvlc.js+libvlc.wasm(~25 MB) — VLC'slibvlc,libvlccoreand ~200 statically-linked plugins (FFmpeg, dav1d, libass, …), built forwasm32-unknown-emscriptenwith pthreads. The build recipe and the C bridge come from lucasgelfond/libvlc-wasm (MIT — thank you!); the engine itself is GPL as VLC and its contribs are. - Threading: the engine runs in a dedicated Web Worker; VLC's pthreads are
Workers on a pre-started pool. That needs
SharedArrayBuffer, so the page must be cross-origin isolated: on GitHub Pages (no custom headers) the service worker re-serves every response with COOP/COEP headers (coi.js+sw.js), costing one reload on the first visit. The install precaches only the small app shell, so that reload happens within seconds; the ~25 MB engine downloads lazily on the first boot and is cached then. - Video: the vout (
native/webframe.c) copies each decoded frame into triple-buffered wasm memory; the page claims the newest buffer per animation frame and uploads it as WebGL2 textures (YUV→RGB in the shader, HDR matrices and full/limited range respected). When the browser offers WebCodecs, H.264/HEVC/VP9/AV1 decode on the platform's (usually hardware) decoders instead of FFmpeg-in-wasm. - Audio: the aout (
native/webaudio.c) writes into a ring buffer in wasm memory; an AudioWorklet drains it on the audio thread and reports the playhead back, so audio stays VLC's master clock and video syncs to it. - Files never leave your device: dropped files are mounted with WORKERFS and read lazily (FileReaderSync) — a 4 GB file costs no memory up front.
When installed, the PWA doubles as a system-level media handler. Everything here is feature-detected; browsers without support keep the dropzone:
- Open with (desktop Chrome/Edge):
file_handlersinmanifest.webmanifestregisters the app for ~130 media extensions; launched files arrive viawindow.launchQueue. - Share sheet (Android Chrome):
share_targetreceives shared files as a POST to./__share, which the service worker stashes in a dedicated cache; the app picks them up on the next load. - Native pickers (Chromium): “Open file…” (
showOpenFilePicker), “Open folder…” (showDirectoryPicker— a VIDEO_TS folder opens as a DVD with menus, clickable). - Everywhere else (Firefox, Safari, …) these entry points stay hidden.
.github/workflows/publish.yml runs on every push to main (README- and
tests-only commits are ignored — they never reach the site), every Monday
04:00 UTC (to pick up new VLC commits), and on demand:
- Resolves the head of VLC master (or a commit you specify via
workflow_dispatch) and checks it out - Restores the contribs cache keyed on that commit (FFmpeg & co. take about an hour cold, minutes warm)
- Builds VLC's static libraries with emscripten (emsdk 6.0.0), applies the
patches in
patches/for the wasm-specific fixes, then links everything with ournative/bridge modules intowasm/libvlc.{js,wasm} - Smoke-tests the engine under Node (boots it, creates a player, round-trips the bridge RPC)
- Generates
sw-manifest.js(asset list + cache version) and deploys to GitHub Pages
workflow_dispatch with skip_build: true redeploys the site using the wasm
from the most recent successful run (useful for shipping UI fixes without an
hour-long rebuild).
| Path | Purpose |
|---|---|
index.html, app.js |
UI (no frameworks, no external resources) |
src/ |
Page-side player client: worker protocol, engine RPC, WebGL renderer, AudioWorklet |
native/ |
The C bridge: libvlc RPC + webaudio/webframe/webwindow/webcodecs VLC modules |
patches/ |
VLC-tree patches the build applies (contrib fixes + wasm playback fixes) |
coi.js, sw.js, offline.html, manifest.webmanifest |
PWA / offline / cross-origin-isolation layer |
fonts/ |
Noto Sans — VLC's freetype needs a font file for subtitles |
.github/workflows/publish.yml |
Weekly build & publish pipeline |
.github/scripts/build-vlc.sh |
The wasm build: contribs → static libs → emcc link |
.github/scripts/wasm-smoke-test.mjs |
Node boot test run in CI |
.github/scripts/generate-sw-manifest.mjs |
Writes the versioned precache manifest |
icons/ |
PWA icons (a traffic cone, naturally) |
wasm/ |
Build output — gitignored; CI generates it, drop local builds here for dev |
tests/ |
Node test suites (node tests/run-all.mjs) |
# put a wasm build in wasm/ (CI artifact, or build it — see below), then:
python3 -m http.server -d . 8080
# open http://localhost:8080 (needs TWO loads the first time: coi.js reloads
# once so the service worker can make the page cross-origin isolated)The engine can be built locally the same way CI does it (no Docker needed):
git clone https://github.com/videolan/vlc.git ../vlc # official mirror
# emsdk 6.0.0 on PATH + the apt packages from publish.yml, then:
VLC_SRC=../vlc .github/scripts/build-vlc.shVLC is Copyright © the VideoLAN contributors, LGPL-2.1+ with GPL modules; the compiled engine this site ships is GPL. The web wrapper, CI pipeline, bridge adaptation and PWA assets in this repository are MIT where not derivative of libvlc-wasm (which is MIT for its own code, GPL for the binary it builds). This site is not affiliated with VideoLAN.