# 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=&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.