Namespace discovery
A subscriber does not have to know track names in advance. It can watch a
namespace prefix and let the hub tell it which namespaces exist under it,
as publishers come and go. This page runs one watcher on the prefix
guide and four publisher announcements: one before the watcher, two
after it, and one the server’s policy refuses. Then one publisher leaves.
Code
#define WIRED_MAIN#include "wired.h"
/* draft-ietf-moq-transport-22 9.14 / 9.15: the hub asks this hook before * it accepts a PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE. This sample's * policy refuses any namespace ending in "secret" (a real deployment * would verify the token too; this demo ignores it). */static int authorize_ns( void* ctx, u64 msg_type, const moqctl_ns* ns, const moqctl_token* token) { (void)ctx; (void)msg_type; (void)token; return ns->n == 0 || !wired_span_eq_cstr(ns->fields[ns->n - 1], "secret");}
static wired_moqt_hub g_hub;
static void on_step(void* ctx, u64 now_ms) { wired_moqt_tick((wired_moqt_hub*)ctx, now_ms);}
int wired_main(int argc, char** argv) { /* The same fixed demo identity as the other MoQT pages, plus QUIC * DATAGRAM support, which WebTransport clients ask for. */ static wired_srvboot_demo_keys keys; wired_srvboot_id id; wired_srvboot_demo(&id, &keys, 0x50, "guide-nd"); id.max_datagram_frame_size = 65535;
/* PUBLISH_NAMESPACE and SUBSCRIBE_NAMESPACE arrive on the client's own * bidi streams (draft-ietf-moq-transport-22 6.4.2) and stay open: the hub * answers on them through stream_reply_open, and later pushes NAMESPACE / * NAMESPACE_DONE onto the subscriber's stream with stream_send. */ wired_moqt_io io = wired_moqraw_io(); wired_moqt_init(&g_hub, io); g_hub.authorize_namespace = authorize_ns;
/* on_session_close withdraws every namespace a leaving session * published (NAMESPACE_DONE to its watchers); the tick retries a push * the transport refused. "moqt-22" selects draft-22 (see the hub page). */ wired_srvrun_opt opt = { .incoming_cpu = -1, .wt_protocols = "moqt-22", .on_step = on_step, .on_step_ctx = &g_hub, .wt_on_session = wired_moqt_on_session, .wt_session_ctx = &g_hub, .wt_on_stream_data = wired_moqt_on_stream_data, .wt_stream_data_ctx = &g_hub, .wt_on_stream_reset = wired_moqt_on_stream_reset, .wt_stream_reset_ctx = &g_hub, .wt_on_session_close = wired_moqt_on_session_close, .wt_session_close_ctx = &g_hub, };
u16 port = (u16)wired_cliargs_int(argc, argv, "--port", 4433); wired_srvrun_handler h = {0}; wired_srvrun_obs obs = {0}; return wired_server_run_opt(port, &id, h, obs, &opt) ? 0 : 1;}Client
package main
import ( "bufio" "bytes" "context" "fmt" "log" "os" "strings" "time"
"wired-guide/moqtclient")
// request sends one request on a fresh bidi request stream// (draft-ietf-moq-transport-22 SS6.4.2) and prints the hub's first// answer: REQUEST_OK (0x7) or REQUEST_ERROR (0x5) with its error code.// The stream is left alone: resetting it would cancel the request.func request(ctx context.Context, s *moqtclient.Session, who string, typ byte, id byte, name string) *bufio.Reader { label := map[byte]string{0x6: "PUBLISH_NAMESPACE", 0x50: "SUBSCRIBE_NAMESPACE"}[typ] // Request ID, Track Namespace (or Prefix), no parameters. _, r := s.Request(ctx, typ, append(append([]byte{id}, moqtclient.Namespace(strings.Split(name, "/")...)...), 0)) switch t, body := moqtclient.ReadMsg(r); t { case 0x7: fmt.Printf("%s: %s %s -> REQUEST_OK\n", who, label, name) case 0x5: code := moqtclient.Varint(bufio.NewReader(bytes.NewReader(body))) fmt.Printf("%s: %s %s -> REQUEST_ERROR error_code=%d\n", who, label, name, code) default: log.Fatalf("%s answered with type %#x", label, t) } return r}
// publish opens a publisher session (one MoQT peer; closing its QUIC// connection is how it leaves) that announces name (Request ID 0).func publish(ctx context.Context, who, name string) *moqtclient.Session { s := moqtclient.Dial(ctx, os.Args[1]+"/moqt") request(ctx, s, who, 0x6, 0, name) return s}
// watch reads the next push on the SUBSCRIBE_NAMESPACE stream and prints// it with the namespace suffix (the fields after the prefix "guide").func watch(r *bufio.Reader) { typ, body := moqtclient.ReadMsg(r) label := map[uint64]string{0x8: "NAMESPACE", 0xE: "NAMESPACE_DONE"}[typ] if label == "" { log.Fatalf("unexpected push type %#x", typ) } fmt.Printf("watcher: %s %s\n", label, strings.Join(moqtclient.ReadNamespace(body), "/"))}
func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
// A namespace already published before anyone watches. lobby := publish(ctx, "lobby", "guide/lobby") defer lobby.Conn.CloseWithError(0, "")
// The watcher subscribes to the prefix "guide". After REQUEST_OK the // hub sends one NAMESPACE per namespace already published under it: // the initial set. w := moqtclient.Dial(ctx, os.Args[1]+"/moqt") defer w.Conn.CloseWithError(0, "") watcher := request(ctx, w, "watcher", 0x50, 0, "guide") watch(watcher)
// Two publishers join. Each waits for its own REQUEST_OK, and the // watcher then reads the NAMESPACE the hub pushed for it, so every // step is ordered by messages on the wire, never by a sleep. roomA := publish(ctx, "room-a", "guide/room-a") watch(watcher) roomB := publish(ctx, "room-b", "guide/room-b") defer roomB.Conn.CloseWithError(0, "") watch(watcher)
// The server's authorize_namespace hook refuses "secret": the // publisher gets REQUEST_ERROR UNAUTHORIZED (0x1) and the watcher is // never told about it (the next push it sees is room-a leaving). request(ctx, roomB, "room-b", 0x6, 2, "guide/secret")
// room-a leaves: closing its session withdraws its namespace, and the // hub pushes NAMESPACE_DONE to the watcher. moqtclient.Check(roomA.Conn.CloseWithError(0, "")) fmt.Println("room-a: session closed") watch(watcher)}Output
$ ./moqt-discovery --port 4433conn closed by peer (CONNECTION_CLOSE)conn closed by peer (CONNECTION_CLOSE)conn closed by peer (CONNECTION_CLOSE)conn closed by peer (CONNECTION_CLOSE)$ go run . https://127.0.0.1:4433lobby: PUBLISH_NAMESPACE guide/lobby -> REQUEST_OKwatcher: SUBSCRIBE_NAMESPACE guide -> REQUEST_OKwatcher: NAMESPACE lobbyroom-a: PUBLISH_NAMESPACE guide/room-a -> REQUEST_OKwatcher: NAMESPACE room-aroom-b: PUBLISH_NAMESPACE guide/room-b -> REQUEST_OKwatcher: NAMESPACE room-broom-b: PUBLISH_NAMESPACE guide/secret -> REQUEST_ERROR error_code=1room-a: session closedwatcher: NAMESPACE_DONE room-aHow it works
Section titled “How it works”- The sample negotiates draft-ietf-moq-transport-22 through the
WebTransport subprotocol
moqt-22(the server setsopt.wt_protocols). Each side opens its own unidirectional control stream and sends SETUP (9.1) on it, and the hub serves requests only after both SETUPs have arrived. - Each request then goes on a fresh bidi stream the client opens
(draft-ietf-moq-transport-22 6.4.2), and the hub answers on that stream
through
stream_reply_open. Both namespace requests stay in force after their answer, until the stream is cancelled (6.4.2.3) or the session ends. NAMESPACE and NAMESPACE_DONE are pushed on the SUBSCRIBE_NAMESPACE stream itself. - PUBLISH_NAMESPACE (9.14, 4.1) announces a Track Namespace (
guide/lobby,guide/room-a, …), and the hub answers REQUEST_OK. - SUBSCRIBE_NAMESPACE (9.15, 4.2) names a Track Namespace Prefix. After
REQUEST_OK the hub sends one NAMESPACE (9.16) for every namespace
already published under the prefix (the initial set, here
lobby), then one more each time a matching namespace is published. Each carries only the suffix: the fields after the prefix. hub.authorize_namespacesees every PUBLISH_NAMESPACE and SUBSCRIBE_NAMESPACE (itsmsg_typesays which) with the namespace and the presented AUTHORIZATION TOKEN. This sample refuses any namespace ending insecret: the publisher gets REQUEST_ERROR UNAUTHORIZED (1) and the watcher is never told about it.- When room-a closes its session,
wired_moqt_on_session_closewithdraws every namespace it published, and the hub sends NAMESPACE_DONE (9.17) to each watcher that was told about it. - The client never sleeps to wait for the hub: each publisher waits for
its own REQUEST_OK, and the watcher reads the NAMESPACE or
NAMESPACE_DONE the hub pushes, so every step is ordered by messages on
the wire. The hub’s tick (
wired_moqt_tickfromon_step) retries a push the transport refused.