Confirmed live in mediamtx logs: the HLS muxer for a browser-published stream gets created then immediately destroyed — 'the stream doesn't contain any supported codec, which are currently AV1, VP9, H265, H264, Opus, MPEG-4 Audio, KLV'. Chrome's getUserMedia()/getDisplayMedia() default video codec for WebRTC is VP8, which isn't in that list. The WHIP publish itself succeeds (stream correctly shows live, mediamtx's own API confirms bytes arriving) so this was easy to miss — only hls/live/<id>/index.m3u8 silently 404s with 'muxer is waiting to be created' forever. Fixed with RTCRtpTransceiver.setCodecPreferences(), reordering the video codec list so H264 is offered first — falls through safely if unavailable. Matches what OBS already sends over RTMP, so recording and HLS both stay on the one already-tested codec instead of gaining a second, broken one.
podsteadr
Self-hosted, nostr-native podcast publishing and livestreaming.
- Log in with nostr — NIP-07 browser extension (Alby, nos2x, …); the server verifies NIP-98 signed requests. No passwords, no email.
- Upload an mp4 → it is stored on a Blossom server
(bundled, or point at any external one) and published in an RSS 2.0 podcast feed with
Podcasting 2.0
<podcast:value>lightning-address payment info. - Go live — stream from OBS (RTMP) or straight from the browser (WebRTC/WHIP) through MediaMTX; viewers watch via HLS and the stream is announced on nostr as a NIP-53 (kind 30311) live event.
- Publish recordings — live streams are recorded; one click remuxes and publishes a recording as a podcast episode.
Quick start
cp .env.example .env # edit PUBLIC_HOST if not localhost
docker compose up --build -d
Then open http://localhost:8095, log in with a NIP-07 extension, and follow the wizard.
Services / ports
| Service | Host port | Purpose |
|---|---|---|
| podsteadr | 8095 | Web UI + API + RSS feeds |
| MediaMTX | 1935 | RTMP ingest (OBS) |
| MediaMTX | 8889 (+8189/udp) | WebRTC/WHIP ingest (browser) |
| MediaMTX | 8890 | HLS playback |
| blossom-server | 8098 | Media blob storage (sha256-addressed) |
Streaming with OBS
Create a stream in the UI; it gives you:
- Server:
rtmp://<host>:1935/live - Stream key:
<streamId>?key=<secret>
The HLS playback URL (http://<host>:8890/live/<streamId>/index.m3u8) is public and never
contains the secret.
Development
docker compose up mediamtx blossom -d # backends
cd server && npm install && npm run dev # API on :8095
cd frontend && npm install && npm run dev # Vite on :5173, proxies /api + /feeds
Tests: cd server && npm test.
Data
Everything lives in named docker volumes: podsteadr-data (SQLite, server nostr key,
covers), mediamtx-recordings (stream recordings, 7-day retention), blossom-data
(media blobs). docker compose down keeps them; down -v wipes them.
Verification
- Unit tests:
cd server && npm test(35 tests). - Against a running stack:
node scripts/e2e.mjs(14 checks: login, blossom upload, feed, stream keys, MediaMTX auth webhook).
Status & internals
See docs/STATUS.md for the architecture, the full verification record, known gotchas (blossom v4 config nesting, no range requests, split-horizon blossom URL, MediaMTX polling rationale), and the remaining-work list (Archipelago packaging, git remote).