every.channel/evolution/proposals/ECP-0058-ws-relay-one-to-many-bootstrap.md
2026-02-15 16:17:27 -05:00

2.8 KiB

ECP-0058: One-to-Many Web Bootstrap via Stream Relay DO (WebSocket)

Status: Draft

Problem

Our initial web viewer path used a direct WebRTC data channel between publisher and viewer. That path is fundamentally 1:1: once one viewer consumes the offer/answer, additional viewers cannot join, which breaks the "send a link to mom" requirement and contradicts the project's one-to-many goal.

TURN improves NAT traversal but does not change the 1:1 nature of WebRTC offers/answers.

We need a bootstrap path that:

  • is one-to-many by construction
  • works in a normal browser without plugins
  • carries the same CMAF object stream the native pipeline already produces
  • remains obviously replaceable by MoQ/WebTransport later (no long-term lock-in)

Proposal

Add a per-stream fanout relay implemented as a Durable Object:

  • GET /api/stream/ws?stream_id=<id>&role=sub|pub
    • Upgrades to a WebSocket.
    • role=pub registers a single publisher for the stream.
    • role=sub registers a subscriber.

Publisher behavior:

  • Connect once to the relay DO (role=pub).
  • Send the CMAF object stream as "direct-wire" chunked messages (same framing as the existing WebRTC direct path).
  • Continue to refresh the directory listing while publishing (multiple viewers are supported).

Subscriber behavior:

  • Connect to the relay DO (role=sub).
  • Receive the live message stream.
  • On connect, receive a buffered init segment (chunk_index=0) plus a small ring buffer of recent segments so playback can start immediately.

Web app behavior:

  • The global live list (/api/directory) remains the discovery surface.
  • "Watch" connects by stream_id to the relay websocket and plays via MSE.

Wire Framing

We reuse the existing "direct-wire" message format:

  • Each WebSocket message is binary and begins with a 1-byte tag:
    • 0x01 = STREAM chunk (payload is a slice of [u32be frame_len][frame_bytes...])
    • 0x00 = FRAME (optional; not required)
    • 0x02 = PING (ignored)
  • frame_bytes is ec-moq::encode_object_frame(meta_json, data):
    • [u32be meta_len][meta_json_bytes][data_bytes...]

The relay DO decodes publisher STREAM chunks into frames only for buffering (to identify init vs segments). For live fanout, it forwards publisher messages to subscribers as-is.

Limits / Abuse Notes

This is not spam-resistant. It is a bootstrap relay.

Defensive bounds:

  • Buffer only init plus the last N segments (currently 12) per stream DO.
  • If publisher reassembly buffer grows beyond a few MiB (garbage input), reset it.

Future ECPs cover signatures/manifests/merkle/anti-junk.

Rollout

  1. Deploy Worker changes:
    • add Durable Object binding/class StreamRelayDO
    • route /api/stream/ws to it
  2. Add ec-node ws-publish / ec-node ws-subscribe.
  3. Update the web viewer to prefer the relay path for global watching.