Files
bitvid/docs/live-publish-plan.md
T
thePR0M3TH3ANandClaude Opus 4.8 50f1a7c2aa [docs] Document static-first priority principle + the recommended build sequence
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]>
2026-07-01 13:53:10 -04:00

25 KiB
Raw Blame History

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 adds buildLiveStreamEvent/buildLiveChatMessageEvent/ updateLiveStreamStatusEvent. Do them in js/nostrEventSchemas.js together 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 streaming URL. A VPS is only the most sovereign tier, not a requirement. See "Provider options" below:

  1. Platform embed (StreamYard/OBS → YouTube/Twitch) — free, zero infra; bitvid embeds their player. Ships first (Phase 1).
  2. 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.
  3. 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 streaming URL) 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_POST from #16). No umbrella FEATURE_LIVE. FEATURE_LIVE_PUBLISH implies 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):

  1. "Go Live" / Studio entry points — no Studio launch anywhere (sidebar, upload modal, profile).
  2. My Videos → Live sub-tab — not rendered (shorts-plan sub-tab treatment).
  3. Live-event builders — buildLiveStreamEvent / updateLiveStreamStatusEvent are not wired into any UI (the schema definitions can exist dormant; nothing invokes them).
  4. Bridge/session UI — pairing, ingest settings, health dashboard (also gated on FEATURE_LIVE_BRIDGE).
  5. Restream UI — output manager (also gated on FEATURE_LIVE_RESTREAM).
  6. Archive→VOD "publish recording" live-origin flow (a normal video upload is unaffected).
  7. Routes — any #view=live-studio (or equivalent) falls back to default.
  8. 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 canonical a coordinate.

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)

  1. BitVid signs; the Bridge routes. The Media Node/Bridge never receives the creator's Nostr private key. It controls media sessions, not identity.
  2. 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.
  3. 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 streaming tag.
  • 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 — MediaMTX runOnReady + 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.js already 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:

  1. Updated 30311 — status=ended + recording=<url>.
  2. Canonical bitvid video — kind 30078 (Content Schema v3, the same event the upload modal produces) with the hosted URL + s pointer.
  3. 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 streaming URL / 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 canonical streaming URL; mirrors are optional r links.
  • 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)

  1. BitVid signs; the Bridge routes — the node never owns the creator's Nostr identity.
  2. Public playback URLs are safe; ingest URLs are secret — publish the .m3u8, never rtmp://…?key=SECRET.
  3. Pass-through first, transcode later — copy the H.264/AAC stream in v1.
  4. One canonical stream, many optional mirrors — the 30311 has one streaming URL; external platforms are outputs, not the source of truth (unless the creator picks an external provider as primary in Phase-1 mode).
  5. 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