VODPlace: the place for VODs #
Available at vod.place!
This is a viewer for Streamplace videos, in particular the VODs from AtmosphereConf 2026.
It piggybacks off of ionosphere.tv's metadata for the moment. Backfills via lightrail and sync.getRepo because eli's PDS is slightly weird.
Uses slingshot's experimental hydration for some stuff.
Reasonably claude-coded, lots of stuff is rough and experimental.
Some videos (notably @knotbin.com's talk, a few others) seem to have encoding issues, which I need to add transmuxing to wasp-hls to fix, but haven't gotten round to yet.
Getting started #
Prerequisites #
If you have Nix, the flake dev shell is by far the easiest path. It gives you everything: Rust nightly, dioxus-cli, wasm-bindgen-cli (version-matched), Playwright browsers, ffmpeg, LLVM/clang for WASM compilation, and sqlite.
nix develop # or use direnv
Without Nix, you'll need at minimum:
- Rust nightly with the
wasm32-unknown-unknowntarget dioxus-cli(dx)- A C compiler and LLVM/clang-18 for WASM C compilation
- ffmpeg (for server-side thumbnail extraction)
- sqlite3
Building the player worker #
The player runs HLS in a Web Worker compiled to WASM. This needs to be built before anything else:
./build-workers.sh
This compiles vodplace-player-worker to wasm32-unknown-unknown, runs wasm-bindgen --target web, and drops the output into public/player_worker.js + public/player_worker_bg.wasm. Dioxus does NOT build this automatically for reasons that should be obvious. If you see a blank player, you probably forgot this step.
Running #
dx serve
Dev server at http://localhost:8080. Browse page at /, watch page at /watch?uri=<at-uri>.
There's a sqlite database in ./data/ that acts as a durable cache for chat messages and the video catalog. First load will take a bit while it discovers and enriches all the videos from the network.
Architecture #
This is a Dioxus fullstack app.
Player #
The player uses a main-thread/worker split. The Dioxus component on the main thread owns the <video> element and MSE SourceBuffers. An HLS engine (wasp-hls, vendored and substantially modified) runs in a dedicated Web Worker and handles manifest parsing, segment fetching, and adaptive bitrate. They talk over a postcard-serialized binary message protocol (PlayerCommand / PlayerEvent).
Video discovery #
Videos are discovered from the atproto like so:
- Relay (
lightrail.microcosm.blue):listReposByCollectionfinds all DIDs withplace.stream.videorecords - PDS
listRecords: For each DID, paginate through their PDS to get all video records sync.getRepofallback: Some PDSes (hi eli) return 501 forlistRecords. In that case we download the full repo as a CAR file, parse the MST withjacquard-repo, and extract the video records that way
Results are cached in sqlite (persistent across restarts) and moka (in-memory hot cache with 1h TTL). On warm start the cached catalog is served immediately while a background refresh happens.
Chat replay #
Chat messages are fetched via slingshot's experimental hydration endpoint (figproto ftw, this made things SO much easier), cached in sqlite, and replayed in a sliding window synced to video playback time. The sidebar auto-scrolls and shows a "now" divider. Clicking on a chat message will seek to the point in the video that message was sent.
Comments #
Ionosphere comments are fetched via constellation backlinks and slingshot. They can be anchored to specific byte ranges in a transcript, which get mapped to word indices for highlighting. Threaded replies are supported (in theory, ionosphere's UI doesn't support them and we don't have a write path yet).
Subtitles #
This player has a vendored pure Rust implementation of ASS subtitles (ass-core for parsing, ass-renderer for software rendering with zeno rasterisation), rewritten substantially from the unfortinately rather poorly vibecoded ass-rs to correctly support most ASS features with currently some minor render bugs in the most complex subtitles and not perform like, well, complete ass (just sort of ass). It also does transcoding from VTT and SRT to ASS. Ionosphere automatic transcripts are also rendered as subtitles. Intent is to provide a clear way to edit and improve subtitles for a video, as people settle on a lexicon for those. Subtitles in an HLS playlist are automatically extracted, converted to ASS, and made available in the selector. You can also manually load subtitles from a file.
The player supports arbitrary HLS playlist URLs within reason, in addition to Streamplace VOD URIs. The ./public/test-content/ folder is served by default, and you can point the /watch page at anything in there. It will attempt to find and load the matching subtitle file if it's in the same directory (assuming the format matches how ass-tool extracts mkvs to HLS playlists). Other stuff on the web should also work. It all ends up being loaded by the player worker basically the same way internally.
ASS testing #
The vod.place server serves a few test files for this purpose:
- Episode 1 of Nisekoi (subs in this repo at ./public/commie-nisekoi-01.ass)
- https://vod.place/watch?uri=/test-content/q4eab5-master.m3u8 (subs at ./public/q4eab5.ass)
- https://vod.place/watch?uri=/test-content/ioxho8-master.m3u8 (subs at ./public/ioxho8.ass)
- Zaregoto OVA clip (subs at ./public/q4eab5.ass)
The latter three don't perform well on WASM as of yet, but they are also doing quite a lot at once in the subtitles. Need to do another round of optimization.
ass-tool #
There's a CLI for working with ASS subtitles:
# Extract subtitles + fonts from an MKV
cargo run -p ass-tool -- extract-mkv video.mkv --output subs.ass
# Render subtitle frames to PNG
cargo run -p ass-tool -- render subs.ass --timestamps 3000,5000 --output-dir /tmp/out/
# Side-by-side comparison with libass (via ffmpeg)
cargo run -p ass-tool -- compare-video subs.ass --video video.mkv --output comparison.mp4
If you have an MKV with embedded subtitles, extract with ass-tool, point the player at the -master.m3u8 playlist and load the .ass file, and you'll get embedded font rendering and everything. You could just use mpv or VLC for this, but where's the fun in that?
Crate map #
| Crate | What it does |
|---|---|
ass-core |
Zero-copy ASS/SSA subtitle parser, no_std compatible |
ass-renderer |
Software ASS renderer (zeno rasteriser, rustybuzz shaping, bitmap caching) |
ass-tool |
CLI for rendering, comparison, MKV extraction |
wasp-hls |
Vendored HLS adaptive streaming engine |
vodplace-player-protocol |
PlayerCommand/PlayerEvent message types (postcard serialized) |
vodplace-player-worker |
WASM Web Worker entry point |
vodplace-subtitle |
Subtitle format detection and VTT/SRT→ASS conversion |
vodplace-api |
AT Protocol lexicon types for ionosphere and place.stream |
Running tests #
# Unit tests (native target)
cargo nextest run --bin vodplace
# Server-side tests (IMPORTANT: server-only code like chat_cache is gated behind this feature)
cargo nextest run --features server --bin vodplace
# Both of these must pass. The server tests catch stuff that native misses because
# of #[cfg(feature = "server")] gating. If you only run the first one, you might
# have silently broken server code and not know it.
# Workspace tests (all crates)
cargo nextest run
# E2E tests (needs worker WASM built and dx serve running, or let Playwright start it)
cd e2e && npx playwright test