MoQ is under active development. APIs and protocols may change between releases.

Skip to content

@moq/publish ​

npm

The publisher: captures a camera, microphone, screen, or file, encodes with WebCodecs, writes the catalog, and publishes a hang broadcast.

html
<script type="module">
    import "@moq/publish/element";
    import "@moq/publish/ui";     // optional device picker and controls
</script>

<moq-publish-ui>
    <moq-publish url="https://relay.example.com/anon" name="room/alice.hang" source="camera">
        <video muted autoplay></video>
    </moq-publish>
</moq-publish-ui>

Attributes ​

Attribute
url, nameRelay URL (with ?jwt= if needed) and broadcast name.
sourcecamera, screen, or file.
muted, invisibleDisable audio or video capture.
previewWhat the nested element shows: the raw source (default), a decoded copy of the encoded stream to see what viewers get, or none.
announceWhen to advertise: once a source is live (default), always, or never. A camera source waits for every enabled track, and source waits until each captured track's config resolves or fails, so the first catalog lists every rendition. The broadcast is created while connected either way, but nobody can see or subscribe to it until it is announced.

A nested <video> gets the raw capture stream; a <canvas> is drawn by the element. <moq-publish-support> shows what the browser can encode.

Camera and microphone failures are readable through the element's el.sources.video and el.sources.audio signals. When these hold a Publish.Source.Camera or Publish.Source.Microphone, their out.error signal contains an Error, such as NotAllowedError when permission is refused, and clears when capture restarts. A refused capture waits for a permission, device, constraint, or enabled-state change instead of repeatedly prompting. <moq-publish-ui> displays the failure in its status badge.

Encoding ​

The video encoder follows its share of the connection's send-rate estimate (via Bandwidth.Allocator), so several publishers on one connection do not each target the whole uplink. Audio reserves its configured bitrate so video's share is honest, and keeps encoding at that rate. Codec, resolution, framerate, and bitrate are tunable through el.video.config; the audio encoder exposes its codec and volume. For simulcast or several renditions, drop the element and register your own encoders on a Publish.Broadcast.

The video and audio encoders measure how far their output falls behind the media clock when they flush frames. Catalog jitter is the spread above each rendition's own recent minimum lateness, so a constant encoder delay is not jitter. The advertised value only rises; frame duration alone does not set it.

Clock ​

Every timestamp the publisher writes is performance.now() in microseconds, so camera, microphone, screen, and file sources share one timeline. The catalog advertises that mapping as its root clock from the first snapshot, with PTS zero at performance.timeOrigin, so a viewer or an HLS export can name any frame's wall time. The mapping is fixed for the page: a system-clock adjustment never retimes the broadcast. Stamp your own tracks (e.g. text cues) on the same timeline to stay in sync.

Custom tracks ​

broadcast.net is the underlying Moq.Broadcast.Producer, so an application can serve its own tracks alongside the media. It is recreated on each (re)connection, so acquire it from an effect and reseed the track each time:

ts
import * as Json from "@moq/json";

signals.run((effect) => {
    const net = effect.get(broadcast.net);
    if (!net) return;

    // A day-long retention so a late viewer still replays the last value.
    const track = net.createTrack("meta.json", { maxAge: 86_400_000 });
    effect.cleanup(() => track.close());

    const meta = new Json.Snapshot.Producer<Meta>({ track });
    meta.update(current);
});

broadcast.catalog.mutate(c => { c.yourSection = ... }) advertises it without touching the media sections, which are folded in from the registered renditions. The catalog schema is loose, so an unknown root section passes through untouched; cast to name your own:

ts
broadcast.catalog.mutate((catalog) => {
    (catalog as Catalog.Root & { metadata?: string[] }).metadata = ["meta.json"];
});

Without the element ​

ts
import * as Moq from "@moq/net";
import * as Publish from "@moq/publish";

// Shared with every other component pointed at the same relay; its origin holds
// the broadcasts, so they survive a reconnect.
const connection = new Moq.Connection({ url: new URL("https://relay.example.com/anon") });

const broadcast = new Publish.Broadcast({
    origin: connection.origin,
    enabled: true,
    name: Publish.Net.Path.from("alice.hang"),
});

const camera = new Publish.Source.Camera({ enabled: true });
const microphone = new Publish.Source.Microphone({ enabled: true });
const video = new Publish.Signals.Computed((effect) => effect.get(camera.out.source)?.video);
const capture = new Publish.Video.Capture({ source: video });

// Each encoder registers a rendition on the broadcast (`broadcast.video(name)`) and
// encodes only while someone is subscribed.
new Publish.Video.Encoder("video/hd", { broadcast, capture, enabled: true });
new Publish.Video.Encoder("video/sd", { broadcast, capture, enabled: true, config: { maxScale: 0.25 } });
const audioSource = new Publish.Signals.Computed((effect) => effect.get(microphone.out.source)?.audio);
const audioCapture = new Publish.Audio.Capture({ source: audioSource });
new Publish.Audio.Encoder("audio", { broadcast, capture: audioCapture, enabled: true });

Standalone components start enabled unless you pass enabled: false (or a signal). Camera and microphone sources may prompt for permission on construction, so build an enabled screen source inside the user gesture that authorizes screen capture. Audio capture that starts before the page's first click or keypress waits for one: browsers suspend Web Audio until then, and the audio rendition stays out of the catalog until samples flow.

Every input and output is a signal from @moq/signals. Load from a CDN (https://esm.sh/@moq/publish/element) for a no-build embed.

Licensed under MIT or Apache-2.0