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=pubregisters a single publisher for the stream.role=subregisters 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_idto 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_bytesisec-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
initplus 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
- Deploy Worker changes:
- add Durable Object binding/class
StreamRelayDO - route
/api/stream/wsto it
- add Durable Object binding/class
- Add
ec-node ws-publish/ec-node ws-subscribe. - Update the web viewer to prefer the relay path for global watching.