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.
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.
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?.
To obtain a MoQ connection URL you'll need your Sandbox API URL. If you don't have it already, see Sandbox API URL.
- React
- TS
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"; constSUBSCRIBER_PATH = "stream-alice"; constSANDBOX_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 } = awaitgetSandboxMoqSubscriberAccess (SUBSCRIBER_PATH );
If you don't want to use React or pull in the whole client library just for the useSandbox hook, you can call the Sandbox API directly with fetch:
constSUBSCRIBER_PATH = "stream-alice"; constSANDBOX_API_URL = "YOUR_SANDBOX_API_URL"; constresponse = awaitfetch ( `${SANDBOX_API_URL }/moq/${SUBSCRIBER_PATH }/subscriber`, ); const {connection_url :subscribeUrl } = awaitresponse .json ();
Connecting and subscribing
Install the MoQ packages:
- npm
- Yarn
- pnpm
- Bun
npm install @moq/lite @moq/watch
yarn add @moq/lite @moq/watch
pnpm add @moq/lite @moq/watch
bun add @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 constconnection = awaitMoq .Connection .connect (newURL (subscribeUrl )); // Subscribe to the broadcast constbroadcast = newWatch .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 constcanvas =document .getElementById ("remote") asHTMLCanvasElement ; constbackend = newWatch .MultiBackend ({element :canvas ,broadcast ,paused : false, });
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:
- TypeScript
- Python
import {FishjamClient } from '@fishjam-cloud/js-server-sdk'; constfishjamClient = newFishjamClient ({fishjamId ,managementToken , }); conststreamPath = 'stream-alice'; // Generate a connection URL that allows subscribing to 'stream-alice' const {connection_url :subscribeUrl } = awaitfishjamClient .createMoqAccess ({subscribePath :streamPath , });
from fishjam import FishjamClient fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ) stream_path = 'stream-alice' # Generate a connection URL that allows subscribing to 'stream-alice' subscribe_url = fishjam_client.create_moq_access(subscribe_path=stream_path).connection_url
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.
- TypeScript
- Python
import {FishjamClient } from '@fishjam-cloud/js-server-sdk'; constfishjamClient = newFishjamClient ({fishjamId ,managementToken }); constroomName = 'my-room'; const {connection_url :alicePublisherUrl } = awaitfishjamClient .createMoqAccess ({publishPath :roomName + "/alice", }); const {connection_url :bobPublisherUrl } = awaitfishjamClient .createMoqAccess ({publishPath :roomName + "/bob", }); const {connection_url :namespaceUrl } = awaitfishjamClient .createMoqAccess ({subscribePath :roomName , });
from fishjam import FishjamClient fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ) room_name = 'my-room' alice_publisher_url = fishjam_client.create_moq_access( publish_path=room_name + "/alice", ).connection_url bob_publisher_url = fishjam_client.create_moq_access( publish_path=room_name + "/bob", ).connection_url namespace_url = fishjam_client.create_moq_access(subscribe_path=room_name).connection_url
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. constconnection = newMoq .Connection .Reload ({url : newSignal (newURL (namespaceUrl )),enabled : newSignal (true), }); // Tracks which broadcasts already have a tile, so we don't mount duplicates // when the Effect below re-runs on every announce change. constmountedStreams = newSet <string>(); newEffect ().run ((effect ) => { for (constpath ofeffect .get (connection .announced )) { constkey =path .toString (); if (mountedStreams .has (key )) continue;mountedStreams .add (key ); constcanvas =document .createElement ("canvas");document .body .appendChild (canvas ); newWatch .MultiBackend ({connection :connection .established ,broadcast : newWatch .Broadcast ({connection :connection .established ,name :path ,enabled : true, }),element :canvas , }); } });
See also
- Web Publishing — broadcast a MoQ stream from the browser
- MoQ with Fishjam — how MoQ works in Fishjam
- Livestreaming — the WebRTC (WHIP/WHEP) approach
- WHIP/WHEP with Fishjam