# ECP-0063: Cloudflare MoQ Relay + WebTransport-Only Web Watch

Status: Implemented

## Decision

Adopt Cloudflare's MoQ relay preview as the default "global" distribution layer and make the web watcher path WebTransport-only.

Concrete changes:

1. `ec-node` gains a WebTransport MoQ publisher path that can publish a live CMAF (fMP4) stream to a relay URL (default: `https://cdn.moq.dev/anon`).
2. `every.channel` (the deployed static site) becomes a real web watcher by embedding a WebTransport-capable MoQ player component (`<moq-watch>` from `@moq/watch`).
3. The existing WebRTC/WS bootstrap directory/relay remains temporarily for compatibility, but is treated as deprecated once MoQ/WebTransport is stable.

## Motivation

The project goal is one-to-many live streaming at global scale without rebuilding a bespoke stateful SFU stack.

MoQ over WebTransport moves the core system boundary to:
- publisher produces timed objects (CMAF fragments),
- relays cache/fanout,
- subscribers fetch via a single WebTransport session.

This aligns the "global watcher" story with an infrastructure-native model rather than point-to-point rendezvous.

## Scope

In scope for this ECP:
- "Watch from the web" using WebTransport to a MoQ relay.
- "Publish from a node" to the relay, using ffmpeg to create fMP4 fragments.
- A shareable link format for `every.channel` to open a specific relay + broadcast name.

Out of scope (explicitly deferred):
- Global discovery/index of broadcasts.
- Manifest signing / Merkle availability / anti-junk (separate ECPs already exist).
- Safari support guarantees (implement anyway; provide guidance/flag notes).
- Multi-variant ladders and ABR (follow-on ECP once single-variant publish/watch works end-to-end).

## Technical Notes

### Relay default

Default relay endpoint is:
- `https://cdn.moq.dev/anon`

Nodes may override via CLI flags for self-hosted relays or future Cloudflare relay endpoints.
As of February 21, 2026, browser sessions against `https://interop-relay.cloudflare.mediaoverquic.com/` are observed to stall/fail during MoQ session establishment with current web clients, while `https://cdn.moq.dev/anon` establishes browser sessions in Chrome.

### Web player

Use the `@moq/watch` web component:
- `<moq-watch url="..." path="...">`

This is WebTransport + WebCodecs based, and is expected to interoperate with Cloudflare's current relay preview.

### CMAF passthrough default

`ec-node wt-publish` defaults `--passthrough=false` (and Nix module default `passthrough = false`).

Reason:
- Browser `@moq/watch` playback against live OTA ingest currently hits repeated `Invalid sample duration 0 in trun` decode failures when fed raw passthrough fMP4 fragments.
- Non-passthrough mode avoids this incompatibility and restores end-to-end playback reliability.

### Transport compatibility

Cloudflare's public relay currently implements a subset of the IETF MoQ Transport draft-07 and may not interoperate with
newer draft implementations.

Implementation choice:
- Cloudflare's relay preview currently does **not** support `ANNOUNCE` (namespace-style publishing). `ec-node wt-publish` uses the `moq-lite` publish model via `quinn` + `web-transport-quinn` + `moq-lite` and `moq-mux` (fMP4 ingestion) for Cloudflare relay compatibility.
- `ec-node wt-publish` is QUIC/WebTransport-only (no WebSocket fallback). NixOS deployments also set `MOQ_CLIENT_WEBSOCKET_ENABLED=false` as a belt-and-suspenders default for any other binaries that use `moq-native`.
- For Cloudflare relay interop, we patch `web-transport-proto` to send and accept the standard WebTransport subprotocol negotiation header (`sec-webtransport-protocol`) in addition to the legacy `wt-available-protocols`/`wt-protocol` headers. Without subprotocol negotiation, the relay may not select a `moqt-*` protocol and can close the session immediately after MoQ `SETUP`.
- If the relay does not select a WebTransport subprotocol, `ec-node wt-publish` attempts MoQ `SETUP` with protocol overrides (`moqt-16`, `moqt-15`, `moq-00`) before falling back to "no protocol".

### Share link

Web share link:
- `https://every.channel/watch?url=<relay-url>&name=<broadcast-name>`

## Alternatives considered

- Keep the legacy WebRTC/WS path as primary. Rejected because it does not align with relay-native MoQ fanout goals.
- Wait for full draft parity across all relays before shipping. Rejected because live interop was already sufficient on the chosen relay path.

## Rollout / Reversibility

- Keep existing `/api/*` bootstrap endpoints during migration.
- Make the web site prefer MoQ/WebTransport; keep legacy paths hidden behind "advanced" until removed.
- Reversible by switching the deployed assets back to the previous UI build, and/or pointing users at the legacy paths.
