Skip to main content
Version: Next

Web Subscribing

This tutorial explains how to subscribe to a live stream and render it in the browser using Media over QUIC (MoQ) with Fishjam. To broadcast a stream instead, see Web Publishing.

info

If you're new to MoQ, then we recommend getting familiar with the MoQ with Fishjam explanation.

To receive a MoQ stream you need one thing: a subscriber connection URL — the relay URL with a subscriber token embedded as a ?jwt= query parameter. We show how to quickly prototype with the Sandbox API and how to get ready for production.

tip

MoQ is a protocol with a well-defined negotiation, so in theory any compliant MoQ client should work. That said, we recommend using the @moq client libraries — the reference TypeScript implementation maintained alongside the protocol. For more details, see the documentation.

Quickstart with the Sandbox API

If you don't have a backend server set up, you can prototype subscribing using the Sandbox API.

Obtaining a subscriber connection URL

For more on what the Sandbox API is and its limitations, see What is the Sandbox API?.

info

To obtain a MoQ connection URL you'll need your Sandbox API URL. If you don't have it already, see Sandbox API URL.

If you're using React, the useSandbox hook from @fishjam-cloud/react-client wraps the Sandbox API request for you:

import { useSandbox } from "@fishjam-cloud/react-client"; const SUBSCRIBER_PATH = "stream-alice"; const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL"; // Inside a React component: const { getSandboxMoqSubscriberAccess } = useSandbox({ sandboxApiUrl: SANDBOX_API_URL, }); // Request a subscriber connection URL scoped to the subscriber path const { connection_url: subscribeUrl } = await getSandboxMoqSubscriberAccess(SUBSCRIBER_PATH);

Connecting and subscribing

Install the MoQ packages:

npm install @moq/lite @moq/watch

Add a <canvas> element to your page — MultiBackend will paint decoded frames onto it:

<canvas id="remote"></canvas>

Then use the connection URL to connect and render the stream onto that canvas:

// Connect to the Fishjam MoQ relay using the subscriber connection URL const connection = await Moq.Connection.connect(new URL(subscribeUrl)); // Subscribe to the broadcast const broadcast = new Watch.Broadcast({ connection, name: Moq.Path.from(SUBSCRIBER_PATH), enabled: true, }); // Grab the canvas from the DOM and hand it to MultiBackend, // which handles decoding and rendering of both video and audio const canvas = document.getElementById("remote") as HTMLCanvasElement; const backend = new Watch.MultiBackend({ element: canvas, broadcast, paused: false, });
info

Why <canvas>?
Watch.MultiBackend renders to a <canvas> so it can decode frames directly through the WebCodecs API, which natively supports the codecs MoQ delivers. A <video> element is also supported, but it relies on Media Source Extensions and requires remuxing each stream into fMP4 first.

That's it! The stream will appear in the canvas once the publisher starts broadcasting.

Using the <moq-watch> web component

If you don't need fine-grained control over the rendering pipeline, the @moq/watch package ships a ready-to-use custom element that wraps connection, subscription, decoding, and rendering behind a single HTML tag.

Import once to register the elements — @moq/watch/element registers <moq-watch> (which renders the stream) and @moq/watch/ui registers <moq-watch-ui> (which decorates a child <moq-watch> with play/pause, volume, latency, and buffer controls):

import "@moq/watch/element"; import "@moq/watch/ui";

Then drop <moq-watch-ui> wrapping <moq-watch> into your HTML — pass the connection URL (returned by the SDK or Sandbox API, with the JWT already in the query string) and the broadcast path:

<moq-watch-ui> <moq-watch url="https://relay.fishjam.io/YOUR_FISHJAM_ID?jwt=YOUR_TOKEN" name="stream-alice" latency="real-time" reload > <canvas></canvas> </moq-watch> </moq-watch-ui>

The element observes attributes like url, name, paused, muted, volume, latency, and reload, so you can drive playback from JavaScript by updating them at runtime.

Production with Server SDKs

The Quickstart gets you watching quickly. In production, your backend generates tokens with proper authorization, so you control who can subscribe.

A subscriber connection URL grants read access to a specific path. Generate one on your backend and deliver it to the viewing client:

import { FishjamClient } from '@fishjam-cloud/js-server-sdk'; const fishjamClient = new FishjamClient({ fishjamId, managementToken, }); const streamPath = 'stream-alice'; // Generate a connection URL that allows subscribing to 'stream-alice' const { connection_url: subscribeUrl } = await fishjamClient.createMoqAccess({ subscribePath: streamPath, });

Deliver this connection URL to the viewing client, then use it to connect as described in Connecting and subscribing.

Subscribe to a namespace

When multiple publishers join a room, you won't know their exact paths in advance. Instead of consuming a single path, you can discover all broadcasts published under a namespace prefix and subscribe to each one as they appear.

To do this, generate a subscriber connection URL scoped to the room namespace instead of a single stream path.

import { FishjamClient } from '@fishjam-cloud/js-server-sdk'; const fishjamClient = new FishjamClient({ fishjamId, managementToken }); const roomName = 'my-room'; const { connection_url: alicePublisherUrl } = await fishjamClient.createMoqAccess({ publishPath: roomName + "/alice", }); const { connection_url: bobPublisherUrl } = await fishjamClient.createMoqAccess({ publishPath: roomName + "/bob", }); const { connection_url: namespaceUrl } = await fishjamClient.createMoqAccess({ subscribePath: roomName, });

Then on the client, read connection.announced — a reactive Set of the paths currently being published. Wrap the read in an Effect, and the callback re-runs every time a publisher joins or leaves, with the Set reflecting the live state. Iterate it to subscribe to any path you haven't seen yet:

// Reload manages the WebTransport session: it connects, auto-reconnects on drop, // and exposes `announced` as a reactive Set<Path> of publishers currently online. const connection = new Moq.Connection.Reload({ url: new Signal(new URL(namespaceUrl)), enabled: new Signal(true), }); // Tracks which broadcasts already have a tile, so we don't mount duplicates // when the Effect below re-runs on every announce change. const mountedStreams = new Set<string>(); new Effect().run((effect) => { for (const path of effect.get(connection.announced)) { const key = path.toString(); if (mountedStreams.has(key)) continue; mountedStreams.add(key); const canvas = document.createElement("canvas"); document.body.appendChild(canvas); new Watch.MultiBackend({ connection: connection.established, broadcast: new Watch.Broadcast({ connection: connection.established, name: path, enabled: true, }), element: canvas, }); } });

See also