@moq/room
v0.1.0
Published
Headless multi-participant rooms over MoQ: announce-derived roster, local publish, remote watch, and a chat track
Readme
@moq/room
Headless multi-participant rooms over Media over QUIC. A room is a path prefix. There is no service and no storage: joining is minting a moq-token rooted at that prefix (the LiveKit AccessToken analogue) and dialing the relay.
Participants are discovered from the announce stream. Identity is the path before camera.hang / screen.hang. Each participant publishes:
{identity}/camera.hang: camera + microphone, hd/sd renditions{identity}/screen.hang: screenshare; its announce/unannounce is the share lifecycle
This is the generic room layer extracted from hang.live (roster, local/remote, hang/*.json metadata) and iroh-live (the chat track iroh-rooms is moving onto the announce bus). Memes, 3D layout, chat UI, and accounts stay in the app. The native twin is moq-room.
Install
bun add @moq/roomToken
Sign with @moq/token. root is the room, get: "" subscribes to everyone, put: "<identity>/" so a participant cannot publish at someone else's paths.
import { claims } from "@moq/room";
import { sign } from "@moq/token";
const token = await sign(key, claims("meet/demo", "alice"));
// Dial https://relay.example.com/meet/demo?jwt=<token>On a public prefix (anon/), skip the token and dial that path directly.
Usage
import { Local, Room } from "@moq/room";
import { Connection, Path } from "@moq/net";
const connection = new Connection.Reload({
url: new URL("https://relay.example.com/anon/meet/demo"),
enabled: true,
});
const identity = Path.from("alice");
const local = new Local({
connection: connection.established,
identity,
user: { name: "Alice" },
});
local.enabled.set(true);
local.cameraEnabled.set(true);
local.microphoneEnabled.set(true);
const room = new Room({ connection, identity });
// room.remotes is a Map<identity, Remote>. Each Remote has camera/screen
// Members; assign member.canvas and set member.muted to false to play audio.hang.live should depend on this package for Room, Local, Remote, and the hang/*.json metadata tracks. Location stays an app-defined catalog extension (TRACK.location). hang.live's JSON chat (TRACK.chat = hang/chat.json) is also an extension; the JSON window track is Chat.TRACK ("chat").
import { Chat } from "@moq/room";
const publisher = Chat.Publisher.create(broadcast);
publisher.send("hello");
const subscriber = Chat.Subscriber.subscribe(broadcast.consume());
const event = await subscriber.recv(); // push, pop, or skip
// Call publisher.finish() and subscriber.close() when done.A conferencing demo (no memes, no 3D, no chat UI) lives at demo/web/src/meet.html.
Room members start muted; set member.muted to false to play audio. Chat uses uncompressed JSON strings, a retained ten-second window with push/pop/skip events; it is not compatible with the raw UTF-8 iroh-live track. Empty normalized identities are rejected by claims.
