Maintainer principle: put as much effort into the static bitvid client as possible before branching into any separate application. - bitvid-bridge-plan: prominent "PRIORITY PRINCIPLE — static bitvid first" banner up top; the Bridge (a second app) is deliberately DEFERRED until the static client's live features (Go-Live Tier-1/2 MVP) ship and are maximally developed; build it only when live proves demand for its sovereignty/power features. - live-publish-plan: new "Build order & the static-first principle" section with the recommended sequence — 1) Shorts (#16b), 2) Live ingest (#16 watch foundation), 3) Go-Live MVP (#16c P0-1, Tiers 1&2, client-side only, no Bridge/VPS), then 4) the Bridge later only if warranted. Co-Authored-By: Claude Opus 4.8 <[email protected]>
25 KiB
BitVid Live — PUBLISH / "Go Live" — Dev Plan
TODO ref: #16c in todo/TODO_2026-06-20_pre-launch.md (this doc expands the
stub). Companion to docs/live-ingest-plan.md (#16, watch-only) — the watch
side (Live tab, player, chat, zaps) is built there and reused here; this doc
adds the broadcast side.
Status: FUTURE / exploratory — all six decisions LOCKED (see Core Decision's
blanket rule: bitvid hosts nothing; self-host or external only). Sequenced
after #16 (live ingest) and #16b (shorts). The largest plan, and the only one
touching (self-hosted, never bitvid-run) media infrastructure.
Core decision
BLANKET RULE (maintainer, non-negotiable): bitvid.network hosts NOTHING but a static client — no servers, no media infrastructure, ever. All media receiving/routing is self-hosted by the creator (the Bridge run headless — a single self-hosted binary, see
docs/bitvid-bridge-plan.md) or handled by an external provider (zap.stream/YouTube/Twitch). There is no bitvid-operated Media Node and no managed publishing — those options are off the table.
Build BitVid Live as a Nostr-native live-stream control layer, not a video-hosting backend. BitVid the client stays exactly what it is — a static site that signs client-side, holds no keys, runs no server. The one thing a static browser app genuinely cannot do (receive + route live media) is isolated into a separate, self-hosted-or-external service (never bitvid-operated). Split:
BitVid static client = identity, metadata, NIP-53 publishing, discovery, chat,
zaps, player UI, archive→VOD publishing
Media Node / Bridge = RTMP/SRT/WHIP ingest, HLS/WebRTC output, recording,
(self-hosted OR restreaming, status API
external provider)
This keeps BitVid's architecture intact while adding broadcast. Crucially, most of these modes need no bitvid infrastructure at all — see the three-tier spectrum next.
Provider options — three tiers (bitvid hosts none of them)
The creator chooses who receives their media; bitvid only ever publishes the
resulting streaming URL and plays it. A VPS is only the most sovereign tier,
not a requirement — most creators never run a server.
| Tier | Setup / cost | Playback in bitvid | Trade-off | bitvid work |
|---|---|---|---|---|
| 1. Platform embed — StreamYard/OBS → YouTube/Twitch | free, zero infra | their iframe embed | their rules/ads; no native playback, no P2P/WebTorrent | external-embed adapter |
| 2. Serverless RTMP→HLS API — Livepeer / Mux / Cloudflare Stream | ~free–cheap, no server | native HLS (+ P2P-assist possible) | small cost at scale; a 3rd-party account | native HLS (from #16) |
| 3. Self-hosted node — the Bridge run headless on a VPS | most effort/cost | native HLS | full sovereignty + WebTorrent VOD + restream | native HLS + Bridge |
- Recommended default "no-VPS but native": Tier 2, Livepeer — decentralized, crypto-native, cheapest-aligned; the creator points OBS/StreamYard at Livepeer's RTMP ingest and pastes the HLS URL into bitvid. Mux (free delivery allotment; live needs a paid quality tier) and Cloudflare Stream (free ingest/encoding, cheap delivery) are alternatives.
- StreamYard is a studio, not a CDN — it broadcasts out to YouTube/Twitch/custom-RTMP (custom RTMP is paid), so with bitvid it's a Tier-1 encoder feeding YouTube/Twitch, or a Tier-2/3 encoder feeding a serverless API / your node.
- The same NIP-53 event + player serve all three tiers — the only per-tier code is the playback adapter (external embed vs native HLS). So supporting the spectrum is cheap, and the VPS/Bridge is the advanced tier, not the default.
Relationship to the other two plans
- #16 (live-ingest) is a hard dependency. It builds the NIP-53 parsers
(30311/1311), the Live tab,
LiveCard,LivePlayer,LiveChatPanel, HLS playback (hls.js), and host zaps. This plan reuses all of that to watch streams and adds the builders + Studio + Media Node/Bridge to create them. - Shared Phase 0 schema. #16 adds
parseLiveStreamEvent/parseLiveChatMessageEvent; this plan addsbuildLiveStreamEvent/buildLiveChatMessageEvent/updateLiveStreamStatusEvent. Do them injs/nostrEventSchemas.jstogether so the catalogue stays consistent — coordinate so they aren't duplicated. - My Videos "Live" sub-tab (noted in the shorts plan) belongs to this feature — managing your own streams only makes sense once you can publish.
Why BitVid Live — advantages over the incumbents
Lean into what only bitvid can offer; these are the reasons a creator would pick BitVid Live over zap.stream/Twitch/YouTube.
- P2P bandwidth offload (WebTorrent-flavored live). Classic WebTorrent can't carry a live stream (its infohash needs the complete file), but P2P-assisted HLS can: hls.js + P2P Media Loader (historically WebTorrent-tracker-based) lets concurrent viewers share HLS segments over WebRTC, so each extra viewer helps serve the stream. This directly cuts the Media Node's egress — the biggest cost of running live infra — and scales better the more popular a stream gets. (Enable when concurrency warrants; see the ingest plan.)
- Recordings become full WebTorrent VODs. The moment a stream ends, its recording is an ordinary bitvid video: complete file → infohash → seed + webseed, playable P2P + URL like everything else. Twitch/YouTube VODs are origin-only; bitvid VODs are P2P-distributable and self-hostable.
- Nostr-native = portable + no lock-in. The stream is a signed NIP-53 event on relays, not a row in a platform DB. It's discoverable by any Nostr app, the creator owns it, and it can't be de-platformed by a single company.
- Zaps built in. Instant Lightning value-for-value to the host during the stream (bitvid's existing NWC/zap system) — no ads, no 50% platform cut, no payout thresholds.
- Bring-your-own everything. Own storage (R2/B2/S3/MinIO) for recordings, own relays, self-hostable Media Node, and an external-provider-only mode that needs zero bitvid infra. No mandatory middleman.
- One canonical stream, many mirrors. Restream to YouTube/Twitch/zap.stream simultaneously while bitvid stays the sovereign home — reach without lock-in.
- Archive is a first-class video, not a throwaway. VODs flow into the same
feeds, search, moderation, comments, and zaps as every other bitvid video (the
s-tag/info.json/30078 model) — no separate "past broadcasts" ghetto. - Self-sovereign identity. The creator signs; no BitVid account, no bridge ever holds their key.
Decisions needed
DECISION 1 — Media hosting model. ✅ LOCKED by the blanket rule: a three-tier provider spectrum — bitvid hosts NONE of them. The creator picks who receives their media; bitvid just publishes the resulting
streamingURL. A VPS is only the most sovereign tier, not a requirement. See "Provider options" below:
- Platform embed (StreamYard/OBS → YouTube/Twitch) — free, zero infra; bitvid embeds their player. Ships first (Phase 1).
- Serverless RTMP→HLS API (Livepeer / Mux / Cloudflare Stream) — no server to run; the creator gets a real HLS URL bitvid plays natively. The recommended "no-VPS but native" default. Livepeer is the most aligned.
- Self-hosted node (the Bridge run headless on a VPS) — full sovereignty + WebTorrent VOD + restream control; most effort. The advanced tier.
The managed-BitVid-node option is removed. bitvid's job (publish the event, play the
streamingURL) is identical across all three — the only code difference is a per-tier playback adapter (external embed vs native HLS).
DECISION 2 — Signing during a live stream. ✅ LOCKED: A for MVP, B (NIP-46) later; D removed by the blanket rule. The node must NOT hold the creator's key. (A) BitVid tab/app stays open and signs status=live/ended (matches the Bridge's embedded-webview design) — MVP. (B) NIP-46 remote signer for a hands-off experience — later. (C) a self-hosted node may also publish provider-status under its own pubkey while the canonical 30311 stays user-signed — available to self-hosters. (D) managed publishing account is off the table (blanket rule).
DECISION 3 — Transcode. ✅ LOCKED: pass-through only (
-c copy) in v1. Require H.264/AAC + ~2s keyframes in; no transcode ladders. Transcoding is a heavy, self-hoster-only opt-in much later, if ever.
DECISION 4 — WebRTC low-latency. ✅ LOCKED: HLS first, WebRTC later. HLS (~6–30s) works everywhere via #16's hls.js; WebRTC (<500ms) is a later phase.
DECISION 5 — Local Bridge in MVP. ✅ LOCKED: not required for the external- provider MVP; it IS the self-hosting path (DECISION 1b). With the blanket rule, self-hosting a Media Node is running the Bridge headless, so the Bridge isn't an optional extra for self-hosters — it's the mechanism. The external-provider MVP (Phase 1) still needs neither. Full plan:
docs/bitvid-bridge-plan.md.
DECISION 6 — Flag naming. ✅ LOCKED: keep the specific split.
FEATURE_LIVE_INGEST/_PUBLISH/_BRIDGE/_RESTREAM(+_CHAT_POSTfrom #16). No umbrellaFEATURE_LIVE.FEATURE_LIVE_PUBLISHimplies ingest is on.
Config flags (off = no trace)
Every "Go Live" surface is instance-config gated and defaults off, threaded
config/instance-config.js → js/config.js → js/constants.js like
FEATURE_NIP71_INGEST. An operator can ship the code and have publishing be
completely invisible until they opt in.
FEATURE_LIVE_INGEST— watch side (from #16; a dependency of publish).FEATURE_LIVE_PUBLISH— the Studio / Go-Live publishing UI (the master publish switch).FEATURE_LIVE_BRIDGE— Media Node / Bridge session API integration.FEATURE_LIVE_RESTREAM— external restream output management.
Independent of watching. These are separate from FEATURE_LIVE_INGEST, so an
instance can enable watching live streams without enabling broadcasting (or
vice-versa). FEATURE_LIVE_PUBLISH implies ingest is on (you watch your own
stream); it does not force ingest on for everyone if the operator wants
watch-only.
When FEATURE_LIVE_PUBLISH is off, all of the following are absent — no tab,
no button, no trace (as if never added):
- "Go Live" / Studio entry points — no Studio launch anywhere (sidebar, upload modal, profile).
- My Videos → Live sub-tab — not rendered (shorts-plan sub-tab treatment).
- Live-event builders —
buildLiveStreamEvent/updateLiveStreamStatusEventare not wired into any UI (the schema definitions can exist dormant; nothing invokes them). - Bridge/session UI — pairing, ingest settings, health dashboard (also gated
on
FEATURE_LIVE_BRIDGE). - Restream UI — output manager (also gated on
FEATURE_LIVE_RESTREAM). - Archive→VOD "publish recording" live-origin flow (a normal video upload is unaffected).
- Routes — any
#view=live-studio(or equivalent) falls back to default. - Config surface — the flags are the only footprint.
Bridge and restream have their own sub-flags so an operator can allow, e.g., publishing to their own Media Node without exposing external restreams.
Who may go live (operator control). Beyond the on/off flags, publishing on an
instance should respect the existing whitelist/WoT model — i.e. an operator
can allow only whitelisted creators to broadcast on their instance (reusing
accessControl), the same way the feeds are gated. Decide whether go-live is
whitelist-gated by default (recommended: yes on whitelist-mode instances) so a
public instance isn't an open ingest relay.
Event model (NIP-53 — shared with #16)
Canonical live address = 30311:<creator-pubkey>:<d-tag> (an addressable
replaceable event; dedupe by pubkey + d, exactly like bitvid treats video
d-tags). Key tags: d, title, summary, image, streaming (HLS/WebRTC
playback URL — the canonical first entry), recording (VOD when ended), starts,
ends, status (planned/live/ended), relays, t (hashtags), p (host +
participants), service.
Live chat = kind 1311, anchored to the activity via
['a', '30311:<pubkey>:<d>', '<relay>', 'root'] — so bitvid live chat is just
Nostr, reusing #16's chat pipeline.
Schema work (in js/nostrEventSchemas.js, coordinated with #16):
NOTE_TYPES.LIVE_STREAM(30311),NOTE_TYPES.LIVE_CHAT_MESSAGE(1311).- Builders:
buildLiveStreamEvent,updateLiveStreamStatusEvent,buildLiveChatMessageEvent. Parsers: from #16. - Helper:
getLiveAddress(pubkey, d)→ the canonicalacoordinate.
Safety invariant: the public 30311 event carries only the playback URL and public metadata. Never publish RTMP/SRT ingest URLs or stream keys into Nostr.
Auth & key custody (the non-negotiables)
- BitVid signs; the Bridge routes. The Media Node/Bridge never receives the creator's Nostr private key. It controls media sessions, not identity.
- Bridge auth via NIP-98 HTTP-Auth (kind 27235). bitvid already has this
builder (
buildHttpAuthEvent,js/nostrEventSchemas.js). The client signs a 27235 event ("I, pubkey X, authorize creating session Y at URL Z"); the node verifies the signature, mints a stream key, returns ingest/playback URLs. - Stream keys stay out of Nostr — held node-side (encrypted config / secret store) or, in local-Bridge mode, in the OS keychain. Public event gets the HLS URL only.
Architecture — four pieces
- BitVid Live Client (static app; reuses #16) — browse/watch, render NIP-53, play HLS/WebRTC/external, chat + zaps, planned/live/ended state, and the archive→VOD convert flow.
- BitVid Live Studio (static app; new, this plan) — create live-event metadata, publish planned→live→ended NIP-53 updates, show OBS/encoder ingest settings, connect to Bridge/Node, monitor health, manage restream outputs, publish the recording.
- Media Node (server-side; self-hosted by the creator, never bitvid-run) — RTMP/SRT/WHIP ingest → HLS/WebRTC out, recording, S3-compatible upload, status API, optional restream. Built on MediaMTX — not from scratch. Per DECISION 7 it's the same binary as the Bridge, run headless.
- BitVid Bridge (the self-hosting vehicle — desktop app or headless node) — local RTMP/SRT receiver for hardware encoders, one-input→many-outputs restream, local recording, archive upload, OBS-WebSocket control. This is how a creator self-hosts (DECISION 1b) and the seed of a future "BitVid Box".
New client modules (matching repo style): js/services/liveEventService.js,
liveChatService.js, liveBridgeClient.js, livePlaybackService.js (or extend
#16's), liveArchiveService.js; UI js/ui/views/LiveStudioView.js,
js/ui/components/{LiveBridgeSetup,LiveRestreamOutputs}.js (LiveCard/LivePlayer/
LiveChatPanel come from #16).
Media Node & Bridge API
One stable path per session: /live/<creator-pubkey>/<d-tag>. Ingest via RTMP /
SRT / WHIP (key/token kept secret); playback via HLS (/index.m3u8) / WebRTC. The
client publishes only the playback URL into the 30311 streaming tag.
Bridge API surface (session lifecycle + SSE/WebSocket status):
POST /sessions, GET /sessions/:id, .../start, .../end, .../outputs*,
GET /sessions/:id/status|manifest, GET /events (SSE stream of
input_connected|input_live|input_lost|recording_finalized|upload_complete|…).
The client listens and decides when to sign+publish NIP-53 updates (DECISION 2).
Outgoing streams — three distinct concepts
- A. Primary BitVid output — OBS/encoder → Media Node → HLS/WebRTC → the URL
that goes in the 30311
streamingtag. - B. External restream outputs (
FEATURE_LIVE_RESTREAM) — the node forwards the same encoded stream to YouTube/Twitch/zap.stream/custom RTMP/SRT. Stream-copy only in v1 (-c copy, no transcode — MediaMTXrunOnReady+ FFmpeg forward). Output keys stay node-private; public mirrors optionally added as['r', <public-watch-url>]. - C. Archive / VOD output — on end, MediaMTX records fMP4/TS → finalize →
upload to R2/B2/S3/MinIO → produce
recording.mp4+.info.json(+ optional thumbnail/torrent). This maps directly onto bitvid's existing storage-pointer model: the['s', '<provider>:<prefix>']tag +js/utils/storagePointer.jsalready derives<prefix>.info.json, so the archived stream becomes an ordinary bitvid video with zero new playback/metadata code.
Archive → VOD publishing (reuses everything)
When the Bridge returns a recording manifest (url, thumbnail, duration, sha256, infoJson, optional torrent/magnet/webseed), the Studio shows "Publish recording as a BitVid video." On click bitvid publishes:
- Updated 30311 —
status=ended+recording=<url>. - Canonical bitvid video — kind 30078 (Content Schema v3, the same event
the upload modal produces) with the hosted URL +
spointer. - Optional NIP-71 mirror — bitvid's addressable 34235 (normal) via the existing mirror service, so other apps see the VOD (per the nip71 plan; note bitvid mirrors to 34235/34236, not raw kind 21).
This is a specialized upload-modal flow, not a new publishing system.
Playback strategy
Live: HLS first (works everywhere via #16's hls.js), then WebRTC later (DECISION 4), then external-embed fallback. Mirror bitvid's URL-first ethos: primary live URL → external mirror → ended recording (VOD). Optional P2P-assisted HLS (P2P Media Loader) offloads Media Node egress once a stream has enough concurrent viewers — see the ingest plan + Advantages above. The ended recording gets full classic WebTorrent (seed + webseed), like any bitvid video.
Build order & the static-first principle
PRIORITY (maintainer): pour effort into the static bitvid client before branching into any separate app. Everything through the Go-Live MVP below lives entirely in the static client — no Bridge, no VPS, no bitvid infra. The Bridge (a second application) is deliberately deferred until this is shipped and maximally developed. See
docs/bitvid-bridge-plan.md.
Recommended sequence across the live/short work:
1. Shorts (#16b) ← smallest, ready to build, unrelated to live
2. Live ingest (#16) ← the WATCH foundation (Live tab, player, hls.js,
chat, zaps) — a hard dependency of publishing
3. Go-Live MVP (#16c P0–P1) ← Tiers 1 & 2 (external embed + serverless HLS);
CLIENT-SIDE ONLY, no Bridge / no VPS / no infra
── ship it; validate that people actually want live ──
4. (only if warranted) the Bridge app ← Tier-3 sovereignty/power features;
its own repo/toolchain/code-signing — a later track
The Go-Live MVP (steps 1–3) is the "as much static bitvid as possible" target: a real, working live experience where the creator brings their own provider (StreamYard→YouTube, or OBS→Livepeer/Mux) and bitvid does the Nostr-native part — publish, discover, watch, chat, zap, archive→VOD. Only after that proves out does a second app (the Bridge) earn the investment.
Phases
- Phase 0 — Flags + schema (small).
FEATURE_LIVE_PUBLISH/BRIDGE/RESTREAM; 30311/1311 builders +updateLiveStreamStatusEvent+getLiveAddress(coordinate with #16's parsers);docs/live.md; schema-doc update; unit tests. - Phase 1 — Provider-agnostic publishing (small; NO infra — Tiers 1 & 2).
"Create Live Stream" modal: title/summary/thumbnail/tags/starts + a paste-in
streamingURL / platform link; publish planned→live→ended; chat + zaps against the live address (reusing #16). Playback adapter picks external embed (Tier 1: YouTube/Twitch) or native HLS (Tier 2: Livepeer/Mux/Cloudflare). This is BitVid Live v0, needs zero servers, and covers the recommended no-VPS path (Tier 2 / Livepeer). - Phase 2 — Bridge API client + mock backend (medium).
liveBridgeClient, Pair-Bridge UI, mocked/sessions, setup wizard, status dashboard, recording-ready flow — builds the client contract before real infra exists. - Phase 3 — Self-hosted Media Node MVP (large, server-side). A reference node the creator runs themselves (never bitvid-operated) around MediaMTX: session API, NIP-98-verified auth, token'd RTMP ingest, HLS playback, start/stop detection, status endpoint. OBS → the creator's node → bitvid watches. No keys in Nostr. Per DECISION 7 (Bridge plan) this is the same binary as the Bridge, run headless — so Phase 3 and Phase 7 share code. This is BitVid Live v1.
- Phase 4 — Recording → archive → VOD (medium). MediaMTX recording → finalize → S3 upload → manifest → the archive→VOD publish flow above.
- Phase 5 — Restream outputs (medium;
FEATURE_LIVE_RESTREAM). Output manager, provider presets (YouTube/Twitch/zap.stream/custom), copy-only forward, retry/backoff, per-output status. One canonicalstreamingURL; mirrors are optionalrlinks. - Phase 6 — OBS Companion (medium). Start/stop OBS + read bitrate/dropped frames via OBS-WebSocket (built into OBS 28+). Prefer routing through the local Bridge to avoid browser↔localhost/mixed-content pain.
- Phase 7 — Local Bridge app (large;
FEATURE_LIVE_BRIDGE). Packaged desktop helper: local RTMP/SRT receiver, forward to Node + external, local record + archive upload, embedded Studio webview (signs) + optional localhost API. BitVid Live v2 / "BitVid Box" seed. Full plan:docs/bitvid-bridge-plan.md(cross-platform app; note DECISION 7 there — the Bridge and this Media Node can share one codebase running headless vs with-UI). - Phase 8 — WebRTC low-latency (later, DECISION 4).
Engineering rules (invariants)
- BitVid signs; the Bridge routes — the node never owns the creator's Nostr identity.
- Public playback URLs are safe; ingest URLs are secret — publish the
.m3u8, neverrtmp://…?key=SECRET. - Pass-through first, transcode later — copy the H.264/AAC stream in v1.
- One canonical stream, many optional mirrors — the 30311 has one
streamingURL; external platforms are outputs, not the source of truth (unless the creator picks an external provider as primary in Phase-1 mode). - Archived live streams become normal BitVid videos — no separate archive
system; reuse the
s-tag / info.json / 30078 model.
Risks / watch-items
- bitvid stays static; infra is the creator's. The Media Node/Bridge is real server infra, but per the blanket rule it's never bitvid-operated — it's self-hosted by the creator or replaced by an external provider. So the ops/cost burden lives with whoever self-hosts, not bitvid.network. The static client is unaffected; Phase-1 external mode needs no infra at all.
- Self-hoster burden (DECISION 1b). Bandwidth/storage cost, encoder config, and keeping the node reachable fall on the self-hoster. Good docs + a turnkey headless Bridge (one binary) are the mitigation.
- Signing during long streams (DECISION 2). Tab-open (A) is fragile for long broadcasts; NIP-53 lets clients treat a live event with no update in ~1h as ended, so plan a heartbeat and a graceful "stale → ended" story.
- CORS / cross-origin HLS — segments served by the (self-hosted) node's origin must allow the bitvid origin (same class as the storage/edge CORS work).
- Encoder compatibility — pass-through requires H.264/AAC + sane keyframes; document encoder settings; reject or warn on incompatible input.
- Scope discipline — publishing is genuinely large; ship Phase 1 (no infra) first and treat the Media Node as a separate service milestone.
Sources
- NIP-53 (Live Activities; kind 30311, live chat 1311): https://nips.nostr.com/53
- MediaMTX (ingest/playback/record/forward/control API): https://mediamtx.org/docs/kickoff/introduction · forward · record
- OBS: SRT guide · WebSocket remote control
- Existing bitvid code this reuses:
js/nostrEventSchemas.js(HTTP_AUTH/27235, 30078 video),js/utils/storagePointer.js(stag →.info.json),docs/live-ingest-plan.md(#16 watch side),docs/nip71-migration-plan.md(mirror to 34235/34236),js/constants.js(FEATURE_*flags).