Skip to content

moq-cli: unified endpoint CLI (--client/--server + import/export + --<proto>-connect/listen) #1983

Description

@kixelated

Summary

Redesign the entire moq-cli surface into a unified endpoint model: moq-cli becomes a media router that wires endpoints onto one shared Origin. This replaces the current per-purpose subcommands (publish/subscribe/serve/accept/hls) and folds in the network gateways (RTMP/SRT/WebRTC) under one grammar.

This is the follow-up to the library work already landed:

The grammar

moq-cli  <MoQ endpoint>  <import|export>  <other endpoints…>
  • MoQ side (bare flags, before the verb — the Origin's link to the MoQ network):
    • --client <relay-url> — dial a relay. URL path is the broadcast, e.g. https://cdn.moq.dev/demo/bbb.hang. Carries moq_native::ClientConfig (--client-*).
    • --server <bind> — host MoQ sessions (moq-native Server). Carries moq_native::tls::Server (--tls-*, --server-tls-*).
  • Direction verb (import = into MoQ / export = out of MoQ). It's the pivot: it declares direction and separates the MoQ side (left) from the other endpoints (right), so "which is source / which is sink" is unambiguous.
  • Other endpoints (after the verb, protocol-prefixed so there are no flag collisions and multiple can compose):
    • stdin/stdout: --stdin <format> (import-only), --stdout <format> (export-only).
    • HLS: --hls-connect <playlist-url> (import-only), --hls-listen <addr> + TLS/part-target/window (export-only).
    • RTMP: --rtmp-connect <url> / --rtmp-listen <addr> (+ --rtmp-prefix, RTMPS via moq_native::tls::Server).
    • SRT: --srt-connect <url> / --srt-listen <addr> (+ --srt-latency, --srt-prefix).
    • WebRTC: --rtc-connect <url> / --rtc-listen <addr> (+ --rtc-udp-bind, --rtc-public-addr, --rtc-tls-*). WHIP/WHEP is implied by direction + connect/listen (import: listen=WHIP server, connect=WHEP client; export: listen=WHEP server, connect=WHIP client), so no --whip-*/--whep-*.

Command mapping (old → new)

Today Endpoint model
publish --url R --broadcast N ts --client R/N import --stdin ts
subscribe --url R --broadcast N --format ts --client R/N export --stdout ts
serve --broadcast N ts --server <bind> import --stdin ts
accept --broadcast N --server <bind> export --stdout <fmt>
hls --url R import --playlist P --client R/N import --hls-connect P
hls --url R export --listen L --client R export --hls-listen L
(new) RTMP ingest → relay --client R import --rtmp-listen [::]:1935
(new) restream MoQ → Twitch --client R/N export --rtmp-connect rtmps://twitch/KEY
(new) simulcast to RTMP + SRT --client R/N export --rtmp-connect … --srt-connect …

Key design decisions (settled)

  1. Everything is an endpoint on one shared Origin. No flag is conditional/ignored — passing a flag always adds that endpoint. That's what makes the flag model safe (vs. the mutually-exclusive-modes-as-flags that caused "args in arbitrary places, silently ignored").
  2. No backdoor — listeners are directional. A --rtmp-listen under import accepts pushes only and rejects plays; under export it serves plays only. The operator declares direction; the connecting peer can't choose. (Library follow-up: moq_rtmp::run/moq_srt::run are bidirectional today — the directional server just refuses the wrong request type.)
  3. import/export don't share an endpoint set. --stdin/--hls-connect are import-only (sources); --stdout/--hls-listen are export-only (sinks); only the foreign --rtmp/srt/rtc-connect/listen appear under both. Implement as enum Direction { Import(ImportEndpoints), Export(ExportEndpoints) } with a shared flattened ForeignEndpoints sub-struct for the rtmp/srt/rtc flags.
  4. Naming: bare --client/--server for MoQ (so the TLS sub-flags --client-tls-*/--server-tls-*/--tls-* from moq-native line up); protocol-prefixed --<proto>-connect/--<proto>-listen for everything else. No collisions.
  5. help_heading grouping per protocol so the (necessarily larger) --help stays legible.
  6. Fan-out is free (multiple foreign endpoints after the verb = simulcast). Cross-protocol or mixed-direction in one process is out of scope — that's what the relay is for (run two processes joined by a relay). Direction is a single verb per invocation.
  7. Broadcast rides in the --client URL path. For an import listener the name comes from the protocol (RTMP app/key, SRT streamid); for --server hosting, TBD (see open questions).

Library APIs to wire

  • stdin/stdout: existing moq-cli publish.rs (PublishFormat) / subscribe.rs (SubscribeFormat, catalog).
  • HLS: moq_hls::import / moq_hls::Server (export router).
  • RTMP: moq_rtmp::run (listen, make directional) / moq_rtmp::Client::{publish,pull} (connect, from moq-rtmp/moq-srt: add client (dial-out) role #1982).
  • SRT: moq_srt::run (listen, make directional) / moq_srt::dial::{publish,pull} (connect, from moq-rtmp/moq-srt: add client (dial-out) role #1982).
  • WebRTC: moq_rtc::{Server, Client} (routers / dial).
  • MoQ: moq_native::Client/Server reconnect/accept, shared Origin.
  • URL parsing: rtmp://host[:1935]/<app>/<key>, srt://host:port?streamid=…&latency=…, mirror resolve_path in moq-rtmp/src/listen.rs.

The ripple (don't forget)

This is a breaking change to every documented moq-cli invocation. Update:

  • doc/bin/cli.md — full rewrite.
  • demo/ justfiles (just pub, etc.) and configs.
  • test/smoke/ — the interop matrix uses moq-cli publish/subscribe.
  • Example commands scattered across doc/ (grep moq-cli publish|subscribe|serve|accept|hls).

Suggested approach

  1. Branch off main (after moq-rtmp/moq-srt: add client (dial-out) role #1982 lands) so the dial-out Client/dial are available.
  2. Build the clap surface first (Cli + Direction + Import/Export + ForeignEndpoints, help_heading) and get --help reviewed before wiring runtime.
  3. Wire the shared-Origin runtime: spawn each endpoint as a task feeding/draining the Origin; MoQ side is the reconnect/accept.
  4. Make the listeners directional (library tweak in moq-rtmp/moq-srt).
  5. Do the repo-wide ripple + rewrite doc/bin/cli.md.
  6. just check.

Open questions

  • --server (MoQ host) mode: how is the broadcast named for --stdin/dial endpoints when there's no --client URL path? (A --broadcast fallback flag, or require the URL path form only with --client?)
  • Should --client and --server be combinable (a "relay node" that hosts and forwards)? The shared-Origin model allows it; decide whether to expose it now.
  • Format handling for --stdin/--stdout: reuse the existing PublishFormat/SubscribeFormat enums as the flag value.

(Filed by Claude Opus 4.8 — captures a long design discussion; the grammar and decisions above are settled, the open questions are for the build.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions