From 6300a24626437fc96104eda50d2b28638dcd475f Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Mon, 4 May 2026 17:38:48 +0200 Subject: [PATCH 1/7] Add MoQ streaming tutorial to docs --- api/fishjam-server | 2 +- api/protos | 2 +- api/room-manager | 2 +- docs/tutorials/moq.mdx | 363 +++++++++++++++++++++++++++++++++++++ packages/js-server-sdk | 2 +- packages/python-server-sdk | 2 +- packages/web-client-sdk | 2 +- 7 files changed, 369 insertions(+), 6 deletions(-) create mode 100644 docs/tutorials/moq.mdx diff --git a/api/fishjam-server b/api/fishjam-server index 62073a78..e1f449a2 160000 --- a/api/fishjam-server +++ b/api/fishjam-server @@ -1 +1 @@ -Subproject commit 62073a78d3b6e176a37f52c32cacfc38681bc7f2 +Subproject commit e1f449a22265b11c54d6adf330751dd39ceea88b diff --git a/api/protos b/api/protos index 1a8a029d..ee2e9b34 160000 --- a/api/protos +++ b/api/protos @@ -1 +1 @@ -Subproject commit 1a8a029d4ee99664ba5990c77df688e7bfe81ef5 +Subproject commit ee2e9b34faccfcf7e5ad6e7af57a36bd3a059a02 diff --git a/api/room-manager b/api/room-manager index 31836f4e..15035bf1 160000 --- a/api/room-manager +++ b/api/room-manager @@ -1 +1 @@ -Subproject commit 31836f4ed6c8551b3892cea50d2872cdf2292e71 +Subproject commit 15035bf1d3b516acc6082362b48c049d494a0681 diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx new file mode 100644 index 00000000..f68272f0 --- /dev/null +++ b/docs/tutorials/moq.mdx @@ -0,0 +1,363 @@ +--- +type: tutorial +sidebar_position: 5 +title: MoQ Streaming +description: Stream live video and audio over Media over QUIC (MoQ) with Fishjam, from sandbox prototyping to production deployment. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# MoQ Streaming with Fishjam + +This section explains how to publish and subscribe to live streams using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) 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. + +Before diving into the tutorial, we recommend getting familiar with the [MoQ protocol concepts](https://datatracker.ietf.org/wg/moq/about/). +We show how to quickly prototype in [Quickstart with Sandbox API](#quickstart-with-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). + +:::info +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 with native browser support, built for low-latency conferencing and interactive streaming. + +See [Livestreaming](./livestreaming) for the WebRTC approach. +::: + +## Quickstart with Sandbox API + +If you don't have a backend server set up, you can prototype a MoQ streaming scenario using the Sandbox API provided by the Room Manager. + +### 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: + +```ts +const FISHJAM_ID = 'YOUR_FISHJAM_ID'; +const STREAM_NAME = 'my-stream'; + +const response = await fetch( + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/publisher` +); +const { token: publishToken } = await response.json(); +``` + +#### Connecting and publishing + +Install the MoQ packages: + +```bash npm2yarn +npm install @moq/lite @moq/publish +``` + +Use the token to connect to the Fishjam MoQ relay and start broadcasting: + +```ts +// @noErrors +import * as Moq from '@moq/lite'; +import * as Publish from '@moq/publish'; + +const FISHJAM_ID = 'YOUR_FISHJAM_ID'; +const STREAM_NAME = 'my-stream'; +const publishToken = ''; + +// ---cut--- +// Build the relay URL using the publisher token +const relayUrl = new URL( + `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}` +); + +// Connect to the Fishjam MoQ relay +const connection = await Moq.Connection.connect(relayUrl); + +// Get camera and microphone access +const camera = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + +// Set up a broadcast with video and audio tracks +const broadcast = new Publish.Broadcast({ + connection, + name: Moq.Path.from(STREAM_NAME), + enabled: true, + video: { + source: camera.getVideoTracks()[0] as Publish.Video.Source, + // Set up two layers + hd: { enabled: true }, + sd: { enabled: true }, + }, + audio: { + enabled: true, + source: camera.getAudioTracks()[0] as Publish.Audio.Source, + }, +}); +``` + +The stream is now live on the MoQ relay! + +### Subscriber setup + +To receive a MoQ stream you need one thing: a _subscriber token_. + +#### Obtaining a subscriber token + +Fetch a sandbox subscriber token from the Room Manager: + +```ts +const FISHJAM_ID = 'YOUR_FISHJAM_ID'; +const STREAM_NAME = 'my-stream'; + +const response = await fetch( + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/subscriber` +); +const { token: subscribeToken } = await response.json(); +``` + +#### Connecting and subscribing + +Install the MoQ packages: + +```bash npm2yarn +npm install @moq/lite @moq/watch +``` + +Use the token to connect and receive the stream: + +```ts +// @noErrors +import * as Moq from '@moq/lite'; +import * as Watch from '@moq/watch'; + +const FISHJAM_ID = 'YOUR_FISHJAM_ID'; +const STREAM_NAME = 'my-stream'; +const subscribeToken = ''; + +// ---cut--- +// Build the relay URL using the subscriber token +const relayUrl = new URL( + `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${subscribeToken}` +); + +// Connect to the Fishjam MoQ relay +const connection = await Moq.Connection.connect(relayUrl); + +// Subscribe to the broadcast +const broadcast = new Watch.Broadcast({ + connection, + name: Moq.Path.from(STREAM_NAME), + enabled: true, +}); + +// Attach to a canvas element — MultiBackend handles decoding and rendering +const canvas = document.querySelector('canvas')!; +const backend = new Watch.MultiBackend({ + element: canvas, + broadcast, + paused: false, +}); +``` + +That's it! The stream will appear in the canvas once the publisher starts broadcasting. + +## Production MoQ with Server SDKs + +The [Quickstart with Sandbox API](#quickstart-with-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: + +1. Generate a publisher token for the path your broadcaster will use. +2. Generate a subscriber token for the path your viewers will use. +3. Deliver each token to the appropriate client. + + + + + ```ts + const fishjamId = ''; + const managementToken = ''; + + // ---cut--- + import { FishjamClient } from '@fishjam-cloud/js-server-sdk'; + + const fishjamClient = new FishjamClient({ + fishjamId, + managementToken, + }); + + const streamPath = 'my-stream'; + + // Generate a token that allows publishing to 'my-stream' + const { token: publishToken } = await fishjamClient.createMoqToken({ + publishPath: streamPath, + }); + + // Generate a token that allows subscribing to 'my-stream' + const { token: subscribeToken } = await fishjamClient.createMoqToken({ + subscribePath: streamPath, + }); + ``` + + + + + + ```python + from fishjam import FishjamClient + + fishjam_client = FishjamClient( + fishjam_id=fishjam_id, + management_token=management_token, + ) + + stream_path = 'my-stream' + + # Generate a token that allows publishing to 'my-stream' + publish_token = fishjam_client.create_moq_token(publish_path=stream_path) + + # Generate a token that allows subscribing to 'my-stream' + 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](#connecting-and-publishing) and [Connecting and subscribing](#connecting-and-watching). + +:::tip +A single token should only grant either `publishPath` _or_ `subscribePath` — not both. +Broadcast clients 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 + +You can subscribe not only to a specific stream, but also to an entire namespace — automatically receiving every broadcast published under it. +This is useful when multiple publishers join a room and you want to render all of their streams without knowing their paths in advance. + +To do this, generate a subscriber token scoped to the room namespace instead of a single stream path. + + + + + ```ts + const fishjamId = ''; + const managementToken = ''; + + // ---cut--- + import { FishjamClient } from '@fishjam-cloud/js-server-sdk'; + + const fishjamClient = new FishjamClient({ fishjamId, managementToken }); + + const roomName = 'my-room'; + // Publishers under this namespace might use paths like: + // 'my-room/alice-camera', 'my-room/bob-screen', etc. + + const { token: namespaceToken } = await fishjamClient.createMoqToken({ + subscribePath: roomName, + }); + ``` + + + + + + ```python + 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 use `Moq.Connection.Reload` on the client — it exposes `announced`, a reactive signal that holds the current set of announced paths and fires a callback whenever it changes: + +```ts +// @noErrors +import * as Moq from '@moq/lite'; +import * as Watch from '@moq/watch'; + +const FISHJAM_ID = 'YOUR_FISHJAM_ID'; +const ROOM_NAME = 'my-room' +const namespaceToken = ''; + +// ---cut--- +const relayUrl = new URL( + `https://relay.fishjam.io/${FISHJAM_ID}/${ROOM_NAME}?jwt=${namespaceToken}` +); + +const connection = new Moq.Connection.Reload({ url: relayUrl, enabled: true }); + +// Keep references so we can clean up when a stream goes offline +const slots = new Map(); + +// Subscribe — callback fires with the full current set whenever it changes +const stopDiscovery = connection.announced.subscribe((announcedPaths) => { + const activePaths = new Set([...announcedPaths].map((p) => p.toString())); + + // Tear down streams that are no longer announced + for (const [key, slot] of slots) { + if (!activePaths.has(key)) { + slot.backend.close(); + slot.broadcast.enabled.set(false); + slot.canvas.remove(); + slots.delete(key); + } + } + + // Set up streams that just appeared + for (const path of announcedPaths) { + const key = path.toString(); + if (slots.has(key)) continue; + + const broadcast = new Watch.Broadcast({ + connection: connection.established, + name: path, + enabled: true, + }); + + const canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + + const backend = new Watch.MultiBackend({ + element: canvas, + broadcast, + paused: false, + }); + + slots.set(key, { broadcast, backend, canvas }); + } +}); + +// To stop discovery and tear down all streams: +// stopDiscovery(); +// connection.close(); +``` + +## See also + +If you want a better understanding of how MoQ works, see: + +- [MoQ IETF Working Group](https://datatracker.ietf.org/wg/moq/about/) +- Blog Post etc. + +If you need a different streaming approach, see: + +- [Livestreaming](./livestreaming) +- [WHIP/WHEP with Fishjam](../how-to/backend/whip-whep) diff --git a/packages/js-server-sdk b/packages/js-server-sdk index 0007dea8..46431580 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit 0007dea898b99b6923059b364584054cf1580b02 +Subproject commit 46431580868408f891835b0ea0e26efaeeb835a0 diff --git a/packages/python-server-sdk b/packages/python-server-sdk index 2f18378a..91bbec29 160000 --- a/packages/python-server-sdk +++ b/packages/python-server-sdk @@ -1 +1 @@ -Subproject commit 2f18378a2339b3bf5870939864e0fff94f2e5a76 +Subproject commit 91bbec2947377df1ae263884b6cbbf7651ad89b2 diff --git a/packages/web-client-sdk b/packages/web-client-sdk index ae228527..798efc1a 160000 --- a/packages/web-client-sdk +++ b/packages/web-client-sdk @@ -1 +1 @@ -Subproject commit ae228527ec61ba2db2a61a590b4c999f065dcfeb +Subproject commit 798efc1a35ac2c0921bfd5752ed9f5f6906b993f From 537bd609b757b8d1052ad112d0e46641f5655d6e Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Mon, 4 May 2026 17:48:16 +0200 Subject: [PATCH 2/7] Format code snippets --- docs/tutorials/moq.mdx | 72 +++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index f68272f0..88ea5a67 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -37,11 +37,11 @@ To start publishing a MoQ stream, you need two things: a _publisher token_ and t Fetch a sandbox publisher token from the Room Manager: ```ts -const FISHJAM_ID = 'YOUR_FISHJAM_ID'; -const STREAM_NAME = 'my-stream'; +const FISHJAM_ID = "YOUR_FISHJAM_ID"; +const STREAM_NAME = "my-stream"; const response = await fetch( - `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/publisher` + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/publisher`, ); const { token: publishToken } = await response.json(); ``` @@ -58,24 +58,27 @@ Use the token to connect to the Fishjam MoQ relay and start broadcasting: ```ts // @noErrors -import * as Moq from '@moq/lite'; -import * as Publish from '@moq/publish'; +import * as Moq from "@moq/lite"; +import * as Publish from "@moq/publish"; -const FISHJAM_ID = 'YOUR_FISHJAM_ID'; -const STREAM_NAME = 'my-stream'; -const publishToken = ''; +const FISHJAM_ID = "YOUR_FISHJAM_ID"; +const STREAM_NAME = "my-stream"; +const publishToken = ""; // ---cut--- // Build the relay URL using the publisher token const relayUrl = new URL( - `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}` + `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}`, ); // Connect to the Fishjam MoQ relay const connection = await Moq.Connection.connect(relayUrl); // Get camera and microphone access -const camera = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); +const camera = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, +}); // Set up a broadcast with video and audio tracks const broadcast = new Publish.Broadcast({ @@ -106,11 +109,11 @@ To receive a MoQ stream you need one thing: a _subscriber token_. Fetch a sandbox subscriber token from the Room Manager: ```ts -const FISHJAM_ID = 'YOUR_FISHJAM_ID'; -const STREAM_NAME = 'my-stream'; +const FISHJAM_ID = "YOUR_FISHJAM_ID"; +const STREAM_NAME = "my-stream"; const response = await fetch( - `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/subscriber` + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/subscriber`, ); const { token: subscribeToken } = await response.json(); ``` @@ -127,17 +130,17 @@ Use the token to connect and receive the stream: ```ts // @noErrors -import * as Moq from '@moq/lite'; -import * as Watch from '@moq/watch'; +import * as Moq from "@moq/lite"; +import * as Watch from "@moq/watch"; -const FISHJAM_ID = 'YOUR_FISHJAM_ID'; -const STREAM_NAME = 'my-stream'; -const subscribeToken = ''; +const FISHJAM_ID = "YOUR_FISHJAM_ID"; +const STREAM_NAME = "my-stream"; +const subscribeToken = ""; // ---cut--- // Build the relay URL using the subscriber token const relayUrl = new URL( - `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${subscribeToken}` + `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${subscribeToken}`, ); // Connect to the Fishjam MoQ relay @@ -151,7 +154,7 @@ const broadcast = new Watch.Broadcast({ }); // Attach to a canvas element — MultiBackend handles decoding and rendering -const canvas = document.querySelector('canvas')!; +const canvas = document.querySelector("canvas")!; const backend = new Watch.MultiBackend({ element: canvas, broadcast, @@ -226,7 +229,7 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific Deliver these tokens to your clients, then use them to connect as described in [Connecting and publishing](#connecting-and-publishing) and [Connecting and subscribing](#connecting-and-watching). -:::tip +:::tip A single token should only grant either `publishPath` _or_ `subscribePath` — not both. Broadcast clients receive a publisher token; viewers receive a subscriber token. This separation ensures a viewer can never accidentally publish to the stream. @@ -286,26 +289,29 @@ Then use `Moq.Connection.Reload` on the client — it exposes `announced`, a rea ```ts // @noErrors -import * as Moq from '@moq/lite'; -import * as Watch from '@moq/watch'; +import * as Moq from "@moq/lite"; +import * as Watch from "@moq/watch"; -const FISHJAM_ID = 'YOUR_FISHJAM_ID'; -const ROOM_NAME = 'my-room' -const namespaceToken = ''; +const FISHJAM_ID = "YOUR_FISHJAM_ID"; +const ROOM_NAME = "my-room"; +const namespaceToken = ""; // ---cut--- const relayUrl = new URL( - `https://relay.fishjam.io/${FISHJAM_ID}/${ROOM_NAME}?jwt=${namespaceToken}` + `https://relay.fishjam.io/${FISHJAM_ID}/${ROOM_NAME}?jwt=${namespaceToken}`, ); const connection = new Moq.Connection.Reload({ url: relayUrl, enabled: true }); // Keep references so we can clean up when a stream goes offline -const slots = new Map(); +const slots = new Map< + string, + { + broadcast: Watch.Broadcast; + backend: Watch.MultiBackend; + canvas: HTMLCanvasElement; + } +>(); // Subscribe — callback fires with the full current set whenever it changes const stopDiscovery = connection.announced.subscribe((announcedPaths) => { @@ -332,7 +338,7 @@ const stopDiscovery = connection.announced.subscribe((announcedPaths) => { enabled: true, }); - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); document.body.appendChild(canvas); const backend = new Watch.MultiBackend({ From 46e560075d46e4df6242cae78e8da6f5de9865ef Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Mon, 4 May 2026 17:55:38 +0200 Subject: [PATCH 3/7] Fix MoQ tutorial reference --- docs/tutorials/moq.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index 88ea5a67..89e10323 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -227,7 +227,7 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific -Deliver these tokens to your clients, then use them to connect as described in [Connecting and publishing](#connecting-and-publishing) and [Connecting and subscribing](#connecting-and-watching). +Deliver these tokens to your clients, then use them to connect as described in [Connecting and publishing](#connecting-and-publishing) and [Connecting and subscribing](#connecting-and-subscribing). :::tip A single token should only grant either `publishPath` _or_ `subscribePath` — not both. From b0775ddf9befa2cabe2ae908665769b5fa76519f Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Wed, 6 May 2026 17:11:49 +0200 Subject: [PATCH 4/7] Add MoQ explanation section --- api/fishjam-server | 2 +- api/protos | 2 +- api/room-manager | 2 +- docs/explanation/moq-streaming.mdx | 139 +++++++++++++++++++++++++++++ docs/tutorials/moq.mdx | 88 +++++++++--------- packages/js-server-sdk | 2 +- packages/python-server-sdk | 2 +- packages/web-client-sdk | 2 +- 8 files changed, 186 insertions(+), 53 deletions(-) create mode 100644 docs/explanation/moq-streaming.mdx diff --git a/api/fishjam-server b/api/fishjam-server index e1f449a2..80b73b6b 160000 --- a/api/fishjam-server +++ b/api/fishjam-server @@ -1 +1 @@ -Subproject commit e1f449a22265b11c54d6adf330751dd39ceea88b +Subproject commit 80b73b6bf84fc37e874ff120f0d37045ad0d6ed9 diff --git a/api/protos b/api/protos index ee2e9b34..244345c4 160000 --- a/api/protos +++ b/api/protos @@ -1 +1 @@ -Subproject commit ee2e9b34faccfcf7e5ad6e7af57a36bd3a059a02 +Subproject commit 244345c478e8cec97b5603268e16eb831a54e0df diff --git a/api/room-manager b/api/room-manager index 15035bf1..94c2b0b3 160000 --- a/api/room-manager +++ b/api/room-manager @@ -1 +1 @@ -Subproject commit 15035bf1d3b516acc6082362b48c049d494a0681 +Subproject commit 94c2b0b3b21624d251d666b2000d514eccaad08b diff --git a/docs/explanation/moq-streaming.mdx b/docs/explanation/moq-streaming.mdx new file mode 100644 index 00000000..c582b967 --- /dev/null +++ b/docs/explanation/moq-streaming.mdx @@ -0,0 +1,139 @@ +--- +type: explanation +sidebar_position: 6 +title: MoQ Streaming +description: Understand how Media over QUIC (MoQ) works in Fishjam — the relay model, publish/subscribe architecture, paths, and token-based access control. +--- + +# MoQ Streaming with Fishjam + +_How Media over QUIC (MoQ) works in Fishjam_ + +## What is MoQ? + +[Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) is a new internet standard for live media delivery, designed from the ground up for **scalable, low-latency streaming to large audiences**. + +Unlike WebRTC — which was primarily built for interactive, peer-to-peer conferencing — MoQ is optimized for the one-to-many broadcast model: one publisher, potentially thousands of simultaneous subscribers, all receiving the stream with minimal delay. + +A few properties make MoQ stand out: + +- **Built on QUIC.** QUIC is a modern transport protocol that eliminates head-of-line blocking, recovers from packet loss more gracefully than TCP, and establishes connections faster. For live video, this means more resilient delivery at low latency. +- **Standardized negotiation.** Because MoQ defines a common signaling and subscription protocol, any MoQ-compliant client can connect to any MoQ-compliant relay — you are not locked into a proprietary stack. +- **Relay-based architecture.** The relay is a first-class part of the MoQ protocol, not an add-on. Because relaying is built into the protocol's design, scaling delivery to large audiences is a native capability. + +:::info +Fishjam also supports WebRTC-based livestreaming (WHIP/WHEP). See [Livestreams](./livestreams) for that approach. +::: + +## How MoQ Works in Fishjam + +Fishjam provides a **MoQ relay** that publishers push media to and subscribers pull media from. + +``` +Publisher → Fishjam MoQ Relay → Subscriber(s) +``` + +The relay is responsible for distributing the stream: it receives media from the publisher once and fans it out to every subscriber. + +### The publish/subscribe model + +MoQ uses a **publish/subscribe** model: + +- A **publisher** connects to the relay, announces a stream under a specific path, and starts sending media. +- **Subscribers** connect to the relay, discover announced paths, and receive the media. + +The relay manages the flow between them. Neither side needs a direct connection to the other. + +### Paths + +Every stream is identified by a **path** — a slash-separated string like `my-room/alice-camera`. Paths are used in two distinct ways: + +1. **Addressing** — a subscriber consumes an exact path to receive that specific stream (e.g. `my-room/alice-camera`). +2. **Discovery** — a subscriber watches a prefix (e.g. `my-room`) to learn which streams are currently live under it. This returns a live feed of announced paths — each of which must then be consumed individually. This is how you can display all participants in a room without knowing their paths in advance. + +Note that consuming an exact path and discovering a prefix are separate operations. Consuming `my-room` directly would fail unless a publisher is broadcasting at that exact path. + +### Path Scoping + +Every connection goes to `relay.fishjam.io/`. Your Fishjam ID is automatically used as the token's root namespace by the Fishjam Server — you never include it in `publishPath` or `subscribePath`; it is set for you. All paths you specify are **relative to that root**. + +Path matching is **prefix-based**: a path of `"stream-name"` permits any broadcast whose full path starts with `stream-name/`, not just the exact string `"stream-name"`. + +#### Publisher paths + +The `publishPath` you set determines how much freedom the broadcaster has when naming their broadcast: + +- **Broad path** (`publishPath: "stream-name"`) — the client can publish as any sub-path under `stream-name`, such as `stream-name/alice` or `stream-name/bob-camera`. The client chooses its own identity; the relay only enforces the prefix. +- **Specific path** (`publishPath: "stream-name/alice"`) — the client can **only** publish as `stream-name/alice`. If the broadcaster tries to use `stream-name/bob`, the relay rejects the announcement. This is how you enforce a broadcaster's identity from the server side. + +Use the broad form when clients self-identify (e.g., users pick their own stream name). Use the specific form when your backend assigns identities (e.g., you issue a per-user token for a managed conference). + +#### Subscriber paths + +The `subscribePath` works the same way: it is a prefix that limits which broadcasts the subscriber can consume and discover. + +- **Broad path** (`subscribePath: "stream-name"`) — the subscriber can consume any broadcast under `stream-name/` and will surface all publishers in that namespace as they come and go. +- **Specific path** (`subscribePath: "stream-name/alice"`) — the subscriber can only receive from `stream-name/alice`. Broadcasts at `stream-name/bob` are invisible to this client. + +#### Example: a multi-publisher room + +A typical room setup uses a combination of both patterns: + +1. The backend issues each broadcaster a **specific** publisher token — `publishPath: "my-room/"` — so each user can only occupy their own slot. +2. The backend issues viewers a **broad** subscriber token — `subscribePath: "my-room"` — so they discover and consume every broadcast in the room. +3. When a new publisher joins or leaves, the viewer is informed by the relay + +## Access Control: MoQ Tokens + +Access to the relay is controlled by **MoQ tokens** — short-lived JWTs that are path-scoped: + +| Token type | Grants | Typical recipient | +| ---------------- | ---------------------------------- | ----------------- | +| Publisher token | Write access to a specific path | Streamer | +| Subscriber token | Read access to a path or namespace | Viewer | + +A token is attached to the relay URL as a query parameter (`?jwt=`). The relay validates the token and enforces its scope before allowing any media to flow. + +Keeping publisher and subscriber tokens separate ensures that a viewer can never accidentally publish to the stream, and a publisher cannot subscribe to paths it does not own. + +## Getting Tokens + +There are two ways to obtain MoQ tokens, depending on where you are in the development lifecycle. + +### Sandbox API (prototyping) + +The **Sandbox API** is a ready-made backend provided by Fishjam for development and prototyping. It issues tokens without requiring you to build your own server, so you can start streaming immediately. + +To get a publisher token, call: + +``` +GET https://fishjam.io/api/v1/connect/{FISHJAM_ID}/room-manager/moq/{PUBLISHER-PATH}/publisher +``` + +To get a subscriber token, call: + +``` +GET https://fishjam.io/api/v1/connect/{FISHJAM_ID}/room-manager/moq/{SUBSCRIBER-PATH}/subscriber +``` + +The Sandbox API is **not intended for production** — it has no authentication and is only available in the Sandbox environment. See [What is the Sandbox API?](./sandbox-api-concept) for more context. + +### Fishjam Server SDK (production) + +In production, your backend generates tokens using the **Fishjam Server SDK**. This gives you full control over who can publish and who can subscribe. + +The SDK's `createMoqToken` method accepts either a `publishPath` or a `subscribePath`: + +- `publishPath` — issues a publisher token scoped to that path. +- `subscribePath` — issues a subscriber token scoped to that path or namespace prefix. + +Your backend then delivers each token to the appropriate client (publisher or viewer), which uses it to connect to the relay. + +See the [MoQ Streaming tutorial](../tutorials/moq) for working code examples of both approaches. + +## See also + +- [MoQ Streaming tutorial](../tutorials/moq) — step-by-step guide to publishing and subscribing +- [What is the Sandbox API?](./sandbox-api-concept) — when and why to use the Sandbox API +- [Security & Token Model](./security-tokens) — broader overview of Fishjam's token system +- [Livestreams](./livestreams) — WebRTC-based livestreaming with WHIP/WHEP diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index 89e10323..b3be9a70 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -8,12 +8,12 @@ description: Stream live video and audio over Media over QUIC (MoQ) with Fishjam import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; -# MoQ Streaming with Fishjam +# MoQ Streaming This section explains how to publish and subscribe to live streams using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) 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. -Before diving into the tutorial, we recommend getting familiar with the [MoQ protocol concepts](https://datatracker.ietf.org/wg/moq/about/). +Before diving into the tutorial, we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explenation. We show how to quickly prototype in [Quickstart with Sandbox API](#quickstart-with-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). :::info @@ -38,10 +38,10 @@ Fetch a sandbox publisher token from the Room Manager: ```ts const FISHJAM_ID = "YOUR_FISHJAM_ID"; -const STREAM_NAME = "my-stream"; +const PUBLISHER_PATH = "stream/alice"; const response = await fetch( - `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/publisher`, + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${PUBLISHER_PATH}/publisher`, ); const { token: publishToken } = await response.json(); ``` @@ -62,7 +62,7 @@ import * as Moq from "@moq/lite"; import * as Publish from "@moq/publish"; const FISHJAM_ID = "YOUR_FISHJAM_ID"; -const STREAM_NAME = "my-stream"; +const PUBLISHER_PATH = "stream/alice"; const publishToken = ""; // ---cut--- @@ -83,7 +83,7 @@ const camera = await navigator.mediaDevices.getUserMedia({ // Set up a broadcast with video and audio tracks const broadcast = new Publish.Broadcast({ connection, - name: Moq.Path.from(STREAM_NAME), + name: Moq.Path.from(PUBLISHER_PATH), enabled: true, video: { source: camera.getVideoTracks()[0] as Publish.Video.Source, @@ -110,10 +110,10 @@ Fetch a sandbox subscriber token from the Room Manager: ```ts const FISHJAM_ID = "YOUR_FISHJAM_ID"; -const STREAM_NAME = "my-stream"; +const SUBSCRIBER_PATH = "stream/alice"; const response = await fetch( - `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${STREAM_NAME}/subscriber`, + `https://fishjam.io/api/v1/connect/${FISHJAM_ID}/room-manager/moq/${SUBSCRIBER_PATH}/subscriber`, ); const { token: subscribeToken } = await response.json(); ``` @@ -134,7 +134,7 @@ import * as Moq from "@moq/lite"; import * as Watch from "@moq/watch"; const FISHJAM_ID = "YOUR_FISHJAM_ID"; -const STREAM_NAME = "my-stream"; +const SUBSCRIBER_PATH = "stream/alice"; const subscribeToken = ""; // ---cut--- @@ -149,7 +149,7 @@ const connection = await Moq.Connection.connect(relayUrl); // Subscribe to the broadcast const broadcast = new Watch.Broadcast({ connection, - name: Moq.Path.from(STREAM_NAME), + name: Moq.Path.from(SUBSCRIBER_PATH), enabled: true, }); @@ -190,14 +190,14 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific managementToken, }); - const streamPath = 'my-stream'; + const streamPath = 'stream/alice'; - // Generate a token that allows publishing to 'my-stream' + // Generate a token that allows publishing to 'stream/alice' const { token: publishToken } = await fishjamClient.createMoqToken({ publishPath: streamPath, }); - // Generate a token that allows subscribing to 'my-stream' + // Generate a token that allows subscribing to 'stream/alice' const { token: subscribeToken } = await fishjamClient.createMoqToken({ subscribePath: streamPath, }); @@ -215,12 +215,12 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific management_token=management_token, ) - stream_path = 'my-stream' + stream_path = 'stream/alice' - # Generate a token that allows publishing to 'my-stream' + # 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 'my-stream' + # Generate a token that allows subscribing to 'stream/alice' subscribe_token = fishjam_client.create_moq_token(subscribe_path=stream_path) ``` @@ -237,8 +237,8 @@ This separation ensures a viewer can never accidentally publish to the stream. ### Subscribe to a namespace -You can subscribe not only to a specific stream, but also to an entire namespace — automatically receiving every broadcast published under it. -This is useful when multiple publishers join a room and you want to render all of their streams without knowing their paths in advance. +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. @@ -285,7 +285,7 @@ To do this, generate a subscriber token scoped to the room namespace instead of -Then use `Moq.Connection.Reload` on the client — it exposes `announced`, a reactive signal that holds the current set of announced paths and fires a callback whenever it changes: +Then on the client, call `connection.announced()` to receive a live feed of announce events. Each event tells you whether a path is becoming **active** (a publisher just joined) or going inactive (a publisher disconnected). Loop over them to subscribe and clean up as publishers come and go: ```ts // @noErrors @@ -301,9 +301,9 @@ const relayUrl = new URL( `https://relay.fishjam.io/${FISHJAM_ID}/${ROOM_NAME}?jwt=${namespaceToken}`, ); -const connection = new Moq.Connection.Reload({ url: relayUrl, enabled: true }); +const connection = await Moq.Connection.connect(relayUrl); -// Keep references so we can clean up when a stream goes offline +// Keep references so we can clean up when a publisher goes offline const slots = new Map< string, { @@ -313,28 +313,19 @@ const slots = new Map< } >(); -// Subscribe — callback fires with the full current set whenever it changes -const stopDiscovery = connection.announced.subscribe((announcedPaths) => { - const activePaths = new Set([...announcedPaths].map((p) => p.toString())); - - // Tear down streams that are no longer announced - for (const [key, slot] of slots) { - if (!activePaths.has(key)) { - slot.backend.close(); - slot.broadcast.enabled.set(false); - slot.canvas.remove(); - slots.delete(key); - } - } +const announced = connection.announced(); + +while (true) { + const entry = await announced.next(); + if (!entry) break; // connection closed - // Set up streams that just appeared - for (const path of announcedPaths) { - const key = path.toString(); - if (slots.has(key)) continue; + const key = entry.path.toString(); + if (entry.active) { + // New publisher — subscribe and render const broadcast = new Watch.Broadcast({ - connection: connection.established, - name: path, + connection, + name: entry.path, enabled: true, }); @@ -348,20 +339,23 @@ const stopDiscovery = connection.announced.subscribe((announcedPaths) => { }); slots.set(key, { broadcast, backend, canvas }); + } else { + // Publisher disconnected — tear down its slot + const slot = slots.get(key); + if (!slot) continue; + slot.backend.close(); + slot.broadcast.close(); + slot.canvas.remove(); + slots.delete(key); } -}); - -// To stop discovery and tear down all streams: -// stopDiscovery(); -// connection.close(); +} ``` ## See also If you want a better understanding of how MoQ works, see: -- [MoQ IETF Working Group](https://datatracker.ietf.org/wg/moq/about/) -- Blog Post etc. +- [MoQ Streaming with Fishjam](../explanation/moq-streaming) If you need a different streaming approach, see: diff --git a/packages/js-server-sdk b/packages/js-server-sdk index 46431580..ad25200e 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit 46431580868408f891835b0ea0e26efaeeb835a0 +Subproject commit ad25200e98571af994b8d46ff284063493d800cd diff --git a/packages/python-server-sdk b/packages/python-server-sdk index 91bbec29..0889d548 160000 --- a/packages/python-server-sdk +++ b/packages/python-server-sdk @@ -1 +1 @@ -Subproject commit 91bbec2947377df1ae263884b6cbbf7651ad89b2 +Subproject commit 0889d54851167442d6a9318847a269f9c7466854 diff --git a/packages/web-client-sdk b/packages/web-client-sdk index 798efc1a..f9c524fd 160000 --- a/packages/web-client-sdk +++ b/packages/web-client-sdk @@ -1 +1 @@ -Subproject commit 798efc1a35ac2c0921bfd5752ed9f5f6906b993f +Subproject commit f9c524fd1ea91e5bcd57ee915282fefeaed45b65 From 78d33e65fe5fc1d7abfd59bbc76bdc480ad15c33 Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Wed, 6 May 2026 17:22:25 +0200 Subject: [PATCH 5/7] Requested changes --- docs/tutorials/moq.mdx | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index b3be9a70..52ab8794 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -13,20 +13,20 @@ import TabItem from "@theme/TabItem"; This section explains how to publish and subscribe to live streams using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) 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. -Before diving into the tutorial, we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explenation. +Before diving into the tutorial, we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation. We show how to quickly prototype in [Quickstart with Sandbox API](#quickstart-with-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). :::info 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 with native browser support, built for low-latency conferencing and interactive streaming. +**WebRTC** is a mature, battle-tested technology built for low-latency peer-to-peer conferencing and interactive streaming. See [Livestreaming](./livestreaming) for the WebRTC approach. ::: -## Quickstart with Sandbox API +## 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 provided by the Room Manager. +If you don't have a backend server set up, you can prototype a MoQ streaming scenario using the Sandbox API. ### Publisher setup @@ -98,7 +98,7 @@ const broadcast = new Publish.Broadcast({ }); ``` -The stream is now live on the MoQ relay! +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) ### Subscriber setup @@ -106,7 +106,7 @@ To receive a MoQ stream you need one thing: a _subscriber token_. #### Obtaining a subscriber token -Fetch a sandbox subscriber token from the Room Manager: +Fetch a sandbox subscriber token using the Sandbox API: ```ts const FISHJAM_ID = "YOUR_FISHJAM_ID"; @@ -231,7 +231,7 @@ Deliver these tokens to your clients, then use them to connect as described in [ :::tip A single token should only grant either `publishPath` _or_ `subscribePath` — not both. -Broadcast clients receive a publisher token; viewers receive a subscriber token. +Streamers receive a publisher token; viewers receive a subscriber token. This separation ensures a viewer can never accidentally publish to the stream. ::: @@ -255,8 +255,15 @@ To do this, generate a subscriber token scoped to the room namespace instead of const fishjamClient = new FishjamClient({ fishjamId, managementToken }); const roomName = 'my-room'; - // Publishers under this namespace might use paths like: - // 'my-room/alice-camera', 'my-room/bob-screen', etc. + + const { token: alicePublisherToken } = await fishjamClient.createMoqToken({ + publisherPath: roomName + "/alice", + }); + + const { token: bobPublisherToken } = await fishjamClient.createMoqToken({ + publisherPath: roomName + "/bob", + }); + const { token: namespaceToken } = await fishjamClient.createMoqToken({ subscribePath: roomName, @@ -357,7 +364,7 @@ If you want a better understanding of how MoQ works, see: - [MoQ Streaming with Fishjam](../explanation/moq-streaming) -If you need a different streaming approach, see: +If you want to learn about streaming using WebRTC instead of MoQ, see: - [Livestreaming](./livestreaming) - [WHIP/WHEP with Fishjam](../how-to/backend/whip-whep) From d9f4e5d6e7df6e0656262d825e2b799754406a1b Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Wed, 6 May 2026 17:44:31 +0200 Subject: [PATCH 6/7] Requested changes --- docs/tutorials/moq.mdx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index 52ab8794..f2b50164 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -13,7 +13,10 @@ import TabItem from "@theme/TabItem"; This section explains how to publish and subscribe to live streams using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) 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. -Before diving into the tutorial, we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation. +:::info +If you're new to MoQ, then we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation. +::: + We show how to quickly prototype in [Quickstart with Sandbox API](#quickstart-with-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). :::info @@ -28,6 +31,10 @@ See [Livestreaming](./livestreaming) for the WebRTC approach. If you don't have a backend server set up, you can prototype a MoQ streaming scenario using the Sandbox API. +:::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`](https://github.com/moq-dev/moq) client libraries — the reference TypeScript implementation maintained alongside the protocol. For more details, see the [documentation](https://doc.moq.dev/js/). +::: + ### Publisher setup To start publishing a MoQ stream, you need two things: a _publisher token_ and the relay URL. @@ -87,9 +94,7 @@ const broadcast = new Publish.Broadcast({ enabled: true, video: { source: camera.getVideoTracks()[0] as Publish.Video.Source, - // Set up two layers hd: { enabled: true }, - sd: { enabled: true }, }, audio: { enabled: true, @@ -257,11 +262,11 @@ To do this, generate a subscriber token scoped to the room namespace instead of const roomName = 'my-room'; const { token: alicePublisherToken } = await fishjamClient.createMoqToken({ - publisherPath: roomName + "/alice", + publishPath: roomName + "/alice", }); const { token: bobPublisherToken } = await fishjamClient.createMoqToken({ - publisherPath: roomName + "/bob", + publishPath: roomName + "/bob", }); From 3af1bb8116a984598fa083e19c8f2c3989860c56 Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Thu, 7 May 2026 15:19:00 +0200 Subject: [PATCH 7/7] Fix build --- docs/tutorials/moq.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq.mdx index f2b50164..3d1d0ee3 100644 --- a/docs/tutorials/moq.mdx +++ b/docs/tutorials/moq.mdx @@ -17,7 +17,7 @@ MoQ uses QUIC as its transport layer (with a WebSocket fallback), delivering ult If you're new to MoQ, then we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation. ::: -We show how to quickly prototype in [Quickstart with Sandbox API](#quickstart-with-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). +We show how to quickly prototype in [Quickstart with the Sandbox API](#quickstart-with-the-sandbox-api) and how to get ready for production in [Production MoQ with Server SDKs](#production-moq-with-server-sdks). :::info Fishjam supports both WebRTC-based livestreaming (WHIP/WHEP) and MoQ streaming. \ @@ -171,7 +171,7 @@ That's it! The stream will appear in the canvas once the publisher starts broadc ## Production MoQ with Server SDKs -The [Quickstart with Sandbox API](#quickstart-with-sandbox-api) shows how to get MoQ streaming up and running quickly. +The [Quickstart with the Sandbox API](#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: