From 560f9318a0feec5b809cfb4c4066edeb7af07ef0 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 12:13:55 +0200 Subject: [PATCH 01/10] Bump server SDK submodules to v0.30.2 Composition support ships in 0.30.2 on both npm and PyPI. The pinned 0.29.0 predates CompositionClient and createRecording, so twoslash failed to typecheck any snippet using them. --- .../compositions/inputs-and-outputs.mdx | 157 ++++++++++++++++++ packages/js-server-sdk | 2 +- packages/python-server-sdk | 2 +- 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/docs/how-to/compositions/inputs-and-outputs.mdx b/docs/how-to/compositions/inputs-and-outputs.mdx index b5b81f28..e6820e4c 100644 --- a/docs/how-to/compositions/inputs-and-outputs.mdx +++ b/docs/how-to/compositions/inputs-and-outputs.mdx @@ -4,6 +4,9 @@ sidebar_position: 4 description: Choose the right input and output protocols for a composition, from WebRTC to RTMP and MP4. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Choose inputs and outputs A composition pulls media in through **inputs** and pushes the composed result out through **outputs**. Each is a tagged object whose `type` selects the protocol. This guide summarizes the available types and when to use each. @@ -75,6 +78,77 @@ Other output operations: Replace an output's [scene](./../../explanation/compositions#scenes) while the composition is running with `POST …/output/{output_id}/update`: + + + +```ts +import { + CompositionClient, + CompositionId, + OutputId, + RendererId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const rendererId = "" as RendererId; +// ---cut--- +await compositionClient.updateOutput(compositionId, outputId, { + video: { + root: { + type: "rescaler", + child: { type: "input_stream", inputId: "camera_1" }, + }, + }, + audio: { inputs: [{ inputId: "camera_1" }] }, +}); +``` + + + + +```python +import os + +from fishjam import CompositionClient +from fishjam.composition import ( + AudioScene, + AudioSceneInput, + InputStream, + InputStreamType, + Rescaler, + RescalerType, + UpdateOutputRequest, + VideoScene, +) + +composition_client = CompositionClient( + management_token=os.environ["MANAGEMENT_TOKEN"], +) + +composition_client.update_output( + "", + "main", + UpdateOutputRequest( + video=VideoScene( + root=Rescaler( + type_=RescalerType.RESCALER, + child=InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="camera_1" + ), + ) + ), + audio=AudioScene(inputs=[AudioSceneInput(input_id="camera_1")]), + ), +) +``` + + + + ```bash curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/update" \ -H "Authorization: Bearer $TOKEN" \ @@ -90,6 +164,9 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/update" }' ``` + + + An update has to mirror the sides the output was registered with. If you registered the output with both `video` and `audio`, every update must carry both, even when only one of them changed. If you registered only one of them, the update may only carry that one. Sending a mismatched update fails. Add `schedule_time_ms` to apply the change at a chosen offset on the composition timeline, in milliseconds, instead of immediately: @@ -110,6 +187,49 @@ Renderers are shared assets you register once and then place in any scene. Register an image with `POST …/image/{image_id}/register`: + + + +```ts +import { + CompositionClient, + CompositionId, + OutputId, + RendererId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const rendererId = "" as RendererId; +// ---cut--- +await compositionClient.registerImage(compositionId, rendererId, { + assetType: "auto", + url: "https://example.com/logo.png", +}); +``` + + + + +```python +from fishjam.composition import ImageSpecAuto, ImageSpecAutoAssetType + +composition_client.register_image( + "", + "logo", + ImageSpecAuto( + asset_type=ImageSpecAutoAssetType.AUTO, + url="https://example.com/logo.png", + ), +) +``` + + + + ```bash curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/image/logo/register" \ -H "Authorization: Bearer $TOKEN" \ @@ -117,6 +237,9 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/image/logo/register" -d '{ "asset_type": "auto", "url": "https://example.com/logo.png" }' ``` + + + `asset_type` selects the format: `png`, `jpeg`, `svg`, `gif`, or `auto` to detect it from the URL. Each takes a `url`; `svg` also accepts a `resolution`. Place the image in a scene with an `image` component: ```json @@ -129,12 +252,46 @@ Unregister with `POST …/image/{image_id}/unregister`. Register a font with a multipart request carrying the font file in a single `font` part: + + + +```ts +import { + CompositionClient, + CompositionId, + OutputId, + RendererId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const rendererId = "" as RendererId; +// ---cut--- +await compositionClient.registerFont(compositionId, "./BrandFont.ttf"); +``` + + + + +```python +composition_client.register_font("", "./BrandFont.ttf") +``` + + + + ```bash curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/font/register" \ -H "Authorization: Bearer $TOKEN" \ -F "font=@BrandFont.ttf" ``` + + + The font is then available to `text` components. See the [Composition API](./../../api/reference#compositions) reference for the complete request and response schemas. diff --git a/packages/js-server-sdk b/packages/js-server-sdk index c83334b9..48eb59b6 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit c83334b9d16d3a595427a53d5ddd6043d52a3841 +Subproject commit 48eb59b6264dc4347f5570db65d2bfd4254ec872 diff --git a/packages/python-server-sdk b/packages/python-server-sdk index fdd55700..c4890146 160000 --- a/packages/python-server-sdk +++ b/packages/python-server-sdk @@ -1 +1 @@ -Subproject commit fdd5570033f84a4e8f49bf2ac7fedc019b5b99df +Subproject commit c4890146739fee7e77bbd5d05ac6dd302ed1d4ce From 73d90c44af230103d08eb9a9dfa2da9db19e8535 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 12:14:16 +0200 Subject: [PATCH 02/10] Show SDK calls alongside curl in composition inputs and outputs Add Typescript and Python tabs to every request, name SDK methods instead of raw endpoints in prose, and map each input and output type to its method in the tables. --- .../compositions/inputs-and-outputs.mdx | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/docs/how-to/compositions/inputs-and-outputs.mdx b/docs/how-to/compositions/inputs-and-outputs.mdx index e6820e4c..41e00180 100644 --- a/docs/how-to/compositions/inputs-and-outputs.mdx +++ b/docs/how-to/compositions/inputs-and-outputs.mdx @@ -13,14 +13,14 @@ A composition pulls media in through **inputs** and pushes the composed result o ## Inputs -Register an input with `POST /api/composition/{composition_id}/input/{input_id}/register` and a body whose `type` is one of: +Register an input with the method for its protocol: -| `type` | Use it when | Key fields | -| ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| `whip_server` | A WebRTC client should publish **to** the composition. | `bearer_token` (optional; generated and returned if omitted), `video` (optional, default `true`). | -| `whep_client` | The composition should **pull** a WebRTC stream from a WHEP endpoint. | `endpoint_url` (required), `bearer_token` (optional), `video` (optional). | -| `rtmp_server` | An encoder (OBS, hardware) should push RTMP in. | `stream_key` (required). | -| `mp4` | You want to compose a file, optionally looped. | `url` (required), `loop` (optional). | +| SDK method | `type` | Use it when | Key fields | +| ------------------- | ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `registerWhipInput` | `whip_server` | A WebRTC client should publish **to** the composition. | `bearer_token` (optional; generated and returned if omitted), `video` (optional, default `true`). | +| `registerWhepInput` | `whep_client` | The composition should **pull** a WebRTC stream from a WHEP endpoint. | `endpoint_url` (required), `bearer_token` (optional), `video` (optional). | +| `registerRtmpInput` | `rtmp_server` | An encoder (OBS, hardware) should push RTMP in. | `stream_key` (required). | +| `registerMp4Input` | `mp4` | You want to compose a file, optionally looped. | `url` (required), `loop` (optional). | For `whip_server`, the register response returns the `bearer_token` a publisher uses to authenticate against the input's WHIP endpoint, together with the route to push to: @@ -32,16 +32,16 @@ That token is the only one accepted on the publish endpoint. Your Management Tok `endpoint_route` is relative to the composition, so the full address is `https://rtc.fishjam.io/api/composition//whip/`. Point any WHIP publisher there and authenticate with that token. A `whip_server` input takes WebRTC from anything that speaks WHIP: a phone's camera, a laptop webcam, a browser tab, a screen share, or a hardware encoder. OBS has WHIP output built in. -Unregister any input with `POST …/input/{input_id}/unregister`. +Unregister any input with `unregisterInput`. ## Outputs -Register an output with `POST /api/composition/{composition_id}/output/{output_id}/register` and a body whose `type` is one of: +Register an output with the method for its protocol: -| `type` | Use it when | Key fields | -| ------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | -| `whip_client` | Publish the composed result over WebRTC (for example to a Fishjam livestream). | `endpoint_url` (required), `bearer_token` (optional), `video`, `audio`. | -| `rtmp_client` | Publish to an RTMP destination (for example a social platform). | `url` (required), `video`, `audio`. | +| SDK method | `type` | Use it when | Key fields | +| -------------------- | ------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| `registerWhipOutput` | `whip_client` | Publish the composed result over WebRTC (for example to a Fishjam livestream). | `endpoint_url` (required), `bearer_token` (optional), `video`, `audio`. | +| `registerRtmpOutput` | `rtmp_client` | Publish to an RTMP destination (for example a social platform). | `url` (required), `video`, `audio`. | There is no URL you can point a player at to watch a composition. An output **pushes** the composed video to a destination you name when you register it, and your viewers connect to that destination's own playback URL: @@ -68,34 +68,36 @@ An output's `video` carries the resolution and initial [scene](./../../explanati Other output operations: -- **Register a templated output** with `POST …/output/{output_id}/template` instead of `…/register`: a multipart request carrying the same configuration plus a template bundle (see [Write and deploy a template](./write-and-deploy-a-template)). -- **Update the scene** live with `POST …/output/{output_id}/update` (see [Update a scene](#update-a-scene) below). -- **Force a keyframe** with `POST …/output/{output_id}/request_keyframe`, useful when a new subscriber joins. -- **Unregister** with `POST …/output/{output_id}/unregister`. +- **Register a templated output** with `registerTemplateOutput` instead: it carries the same configuration plus a template bundle (see [Write and deploy a template](./write-and-deploy-a-template)). +- **Update the scene** live with `updateOutput` (see [Update a scene](#update-a-scene) below). +- **Force a keyframe** with `requestKeyframe`, useful when a new subscriber joins. +- **Unregister** with `unregisterOutput`. - **Record** any output into an MP4 through the Fishjam Server API (see [Record a composition](./record-a-composition)). ## Update a scene -Replace an output's [scene](./../../explanation/compositions#scenes) while the composition is running with `POST …/output/{output_id}/update`: +Replace an output's [scene](./../../explanation/compositions#scenes) while the composition is running: ```ts import { - CompositionClient, CompositionId, OutputId, RendererId, } from "@fishjam-cloud/js-server-sdk"; -const compositionClient = new CompositionClient({ - managementToken: process.env.MANAGEMENT_TOKEN!, -}); +const managementToken = ""; const compositionId = "" as CompositionId; const outputId = "" as OutputId; const rendererId = "" as RendererId; + // ---cut--- +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken }); + await compositionClient.updateOutput(compositionId, outputId, { video: { root: { @@ -111,8 +113,6 @@ await compositionClient.updateOutput(compositionId, outputId, { ```python -import os - from fishjam import CompositionClient from fishjam.composition import ( AudioScene, @@ -125,9 +125,7 @@ from fishjam.composition import ( VideoScene, ) -composition_client = CompositionClient( - management_token=os.environ["MANAGEMENT_TOKEN"], -) +composition_client = CompositionClient(management_token=management_token) composition_client.update_output( "", @@ -185,26 +183,28 @@ Renderers are shared assets you register once and then place in any scene. ### Images -Register an image with `POST …/image/{image_id}/register`: +Register an image with `registerImage`: ```ts import { - CompositionClient, CompositionId, OutputId, RendererId, } from "@fishjam-cloud/js-server-sdk"; -const compositionClient = new CompositionClient({ - managementToken: process.env.MANAGEMENT_TOKEN!, -}); +const managementToken = ""; const compositionId = "" as CompositionId; const outputId = "" as OutputId; const rendererId = "" as RendererId; + // ---cut--- +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken }); + await compositionClient.registerImage(compositionId, rendererId, { assetType: "auto", url: "https://example.com/logo.png", @@ -246,7 +246,7 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/image/logo/register" { "type": "image", "image_id": "logo" } ``` -Unregister with `POST …/image/{image_id}/unregister`. +Unregister with `unregisterImage`. ### Fonts @@ -257,19 +257,21 @@ Register a font with a multipart request carrying the font file in a single `fon ```ts import { - CompositionClient, CompositionId, OutputId, RendererId, } from "@fishjam-cloud/js-server-sdk"; -const compositionClient = new CompositionClient({ - managementToken: process.env.MANAGEMENT_TOKEN!, -}); +const managementToken = ""; const compositionId = "" as CompositionId; const outputId = "" as OutputId; const rendererId = "" as RendererId; + // ---cut--- +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken }); + await compositionClient.registerFont(compositionId, "./BrandFont.ttf"); ``` From 67792857f043ed0af6c4b47f76850f71ef3be7f3 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 12:43:38 +0200 Subject: [PATCH 03/10] Inline the composition API host in curl examples The host is the same for every reader and the SDK tabs never set it, so a shell variable made curl look like it needed setup the other tabs did not. --- docs/how-to/compositions/inputs-and-outputs.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how-to/compositions/inputs-and-outputs.mdx b/docs/how-to/compositions/inputs-and-outputs.mdx index 41e00180..1bec9a65 100644 --- a/docs/how-to/compositions/inputs-and-outputs.mdx +++ b/docs/how-to/compositions/inputs-and-outputs.mdx @@ -148,7 +148,7 @@ composition_client.update_output( ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/update" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/update" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -231,7 +231,7 @@ composition_client.register_image( ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/image/logo/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/image/logo/register" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "asset_type": "auto", "url": "https://example.com/logo.png" }' @@ -286,7 +286,7 @@ composition_client.register_font("", "./BrandFont.ttf") ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/font/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/font/register" \ -H "Authorization: Bearer $TOKEN" \ -F "font=@BrandFont.ttf" ``` From 7be982ea7ac5096d11c9e6b332f84425030ff822 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 12:44:08 +0200 Subject: [PATCH 04/10] Show SDK calls alongside curl in drive a template with events --- .../drive-a-template-with-events.mdx | 136 +++++++++++++++++- 1 file changed, 129 insertions(+), 7 deletions(-) diff --git a/docs/how-to/compositions/drive-a-template-with-events.mdx b/docs/how-to/compositions/drive-a-template-with-events.mdx index fd011930..3c463074 100644 --- a/docs/how-to/compositions/drive-a-template-with-events.mdx +++ b/docs/how-to/compositions/drive-a-template-with-events.mdx @@ -4,6 +4,8 @@ sidebar_position: 3 description: Send custom events to a running composition to update a template's on-screen state at runtime. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; import IdleCleanup from "../../_common/compositions/idle-cleanup.mdx"; # Drive a template with events @@ -49,11 +51,44 @@ export default function App() { Now create the composition. A caption-only template registers no inputs, and a composition with no input media cleans itself up after five minutes, so create this one with `cleanup_without_inputs` set to `false`: + + + +```ts +const managementToken = ""; + +// ---cut--- +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken }); + +const { compositionId } = await compositionClient.createComposition({ + cleanupWithoutInputs: false, +}); +``` + + + + +```python +from fishjam import CompositionClient +from fishjam.composition import CreateCompositionRequest + +composition_client = CompositionClient(management_token=management_token) + +composition = composition_client.create_composition( + CreateCompositionRequest(cleanup_without_inputs=False), +) +composition_id = composition.composition_id +``` + + + + ```bash -export COMPOSITION_URL="https://rtc.fishjam.io" export TOKEN="" -curl -X POST "$COMPOSITION_URL/api/composition" \ +curl -X POST "https://rtc.fishjam.io/api/composition" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cleanup_without_inputs": false }' @@ -65,30 +100,91 @@ Save the id from the response: export COMPOSITION="" ``` + + + -Build the bundle and register it as a templated output, the same single multipart request to `POST …/output/{output_id}/template` as in [Write and deploy a template](./write-and-deploy-a-template). +Build the bundle and register it as a templated output with `registerTemplateOutput`, as in [Write and deploy a template](./write-and-deploy-a-template). That output has to publish somewhere, so create a livestream and take its streamer token first, as in [Step 3 of the compositions tutorial](./../../tutorials/compositions#step-3-create-a-livestream-to-publish-to). Watching it works the same way as there. ## Send events from your backend -Send an event with `POST /api/composition/{composition_id}/event`. The `event_name` is matched against your `eventBus.on(...)` subscriptions, and `data` is delivered as the handler's argument. +Send an event with `sendEvent`. The event name is matched against your `eventBus.on(...)` subscriptions, and `data` is delivered as the handler's argument. + + + + +```ts +import { CompositionClient, CompositionId } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.sendEvent(compositionId, { + eventName: "SET_CAPTION", + data: { text: "Welcome to the stream" }, +}); +``` + + + + +```python +composition_client.send_event(composition_id, "SET_CAPTION", {"text": "Welcome to the stream"}) +``` + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/event" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/event" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event_name": "SET_CAPTION", "data": { "text": "Welcome to the stream" } }' ``` + + + + + + +```ts +import { CompositionClient, CompositionId } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.sendEvent(compositionId, { + eventName: "SET_LIVE", + data: { live: true }, +}); +``` + + + + +```python +composition_client.send_event(composition_id, "SET_LIVE", {"live": True}) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/event" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/event" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event_name": "SET_LIVE", "data": { "live": true } }' ``` + + + The template re-renders as soon as the event arrives. :::note @@ -99,7 +195,33 @@ Event names and payloads are an application-defined contract between your backen Delete the composition when you have finished testing, so it stops billing: + + + +```ts +import { CompositionClient, CompositionId } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.deleteComposition(compositionId); +``` + + + + +```python +composition_client.delete_composition(composition_id) +``` + + + + ```bash -curl -X DELETE "$COMPOSITION_URL/api/composition/$COMPOSITION" \ +curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ -H "Authorization: Bearer $TOKEN" ``` + + + From ac54c72aebf7390201eb284f63fc2337d5f09e5b Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 12:57:51 +0200 Subject: [PATCH 05/10] Show SDK calls alongside curl in write and deploy a template registerTemplateOutput replaces the hand-rolled FormData fetch, and the JavaScript tab becomes a Typescript one so it joins the shared language tab group. --- .../write-and-deploy-a-template.mdx | 166 ++++++++++++++---- 1 file changed, 134 insertions(+), 32 deletions(-) diff --git a/docs/how-to/compositions/write-and-deploy-a-template.mdx b/docs/how-to/compositions/write-and-deploy-a-template.mdx index 78858a3c..f5808e82 100644 --- a/docs/how-to/compositions/write-and-deploy-a-template.mdx +++ b/docs/how-to/compositions/write-and-deploy-a-template.mdx @@ -84,7 +84,6 @@ npm run build Now create the composition, as in [Step 1 of the tutorial](./../../tutorials/compositions#step-1-create-a-composition), and the livestream with its streamer token, as in [Step 3](./../../tutorials/compositions#step-3-create-a-livestream-to-publish-to): ```bash -export COMPOSITION_URL="https://rtc.fishjam.io" export FISHJAM_URL="https://fishjam.io/api/v1/connect/" export TOKEN="" export COMPOSITION="" @@ -92,7 +91,7 @@ export STREAM="" export STREAMER_TOKEN="" ``` -A templated output is registered in a **single multipart request** to `POST …/output/{output_id}/template`. Do not call the plain `…/register` endpoint first. The request carries two parts: +A templated output is registered with `registerTemplateOutput` in a **single request**. Do not register a plain output first. The request carries two parts: - `config`: the same JSON body a regular output registration takes (see [Choose inputs and outputs](./inputs-and-outputs)). Its `video.initial` scene is only a placeholder; the template takes over rendering as soon as it loads. - `template`: the built bundle. @@ -100,6 +99,72 @@ A templated output is registered in a **single multipart request** to `POST …/ `endpoint_url` is wherever the composed stream should go. The quickest destination to watch is a Fishjam livestream, exactly as in [the tutorial](./../../tutorials/compositions#step-3-create-a-livestream-to-publish-to): create one, take its streamer token, and publish to `https://fishjam.io/api/v1/live/api/whip`. + + +```ts +import { + CompositionClient, + CompositionId, + OutputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const streamerToken = ""; + +// ---cut--- +await compositionClient.registerTemplateOutput( + compositionId, + outputId, + { + type: "whip_client", + endpointUrl: "https://fishjam.io/api/v1/live/api/whip", + bearerToken: streamerToken, + video: { + resolution: { width: 1280, height: 720 }, + initial: { root: { type: "view" } }, + }, + audio: { initial: { inputs: [] } }, + }, + "dist/App.js", +); +``` + + + + +```python +from fishjam.composition import ( + AudioScene, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Resolution, + VideoScene, + View, + ViewType, + WhipOutput, + WhipOutputType, +) + +composition_client.register_template_output( + composition_id, + "main", + WhipOutput( + type_=WhipOutputType.WHIP_CLIENT, + endpoint_url="https://fishjam.io/api/v1/live/api/whip", + bearer_token=streamer_token, + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene(root=View(type_=ViewType.VIEW)), + ), + audio=OutputWhipAudioOptions(initial=AudioScene(inputs=[])), + ), + "dist/App.js", +) +``` + + ```bash @@ -117,69 +182,106 @@ CONFIG=$(cat < - - -```js -const config = { - type: "whip_client", - endpoint_url: "https://fishjam.io/api/v1/live/api/whip", - bearer_token: streamerToken, - video: { - resolution: { width: 1280, height: 720 }, - initial: { root: { type: "view" } }, - }, - audio: { initial: { inputs: [] } }, -}; + -const form = new FormData(); -form.append("config", JSON.stringify(config)); -form.append("template", new Blob([bundle]), "App.js"); +## Redeploy after an edit -await fetch( - `${COMPOSITION_URL}/api/composition/${composition}/output/main/template`, - { - method: "POST", - headers: { Authorization: `Bearer ${token}` }, - body: form, - }, -); +Unregister the output first, then deploy again: + + + + +```ts +import { + CompositionClient, + CompositionId, + OutputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; + +// ---cut--- +await compositionClient.unregisterOutput(compositionId, outputId); ``` - + -## Redeploy after an edit +```python +composition_client.unregister_output(composition_id, "main") +``` -Unregister the output first, then deploy again: + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/unregister" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/unregister" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` + + + Rebuild with `npm run build` and repeat the deploy request. The inputs stay registered, so only the layout changes. ## Clean up Deleting the composition removes its inputs and outputs with it, so a finished experiment takes two calls: + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + RoomId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const roomId = "" as RoomId; + +// ---cut--- +await compositionClient.deleteComposition(compositionId); +await fishjamClient.deleteRoom(roomId); +``` + + + + +```python +composition_client.delete_composition(composition_id) +fishjam_client.delete_room(room_id) +``` + + + + ```bash -curl -X DELETE "$COMPOSITION_URL/api/composition/$COMPOSITION" \ +curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ -H "Authorization: Bearer $TOKEN" curl -X DELETE "$FISHJAM_URL/livestream/$STREAM" \ -H "Authorization: Bearer $TOKEN" ``` + + + ## Next steps To feed a whole Fishjam room's peers into the template, continue with [Compose a Fishjam room](./compose-a-fishjam-room). To update the template's on-screen state at runtime, see [Drive a template with events](./drive-a-template-with-events). From b90684db0c7180eeac2e9c611075df53f0d0a16e Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 13:05:01 +0200 Subject: [PATCH 06/10] Show SDK calls alongside curl in compose a Fishjam room forwardRoomTracks pairs with compositionUrl, and the livestream WHIP address and streamer token come from the client instead of the placeholders curl needs. --- .../compositions/compose-a-fishjam-room.mdx | 291 +++++++++++++++++- 1 file changed, 284 insertions(+), 7 deletions(-) diff --git a/docs/how-to/compositions/compose-a-fishjam-room.mdx b/docs/how-to/compositions/compose-a-fishjam-room.mdx index 1b94ea9e..040bc97d 100644 --- a/docs/how-to/compositions/compose-a-fishjam-room.mdx +++ b/docs/how-to/compositions/compose-a-fishjam-room.mdx @@ -4,6 +4,8 @@ sidebar_position: 2 description: Forward a Fishjam room into a composition and render its peers with a live React template. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; import IdleCleanup from "../../_common/compositions/idle-cleanup.mdx"; # Compose a Fishjam room @@ -22,7 +24,6 @@ Fishjam room (peers) ──forwarded──▶ composition (template) ──WHIP The [Composition API](./../../api/reference#compositions) lives on `https://rtc.fishjam.io`, while rooms and livestreams live on the [Fishjam Server API](./../../api/reference#server). Both take the same Management Token: ```bash -export COMPOSITION_URL="https://rtc.fishjam.io" export FISHJAM_URL="https://fishjam.io/api/v1/connect/" export TOKEN="" ``` @@ -31,6 +32,37 @@ export TOKEN="" Compositions consume h264 video, so the room has to enforce that codec. It is the default, but set it explicitly so a change of default cannot break the composition later: + + + +```ts +import { FishjamClient } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); + +// ---cut--- +const room = await fishjamClient.createRoom({ + roomType: "conference", + videoCodec: "h264", +}); +``` + + + + +```python +from fishjam import FishjamClient, RoomOptions + +fishjam_client = FishjamClient(fishjam_id=fishjam_id, management_token=management_token) + +room = fishjam_client.create_room( + RoomOptions(room_type="conference", video_codec="h264"), +) +``` + + + + ```bash curl -X POST "$FISHJAM_URL/room" \ -H "Authorization: Bearer $TOKEN" \ @@ -44,8 +76,34 @@ The room id comes back under `data.room.id`. Save it: export ROOM_ID="" ``` + + + Every participant needs their own peer token. Create one per person: + + + +```ts +import { FishjamClient, RoomId } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const roomId = "" as RoomId; + +// ---cut--- +const { peer, peerToken } = await fishjamClient.createPeer(roomId); +``` + + + + +```python +peer, peer_token = fishjam_client.create_peer(room.id) +``` + + + + ```bash curl -X POST "$FISHJAM_URL/room/$ROOM_ID/peer" \ -H "Authorization: Bearer $TOKEN" \ @@ -53,6 +111,9 @@ curl -X POST "$FISHJAM_URL/room/$ROOM_ID/peer" \ -d '{ "type": "webrtc", "options": {} }' ``` + + + Hand each `data.token` to a client and have it join, using [Connect to a room](./../client/connecting) or the [React quick start](./../../tutorials/react-quick-start). The composition renders whoever is publishing, so get at least one peer in with a camera on before you expect a picture. ## Step 2: Write a room-aware template @@ -123,8 +184,41 @@ Setting `cleanup_without_inputs` to `false` tightens the cleanup condition, so t + + + +```ts +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); + +// ---cut--- +const { compositionId } = await compositionClient.createComposition({ + autostart: false, + cleanupWithoutInputs: false, +}); +``` + + + + +```python +from fishjam import CompositionClient +from fishjam.composition import CreateCompositionRequest + +composition_client = CompositionClient(management_token=management_token) + +composition = composition_client.create_composition( + CreateCompositionRequest(autostart=False, cleanup_without_inputs=False), +) +composition_id = composition.composition_id +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition" \ +curl -X POST "https://rtc.fishjam.io/api/composition" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "autostart": false, "cleanup_without_inputs": false }' @@ -136,10 +230,90 @@ Save the `composition_id` from the response, every call below uses it: export COMPOSITION="" ``` + + + Register a templated `whip_client` output that pushes to your livestream's WHIP endpoint. Create the livestream and its streamer token first, as in [Step 3 of the tutorial](./../../tutorials/compositions#step-3-create-a-livestream-to-publish-to). The output configuration and the template bundle go together in one multipart request: + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + OutputId, + RoomId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const livestreamId = "" as RoomId; + +// ---cut--- +const { token: streamerToken } = + await fishjamClient.createLivestreamStreamerToken(livestreamId); + +await compositionClient.registerTemplateOutput( + compositionId, + outputId, + { + type: "whip_client", + endpointUrl: fishjamClient.livestreamWhipUrl(), + bearerToken: streamerToken, + video: { + resolution: { width: 1280, height: 720 }, + initial: { root: { type: "view" } }, + }, + audio: { initial: { inputs: [] } }, + }, + "dist/App.js", +); +``` + + + + +```python +from fishjam.composition import ( + AudioScene, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Resolution, + VideoScene, + View, + ViewType, + WhipOutput, + WhipOutputType, +) + +streamer_token = fishjam_client.create_livestream_streamer_token(livestream_id) + +composition_client.register_template_output( + composition_id, + "main", + WhipOutput( + type_=WhipOutputType.WHIP_CLIENT, + endpoint_url=fishjam_client.livestream_whip_url(), + bearer_token=streamer_token, + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene(root=View(type_=ViewType.VIEW)), + ), + audio=OutputWhipAudioOptions(initial=AudioScene(inputs=[])), + ), + "dist/App.js", +) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/template" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/template" \ -H "Authorization: Bearer $TOKEN" \ -F 'config={ "type": "whip_client", @@ -151,38 +325,141 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/template -F "template=@dist/App.js" ``` + + + ## Step 4: Forward the room into the composition -One call to the [Fishjam Server API](./../../api/reference#server) wires everything up: +One call to `forwardRoomTracks` wires everything up: + + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + RoomId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const roomId = "" as RoomId; + +// ---cut--- +await fishjamClient.forwardRoomTracks( + roomId, + compositionClient.compositionUrl(compositionId), +); +``` + + + + +```python +fishjam_client.forward_room_tracks( + room.id, + composition_client.composition_url(composition_id), +) +``` + + + ```bash curl -X POST "$FISHJAM_URL/room/$ROOM_ID/track_forwardings" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ - -d "{ \"compositionURL\": \"$COMPOSITION_URL/api/composition/$COMPOSITION\", \"selector\": \"all\" }" + -d "{ \"compositionURL\": \"https://rtc.fishjam.io/api/composition/$COMPOSITION\", \"selector\": \"all\" }" ``` + + + Everything else happens automatically: Fishjam links the room to the composition, registers an input for every forwarded track, and streams the media in. Your template's `usePeers()` fills with the room's peers as their media starts flowing. You never register room inputs by hand. A room and a composition pair up one to one, in both directions. Repeating the call with the same `compositionURL` is a no-op, but pointing the room at a second composition, or a second room at this composition, fails. To compose two rooms, give each its own composition. ## Step 5: Start the composition + + + +```ts +import { CompositionClient, CompositionId } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.startComposition(compositionId); +``` + + + + +```python +composition_client.start_composition(composition_id) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/start" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/start" \ -H "Authorization: Bearer $TOKEN" ``` + + + Viewers can now watch the composed grid through the livestream's WHEP endpoint. To also keep an MP4 of the composed stream, [record the output](./record-a-composition). ## Step 6: Clean up Delete the composition when you are done. Forwarding stops on the Fishjam side when the room itself stops, so delete the room too once you no longer need it. There is no separate call to remove a forwarding from a live room. + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + RoomId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const roomId = "" as RoomId; + +// ---cut--- +await compositionClient.deleteComposition(compositionId); +await fishjamClient.deleteRoom(roomId); +``` + + + + +```python +composition_client.delete_composition(composition_id) +fishjam_client.delete_room(room.id) +``` + + + + ```bash -curl -X DELETE "$COMPOSITION_URL/api/composition/$COMPOSITION" \ +curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ -H "Authorization: Bearer $TOKEN" curl -X DELETE "$FISHJAM_URL/room/$ROOM_ID" \ -H "Authorization: Bearer $TOKEN" ``` + + + From 11fa1cb3f993d0395e402698a172fa705baca75e Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 13:09:38 +0200 Subject: [PATCH 07/10] Show SDK calls for the recorded output and put Typescript first Step 1 had no SDK tab, and this was the only page where curl was the default tab in the shared language group. --- .../compositions/record-a-composition.mdx | 207 +++++++++++++----- 1 file changed, 148 insertions(+), 59 deletions(-) diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx index f812aec4..c3df1679 100644 --- a/docs/how-to/compositions/record-a-composition.mdx +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -24,21 +24,107 @@ composition ──output──▶ livestream / RTMP ──▶ [viewers] A running composition with its inputs registered, a livestream for it to publish to, and the livestream's streamer token. The [tutorial](./../../tutorials/compositions) sets these up in Steps 1 to 3; stop before Step 4, which registers the same output as Step 1 below. ```bash -export COMPOSITION_URL="https://rtc.fishjam.io" export FISHJAM_URL="https://fishjam.io/api/v1/connect/" export TOKEN="" export COMPOSITION="" export STREAMER_TOKEN="" ``` -Step 1 uses the Composition API. Every recording call from Step 2 onwards is also available as a method on the `FishjamClient` of the [JS and Python server SDKs](./../backend/server-setup), shown in the language tabs. +Step 1 uses the `CompositionClient`; every recording call from Step 2 onwards is a method on the `FishjamClient` of the [JS and Python server SDKs](./../backend/server-setup). Both are shown in the language tabs. ## Step 1: Register the output to record A recording attaches to an output, so the composition must first have an output whose scene will be recorded. Register it like any other output. Every output publishes to a destination, so the output you record is also a live stream: in this example, `main` publishes to your livestream over WHIP, with the tutorial's two inputs side by side. + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + InputId, + OutputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const outputId = "" as OutputId; +const streamerToken = ""; +const race = "race" as InputId; +const player = "player" as InputId; + +// ---cut--- +await compositionClient.registerWhipOutput(compositionId, outputId, { + endpointUrl: fishjamClient.livestreamWhipUrl(), + bearerToken: streamerToken, + video: { + resolution: { width: 1280, height: 720 }, + initial: { + root: { + type: "tiles", + children: [ + { type: "input_stream", inputId: race }, + { type: "input_stream", inputId: player }, + ], + }, + }, + }, + audio: { initial: { inputs: [{ inputId: player }] } }, +}); +``` + + + + +```python +from fishjam.composition import ( + AudioScene, + AudioSceneInput, + InputStream, + InputStreamType, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Resolution, + Tiles, + TilesType, + VideoScene, +) + +composition_client.register_whip_output( + composition_id, + "main", + endpoint_url=fishjam_client.livestream_whip_url(), + bearer_token=streamer_token, + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene( + root=Tiles( + type_=TilesType.TILES, + children=[ + InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="race" + ), + InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="player" + ), + ], + ) + ), + ), + audio=OutputWhipAudioOptions( + initial=AudioScene(inputs=[AudioSceneInput(input_id="player")]) + ), +) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/register" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @- < + + The recording captures this scene and every subsequent update sent to `main`. If your composition already has the output you want to record, for example the templated output from [Compose a Fishjam room](./compose-a-fishjam-room), skip to Step 2. See [Choose inputs and outputs](./inputs-and-outputs) for the other output types. ## Step 2: Start the recording @@ -70,28 +159,6 @@ The recording captures this scene and every subsequent update sent to `main`. If Specify the composition and the output from Step 1 in `source`: - - -```bash -curl -X POST "$FISHJAM_URL/recordings" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"source\": { - \"compositionURL\": \"$COMPOSITION_URL/api/composition/$COMPOSITION\", - \"outputId\": \"main\" - }, - \"metadata\": { \"show\": \"weekly-standup\" } - }" -``` - -The response returns the recording with status `active`; capture has already started. Save its id from `data.id`: - -```bash -export RECORDING="" -``` - - ```ts @@ -138,6 +205,28 @@ recording = fishjam_client.create_recording( The response returns the recording with status `active`; capture has already started. + + + +```bash +curl -X POST "$FISHJAM_URL/recordings" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"source\": { + \"compositionURL\": \"https://rtc.fishjam.io/api/composition/$COMPOSITION\", + \"outputId\": \"main\" + }, + \"metadata\": { \"show\": \"weekly-standup\" } + }" +``` + +The response returns the recording with status `active`; capture has already started. Save its id from `data.id`: + +```bash +export RECORDING="" +``` + @@ -150,14 +239,6 @@ Note the following: ## Step 3: Check the status - - -```bash -curl "$FISHJAM_URL/recordings/$RECORDING" \ - -H "Authorization: Bearer $TOKEN" -``` - - ```ts @@ -179,6 +260,14 @@ const { status } = await fishjamClient.getRecording(recording.id); status = fishjam_client.get_recording(recording.id).status ``` + + + +```bash +curl "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + @@ -196,14 +285,6 @@ Instead of polling, you can subscribe to [server notifications](./../../api/refe ## Step 4: Stop the recording - - -```bash -curl -X POST "$FISHJAM_URL/recordings/$RECORDING/stop" \ - -H "Authorization: Bearer $TOKEN" -``` - - ```ts @@ -225,6 +306,14 @@ await fishjamClient.stopRecording(recording.id); fishjam_client.stop_recording(recording.id) ``` + + + +```bash +curl -X POST "$FISHJAM_URL/recordings/$RECORDING/stop" \ + -H "Authorization: Bearer $TOKEN" +``` + @@ -255,16 +344,6 @@ You can download the file or serve the URL to your users directly. Until the rec `GET /recordings` lists every recording in your app and accepts filters on status and metadata. A metadata filter matches recordings whose metadata contains all of the given pairs. Values are compared as strings, so numeric and boolean metadata values cannot be matched this way. - - -```bash -curl -g "$FISHJAM_URL/recordings?status=available&metadata[show]=weekly-standup" \ - -H "Authorization: Bearer $TOKEN" -``` - -The `-g` flag prevents curl from interpreting the square brackets. - - ```ts @@ -287,6 +366,16 @@ const recordings = await fishjamClient.getAllRecordings({ recordings = fishjam_client.get_all_recordings({"show": "weekly-standup"}) ``` + + + +```bash +curl -g "$FISHJAM_URL/recordings?status=available&metadata[show]=weekly-standup" \ + -H "Authorization: Bearer $TOKEN" +``` + +The `-g` flag prevents curl from interpreting the square brackets. + @@ -297,14 +386,6 @@ The SDK methods filter by metadata only. To filter by status as well, call the R Recordings persist independently of the composition until you delete them. Deleting a recording also removes its files: - - -```bash -curl -X DELETE "$FISHJAM_URL/recordings/$RECORDING" \ - -H "Authorization: Bearer $TOKEN" -``` - - ```ts @@ -326,6 +407,14 @@ await fishjamClient.deleteRecording(recording.id); fishjam_client.delete_recording(recording.id) ``` + + + +```bash +curl -X DELETE "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + From 8df57851a26786a64d073659bc778b8df793d8b0 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 13:16:43 +0200 Subject: [PATCH 08/10] Show SDK calls alongside curl in the compositions tutorial Step 3 stays on the Sandbox API, which is the only way to give a livestream the name the Step 5 viewer looks it up by. --- docs/tutorials/compositions.mdx | 322 +++++++++++++++++++++++++++++++- 1 file changed, 314 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/compositions.mdx b/docs/tutorials/compositions.mdx index d3f6cc48..05b8ee1b 100644 --- a/docs/tutorials/compositions.mdx +++ b/docs/tutorials/compositions.mdx @@ -5,6 +5,8 @@ sidebar_position: 3.5 description: Create your first composition end to end, overlaying one video on another and watching the composed result in your browser. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; import useBaseUrl from "@docusaurus/useBaseUrl"; # Compositions @@ -47,7 +49,6 @@ The composed frame puts the race full-screen with the player in a small overlay The [Composition API](../api/reference#compositions) lives on `https://rtc.fishjam.io` and takes your Management Token. The livestream you will publish to comes from the [Sandbox API](../explanation/sandbox-api-concept), which has its own URL and needs no token at all: ```bash -export COMPOSITION_URL="https://rtc.fishjam.io" export FISHJAM_URL="https://fishjam.io/api/v1/connect/" export SANDBOX_URL="" export TOKEN="" @@ -55,8 +56,42 @@ export TOKEN="" ## Step 1: Create a composition +By default a composition auto-starts, and cleans itself up after five minutes in which none of its inputs carry any media. + + + + +```ts +import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ + managementToken: process.env.MANAGEMENT_TOKEN!, +}); + +const { compositionId } = await compositionClient.createComposition(); +``` + + + + +```python +import os + +from fishjam import CompositionClient + +composition_client = CompositionClient( + management_token=os.environ["MANAGEMENT_TOKEN"], +) + +composition = composition_client.create_composition() +composition_id = composition.composition_id +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition" \ +curl -X POST "https://rtc.fishjam.io/api/composition" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' @@ -68,18 +103,68 @@ The response contains the `composition_id` you will use for every subsequent cal { "composition_id": "abc123", "api_url": "https://rtc.fishjam.io" } ``` -By default a composition auto-starts, and cleans itself up after five minutes in which none of its inputs carry any media. Save the id: +Save the id: ```bash export COMPOSITION="abc123" ``` + + + ## Step 2: Register two inputs Register two `mp4` inputs, looping so they never run dry. The composition downloads them itself, so there is nothing to publish: + + + +```ts +import { + CompositionClient, + CompositionId, + InputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.registerMp4Input(compositionId, "race" as InputId, { + url: "https://smelter.dev/videos/template-scene-race.mp4", + loop: true, +}); + +await compositionClient.registerMp4Input(compositionId, "player" as InputId, { + url: "https://smelter.dev/videos/template-scene-streamer.mp4", + loop: true, +}); +``` + + + + +```python +composition_client.register_mp4_input( + composition_id, + "race", + url="https://smelter.dev/videos/template-scene-race.mp4", + loop=True, +) + +composition_client.register_mp4_input( + composition_id, + "player", + url="https://smelter.dev/videos/template-scene-streamer.mp4", + loop=True, +) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/input/race/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/input/race/register" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -88,7 +173,7 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/input/race/register" "loop": true }' -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/input/player/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/input/player/register" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -98,6 +183,9 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/input/player/registe }' ``` + + + The names `race` and `player` are the `input_id`s you will refer to from the layout in Step 4. The scene only ever refers to inputs by id, so what sits behind an id is interchangeable: register `whip_server` instead of `mp4` to take a live camera, phone, or OBS feed here, or forward a whole [Fishjam room](../how-to/compositions/compose-a-fishjam-room) in. See [Choose inputs and outputs](../how-to/compositions/inputs-and-outputs) for every input type. :::note @@ -138,8 +226,116 @@ Register a `whip_client` output that publishes to the livestream's WHIP endpoint The output connects to `endpoint_url` while it is being registered, so the endpoint has to be reachable already. That is why the livestream comes first. Registering against a URL that does not accept the connection fails. ::: + + + +```ts +import { + CompositionClient, + CompositionId, + InputId, + OutputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; +const streamerToken = ""; +const race = "race" as InputId; +const player = "player" as InputId; + +// ---cut--- +await compositionClient.registerWhipOutput(compositionId, "main" as OutputId, { + endpointUrl: "https://fishjam.io/api/v1/live/api/whip", + bearerToken: streamerToken, + video: { + resolution: { width: 1280, height: 720 }, + initial: { + root: { + type: "view", + children: [ + { type: "rescaler", child: { type: "input_stream", inputId: race } }, + { + type: "rescaler", + top: 20, + left: 20, + width: 355, + height: 200, + borderRadius: 44, + mode: "fill", + child: { type: "input_stream", inputId: player }, + }, + ], + }, + }, + }, + audio: { initial: { inputs: [{ inputId: player }] } }, +}); +``` + + + + +```python +from fishjam.composition import ( + AudioScene, + AudioSceneInput, + InputStream, + InputStreamType, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + RescaleMode, + Rescaler, + RescalerType, + Resolution, + VideoScene, + View, + ViewType, +) + +composition_client.register_whip_output( + composition_id, + "main", + endpoint_url="https://fishjam.io/api/v1/live/api/whip", + bearer_token=streamer_token, + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene( + root=View( + type_=ViewType.VIEW, + children=[ + Rescaler( + type_=RescalerType.RESCALER, + child=InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="race" + ), + ), + Rescaler( + type_=RescalerType.RESCALER, + top=20, + left=20, + width=355, + height=200, + border_radius=44, + mode=RescaleMode.FILL, + child=InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="player" + ), + ), + ], + ) + ), + ), + audio=OutputWhipAudioOptions( + initial=AudioScene(inputs=[AudioSceneInput(input_id="player")]) + ), +) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/register" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @- < + + The scene is a tree of components. A `view` stacks its children in order, so the second `rescaler` draws on top of the first. Giving that second one `top` and `left` positions it in the corner instead of filling the frame, and `mode: "fill"` makes the video cover its box rather than letter-boxing inside it. See [Scenes](../explanation/compositions#scenes) for how scenes work and what each component does. ## Step 5: Watch it @@ -190,8 +389,70 @@ You should see the race running. A scene is not fixed once the output is registered. Leave the player watching and swap the corner overlay for a side-by-side grid: + + + +```ts +import { + CompositionClient, + CompositionId, + InputId, + OutputId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; +const race = "race" as InputId; +const player = "player" as InputId; + +// ---cut--- +await compositionClient.updateOutput(compositionId, "main" as OutputId, { + video: { + root: { + type: "tiles", + children: [ + { type: "input_stream", inputId: race }, + { type: "input_stream", inputId: player }, + ], + }, + }, + audio: { inputs: [{ inputId: player }] }, +}); +``` + + + + +```python +from fishjam.composition import Tiles, TilesType, UpdateOutputRequest + +composition_client.update_output( + composition_id, + "main", + UpdateOutputRequest( + video=VideoScene( + root=Tiles( + type_=TilesType.TILES, + children=[ + InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="race" + ), + InputStream( + type_=InputStreamType.INPUT_STREAM, input_id="player" + ), + ], + ) + ), + audio=AudioScene(inputs=[AudioSceneInput(input_id="player")]), + ), +) +``` + + + + ```bash -curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/update" \ +curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/output/main/update" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -208,6 +469,9 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/update" }' ``` + + + The layout changes on the fly, with no interruption to the stream. Send the Step 4 scene again to go back to the corner overlay. :::warning @@ -218,14 +482,56 @@ An update has to carry the same sides the output was registered with. This outpu Delete the composition and the livestream when you are done. The Sandbox API only creates, so removing the livestream goes through the Server API with your Management Token: + + + +```ts +import { + CompositionClient, + CompositionId, + FishjamClient, + RoomId, +} from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const fishjamClient = new FishjamClient({ fishjamId: "", managementToken: "" }); +const compositionId = "" as CompositionId; +const streamId = "" as RoomId; + +// ---cut--- +await compositionClient.deleteComposition(compositionId); +await fishjamClient.deleteRoom(streamId); +``` + + + + +```python +from fishjam import FishjamClient + +fishjam_client = FishjamClient( + fishjam_id=os.environ["FISHJAM_ID"], + management_token=os.environ["MANAGEMENT_TOKEN"], +) + +composition_client.delete_composition(composition_id) +fishjam_client.delete_room(stream_id) +``` + + + + ```bash -curl -X DELETE "$COMPOSITION_URL/api/composition/$COMPOSITION" \ +curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ -H "Authorization: Bearer $TOKEN" curl -X DELETE "$FISHJAM_URL/livestream/$STREAM" \ -H "Authorization: Bearer $TOKEN" ``` + + + ## Next steps Updating the scene by hand gets tedious once people are joining, leaving, muting, and unmuting, since each of those needs its own call. A **template** is a React component that receives the live room state and re-renders itself as the room changes, so you send no scene updates at all. From ac29f633b73ee3ce3778eee3790923203f0707c2 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 13:18:37 +0200 Subject: [PATCH 09/10] Show SDK calls alongside curl in the compositions explanation --- docs/explanation/compositions.mdx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/explanation/compositions.mdx b/docs/explanation/compositions.mdx index efc1de61..39e45daf 100644 --- a/docs/explanation/compositions.mdx +++ b/docs/explanation/compositions.mdx @@ -5,6 +5,9 @@ sidebar_position: 4.5 description: Understand compositions, the Fishjam feature that mixes multiple live media streams into a single composed output in real time. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Compositions _Understanding real-time stream composition in Fishjam_ @@ -42,11 +45,37 @@ Two defaults keep that in check. A composition auto-starts, and it cleans itself Delete a composition as soon as you are done with it: + + + +```ts +import { CompositionClient, CompositionId } from "@fishjam-cloud/js-server-sdk"; + +const compositionClient = new CompositionClient({ managementToken: "" }); +const compositionId = "" as CompositionId; + +// ---cut--- +await compositionClient.deleteComposition(compositionId); +``` + + + + +```python +composition_client.delete_composition(composition_id) +``` + + + + ```bash -curl -X DELETE "$COMPOSITION_URL/api/composition/$COMPOSITION" \ +curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ -H "Authorization: Bearer $TOKEN" ``` + + + ## Recordings A **recording** saves what one of a composition's outputs publishes into an MP4 file that stays around after the composition is gone. Recordings are a resource of their own, managed through the [Fishjam Server API](./../api/reference#server) rather than the Composition API: [Recordings](./recordings) explains how they work, and [Record a composition](./../how-to/compositions/record-a-composition) walks through making one. From 48e1d37999e2e8efe3a12f7c3cf962ea0be53b02 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 14 Sep 2026 13:39:55 +0200 Subject: [PATCH 10/10] Stop reading an env var the tutorial never exports The setup block exports TOKEN, so process.env.MANAGEMENT_TOKEN was undefined. Take the token from a bare variable, as the other pages do. --- docs/tutorials/compositions.mdx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/compositions.mdx b/docs/tutorials/compositions.mdx index 05b8ee1b..c9efe120 100644 --- a/docs/tutorials/compositions.mdx +++ b/docs/tutorials/compositions.mdx @@ -62,11 +62,12 @@ By default a composition auto-starts, and cleans itself up after five minutes in ```ts +const managementToken = ""; + +// ---cut--- import { CompositionClient } from "@fishjam-cloud/js-server-sdk"; -const compositionClient = new CompositionClient({ - managementToken: process.env.MANAGEMENT_TOKEN!, -}); +const compositionClient = new CompositionClient({ managementToken }); const { compositionId } = await compositionClient.createComposition(); ``` @@ -75,13 +76,9 @@ const { compositionId } = await compositionClient.createComposition(); ```python -import os - from fishjam import CompositionClient -composition_client = CompositionClient( - management_token=os.environ["MANAGEMENT_TOKEN"], -) +composition_client = CompositionClient(management_token=management_token) composition = composition_client.create_composition() composition_id = composition.composition_id @@ -510,8 +507,8 @@ await fishjamClient.deleteRoom(streamId); from fishjam import FishjamClient fishjam_client = FishjamClient( - fishjam_id=os.environ["FISHJAM_ID"], - management_token=os.environ["MANAGEMENT_TOKEN"], + fishjam_id=fishjam_id, + management_token=management_token, ) composition_client.delete_composition(composition_id)