diff --git a/docs/explanation/moq-streaming.mdx b/docs/explanation/moq-with-fishjam.mdx
similarity index 87%
rename from docs/explanation/moq-streaming.mdx
rename to docs/explanation/moq-with-fishjam.mdx
index 18133b80..5a279ea7 100644
--- a/docs/explanation/moq-streaming.mdx
+++ b/docs/explanation/moq-with-fishjam.mdx
@@ -1,14 +1,20 @@
---
type: explanation
-sidebar_position: 6
-title: Media over QUIC (MoQ)
+sidebar_position: 9
+title: Media over QUIC in Fishjam
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
+# Media over QUIC in Fishjam
_How Media over QUIC (MoQ) works in Fishjam_
+:::warning[MoQ is a standalone feature]
+MoQ runs as a **separate delivery path** in Fishjam — it handles publishing and subscribing to a live broadcast over the MoQ relay, and the rest of Fishjam's feature set does not apply to a MoQ stream. In particular, a MoQ broadcast is not part of a WebRTC [room](./rooms), so you cannot use features like [Agents](../tutorials/agents), [data channels](./data-channels), and others that are part of the Fishjam WebRTC ecosystem.
+
+What MoQ supports is covered end to end in the [MoQ tutorials](../tutorials/moq/web-publishing).
+:::
+
## 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**.
@@ -129,11 +135,11 @@ The SDK's `createMoqToken` method accepts either a `publishPath` or a `subscribe
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 the [Web Publishing](../tutorials/moq/web-publishing) and [Web Subscribing](../tutorials/moq/web-subscribing) tutorials (or their [React Native](../tutorials/moq/react-native-publishing) [counterparts](../tutorials/moq/react-native-subscribing)) for working code examples.
## See also
-- [MoQ Streaming tutorial](../tutorials/moq) — step-by-step guide to publishing and subscribing
+- [Web Publishing](../tutorials/moq/web-publishing) / [Web Subscribing](../tutorials/moq/web-subscribing) — step-by-step guides 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/_category_.json b/docs/tutorials/moq/_category_.json
new file mode 100644
index 00000000..87f92e9c
--- /dev/null
+++ b/docs/tutorials/moq/_category_.json
@@ -0,0 +1,9 @@
+{
+ "label": "Media over QUIC (MoQ)",
+ "position": 5,
+ "collapsed": false,
+ "link": {
+ "type": "doc",
+ "id": "tutorials/moq/index"
+ }
+}
diff --git a/docs/tutorials/moq/index.mdx b/docs/tutorials/moq/index.mdx
new file mode 100644
index 00000000..2308eb6a
--- /dev/null
+++ b/docs/tutorials/moq/index.mdx
@@ -0,0 +1,17 @@
+---
+type: tutorial
+title: Media over QUIC (MoQ)
+description: Step-by-step guides for publishing and subscribing to live broadcasts over Media over QUIC (MoQ) with Fishjam, on both web and React Native.
+---
+
+import DocCardList from "@theme/DocCardList";
+
+# Media over QUIC (MoQ)
+
+Step-by-step guides for publishing and subscribing to live broadcasts over [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. For the big-picture overview of how MoQ fits into Fishjam, see [Media over QUIC in Fishjam](../../explanation/moq-with-fishjam.mdx).
+
+:::warning[MoQ is a standalone feature]
+MoQ runs as a **separate delivery path** in Fishjam — it handles publishing and subscribing to a live broadcast over the MoQ relay, and the rest of Fishjam's feature set does not apply to a MoQ stream. In particular, a MoQ broadcast is not part of a WebRTC [room](../../explanation/rooms.mdx), so you cannot use features like [Agents](../agents.mdx), [data channels](../../explanation/data-channels.mdx), and others that are part of the Fishjam WebRTC ecosystem.
+:::
+
+
diff --git a/docs/tutorials/moq/react-native-publishing.mdx b/docs/tutorials/moq/react-native-publishing.mdx
new file mode 100644
index 00000000..2c00b181
--- /dev/null
+++ b/docs/tutorials/moq/react-native-publishing.mdx
@@ -0,0 +1,230 @@
+---
+type: tutorial
+sidebar_position: 3
+title: React Native Publishing
+description: Publish a live video and audio broadcast over Media over QUIC (MoQ) from a React Native mobile app with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# React Native Publishing
+
+This tutorial explains how to **publish** a live stream from a **React Native mobile app** using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To watch a stream instead, see [React Native Subscribing](./react-native-subscribing).
+
+It uses [`react-native-moq`](https://github.com/software-mansion-labs/react-native-moq) — React Native bindings for MoQKit, with a small, reactive hooks-based API. For the web equivalent, see [Web Publishing](./web-publishing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+## Requirements
+
+`react-native-moq` targets the React Native **New Architecture** (Fabric / TurboModules):
+
+- iOS 16+
+- Android API 30+
+
+## Installation
+
+```bash npm2yarn
+npm install react-native-moq
+```
+
+Then install the iOS pods:
+
+```sh
+cd ios && pod install
+```
+
+Publishing captures the camera and microphone, so the host app is responsible for runtime permissions: request `CAMERA` / `RECORD_AUDIO` on Android, and add `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` to `Info.plist` on iOS. The library does not request these for you.
+
+:::tip
+MoQ is a protocol with a well-defined negotiation, so a publisher and a subscriber don't need to use the same client library. A React Native app published with `react-native-moq` can be watched in the browser with [`@moq/watch`](./web-subscribing#connecting-and-subscribing), and vice versa.
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype publishing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+### Obtaining a publisher token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ The `useSandbox` hook from `@fishjam-cloud/react-native-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-native-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqPublisherToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a publisher token scoped to the publisher path
+ const publishToken = await getSandboxMoqPublisherToken(PUBLISHER_PATH);
+ ```
+
+
+
+
+
+ If you don't want to pull in the whole client library just for the `useSandbox` hook, you can call the Sandbox API directly with `fetch`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/publisher`,
+ );
+ const { token: publishToken } = await response.json();
+ ```
+
+
+
+
+### Connecting and publishing
+
+`react-native-moq` is hooks-based. You open a session against the relay, capture the camera and microphone, and hand them to a publisher.
+
+Build the relay URL using the publisher token, open the session, and publish the camera + microphone tracks:
+
+```tsx
+// @noErrors
+import { useEffect } from "react";
+import { Button, PermissionsAndroid, Platform } from "react-native";
+import {
+ PublisherView,
+ useCamera,
+ useMicrophone,
+ usePublisher,
+ useSession,
+} from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const PUBLISHER_PATH = "stream-alice";
+const publishToken = ""; // from the step above
+
+// Build the relay URL using the publisher token
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}`;
+
+function PublishScreen() {
+ // Open the MoQ session against the Fishjam relay
+ const session = useSession(relayUrl, (s) => s.connect());
+ const camera = useCamera({ position: "front" });
+ const microphone = useMicrophone();
+ const publisher = usePublisher(session);
+
+ // Request camera + mic permissions on Android (iOS handles it automatically when Info.plist is configured)
+ useEffect(() => {
+ if (Platform.OS === "android") {
+ PermissionsAndroid.requestMultiple([
+ PermissionsAndroid.PERMISSIONS.CAMERA,
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
+ ]);
+ }
+ }, []);
+
+ const isPublishing = publisher.state === "publishing";
+
+ return (
+ <>
+
+
+ {
+ if (isPublishing) publisher.stop();
+ else
+ publisher.publish({
+ path: PUBLISHER_PATH,
+ tracks: [camera, microphone],
+ });
+ }}
+ />
+ >
+ );
+}
+```
+
+Once `publisher.state === "publishing"`, the stream is live on the MoQ relay! Viewers can start watching by following [React Native Subscribing](./react-native-subscribing).
+
+:::info
+**Why a separate `useSession` and `publish()`?** \
+The session owns the connection to the relay; the publisher reuses it. Because publishing rides on top of an open session, the same connection can subscribe and publish at once — pair `usePublisher(session)` with `useBroadcasts(session, prefix)` to do both.
+:::
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you publishing quickly. In production, your backend generates tokens with proper authorization, so you control who can publish.
+
+A **publisher token** grants write access to a specific path. Generate one on your backend and deliver it to the broadcasting client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows publishing to 'stream-alice'
+ const { token: publishToken } = await fishjamClient.createMoqToken({
+ publishPath: streamPath,
+ });
+ ```
+
+
+
+
+
+ ```python
+ 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)
+ ```
+
+
+
+
+Deliver this token to the mobile client, then use it to build the relay URL and connect as described in [Connecting and publishing](#connecting-and-publishing).
+
+## See also
+
+- [React Native Subscribing](./react-native-subscribing) — watch a MoQ stream on mobile
+- [Web Publishing](./web-publishing) — publish from the browser instead
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
diff --git a/docs/tutorials/moq/react-native-subscribing.mdx b/docs/tutorials/moq/react-native-subscribing.mdx
new file mode 100644
index 00000000..ee62f1e1
--- /dev/null
+++ b/docs/tutorials/moq/react-native-subscribing.mdx
@@ -0,0 +1,360 @@
+---
+type: tutorial
+sidebar_position: 4
+title: React Native Subscribing
+description: Subscribe to and render a live Media over QUIC (MoQ) broadcast in a React Native mobile app with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# React Native Subscribing
+
+This tutorial explains how to **subscribe** to a live stream and render it in a **React Native mobile app** using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To broadcast a stream instead, see [React Native Publishing](./react-native-publishing).
+
+It uses [`react-native-moq`](https://github.com/software-mansion-labs/react-native-moq) — React Native bindings for MoQKit, with a small, reactive hooks-based API. For the web equivalent, see [Web Subscribing](./web-subscribing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+## Requirements
+
+`react-native-moq` targets the React Native **New Architecture** (Fabric / TurboModules):
+
+- iOS 16+
+- Android API 30+
+
+## Installation
+
+```bash npm2yarn
+npm install react-native-moq
+```
+
+Then install the iOS pods:
+
+```sh
+cd ios && pod install
+```
+
+:::tip
+The ready-made player chrome — `` with fullscreen controls, a volume slider, and matching context hooks — lives in a separate package so apps that build their own UI don't pay for it:
+
+```bash npm2yarn
+npm install react-native-moq-ui @react-native-vector-icons/material-icons
+```
+
+This tutorial uses the bare `` from the core package, but `` is a drop-in replacement if you want controls out of the box.
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype subscribing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+:::tip
+MoQ is a protocol with a well-defined negotiation, so a publisher and a subscriber don't need to use the same client library. A stream published from the browser with [`@moq/publish`](./web-publishing) can be watched with `react-native-moq`, and vice versa.
+:::
+
+### Obtaining a subscriber token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ The `useSandbox` hook from `@fishjam-cloud/react-native-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-native-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqSubscriberToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a subscriber token scoped to the subscriber path
+ const subscribeToken = await getSandboxMoqSubscriberToken(SUBSCRIBER_PATH);
+ ```
+
+
+
+
+
+ If you don't want to pull in the whole client library just for the `useSandbox` hook, you can call the Sandbox API directly with `fetch`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
+ );
+ const { token: subscribeToken } = await response.json();
+ ```
+
+
+
+
+### Connecting and subscribing
+
+Open a session, discover the broadcast under its path with `useBroadcasts`, and render it with ``. `useVideoPlayer` turns a discovered broadcast into a reactive player; the decoding and rendering pipeline is handled natively — no canvas, no manual WebCodecs setup:
+
+```tsx
+// @noErrors
+import { Button } from "react-native";
+import {
+ VideoView,
+ useBroadcasts,
+ useSession,
+ useVideoPlayer,
+} from "react-native-moq";
+import type { BroadcastInfo } from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const SUBSCRIBER_PATH = "stream-alice";
+const subscribeToken = ""; // from the step above
+
+// Build the relay URL using the subscriber token
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${subscribeToken}`;
+
+function WatchScreen() {
+ // Connect to the Fishjam MoQ relay on mount
+ const session = useSession(relayUrl, (s) => s.connect());
+
+ // Discover broadcasts under the subscriber path
+ const broadcasts = useBroadcasts(session, SUBSCRIBER_PATH);
+
+ return (
+ <>
+ {broadcasts.map((broadcast) => (
+
+ ))}
+ >
+ );
+}
+
+function BroadcastPlayer({ broadcast }: { broadcast: BroadcastInfo }) {
+ // Create a reactive player and start playback
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+That's it! The stream appears in the `` once the publisher starts broadcasting, and `useBroadcasts` re-populates automatically on reconnect.
+
+:::info
+**No canvas needed.** \
+Unlike the web, where `Watch.MultiBackend` decodes frames through WebCodecs and paints them onto a ``, the React Native player decodes and renders natively. You just hand the `player` to a `` and size it like any other view.
+:::
+
+For audio-only streaming, use [`useAudioPlayer(broadcast)`](https://github.com/software-mansion-labs/react-native-moq/blob/main/docs/API.md#useaudioplayerbroadcast-setup) instead of `useVideoPlayer` — the video track is never subscribed, so no video bandwidth is consumed.
+
+### Using the `` component
+
+If you don't need fine-grained control over the rendering surface, the `react-native-moq-ui` package ships a ready-to-use `` that wraps `` with platform-styled play/pause, a volume slider, and a fullscreen modal — the React Native counterpart of the web `` element.
+
+```tsx
+// @noErrors
+import { VideoPlayerView } from "react-native-moq-ui";
+
+function BroadcastPlayer({ broadcast }: { broadcast: BroadcastInfo }) {
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+
+ return (
+
+ );
+}
+```
+
+It exposes imperative `enterFullscreen()` / `exitFullscreen()` methods on its ref, and the chrome is fully customizable. See [Default UI components](https://github.com/software-mansion-labs/react-native-moq/blob/main/docs/API.md#default-ui-components-react-native-moq-ui) in the API reference.
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you watching quickly. In production, your backend generates tokens with proper authorization, so you control who can subscribe.
+
+A **subscriber token** grants read access to a specific path. Generate one on your backend and deliver it to the viewing client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows subscribing to 'stream-alice'
+ 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 = 'stream-alice'
+
+ # Generate a token that allows subscribing to 'stream-alice'
+ subscribe_token = fishjam_client.create_moq_token(subscribe_path=stream_path)
+ ```
+
+
+
+
+Deliver this token to the mobile client, then use it to build the relay URL and connect as described in [Connecting and subscribing](#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 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';
+
+ const { token: alicePublisherToken } = await fishjamClient.createMoqToken({
+ publishPath: roomName + "/alice",
+ });
+
+ const { token: bobPublisherToken } = await fishjamClient.createMoqToken({
+ publishPath: roomName + "/bob",
+ });
+
+
+ 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'
+
+ alice_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/alice",
+ )
+
+ bob_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/bob",
+ )
+
+ namespace_token = fishjam_client.create_moq_token(subscribe_path=room_name)
+ ```
+
+
+
+
+On the client, pass the namespace token's relay URL to `useSession`, then call `useBroadcasts(session, roomName)` with the room prefix. It returns a reactive `BroadcastInfo[]` that re-renders every time a publisher joins or leaves — no manual diffing of an announce set. Map over it to mount a player per broadcast:
+
+```tsx
+// @noErrors
+import {
+ VideoView,
+ useBroadcasts,
+ useSession,
+ useVideoPlayer,
+} from "react-native-moq";
+import type { BroadcastInfo } from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const ROOM_NAME = "my-room";
+const namespaceToken = "token";
+
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${namespaceToken}`;
+
+function RoomGrid() {
+ const session = useSession(relayUrl, (s) => s.connect());
+
+ // Every broadcast published under the 'my-room' prefix, kept live
+ const broadcasts = useBroadcasts(session, ROOM_NAME);
+
+ return (
+ <>
+ {broadcasts.map((broadcast) => (
+
+ ))}
+ >
+ );
+}
+
+function Tile({ broadcast }: { broadcast: BroadcastInfo }) {
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+ return (
+
+ );
+}
+```
+
+Because `useBroadcasts` starts the native subscription on mount and tears it down on unmount, players appear and disappear in lockstep with the publishers in the room — React's reconciliation handles mounting and unmounting tiles for you.
+
+## See also
+
+- [React Native Publishing](./react-native-publishing) — broadcast a MoQ stream from mobile
+- [Web Subscribing](./web-subscribing) — subscribe from the browser instead
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
diff --git a/docs/tutorials/moq/web-publishing.mdx b/docs/tutorials/moq/web-publishing.mdx
new file mode 100644
index 00000000..f55c0d3a
--- /dev/null
+++ b/docs/tutorials/moq/web-publishing.mdx
@@ -0,0 +1,186 @@
+---
+type: tutorial
+sidebar_position: 1
+title: Web Publishing
+description: Publish a live video and audio broadcast over Media over QUIC (MoQ) from the browser with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# Web Publishing
+
+This tutorial explains how to **publish** a live stream from the browser using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To watch a stream instead, see [Web Subscribing](./web-subscribing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+To start publishing a MoQ stream, you need two things: a _publisher token_ and the relay URL. We show how to quickly prototype with the [Sandbox API](#quickstart-with-the-sandbox-api) and how to get ready for [production](#production-with-server-sdks).
+
+:::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](https://doc.moq.dev/lib/js/).
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype publishing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+### Obtaining a publisher token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ If you're using React, the `useSandbox` hook from `@fishjam-cloud/react-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqPublisherToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a publisher token scoped to the publisher path
+ const publishToken = await getSandboxMoqPublisherToken(PUBLISHER_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`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/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 PUBLISHER_PATH = "stream-alice";
+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);
+
+const camera = new Publish.Source.Camera({ enabled: true });
+const microphone = new Publish.Source.Microphone({ enabled: true });
+
+// Set up a broadcast with video and audio tracks
+const broadcast = new Publish.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 it — follow [Web Subscribing](./web-subscribing).
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you publishing quickly. In production, your backend generates tokens with proper authorization, so you control who can publish.
+
+A **publisher token** grants write access to a specific path. Generate one on your backend and deliver it to the broadcasting client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows publishing to 'stream-alice'
+ const { token: publishToken } = await fishjamClient.createMoqToken({
+ publishPath: streamPath,
+ });
+ ```
+
+
+
+
+
+ ```python
+ 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)
+ ```
+
+
+
+
+Deliver this token to the broadcasting client, then use it to connect as described in [Connecting and publishing](#connecting-and-publishing).
+
+## See also
+
+- [Web Subscribing](./web-subscribing) — watch a MoQ stream in the browser
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
+- [WHIP/WHEP with Fishjam](../../how-to/backend/whip-whep)
diff --git a/docs/tutorials/moq.mdx b/docs/tutorials/moq/web-subscribing.mdx
similarity index 57%
rename from docs/tutorials/moq.mdx
rename to docs/tutorials/moq/web-subscribing.mdx
index fa3742e0..4e69cf83 100644
--- a/docs/tutorials/moq.mdx
+++ b/docs/tutorials/moq/web-subscribing.mdx
@@ -1,128 +1,83 @@
---
type: tutorial
-sidebar_position: 5
-title: MoQ Livestreaming
-description: Stream live video and audio over Media over QUIC (MoQ) with Fishjam, from sandbox prototyping to production deployment.
+sidebar_position: 2
+title: Web Subscribing
+description: Subscribe to and render a live Media over QUIC (MoQ) broadcast in the browser with Fishjam, from sandbox prototyping to production.
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
-# How to stream Media over QUIC
+# Web Subscribing
-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.
+This tutorial explains how to **subscribe** to a live stream and render it in the browser using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To broadcast a stream instead, see [Web Publishing](./web-publishing).
:::info
-If you're new to MoQ, then we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation.
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
:::
-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. \
-**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](./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.
+To receive a MoQ stream you need one thing: a _subscriber token_. We show how to quickly prototype with the [Sandbox API](#quickstart-with-the-sandbox-api) and how to get ready for [production](#production-with-server-sdks).
:::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/).
+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/lib/js/).
:::
-### 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 PUBLISHER_PATH = "stream-alice";
-const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
-
-const response = await fetch(
- `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/publisher`,
-);
-const { token: publishToken } = await response.json();
-```
-
-#### Connecting and publishing
+## Quickstart with the Sandbox API
-Install the MoQ packages:
+If you don't have a backend server set up, you can prototype subscribing using the [Sandbox API](../../explanation/sandbox-api-concept).
-```bash npm2yarn
-npm install @moq/lite @moq/publish
-```
+### Obtaining a subscriber token
-Use the token to connect to the Fishjam MoQ relay and start broadcasting:
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
-```ts
-// @noErrors
-import * as Moq from "@moq/lite";
-import * as Publish from "@moq/publish";
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
-const FISHJAM_ID = "YOUR_FISHJAM_ID";
-const PUBLISHER_PATH = "stream-alice";
-const publishToken = "";
+
+
-// ---cut---
-// Build the relay URL using the publisher token
-const relayUrl = new URL(
- `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}`,
-);
+ If you're using React, the `useSandbox` hook from `@fishjam-cloud/react-client` wraps the Sandbox API request for you:
-// Connect to the Fishjam MoQ relay
-const connection = await Moq.Connection.connect(relayUrl);
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-client";
-const camera = new Publish.Source.Camera({ enabled: true });
-const microphone = new Publish.Source.Microphone({ enabled: true });
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
-// Set up a broadcast with video and audio tracks
-const broadcast = new Publish.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)
+ // Inside a React component:
+ const { getSandboxMoqSubscriberToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
-### Subscriber setup
+ // Request a subscriber token scoped to the subscriber path
+ const subscribeToken = await getSandboxMoqSubscriberToken(SUBSCRIBER_PATH);
+ ```
-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:
+ 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`:
-```ts
-const FISHJAM_ID = "YOUR_FISHJAM_ID";
-const SUBSCRIBER_PATH = "stream-alice";
-const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
+ );
+ const { token: subscribeToken } = await response.json();
+ ```
-const response = await fetch(
- `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
-);
-const { token: subscribeToken } = await response.json();
-```
+
+
-#### Connecting and subscribing
+### Connecting and subscribing
Install the MoQ packages:
@@ -180,7 +135,7 @@ Watch.MultiBackend renders to a `` so it can decode frames directly thro
That's it! The stream will appear in the canvas once the publisher starts broadcasting.
-#### Using the `` web component
+### Using the `` 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.
@@ -208,16 +163,11 @@ Then drop `` wrapping `` into your HTML — pass the re
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](#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.
+## Production with Server SDKs
-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:
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you watching quickly. In production, your backend generates tokens with proper authorization, so you control who can subscribe.
-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.
+A **subscriber token** grants read access to a specific path. Generate one on your backend and deliver it to the viewing client:
@@ -236,11 +186,6 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific
const streamPath = 'stream-alice';
- // Generate a token that allows publishing to 'stream-alice'
- const { token: publishToken } = await fishjamClient.createMoqToken({
- publishPath: streamPath,
- });
-
// Generate a token that allows subscribing to 'stream-alice'
const { token: subscribeToken } = await fishjamClient.createMoqToken({
subscribePath: streamPath,
@@ -261,9 +206,6 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific
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)
```
@@ -271,13 +213,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-subscribing).
-
-:::tip
-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.
-:::
+Deliver this token to the viewing client, then use it to connect as described in [Connecting and subscribing](#connecting-and-subscribing).
### Subscribe to a namespace
@@ -327,8 +263,14 @@ To do this, generate a subscriber token scoped to the room namespace instead of
)
room_name = 'my-room'
- # Publishers under this namespace might use paths like:
- # 'my-room/alice-camera', 'my-room/bob-screen', etc.
+
+ alice_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/alice",
+ )
+
+ bob_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/bob",
+ )
namespace_token = fishjam_client.create_moq_token(subscribe_path=room_name)
```
@@ -385,11 +327,7 @@ new Effect().run((effect) => {
## See also
-If you want a better understanding of how MoQ works, see:
-
-- [MoQ Streaming with Fishjam](../explanation/moq-streaming)
-
-If you want to learn about streaming using WebRTC instead of MoQ, see:
-
-- [Livestreaming](./livestreaming)
-- [WHIP/WHEP with Fishjam](../how-to/backend/whip-whep)
+- [Web Publishing](./web-publishing) — broadcast a MoQ stream from the browser
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
+- [WHIP/WHEP with Fishjam](../../how-to/backend/whip-whep)
diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index 906429c9..feee7a72 100644
--- a/docusaurus.config.ts
+++ b/docusaurus.config.ts
@@ -207,10 +207,8 @@ const config: Config = {
defaultSidebarItemsGenerator,
...args
}) {
- return injectTypeDocSidebar(
- args.version,
- await defaultSidebarItemsGenerator(args),
- );
+ const items = await defaultSidebarItemsGenerator(args);
+ return injectTypeDocSidebar(args.version, items);
},
},
theme: {
diff --git a/redirects/index.ts b/redirects/index.ts
index e3f4797d..90d133d7 100644
--- a/redirects/index.ts
+++ b/redirects/index.ts
@@ -8,6 +8,57 @@ interface RedirectGroup {
}
const redirectGroups: RedirectGroup[] = [
+ {
+ since: "0.28.0",
+ description:
+ "MoQ tutorials nested under Tutorials; MoQ concept moved into Concepts",
+ rules: [
+ {
+ from: "/tutorials/moq-react-native",
+ to: "/tutorials/moq/react-native-publishing",
+ },
+ {
+ from: "/explanation/moq-streaming",
+ to: "/explanation/moq-with-fishjam",
+ },
+ // Paths from the earlier dedicated MoQ section (next-only / 0.28.0), now
+ // folded back into Tutorials and Concepts.
+ { from: "/moq/streaming", to: "/explanation/moq-with-fishjam" },
+ {
+ from: "/moq/concepts/moq-with-fishjam",
+ to: "/explanation/moq-with-fishjam",
+ },
+ { from: "/moq/livestreaming", to: "/tutorials/moq/web-publishing" },
+ {
+ from: "/moq/react-native",
+ to: "/tutorials/moq/react-native-publishing",
+ },
+ {
+ from: "/moq/tutorials/web-publishing",
+ to: "/tutorials/moq/web-publishing",
+ },
+ {
+ from: "/moq/tutorials/web-subscribing",
+ to: "/tutorials/moq/web-subscribing",
+ },
+ {
+ from: "/moq/tutorials/web-livestreaming",
+ to: "/tutorials/moq/web-publishing",
+ },
+ {
+ from: "/moq/tutorials/react-native-publishing",
+ to: "/tutorials/moq/react-native-publishing",
+ },
+ {
+ from: "/moq/tutorials/react-native-subscribing",
+ to: "/tutorials/moq/react-native-subscribing",
+ },
+ {
+ from: "/moq/tutorials/react-native-livestreaming",
+ to: "/tutorials/moq/react-native-publishing",
+ },
+ ],
+ },
{
since: "0.26.0",
description: "Gemini Live moved from Tutorials to Integrations",
diff --git a/versioned_docs/version-0.28.0/explanation/moq-streaming.mdx b/versioned_docs/version-0.28.0/explanation/moq-with-fishjam.mdx
similarity index 87%
rename from versioned_docs/version-0.28.0/explanation/moq-streaming.mdx
rename to versioned_docs/version-0.28.0/explanation/moq-with-fishjam.mdx
index 18133b80..5a279ea7 100644
--- a/versioned_docs/version-0.28.0/explanation/moq-streaming.mdx
+++ b/versioned_docs/version-0.28.0/explanation/moq-with-fishjam.mdx
@@ -1,14 +1,20 @@
---
type: explanation
-sidebar_position: 6
-title: Media over QUIC (MoQ)
+sidebar_position: 9
+title: Media over QUIC in Fishjam
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
+# Media over QUIC in Fishjam
_How Media over QUIC (MoQ) works in Fishjam_
+:::warning[MoQ is a standalone feature]
+MoQ runs as a **separate delivery path** in Fishjam — it handles publishing and subscribing to a live broadcast over the MoQ relay, and the rest of Fishjam's feature set does not apply to a MoQ stream. In particular, a MoQ broadcast is not part of a WebRTC [room](./rooms), so you cannot use features like [Agents](../tutorials/agents), [data channels](./data-channels), and others that are part of the Fishjam WebRTC ecosystem.
+
+What MoQ supports is covered end to end in the [MoQ tutorials](../tutorials/moq/web-publishing).
+:::
+
## 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**.
@@ -129,11 +135,11 @@ The SDK's `createMoqToken` method accepts either a `publishPath` or a `subscribe
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 the [Web Publishing](../tutorials/moq/web-publishing) and [Web Subscribing](../tutorials/moq/web-subscribing) tutorials (or their [React Native](../tutorials/moq/react-native-publishing) [counterparts](../tutorials/moq/react-native-subscribing)) for working code examples.
## See also
-- [MoQ Streaming tutorial](../tutorials/moq) — step-by-step guide to publishing and subscribing
+- [Web Publishing](../tutorials/moq/web-publishing) / [Web Subscribing](../tutorials/moq/web-subscribing) — step-by-step guides 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/versioned_docs/version-0.28.0/tutorials/moq/_category_.json b/versioned_docs/version-0.28.0/tutorials/moq/_category_.json
new file mode 100644
index 00000000..87f92e9c
--- /dev/null
+++ b/versioned_docs/version-0.28.0/tutorials/moq/_category_.json
@@ -0,0 +1,9 @@
+{
+ "label": "Media over QUIC (MoQ)",
+ "position": 5,
+ "collapsed": false,
+ "link": {
+ "type": "doc",
+ "id": "tutorials/moq/index"
+ }
+}
diff --git a/versioned_docs/version-0.28.0/tutorials/moq/index.mdx b/versioned_docs/version-0.28.0/tutorials/moq/index.mdx
new file mode 100644
index 00000000..2308eb6a
--- /dev/null
+++ b/versioned_docs/version-0.28.0/tutorials/moq/index.mdx
@@ -0,0 +1,17 @@
+---
+type: tutorial
+title: Media over QUIC (MoQ)
+description: Step-by-step guides for publishing and subscribing to live broadcasts over Media over QUIC (MoQ) with Fishjam, on both web and React Native.
+---
+
+import DocCardList from "@theme/DocCardList";
+
+# Media over QUIC (MoQ)
+
+Step-by-step guides for publishing and subscribing to live broadcasts over [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. For the big-picture overview of how MoQ fits into Fishjam, see [Media over QUIC in Fishjam](../../explanation/moq-with-fishjam.mdx).
+
+:::warning[MoQ is a standalone feature]
+MoQ runs as a **separate delivery path** in Fishjam — it handles publishing and subscribing to a live broadcast over the MoQ relay, and the rest of Fishjam's feature set does not apply to a MoQ stream. In particular, a MoQ broadcast is not part of a WebRTC [room](../../explanation/rooms.mdx), so you cannot use features like [Agents](../agents.mdx), [data channels](../../explanation/data-channels.mdx), and others that are part of the Fishjam WebRTC ecosystem.
+:::
+
+
diff --git a/versioned_docs/version-0.28.0/tutorials/moq/react-native-publishing.mdx b/versioned_docs/version-0.28.0/tutorials/moq/react-native-publishing.mdx
new file mode 100644
index 00000000..2c00b181
--- /dev/null
+++ b/versioned_docs/version-0.28.0/tutorials/moq/react-native-publishing.mdx
@@ -0,0 +1,230 @@
+---
+type: tutorial
+sidebar_position: 3
+title: React Native Publishing
+description: Publish a live video and audio broadcast over Media over QUIC (MoQ) from a React Native mobile app with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# React Native Publishing
+
+This tutorial explains how to **publish** a live stream from a **React Native mobile app** using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To watch a stream instead, see [React Native Subscribing](./react-native-subscribing).
+
+It uses [`react-native-moq`](https://github.com/software-mansion-labs/react-native-moq) — React Native bindings for MoQKit, with a small, reactive hooks-based API. For the web equivalent, see [Web Publishing](./web-publishing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+## Requirements
+
+`react-native-moq` targets the React Native **New Architecture** (Fabric / TurboModules):
+
+- iOS 16+
+- Android API 30+
+
+## Installation
+
+```bash npm2yarn
+npm install react-native-moq
+```
+
+Then install the iOS pods:
+
+```sh
+cd ios && pod install
+```
+
+Publishing captures the camera and microphone, so the host app is responsible for runtime permissions: request `CAMERA` / `RECORD_AUDIO` on Android, and add `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` to `Info.plist` on iOS. The library does not request these for you.
+
+:::tip
+MoQ is a protocol with a well-defined negotiation, so a publisher and a subscriber don't need to use the same client library. A React Native app published with `react-native-moq` can be watched in the browser with [`@moq/watch`](./web-subscribing#connecting-and-subscribing), and vice versa.
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype publishing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+### Obtaining a publisher token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ The `useSandbox` hook from `@fishjam-cloud/react-native-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-native-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqPublisherToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a publisher token scoped to the publisher path
+ const publishToken = await getSandboxMoqPublisherToken(PUBLISHER_PATH);
+ ```
+
+
+
+
+
+ If you don't want to pull in the whole client library just for the `useSandbox` hook, you can call the Sandbox API directly with `fetch`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/publisher`,
+ );
+ const { token: publishToken } = await response.json();
+ ```
+
+
+
+
+### Connecting and publishing
+
+`react-native-moq` is hooks-based. You open a session against the relay, capture the camera and microphone, and hand them to a publisher.
+
+Build the relay URL using the publisher token, open the session, and publish the camera + microphone tracks:
+
+```tsx
+// @noErrors
+import { useEffect } from "react";
+import { Button, PermissionsAndroid, Platform } from "react-native";
+import {
+ PublisherView,
+ useCamera,
+ useMicrophone,
+ usePublisher,
+ useSession,
+} from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const PUBLISHER_PATH = "stream-alice";
+const publishToken = ""; // from the step above
+
+// Build the relay URL using the publisher token
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}`;
+
+function PublishScreen() {
+ // Open the MoQ session against the Fishjam relay
+ const session = useSession(relayUrl, (s) => s.connect());
+ const camera = useCamera({ position: "front" });
+ const microphone = useMicrophone();
+ const publisher = usePublisher(session);
+
+ // Request camera + mic permissions on Android (iOS handles it automatically when Info.plist is configured)
+ useEffect(() => {
+ if (Platform.OS === "android") {
+ PermissionsAndroid.requestMultiple([
+ PermissionsAndroid.PERMISSIONS.CAMERA,
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
+ ]);
+ }
+ }, []);
+
+ const isPublishing = publisher.state === "publishing";
+
+ return (
+ <>
+
+
+ {
+ if (isPublishing) publisher.stop();
+ else
+ publisher.publish({
+ path: PUBLISHER_PATH,
+ tracks: [camera, microphone],
+ });
+ }}
+ />
+ >
+ );
+}
+```
+
+Once `publisher.state === "publishing"`, the stream is live on the MoQ relay! Viewers can start watching by following [React Native Subscribing](./react-native-subscribing).
+
+:::info
+**Why a separate `useSession` and `publish()`?** \
+The session owns the connection to the relay; the publisher reuses it. Because publishing rides on top of an open session, the same connection can subscribe and publish at once — pair `usePublisher(session)` with `useBroadcasts(session, prefix)` to do both.
+:::
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you publishing quickly. In production, your backend generates tokens with proper authorization, so you control who can publish.
+
+A **publisher token** grants write access to a specific path. Generate one on your backend and deliver it to the broadcasting client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows publishing to 'stream-alice'
+ const { token: publishToken } = await fishjamClient.createMoqToken({
+ publishPath: streamPath,
+ });
+ ```
+
+
+
+
+
+ ```python
+ 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)
+ ```
+
+
+
+
+Deliver this token to the mobile client, then use it to build the relay URL and connect as described in [Connecting and publishing](#connecting-and-publishing).
+
+## See also
+
+- [React Native Subscribing](./react-native-subscribing) — watch a MoQ stream on mobile
+- [Web Publishing](./web-publishing) — publish from the browser instead
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
diff --git a/versioned_docs/version-0.28.0/tutorials/moq/react-native-subscribing.mdx b/versioned_docs/version-0.28.0/tutorials/moq/react-native-subscribing.mdx
new file mode 100644
index 00000000..ee62f1e1
--- /dev/null
+++ b/versioned_docs/version-0.28.0/tutorials/moq/react-native-subscribing.mdx
@@ -0,0 +1,360 @@
+---
+type: tutorial
+sidebar_position: 4
+title: React Native Subscribing
+description: Subscribe to and render a live Media over QUIC (MoQ) broadcast in a React Native mobile app with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# React Native Subscribing
+
+This tutorial explains how to **subscribe** to a live stream and render it in a **React Native mobile app** using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To broadcast a stream instead, see [React Native Publishing](./react-native-publishing).
+
+It uses [`react-native-moq`](https://github.com/software-mansion-labs/react-native-moq) — React Native bindings for MoQKit, with a small, reactive hooks-based API. For the web equivalent, see [Web Subscribing](./web-subscribing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+## Requirements
+
+`react-native-moq` targets the React Native **New Architecture** (Fabric / TurboModules):
+
+- iOS 16+
+- Android API 30+
+
+## Installation
+
+```bash npm2yarn
+npm install react-native-moq
+```
+
+Then install the iOS pods:
+
+```sh
+cd ios && pod install
+```
+
+:::tip
+The ready-made player chrome — `` with fullscreen controls, a volume slider, and matching context hooks — lives in a separate package so apps that build their own UI don't pay for it:
+
+```bash npm2yarn
+npm install react-native-moq-ui @react-native-vector-icons/material-icons
+```
+
+This tutorial uses the bare `` from the core package, but `` is a drop-in replacement if you want controls out of the box.
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype subscribing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+:::tip
+MoQ is a protocol with a well-defined negotiation, so a publisher and a subscriber don't need to use the same client library. A stream published from the browser with [`@moq/publish`](./web-publishing) can be watched with `react-native-moq`, and vice versa.
+:::
+
+### Obtaining a subscriber token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ The `useSandbox` hook from `@fishjam-cloud/react-native-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-native-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqSubscriberToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a subscriber token scoped to the subscriber path
+ const subscribeToken = await getSandboxMoqSubscriberToken(SUBSCRIBER_PATH);
+ ```
+
+
+
+
+
+ If you don't want to pull in the whole client library just for the `useSandbox` hook, you can call the Sandbox API directly with `fetch`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
+ );
+ const { token: subscribeToken } = await response.json();
+ ```
+
+
+
+
+### Connecting and subscribing
+
+Open a session, discover the broadcast under its path with `useBroadcasts`, and render it with ``. `useVideoPlayer` turns a discovered broadcast into a reactive player; the decoding and rendering pipeline is handled natively — no canvas, no manual WebCodecs setup:
+
+```tsx
+// @noErrors
+import { Button } from "react-native";
+import {
+ VideoView,
+ useBroadcasts,
+ useSession,
+ useVideoPlayer,
+} from "react-native-moq";
+import type { BroadcastInfo } from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const SUBSCRIBER_PATH = "stream-alice";
+const subscribeToken = ""; // from the step above
+
+// Build the relay URL using the subscriber token
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${subscribeToken}`;
+
+function WatchScreen() {
+ // Connect to the Fishjam MoQ relay on mount
+ const session = useSession(relayUrl, (s) => s.connect());
+
+ // Discover broadcasts under the subscriber path
+ const broadcasts = useBroadcasts(session, SUBSCRIBER_PATH);
+
+ return (
+ <>
+ {broadcasts.map((broadcast) => (
+
+ ))}
+ >
+ );
+}
+
+function BroadcastPlayer({ broadcast }: { broadcast: BroadcastInfo }) {
+ // Create a reactive player and start playback
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+That's it! The stream appears in the `` once the publisher starts broadcasting, and `useBroadcasts` re-populates automatically on reconnect.
+
+:::info
+**No canvas needed.** \
+Unlike the web, where `Watch.MultiBackend` decodes frames through WebCodecs and paints them onto a ``, the React Native player decodes and renders natively. You just hand the `player` to a `` and size it like any other view.
+:::
+
+For audio-only streaming, use [`useAudioPlayer(broadcast)`](https://github.com/software-mansion-labs/react-native-moq/blob/main/docs/API.md#useaudioplayerbroadcast-setup) instead of `useVideoPlayer` — the video track is never subscribed, so no video bandwidth is consumed.
+
+### Using the `` component
+
+If you don't need fine-grained control over the rendering surface, the `react-native-moq-ui` package ships a ready-to-use `` that wraps `` with platform-styled play/pause, a volume slider, and a fullscreen modal — the React Native counterpart of the web `` element.
+
+```tsx
+// @noErrors
+import { VideoPlayerView } from "react-native-moq-ui";
+
+function BroadcastPlayer({ broadcast }: { broadcast: BroadcastInfo }) {
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+
+ return (
+
+ );
+}
+```
+
+It exposes imperative `enterFullscreen()` / `exitFullscreen()` methods on its ref, and the chrome is fully customizable. See [Default UI components](https://github.com/software-mansion-labs/react-native-moq/blob/main/docs/API.md#default-ui-components-react-native-moq-ui) in the API reference.
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you watching quickly. In production, your backend generates tokens with proper authorization, so you control who can subscribe.
+
+A **subscriber token** grants read access to a specific path. Generate one on your backend and deliver it to the viewing client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows subscribing to 'stream-alice'
+ 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 = 'stream-alice'
+
+ # Generate a token that allows subscribing to 'stream-alice'
+ subscribe_token = fishjam_client.create_moq_token(subscribe_path=stream_path)
+ ```
+
+
+
+
+Deliver this token to the mobile client, then use it to build the relay URL and connect as described in [Connecting and subscribing](#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 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';
+
+ const { token: alicePublisherToken } = await fishjamClient.createMoqToken({
+ publishPath: roomName + "/alice",
+ });
+
+ const { token: bobPublisherToken } = await fishjamClient.createMoqToken({
+ publishPath: roomName + "/bob",
+ });
+
+
+ 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'
+
+ alice_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/alice",
+ )
+
+ bob_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/bob",
+ )
+
+ namespace_token = fishjam_client.create_moq_token(subscribe_path=room_name)
+ ```
+
+
+
+
+On the client, pass the namespace token's relay URL to `useSession`, then call `useBroadcasts(session, roomName)` with the room prefix. It returns a reactive `BroadcastInfo[]` that re-renders every time a publisher joins or leaves — no manual diffing of an announce set. Map over it to mount a player per broadcast:
+
+```tsx
+// @noErrors
+import {
+ VideoView,
+ useBroadcasts,
+ useSession,
+ useVideoPlayer,
+} from "react-native-moq";
+import type { BroadcastInfo } from "react-native-moq";
+
+const FISHJAM_ID = "YOUR_FISHJAM_ID";
+const ROOM_NAME = "my-room";
+const namespaceToken = "token";
+
+const relayUrl = `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${namespaceToken}`;
+
+function RoomGrid() {
+ const session = useSession(relayUrl, (s) => s.connect());
+
+ // Every broadcast published under the 'my-room' prefix, kept live
+ const broadcasts = useBroadcasts(session, ROOM_NAME);
+
+ return (
+ <>
+ {broadcasts.map((broadcast) => (
+
+ ))}
+ >
+ );
+}
+
+function Tile({ broadcast }: { broadcast: BroadcastInfo }) {
+ const player = useVideoPlayer(broadcast, (p) => p.play());
+ return (
+
+ );
+}
+```
+
+Because `useBroadcasts` starts the native subscription on mount and tears it down on unmount, players appear and disappear in lockstep with the publishers in the room — React's reconciliation handles mounting and unmounting tiles for you.
+
+## See also
+
+- [React Native Publishing](./react-native-publishing) — broadcast a MoQ stream from mobile
+- [Web Subscribing](./web-subscribing) — subscribe from the browser instead
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
diff --git a/versioned_docs/version-0.28.0/tutorials/moq/web-publishing.mdx b/versioned_docs/version-0.28.0/tutorials/moq/web-publishing.mdx
new file mode 100644
index 00000000..f55c0d3a
--- /dev/null
+++ b/versioned_docs/version-0.28.0/tutorials/moq/web-publishing.mdx
@@ -0,0 +1,186 @@
+---
+type: tutorial
+sidebar_position: 1
+title: Web Publishing
+description: Publish a live video and audio broadcast over Media over QUIC (MoQ) from the browser with Fishjam, from sandbox prototyping to production.
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+# Web Publishing
+
+This tutorial explains how to **publish** a live stream from the browser using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To watch a stream instead, see [Web Subscribing](./web-subscribing).
+
+:::info
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
+:::
+
+To start publishing a MoQ stream, you need two things: a _publisher token_ and the relay URL. We show how to quickly prototype with the [Sandbox API](#quickstart-with-the-sandbox-api) and how to get ready for [production](#production-with-server-sdks).
+
+:::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](https://doc.moq.dev/lib/js/).
+:::
+
+## Quickstart with the Sandbox API
+
+If you don't have a backend server set up, you can prototype publishing using the [Sandbox API](../../explanation/sandbox-api-concept).
+
+### Obtaining a publisher token
+
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
+
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
+
+
+
+
+ If you're using React, the `useSandbox` hook from `@fishjam-cloud/react-client` wraps the Sandbox API request for you:
+
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-client";
+
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ // Inside a React component:
+ const { getSandboxMoqPublisherToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
+
+ // Request a publisher token scoped to the publisher path
+ const publishToken = await getSandboxMoqPublisherToken(PUBLISHER_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`:
+
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const PUBLISHER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/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 PUBLISHER_PATH = "stream-alice";
+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);
+
+const camera = new Publish.Source.Camera({ enabled: true });
+const microphone = new Publish.Source.Microphone({ enabled: true });
+
+// Set up a broadcast with video and audio tracks
+const broadcast = new Publish.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 it — follow [Web Subscribing](./web-subscribing).
+
+## Production with Server SDKs
+
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you publishing quickly. In production, your backend generates tokens with proper authorization, so you control who can publish.
+
+A **publisher token** grants write access to a specific path. Generate one on your backend and deliver it to the broadcasting client:
+
+
+
+
+ ```ts
+ const fishjamId = '';
+ const managementToken = '';
+
+ // ---cut---
+ import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
+
+ const fishjamClient = new FishjamClient({
+ fishjamId,
+ managementToken,
+ });
+
+ const streamPath = 'stream-alice';
+
+ // Generate a token that allows publishing to 'stream-alice'
+ const { token: publishToken } = await fishjamClient.createMoqToken({
+ publishPath: streamPath,
+ });
+ ```
+
+
+
+
+
+ ```python
+ 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)
+ ```
+
+
+
+
+Deliver this token to the broadcasting client, then use it to connect as described in [Connecting and publishing](#connecting-and-publishing).
+
+## See also
+
+- [Web Subscribing](./web-subscribing) — watch a MoQ stream in the browser
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
+- [WHIP/WHEP with Fishjam](../../how-to/backend/whip-whep)
diff --git a/versioned_docs/version-0.28.0/tutorials/moq.mdx b/versioned_docs/version-0.28.0/tutorials/moq/web-subscribing.mdx
similarity index 57%
rename from versioned_docs/version-0.28.0/tutorials/moq.mdx
rename to versioned_docs/version-0.28.0/tutorials/moq/web-subscribing.mdx
index fa3742e0..4e69cf83 100644
--- a/versioned_docs/version-0.28.0/tutorials/moq.mdx
+++ b/versioned_docs/version-0.28.0/tutorials/moq/web-subscribing.mdx
@@ -1,128 +1,83 @@
---
type: tutorial
-sidebar_position: 5
-title: MoQ Livestreaming
-description: Stream live video and audio over Media over QUIC (MoQ) with Fishjam, from sandbox prototyping to production deployment.
+sidebar_position: 2
+title: Web Subscribing
+description: Subscribe to and render a live Media over QUIC (MoQ) broadcast in the browser with Fishjam, from sandbox prototyping to production.
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
-# How to stream Media over QUIC
+# Web Subscribing
-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.
+This tutorial explains how to **subscribe** to a live stream and render it in the browser using [Media over QUIC (MoQ)](https://datatracker.ietf.org/wg/moq/about/) with Fishjam. To broadcast a stream instead, see [Web Publishing](./web-publishing).
:::info
-If you're new to MoQ, then we recommend getting familiar with the [MoQ Streaming with Fishjam](../explanation/moq-streaming) explanation.
+If you're new to MoQ, then we recommend getting familiar with the [MoQ with Fishjam](../../explanation/moq-with-fishjam) explanation.
:::
-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. \
-**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](./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.
+To receive a MoQ stream you need one thing: a _subscriber token_. We show how to quickly prototype with the [Sandbox API](#quickstart-with-the-sandbox-api) and how to get ready for [production](#production-with-server-sdks).
:::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/).
+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/lib/js/).
:::
-### 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 PUBLISHER_PATH = "stream-alice";
-const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
-
-const response = await fetch(
- `${SANDBOX_API_URL}/moq/${PUBLISHER_PATH}/publisher`,
-);
-const { token: publishToken } = await response.json();
-```
-
-#### Connecting and publishing
+## Quickstart with the Sandbox API
-Install the MoQ packages:
+If you don't have a backend server set up, you can prototype subscribing using the [Sandbox API](../../explanation/sandbox-api-concept).
-```bash npm2yarn
-npm install @moq/lite @moq/publish
-```
+### Obtaining a subscriber token
-Use the token to connect to the Fishjam MoQ relay and start broadcasting:
+For more on what the Sandbox API is and its limitations, see [What is the Sandbox API?](../../explanation/sandbox-api-concept).
-```ts
-// @noErrors
-import * as Moq from "@moq/lite";
-import * as Publish from "@moq/publish";
+:::info
+To obtain a MoQ token you'll need your Fishjam ID and Sandbox API URL. If you don't have them already, see [Sandbox API URL and Fishjam ID](../../explanation/sandbox-api-concept#sandbox-api-url-and-fishjam-id).
+:::
-const FISHJAM_ID = "YOUR_FISHJAM_ID";
-const PUBLISHER_PATH = "stream-alice";
-const publishToken = "";
+
+
-// ---cut---
-// Build the relay URL using the publisher token
-const relayUrl = new URL(
- `https://relay.fishjam.io/${FISHJAM_ID}?jwt=${publishToken}`,
-);
+ If you're using React, the `useSandbox` hook from `@fishjam-cloud/react-client` wraps the Sandbox API request for you:
-// Connect to the Fishjam MoQ relay
-const connection = await Moq.Connection.connect(relayUrl);
+ ```tsx
+ // @noErrors
+ import { useSandbox } from "@fishjam-cloud/react-client";
-const camera = new Publish.Source.Camera({ enabled: true });
-const microphone = new Publish.Source.Microphone({ enabled: true });
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
-// Set up a broadcast with video and audio tracks
-const broadcast = new Publish.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)
+ // Inside a React component:
+ const { getSandboxMoqSubscriberToken } = useSandbox({
+ sandboxApiUrl: SANDBOX_API_URL,
+ });
-### Subscriber setup
+ // Request a subscriber token scoped to the subscriber path
+ const subscribeToken = await getSandboxMoqSubscriberToken(SUBSCRIBER_PATH);
+ ```
-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:
+ 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`:
-```ts
-const FISHJAM_ID = "YOUR_FISHJAM_ID";
-const SUBSCRIBER_PATH = "stream-alice";
-const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+ ```ts
+ // @noErrors
+ const FISHJAM_ID = "YOUR_FISHJAM_ID";
+ const SUBSCRIBER_PATH = "stream-alice";
+ const SANDBOX_API_URL = "YOUR_SANDBOX_API_URL";
+
+ const response = await fetch(
+ `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
+ );
+ const { token: subscribeToken } = await response.json();
+ ```
-const response = await fetch(
- `${SANDBOX_API_URL}/moq/${SUBSCRIBER_PATH}/subscriber`,
-);
-const { token: subscribeToken } = await response.json();
-```
+
+
-#### Connecting and subscribing
+### Connecting and subscribing
Install the MoQ packages:
@@ -180,7 +135,7 @@ Watch.MultiBackend renders to a `` so it can decode frames directly thro
That's it! The stream will appear in the canvas once the publisher starts broadcasting.
-#### Using the `` web component
+### Using the `` 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.
@@ -208,16 +163,11 @@ Then drop `` wrapping `` into your HTML — pass the re
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](#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.
+## Production with Server SDKs
-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:
+The [Quickstart](#quickstart-with-the-sandbox-api) gets you watching quickly. In production, your backend generates tokens with proper authorization, so you control who can subscribe.
-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.
+A **subscriber token** grants read access to a specific path. Generate one on your backend and deliver it to the viewing client:
@@ -236,11 +186,6 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific
const streamPath = 'stream-alice';
- // Generate a token that allows publishing to 'stream-alice'
- const { token: publishToken } = await fishjamClient.createMoqToken({
- publishPath: streamPath,
- });
-
// Generate a token that allows subscribing to 'stream-alice'
const { token: subscribeToken } = await fishjamClient.createMoqToken({
subscribePath: streamPath,
@@ -261,9 +206,6 @@ MoQ tokens are path-scoped: a publisher token grants write access to a specific
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)
```
@@ -271,13 +213,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-subscribing).
-
-:::tip
-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.
-:::
+Deliver this token to the viewing client, then use it to connect as described in [Connecting and subscribing](#connecting-and-subscribing).
### Subscribe to a namespace
@@ -327,8 +263,14 @@ To do this, generate a subscriber token scoped to the room namespace instead of
)
room_name = 'my-room'
- # Publishers under this namespace might use paths like:
- # 'my-room/alice-camera', 'my-room/bob-screen', etc.
+
+ alice_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/alice",
+ )
+
+ bob_publisher_token = fishjam_client.create_moq_token(
+ publish_path=room_name + "/bob",
+ )
namespace_token = fishjam_client.create_moq_token(subscribe_path=room_name)
```
@@ -385,11 +327,7 @@ new Effect().run((effect) => {
## See also
-If you want a better understanding of how MoQ works, see:
-
-- [MoQ Streaming with Fishjam](../explanation/moq-streaming)
-
-If you want to learn about streaming using WebRTC instead of MoQ, see:
-
-- [Livestreaming](./livestreaming)
-- [WHIP/WHEP with Fishjam](../how-to/backend/whip-whep)
+- [Web Publishing](./web-publishing) — broadcast a MoQ stream from the browser
+- [MoQ with Fishjam](../../explanation/moq-with-fishjam) — how MoQ works in Fishjam
+- [Livestreaming](../../tutorials/livestreaming) — the WebRTC (WHIP/WHEP) approach
+- [WHIP/WHEP with Fishjam](../../how-to/backend/whip-whep)