MoQ Streaming
This section explains how to publish and subscribe to live streams using Media over QUIC (MoQ) with Fishjam. MoQ uses QUIC as its transport layer (with a WebSocket fallback), delivering ultra-low latency at scale — making it ideal for interactive broadcasts, live events, and large-audience streams.
If you're new to MoQ, then we recommend getting familiar with the MoQ Streaming with Fishjam explanation.
We show how to quickly prototype in Quickstart with the Sandbox API and how to get ready for production in Production MoQ with Server SDKs.
Fishjam supports both WebRTC-based livestreaming (WHIP/WHEP) and MoQ streaming.
MoQ is a new protocol designed from the ground up for scalable, low-latency delivery to large audiences.
WebRTC is a mature, battle-tested technology built for low-latency peer-to-peer conferencing and interactive streaming.
See Livestreaming for the WebRTC approach.
Quickstart with the Sandbox API
If you don't have a backend server set up, you can prototype a MoQ streaming scenario using the Sandbox API.
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.
Publisher setup
To start publishing a MoQ stream, you need two things: a publisher token and the relay URL.
Obtaining a publisher token
Fetch a sandbox publisher token from the Room Manager:
constFISHJAM_ID = "YOUR_FISHJAM_ID"; constPUBLISHER_PATH = "stream-alice"; constSANDBOX_API_URL = "YOUR_SANDBOX_API_URL"; constresponse = awaitfetch ( `${SANDBOX_API_URL }/moq/${PUBLISHER_PATH }/publisher`, ); const {token :publishToken } = awaitresponse .json ();
Connecting and publishing
Install the MoQ packages:
- npm
- Yarn
- pnpm
- Bun
npm install @moq/lite @moq/publish
yarn add @moq/lite @moq/publish
pnpm add @moq/lite @moq/publish
bun add @moq/lite @moq/publish
Use the token to connect to the Fishjam MoQ relay and start broadcasting:
// Build the relay URL using the publisher token constrelayUrl = newURL ( `https://relay.fishjam.io/${FISHJAM_ID }?jwt=${publishToken }`, ); // Connect to the Fishjam MoQ relay constconnection = awaitMoq .Connection .connect (relayUrl ); constcamera = newPublish .Source .Camera ({enabled : true }); constmicrophone = newPublish .Source .Microphone ({enabled : true }); // Set up a broadcast with video and audio tracks constbroadcast = newPublish .Broadcast ({connection ,name :Moq .Path .from (PUBLISHER_PATH ),enabled : true,video : {source :camera .source ,hd : {enabled : true }, },audio : {enabled : true,source :microphone .source , }, });
The stream is now live on the MoQ relay! Viewers can now start watching the stream by following the steps in Subscriber setup
Subscriber setup
To receive a MoQ stream you need one thing: a subscriber token.
Obtaining a subscriber token
Fetch a sandbox subscriber token using the Sandbox API:
constFISHJAM_ID = "YOUR_FISHJAM_ID"; constSUBSCRIBER_PATH = "stream-alice"; constSANDBOX_API_URL = "YOUR_SANDBOX_API_URL"; constresponse = awaitfetch ( `${SANDBOX_API_URL }/moq/${SUBSCRIBER_PATH }/subscriber`, ); const {token :subscribeToken } = 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 token to connect and render the stream onto that canvas:
// Build the relay URL using the subscriber token constrelayUrl = newURL ( `https://relay.fishjam.io/${FISHJAM_ID }?jwt=${subscribeToken }`, ); // Connect to the Fishjam MoQ relay constconnection = awaitMoq .Connection .connect (relayUrl ); // 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 relay URL (with the JWT token 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 MoQ with Server SDKs
The Quickstart with the Sandbox API shows how to get MoQ streaming up and running quickly. In a production scenario, your backend generates tokens with proper authorization, allowing you to control who can publish and who can subscribe.
MoQ tokens are path-scoped: a publisher token grants write access to a specific path, and a subscriber token grants read access. Your backend needs to:
- Generate a publisher token for the path your broadcaster will use.
- Generate a subscriber token for the path your viewers will use.
- Deliver each token to the appropriate client.
- TypeScript
- Python
import {FishjamClient } from '@fishjam-cloud/js-server-sdk'; constfishjamClient = newFishjamClient ({fishjamId ,managementToken , }); conststreamPath = 'stream-alice'; // Generate a token that allows publishing to 'stream-alice' const {token :publishToken } = awaitfishjamClient .createMoqToken ({publishPath :streamPath , }); // Generate a token that allows subscribing to 'stream-alice' const {token :subscribeToken } = awaitfishjamClient .createMoqToken ({subscribePath :streamPath , });
from fishjam import FishjamClient fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ) stream_path = 'stream-alice' # Generate a token that allows publishing to 'stream-alice' publish_token = fishjam_client.create_moq_token(publish_path=stream_path) # Generate a token that allows subscribing to 'stream-alice' subscribe_token = fishjam_client.create_moq_token(subscribe_path=stream_path)
Deliver these tokens to your clients, then use them to connect as described in Connecting and publishing and Connecting and subscribing.
A single token should only grant either publishPath or subscribePath — not both.
Streamers receive a publisher token; viewers receive a subscriber token.
This separation ensures a viewer can never accidentally publish to the stream.
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 token 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 {token :alicePublisherToken } = awaitfishjamClient .createMoqToken ({publishPath :roomName + "/alice", }); const {token :bobPublisherToken } = awaitfishjamClient .createMoqToken ({publishPath :roomName + "/bob", }); const {token :namespaceToken } = awaitfishjamClient .createMoqToken ({subscribePath :roomName , });
from fishjam import FishjamClient fishjam_client = FishjamClient( fishjam_id=fishjam_id, management_token=management_token, ) room_name = 'my-room' # Publishers under this namespace might use paths like: # 'my-room/alice-camera', 'my-room/bob-screen', etc. namespace_token = fishjam_client.create_moq_token(subscribe_path=room_name)
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 (`https://moq.fishjam.work/${FISHJAM_ID }?jwt=${namespaceToken }`), ),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
If you want a better understanding of how MoQ works, see:
If you want to learn about streaming using WebRTC instead of MoQ, see: