From cb1e4128adf8a5638faa2d861d505ac25c95de92 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Mon, 24 Aug 2026 15:28:20 +0200 Subject: [PATCH 1/5] introduce recordings --- docs/api/reference.md | 2 +- docs/explanation/compositions.mdx | 6 + docs/explanation/recordings.mdx | 57 ++++ .../compositions/compose-a-fishjam-room.mdx | 2 +- .../compositions/inputs-and-outputs.mdx | 1 + .../compositions/record-a-composition.mdx | 301 ++++++++++++++++++ docs/tutorials/compositions.mdx | 1 + packages/js-server-sdk | 2 +- 8 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 docs/explanation/recordings.mdx create mode 100644 docs/how-to/compositions/record-a-composition.mdx diff --git a/docs/api/reference.md b/docs/api/reference.md index 7eed625b..a52d50a5 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -16,7 +16,7 @@ See also: [What is the Sandbox API?](../explanation/sandbox-api-concept) ## Server -Fishjam Server provides a REST API for managing rooms and peers, and +Fishjam Server provides a REST API for managing rooms, peers, and [recordings](../how-to/compositions/record-a-composition), and [Protobufs](https://protobuf.dev) for receiving structured live updates from the server. The notifications can be configured using Webhook or Websocket. diff --git a/docs/explanation/compositions.mdx b/docs/explanation/compositions.mdx index 2a97a2b6..efc1de61 100644 --- a/docs/explanation/compositions.mdx +++ b/docs/explanation/compositions.mdx @@ -18,6 +18,7 @@ Compositions are built on [Smelter](https://smelter.dev), the source-available r - **Multi-party layouts**: arrange the cameras of a conference or livestream into grids, side-by-sides, or picture-in-picture. - **Branded streams**: overlay logos, captions, lower-thirds, and backgrounds on top of live video. - **Cross-protocol bridging**: take WebRTC inputs and republish the composed result over RTMP, for example broadcasting a conference to YouTube or Twitch, or the other way round. +- **Recordings**: capture the composed stream as an MP4 file to store, replay, or serve on demand. ## Core concepts @@ -46,6 +47,10 @@ curl -X DELETE "$COMPOSITION_URL/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. + ## Scenes Every video output carries a **scene**: a tree of components that describes how inputs, text, and images are arranged into the composed frame. Audio outputs carry an **audio scene** that describes which inputs are mixed together. @@ -109,4 +114,5 @@ Either you push those updates yourself, or you hand the job to a **template**: a - [Compositions tutorial](./../tutorials/compositions): create your first composition end to end. - [Write and deploy a template](./../how-to/compositions/write-and-deploy-a-template): build a React layout with the composition SDK. - [Compose a Fishjam room](./../how-to/compositions/compose-a-fishjam-room): turn a room's peers into one composed stream. +- [Record a composition](./../how-to/compositions/record-a-composition): save an output as an MP4. - [Composition API](./../api/reference#compositions): the full REST surface. diff --git a/docs/explanation/recordings.mdx b/docs/explanation/recordings.mdx new file mode 100644 index 00000000..cdaf4fe1 --- /dev/null +++ b/docs/explanation/recordings.mdx @@ -0,0 +1,57 @@ +--- +type: explanation +title: Recordings +sidebar_position: 4.6 +description: Understand recordings, the Fishjam resource that captures a composition's output into an MP4 file that outlives the session it came from. +--- + +# Recordings + +_Understanding how Fishjam turns live composed streams into files_ + +A **recording** captures what one of a [composition](./compositions)'s outputs publishes and turns it into an MP4 file. A composition is a live process: it produces media while it runs and leaves nothing behind when it is deleted. A recording is its durable counterpart, the file that remains. It outlives the composition, the room, and the livestream it came from, and it stays in Fishjam until you delete it. + +```text +composition ──output──▶ livestream / RTMP ──▶ [viewers] + │ + └──recording──▶ MP4 ──▶ [download] +``` + +## A recorder attached to an output + +A composition produces media through its outputs, so outputs are also the thing you record. When you create a recording, you name a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. From that moment the recorder captures exactly what the output plays out: the same layout, the same encoding, and every scene update along the way. There is no separate recording scene; if the file should look different from the live stream, register a dedicated output with its own scene and record that one instead. + +The one knob on the recorder itself is `scaleRatio`, which records at a fraction (or multiple) of the output's resolution, for example `0.5` to store an archive copy at half size. + +An output carries at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. + +## Two APIs, one feature + +Compositions live on the [Composition API](./../api/reference#compositions), but recordings are created and managed through the [Fishjam Server API](./../api/reference#server), directly or with the JS and Python server SDKs. The split follows ownership: a composition is a running session you configure, while a recording is a resource of your Fishjam app, stored alongside your rooms and livestreams and managed with the same management token. You never talk to the composition to record it; the Server API drives the capture inside the composition for you. + +## Lifecycle + +A recording moves through four statuses: + +| Status | Meaning | +| ----------- | -------------------------------------------------------- | +| `active` | Capturing the output right now. | +| `finished` | Capture has ended; the file is being prepared. | +| `available` | The MP4 is ready; `files` holds the download URL. | +| `failed` | Something went wrong along the way; no file will appear. | + +Capture starts the moment the recording is created. It ends when you stop the recording explicitly, or on its own when the recorded output ends or the composition is deleted, so tearing a session down finalizes its recordings too. Finalization is asynchronous: the recording stays `active` until the capture wraps up, then moves through `finished` to `available` once the file is ready. + +Every status change emits a `RecordingStatusChanged` [server notification](./../api/reference#protobufs) over your configured webhook or a websocket, so you can react to a file becoming available without polling. + +Once `available`, the recording's `files` list its media in playback order as direct HTTPS URLs you can download or hand straight to your users. The recording and its files persist until you delete the recording; deleting is the only way they go away, and an `active` recording cannot be deleted. + +## Metadata + +A recording carries optional free-form `metadata` that you set at creation and get back with every read. Because recordings accumulate as sessions come and go, metadata is also the handle for finding them later: listing recordings can filter by metadata pairs, for example everything recorded for a given show or customer. + +## Where to go next + +- [Record a composition](./../how-to/compositions/record-a-composition): start, stop, and download a recording step by step. +- [Compositions](./compositions): the sessions that recordings capture. +- [Server REST API](/api/rest): the complete request and response schemas. diff --git a/docs/how-to/compositions/compose-a-fishjam-room.mdx b/docs/how-to/compositions/compose-a-fishjam-room.mdx index e80d5003..1b94ea9e 100644 --- a/docs/how-to/compositions/compose-a-fishjam-room.mdx +++ b/docs/how-to/compositions/compose-a-fishjam-room.mdx @@ -173,7 +173,7 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/start" \ -H "Authorization: Bearer $TOKEN" ``` -Viewers can now watch the composed grid through the livestream's WHEP endpoint. +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 diff --git a/docs/how-to/compositions/inputs-and-outputs.mdx b/docs/how-to/compositions/inputs-and-outputs.mdx index a43e73b1..b5b81f28 100644 --- a/docs/how-to/compositions/inputs-and-outputs.mdx +++ b/docs/how-to/compositions/inputs-and-outputs.mdx @@ -69,6 +69,7 @@ Other output operations: - **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`. +- **Record** any output into an MP4 through the Fishjam Server API (see [Record a composition](./record-a-composition)). ## Update a scene diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx new file mode 100644 index 00000000..3e565378 --- /dev/null +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -0,0 +1,301 @@ +--- +type: how-to +sidebar_position: 5 +description: Record a composition's output through the Fishjam Server API and download the result as an MP4. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Record a composition + +A **recording** saves what one of a composition's outputs publishes into an MP4 file. You point it at a registered output, it captures exactly what that output plays out from that moment on, and once the capture ends Fishjam prepares the file and hands you a download URL. + +A recording is a standalone resource. It survives the composition, the room, and the livestream it came from, and it stays around until you delete it. Compositions live on the [Composition API](./../../api/reference#compositions), but recordings are managed through the [Fishjam Server API](./../../api/reference#server): you start one with a single call there, and Fishjam drives the capture inside the composition for you. The [Recordings](./../../explanation/recordings) article explains this model and the recording lifecycle in more depth. + +```text +composition ──output──▶ livestream / RTMP ──▶ [viewers] + │ + └──recording──▶ MP4 ──▶ [download] +``` + +## Prerequisites + +A running composition with a registered output, for example from the [tutorial](./../../tutorials/compositions) or [Compose a Fishjam room](./compose-a-fishjam-room). The steps below record the `main` output those guides register. + +```bash +export COMPOSITION_URL="https://rtc.fishjam.io" +export FISHJAM_URL="https://fishjam.io/api/v1/connect/" +export TOKEN="" +export COMPOSITION="" +``` + +Besides curl, every call 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: Start the recording + +Name the composition and the output to record 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 recording comes back with status `active`: capture has already started. Save its id from `data.id`: + +```bash +export RECORDING="" +``` + + + + +```ts +import { FishjamClient } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ + fishjamId: process.env.FISHJAM_ID!, + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +// ---cut--- +const recording = await fishjamClient.createRecording({ + source: { + compositionURL: "https://rtc.fishjam.io/api/composition/", + outputId: "main", + }, + metadata: { show: "weekly-standup" }, +}); +``` + +The recording comes back with status `active`: capture has already started. + + + + +```python +import os + +from fishjam import FishjamClient +from fishjam.recording import CompositionSource + +fishjam_client = FishjamClient( + fishjam_id=os.environ["FISHJAM_ID"], + management_token=os.environ["MANAGEMENT_TOKEN"], +) + +recording = fishjam_client.create_recording( + source=CompositionSource( + composition_url="https://rtc.fishjam.io/api/composition/", + output_id="main", + ), + metadata={"show": "weekly-standup"}, +) +``` + +The recording comes back with status `active`: capture has already started. + + + + +A few things to know at this point: + +- The recording mirrors the output it captures: the same layout, the same encoding, and every scene update the output plays out. To record at a different resolution than the live output, add `scaleRatio` to `source`: `0.5` records at half the output's resolution, values above `1` upscale. It defaults to `1`. +- An output carries at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. +- `metadata` is free-form and optional; it comes back with the recording and you can filter by it later. + +## Step 2: Follow the status + + + + +```bash +curl "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +```ts +import { FishjamClient, type Recording } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ + fishjamId: process.env.FISHJAM_ID!, + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +declare const recording: Recording; +// ---cut--- +const { status } = await fishjamClient.getRecording(recording.id); +``` + + + + +```python +status = fishjam_client.get_recording(recording.id).status +``` + + + + +A recording moves through four statuses: + +| Status | Meaning | +| ----------- | -------------------------------------------------------- | +| `active` | Capturing the output right now. | +| `finished` | Capture has ended; the file is being prepared. | +| `available` | The MP4 is ready; `files` holds the download URL. | +| `failed` | Something went wrong along the way; no file will appear. | + +Rather than polling, you can subscribe to [server notifications](./../../api/reference#protobufs): every change emits a `RecordingStatusChanged` message carrying the recording's id, its new status, and its metadata, over your configured webhook or a websocket. + +## Step 3: Stop the capture + + + + +```bash +curl -X POST "$FISHJAM_URL/recordings/$RECORDING/stop" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +```ts +import { FishjamClient, type Recording } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ + fishjamId: process.env.FISHJAM_ID!, + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +declare const recording: Recording; +// ---cut--- +await fishjamClient.stopRecording(recording.id); +``` + + + + +```python +fishjam_client.stop_recording(recording.id) +``` + + + + +Finalization is asynchronous: the recording stays `active` until the capture is wrapped up, then becomes `finished`. Stopping a recording that is no longer active is a no-op. + +You do not have to stop a recording explicitly. When the recorded output ends, or the composition itself is deleted, the capture finishes on its own, so ending a session by [deleting the composition](./../../explanation/compositions#cost-and-lifecycle) finalizes its recordings too. + +## Step 4: Download the MP4 + +Once the status reaches `available`, the recording's `files` list its media in playback order, as direct HTTPS URLs: + +```json +{ + "data": { + "id": "", + "status": "available", + "files": [{ "url": "https://media.fishjam.io/.../index.mp4" }], + "source": { "...": "..." }, + "metadata": { "show": "weekly-standup" } + } +} +``` + +Download the file or serve the URL to your users as it is; until then, `files` is empty. + +## Step 5: List your recordings + +`GET /recordings` lists every recording of your app, and takes filters on status and metadata. A metadata filter matches recordings whose metadata contains all the given pairs; values are compared as strings, so numbers and booleans in metadata cannot be matched this way. + + + + +```bash +curl -g "$FISHJAM_URL/recordings?status=available&metadata[show]=weekly-standup" \ + -H "Authorization: Bearer $TOKEN" +``` + +(`-g` stops curl from interpreting the square brackets itself.) + + + + +```ts +import { FishjamClient } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ + fishjamId: process.env.FISHJAM_ID!, + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +// ---cut--- +const recordings = await fishjamClient.getAllRecordings({ + show: "weekly-standup", +}); +``` + + + + +```python +recordings = fishjam_client.get_all_recordings({"show": "weekly-standup"}) +``` + + + + +The SDK methods filter by metadata; to filter by status as well, call the REST endpoint directly. + +## Step 6: Clean up + +Recordings persist independently of the composition until you delete them, file and all: + + + + +```bash +curl -X DELETE "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +```ts +import { FishjamClient, type Recording } from "@fishjam-cloud/js-server-sdk"; + +const fishjamClient = new FishjamClient({ + fishjamId: process.env.FISHJAM_ID!, + managementToken: process.env.MANAGEMENT_TOKEN!, +}); +declare const recording: Recording; +// ---cut--- +await fishjamClient.deleteRecording(recording.id); +``` + + + + +```python +fishjam_client.delete_recording(recording.id) +``` + + + + +An `active` recording cannot be deleted; stop it and delete once it leaves `active`. + +See the [Server REST API](/api/rest) reference for the complete request and response schemas. diff --git a/docs/tutorials/compositions.mdx b/docs/tutorials/compositions.mdx index 489e8861..d3f6cc48 100644 --- a/docs/tutorials/compositions.mdx +++ b/docs/tutorials/compositions.mdx @@ -233,3 +233,4 @@ Updating the scene by hand gets tedious once people are joining, leaving, muting - [Write and deploy a template](../how-to/compositions/write-and-deploy-a-template) to replace the static scene with a live React layout. - [Compose a Fishjam room](../how-to/compositions/compose-a-fishjam-room) to feed a whole room's peers in automatically. - [Choose inputs and outputs](../how-to/compositions/inputs-and-outputs) for other protocols like RTMP and WHEP. +- [Record a composition](../how-to/compositions/record-a-composition) to save the composed stream as an MP4. diff --git a/packages/js-server-sdk b/packages/js-server-sdk index 7a2c47c5..c83334b9 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit 7a2c47c51d7cc50c2a4c18fa0c327279e0b2550c +Subproject commit c83334b9d16d3a595427a53d5ddd6043d52a3841 From 3ca43bbe9872e03e67f045f3a9184365adcfa688 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Wed, 26 Aug 2026 15:25:56 +0200 Subject: [PATCH 2/5] commit the recording info about outputs --- docs/explanation/recordings.mdx | 2 +- .../compositions/record-a-composition.mdx | 53 +++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/docs/explanation/recordings.mdx b/docs/explanation/recordings.mdx index cdaf4fe1..011e00b6 100644 --- a/docs/explanation/recordings.mdx +++ b/docs/explanation/recordings.mdx @@ -19,7 +19,7 @@ composition ──output──▶ livestream / RTMP ──▶ [viewers] ## A recorder attached to an output -A composition produces media through its outputs, so outputs are also the thing you record. When you create a recording, you name a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. From that moment the recorder captures exactly what the output plays out: the same layout, the same encoding, and every scene update along the way. There is no separate recording scene; if the file should look different from the live stream, register a dedicated output with its own scene and record that one instead. +A composition produces media through its outputs, so outputs are also the thing you record. When you create a recording, you name a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. From that moment the recorder captures exactly what the output plays out: the same layout and resolution, and every scene update along the way, encoded separately from the live stream. There is no separate recording scene; if the file should look different from the live stream, register a dedicated output with its own scene and record that one instead. The one knob on the recorder itself is `scaleRatio`, which records at a fraction (or multiple) of the output's resolution, for example `0.5` to store an archive copy at half size. diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx index 3e565378..b2622fbc 100644 --- a/docs/how-to/compositions/record-a-composition.mdx +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -21,20 +21,53 @@ composition ──output──▶ livestream / RTMP ──▶ [viewers] ## Prerequisites -A running composition with a registered output, for example from the [tutorial](./../../tutorials/compositions) or [Compose a Fishjam room](./compose-a-fishjam-room). The steps below record the `main` output those guides register. +A running composition with its inputs registered, and a livestream for it to publish to along with the livestream's streamer token. The [tutorial](./../../tutorials/compositions) sets up all of that in its Steps 1 to 3; stop before its Step 4, which registers the same output as Step 1 below does. ```bash export COMPOSITION_URL="https://rtc.fishjam.io" export FISHJAM_URL="https://fishjam.io/api/v1/connect/" export TOKEN="" export COMPOSITION="" +export STREAMER_TOKEN="" ``` -Besides curl, every call 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 talks to the Composition API. Every recording call from Step 2 on 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: Start the recording +## Step 1: Register the output to record -Name the composition and the output to record in `source`: +A recording attaches to an output, so the composition first needs the output whose scene should end up in the file. Register it like any other output. An output always publishes to a destination as it plays out, so the output you record is also a live stream: here `main` publishes to your livestream over WHIP, with the tutorial's two inputs side by side. + +```bash +curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d @- < @@ -110,11 +143,11 @@ The recording comes back with status `active`: capture has already started. A few things to know at this point: -- The recording mirrors the output it captures: the same layout, the same encoding, and every scene update the output plays out. To record at a different resolution than the live output, add `scaleRatio` to `source`: `0.5` records at half the output's resolution, values above `1` upscale. It defaults to `1`. +- The recording mirrors the output it captures: the same layout and resolution, and every scene update the output plays out. The file is encoded separately from the live stream. To record at a different resolution than the live output, add `scaleRatio` to `source`: `0.5` records at half the output's resolution, values above `1` upscale. It defaults to `1`. - An output carries at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. - `metadata` is free-form and optional; it comes back with the recording and you can filter by it later. -## Step 2: Follow the status +## Step 3: Follow the status @@ -160,7 +193,7 @@ A recording moves through four statuses: Rather than polling, you can subscribe to [server notifications](./../../api/reference#protobufs): every change emits a `RecordingStatusChanged` message carrying the recording's id, its new status, and its metadata, over your configured webhook or a websocket. -## Step 3: Stop the capture +## Step 4: Stop the capture @@ -199,7 +232,7 @@ Finalization is asynchronous: the recording stays `active` until the capture is You do not have to stop a recording explicitly. When the recorded output ends, or the composition itself is deleted, the capture finishes on its own, so ending a session by [deleting the composition](./../../explanation/compositions#cost-and-lifecycle) finalizes its recordings too. -## Step 4: Download the MP4 +## Step 5: Download the MP4 Once the status reaches `available`, the recording's `files` list its media in playback order, as direct HTTPS URLs: @@ -217,7 +250,7 @@ Once the status reaches `available`, the recording's `files` list its media in p Download the file or serve the URL to your users as it is; until then, `files` is empty. -## Step 5: List your recordings +## Step 6: List your recordings `GET /recordings` lists every recording of your app, and takes filters on status and metadata. A metadata filter matches recordings whose metadata contains all the given pairs; values are compared as strings, so numbers and booleans in metadata cannot be matched this way. @@ -259,7 +292,7 @@ recordings = fishjam_client.get_all_recordings({"show": "weekly-standup"}) The SDK methods filter by metadata; to filter by status as well, call the REST endpoint directly. -## Step 6: Clean up +## Step 7: Clean up Recordings persist independently of the composition until you delete them, file and all: From 7eef28b16ed469ae15a9a51d3f7b54252285543b Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Wed, 26 Aug 2026 16:27:52 +0200 Subject: [PATCH 3/5] fix wording --- docs/explanation/recordings.mdx | 42 ++++++------ .../compositions/record-a-composition.mdx | 66 +++++++++---------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/explanation/recordings.mdx b/docs/explanation/recordings.mdx index 011e00b6..be12e601 100644 --- a/docs/explanation/recordings.mdx +++ b/docs/explanation/recordings.mdx @@ -2,53 +2,53 @@ type: explanation title: Recordings sidebar_position: 4.6 -description: Understand recordings, the Fishjam resource that captures a composition's output into an MP4 file that outlives the session it came from. +description: Understand recordings, the Fishjam resource that captures a composition's output into an MP4 file that persists after the composition ends. --- # Recordings -_Understanding how Fishjam turns live composed streams into files_ +_Understanding how Fishjam captures composed streams as files_ -A **recording** captures what one of a [composition](./compositions)'s outputs publishes and turns it into an MP4 file. A composition is a live process: it produces media while it runs and leaves nothing behind when it is deleted. A recording is its durable counterpart, the file that remains. It outlives the composition, the room, and the livestream it came from, and it stays in Fishjam until you delete it. +A **recording** captures the media published by one of a [composition](./compositions)'s outputs and stores it as an MP4 file. A composition is a live process: it produces media while it runs and leaves nothing behind once it is deleted. A recording is the persistent artifact of that process. It remains available after the composition, the room, and the livestream it was created from are gone, and it is stored in Fishjam until you delete it. ```text composition ──output──▶ livestream / RTMP ──▶ [viewers] - │ - └──recording──▶ MP4 ──▶ [download] + │ + └──recording──▶ MP4 ──▶ [download] ``` -## A recorder attached to an output +## Recorders and outputs -A composition produces media through its outputs, so outputs are also the thing you record. When you create a recording, you name a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. From that moment the recorder captures exactly what the output plays out: the same layout and resolution, and every scene update along the way, encoded separately from the live stream. There is no separate recording scene; if the file should look different from the live stream, register a dedicated output with its own scene and record that one instead. +A composition produces media through its outputs, and outputs are the unit that is recorded. To create a recording, you specify a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. The recorder captures the output as it is published: the same layout and resolution, including every scene update, encoded separately from the live stream. There is no separate recording scene. If the recorded file should differ from the live stream, register a dedicated output with its own scene and record that output instead. -The one knob on the recorder itself is `scaleRatio`, which records at a fraction (or multiple) of the output's resolution, for example `0.5` to store an archive copy at half size. +The recorder accepts a single option, `scaleRatio`, which sets the recording resolution as a fraction or multiple of the output's resolution. For example, `0.5` stores the recording at half the output resolution. -An output carries at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. +An output can have at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. -## Two APIs, one feature +## Composition API and Server API -Compositions live on the [Composition API](./../api/reference#compositions), but recordings are created and managed through the [Fishjam Server API](./../api/reference#server), directly or with the JS and Python server SDKs. The split follows ownership: a composition is a running session you configure, while a recording is a resource of your Fishjam app, stored alongside your rooms and livestreams and managed with the same management token. You never talk to the composition to record it; the Server API drives the capture inside the composition for you. +Compositions are managed through the [Composition API](./../api/reference#compositions), while recordings are created and managed through the [Fishjam Server API](./../api/reference#server), either directly or with the JS and Python server SDKs. This split reflects ownership: a composition is a running session that you configure, whereas a recording is a resource of your Fishjam app, stored alongside your rooms and livestreams and managed with the same management token. You do not interact with the composition to record it; the Server API controls the capture inside the composition on your behalf. ## Lifecycle -A recording moves through four statuses: +A recording has four statuses: -| Status | Meaning | -| ----------- | -------------------------------------------------------- | -| `active` | Capturing the output right now. | -| `finished` | Capture has ended; the file is being prepared. | -| `available` | The MP4 is ready; `files` holds the download URL. | -| `failed` | Something went wrong along the way; no file will appear. | +| Status | Meaning | +| ----------- | ------------------------------------------------------- | +| `active` | The output is being captured. | +| `finished` | Capture has ended and the file is being prepared. | +| `available` | The MP4 is ready and `files` contains the download URL. | +| `failed` | An error occurred and no file will be produced. | -Capture starts the moment the recording is created. It ends when you stop the recording explicitly, or on its own when the recorded output ends or the composition is deleted, so tearing a session down finalizes its recordings too. Finalization is asynchronous: the recording stays `active` until the capture wraps up, then moves through `finished` to `available` once the file is ready. +Capture starts as soon as the recording is created. It ends when you stop the recording explicitly, or automatically when the recorded output ends or the composition is deleted, so deleting a composition also finalizes its recordings. Finalization is asynchronous: the recording remains `active` until capture has ended, then transitions through `finished` to `available` once the file is ready. Every status change emits a `RecordingStatusChanged` [server notification](./../api/reference#protobufs) over your configured webhook or a websocket, so you can react to a file becoming available without polling. -Once `available`, the recording's `files` list its media in playback order as direct HTTPS URLs you can download or hand straight to your users. The recording and its files persist until you delete the recording; deleting is the only way they go away, and an `active` recording cannot be deleted. +Once a recording is `available`, its `files` field lists the media files in playback order as direct HTTPS URLs that you can download or serve to your users. The recording and its files persist until you delete the recording. Deleting the recording is the only way to remove them, and a recording cannot be deleted while it is `active`. ## Metadata -A recording carries optional free-form `metadata` that you set at creation and get back with every read. Because recordings accumulate as sessions come and go, metadata is also the handle for finding them later: listing recordings can filter by metadata pairs, for example everything recorded for a given show or customer. +A recording carries optional free-form `metadata`, set at creation and returned with every read. Because recordings accumulate over time, metadata also serves as the primary way to locate them: listing recordings supports filtering by metadata pairs, for example to retrieve every recording for a given show or customer. ## Where to go next diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx index b2622fbc..4ac754bf 100644 --- a/docs/how-to/compositions/record-a-composition.mdx +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -9,9 +9,9 @@ import TabItem from "@theme/TabItem"; # Record a composition -A **recording** saves what one of a composition's outputs publishes into an MP4 file. You point it at a registered output, it captures exactly what that output plays out from that moment on, and once the capture ends Fishjam prepares the file and hands you a download URL. +A **recording** captures the media published by one of a composition's outputs and stores it as an MP4 file. You attach it to a registered output, it captures that output from the moment it is created, and once capture ends Fishjam prepares the file and returns a download URL. -A recording is a standalone resource. It survives the composition, the room, and the livestream it came from, and it stays around until you delete it. Compositions live on the [Composition API](./../../api/reference#compositions), but recordings are managed through the [Fishjam Server API](./../../api/reference#server): you start one with a single call there, and Fishjam drives the capture inside the composition for you. The [Recordings](./../../explanation/recordings) article explains this model and the recording lifecycle in more depth. +A recording is a standalone resource. It remains available after the composition, the room, and the livestream it was created from are gone, and it is stored until you delete it. Compositions are managed through the [Composition API](./../../api/reference#compositions), while recordings are managed through the [Fishjam Server API](./../../api/reference#server): a single call there starts the recording, and Fishjam controls the capture inside the composition on your behalf. The [Recordings](./../../explanation/recordings) article explains this model and the recording lifecycle in more detail. ```text composition ──output──▶ livestream / RTMP ──▶ [viewers] @@ -21,7 +21,7 @@ composition ──output──▶ livestream / RTMP ──▶ [viewers] ## Prerequisites -A running composition with its inputs registered, and a livestream for it to publish to along with the livestream's streamer token. The [tutorial](./../../tutorials/compositions) sets up all of that in its Steps 1 to 3; stop before its Step 4, which registers the same output as Step 1 below does. +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" @@ -31,11 +31,11 @@ export COMPOSITION="" export STREAMER_TOKEN="" ``` -Step 1 talks to the Composition API. Every recording call from Step 2 on 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 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: Register the output to record -A recording attaches to an output, so the composition first needs the output whose scene should end up in the file. Register it like any other output. An output always publishes to a destination as it plays out, so the output you record is also a live stream: here `main` publishes to your livestream over WHIP, with the tutorial's two inputs side by side. +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. ```bash curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register" \ @@ -63,11 +63,11 @@ curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register EOF ``` -The recording will carry exactly this scene, and every update you send to `main` afterwards. If your composition already has the output you want to record, for example the templated one from [Compose a Fishjam room](./compose-a-fishjam-room), skip ahead. See [Choose inputs and outputs](./inputs-and-outputs) for the other output types. +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 -Name the composition and the output from Step 1 in `source`: +Specify the composition and the output from Step 1 in `source`: @@ -85,7 +85,7 @@ curl -X POST "$FISHJAM_URL/recordings" \ }" ``` -The recording comes back with status `active`: capture has already started. Save its id from `data.id`: +The response returns the recording with status `active`; capture has already started. Save its id from `data.id`: ```bash export RECORDING="" @@ -111,7 +111,7 @@ const recording = await fishjamClient.createRecording({ }); ``` -The recording comes back with status `active`: capture has already started. +The response returns the recording with status `active`; capture has already started. @@ -136,18 +136,18 @@ recording = fishjam_client.create_recording( ) ``` -The recording comes back with status `active`: capture has already started. +The response returns the recording with status `active`; capture has already started. -A few things to know at this point: +Note the following: -- The recording mirrors the output it captures: the same layout and resolution, and every scene update the output plays out. The file is encoded separately from the live stream. To record at a different resolution than the live output, add `scaleRatio` to `source`: `0.5` records at half the output's resolution, values above `1` upscale. It defaults to `1`. -- An output carries at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. -- `metadata` is free-form and optional; it comes back with the recording and you can filter by it later. +- The recording mirrors the output it captures: the same layout and resolution, including every scene update. The file is encoded separately from the live stream. To record at a different resolution than the live output, add `scaleRatio` to `source`: `0.5` records at half the output's resolution, and values above `1` upscale. The default is `1`. +- An output can have at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. +- `metadata` is optional and free-form. It is returned with the recording and can be used to filter recordings later. -## Step 3: Follow the status +## Step 3: Check the status @@ -182,18 +182,18 @@ status = fishjam_client.get_recording(recording.id).status -A recording moves through four statuses: +A recording has four statuses: -| Status | Meaning | -| ----------- | -------------------------------------------------------- | -| `active` | Capturing the output right now. | -| `finished` | Capture has ended; the file is being prepared. | -| `available` | The MP4 is ready; `files` holds the download URL. | -| `failed` | Something went wrong along the way; no file will appear. | +| Status | Meaning | +| ----------- | ------------------------------------------------------- | +| `active` | The output is being captured. | +| `finished` | Capture has ended and the file is being prepared. | +| `available` | The MP4 is ready and `files` contains the download URL. | +| `failed` | An error occurred and no file will be produced. | -Rather than polling, you can subscribe to [server notifications](./../../api/reference#protobufs): every change emits a `RecordingStatusChanged` message carrying the recording's id, its new status, and its metadata, over your configured webhook or a websocket. +Instead of polling, you can subscribe to [server notifications](./../../api/reference#protobufs): every status change emits a `RecordingStatusChanged` message containing the recording's id, its new status, and its metadata, delivered over your configured webhook or a websocket. -## Step 4: Stop the capture +## Step 4: Stop the recording @@ -228,13 +228,13 @@ fishjam_client.stop_recording(recording.id) -Finalization is asynchronous: the recording stays `active` until the capture is wrapped up, then becomes `finished`. Stopping a recording that is no longer active is a no-op. +Finalization is asynchronous: the recording remains `active` until capture has ended, then transitions to `finished`. Stopping a recording that is no longer active has no effect. -You do not have to stop a recording explicitly. When the recorded output ends, or the composition itself is deleted, the capture finishes on its own, so ending a session by [deleting the composition](./../../explanation/compositions#cost-and-lifecycle) finalizes its recordings too. +Stopping a recording explicitly is optional. Capture ends automatically when the recorded output ends or the composition is deleted, so [deleting the composition](./../../explanation/compositions#cost-and-lifecycle) also finalizes its recordings. ## Step 5: Download the MP4 -Once the status reaches `available`, the recording's `files` list its media in playback order, as direct HTTPS URLs: +Once the status is `available`, the recording's `files` field lists its media files in playback order as direct HTTPS URLs: ```json { @@ -248,11 +248,11 @@ Once the status reaches `available`, the recording's `files` list its media in p } ``` -Download the file or serve the URL to your users as it is; until then, `files` is empty. +You can download the file or serve the URL to your users directly. Until the recording is `available`, `files` is empty. ## Step 6: List your recordings -`GET /recordings` lists every recording of your app, and takes filters on status and metadata. A metadata filter matches recordings whose metadata contains all the given pairs; values are compared as strings, so numbers and booleans in metadata cannot be matched this way. +`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. @@ -262,7 +262,7 @@ curl -g "$FISHJAM_URL/recordings?status=available&metadata[show]=weekly-standup" -H "Authorization: Bearer $TOKEN" ``` -(`-g` stops curl from interpreting the square brackets itself.) +The `-g` flag prevents curl from interpreting the square brackets. @@ -290,11 +290,11 @@ recordings = fishjam_client.get_all_recordings({"show": "weekly-standup"}) -The SDK methods filter by metadata; to filter by status as well, call the REST endpoint directly. +The SDK methods filter by metadata only. To filter by status as well, call the REST endpoint directly. ## Step 7: Clean up -Recordings persist independently of the composition until you delete them, file and all: +Recordings persist independently of the composition until you delete them. Deleting a recording also removes its files: @@ -329,6 +329,6 @@ fishjam_client.delete_recording(recording.id) -An `active` recording cannot be deleted; stop it and delete once it leaves `active`. +An `active` recording cannot be deleted. Stop it first and delete it once its status is no longer `active`. See the [Server REST API](/api/rest) reference for the complete request and response schemas. From 3d943610bcdc349d7978e70e01649ef413b8a728 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Wed, 26 Aug 2026 16:39:11 +0200 Subject: [PATCH 4/5] add simple fix --- docs/explanation/recordings.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanation/recordings.mdx b/docs/explanation/recordings.mdx index be12e601..1bbbc36a 100644 --- a/docs/explanation/recordings.mdx +++ b/docs/explanation/recordings.mdx @@ -21,7 +21,7 @@ composition ──output──▶ livestream / RTMP ──▶ [viewers] A composition produces media through its outputs, and outputs are the unit that is recorded. To create a recording, you specify a composition and one of its registered outputs, and Fishjam attaches a recorder to that output. The recorder captures the output as it is published: the same layout and resolution, including every scene update, encoded separately from the live stream. There is no separate recording scene. If the recorded file should differ from the live stream, register a dedicated output with its own scene and record that output instead. -The recorder accepts a single option, `scaleRatio`, which sets the recording resolution as a fraction or multiple of the output's resolution. For example, `0.5` stores the recording at half the output resolution. +Besides composition URL and output ID, the recording API accepts a single option, `scaleRatio`, which sets the recording resolution as a fraction or multiple of the output's resolution. For example, `0.5` stores the recording at half the output resolution. An output can have at most one recording at a time. To record the same output again, wait until the current recording is no longer `active`. From bb290031ec5f448622782b2902a26d8aa3caf86a Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Wed, 26 Aug 2026 16:40:18 +0200 Subject: [PATCH 5/5] move arrow to output --- docs/how-to/compositions/record-a-composition.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx index 4ac754bf..f812aec4 100644 --- a/docs/how-to/compositions/record-a-composition.mdx +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -15,8 +15,8 @@ A recording is a standalone resource. It remains available after the composition ```text composition ──output──▶ livestream / RTMP ──▶ [viewers] - │ - └──recording──▶ MP4 ──▶ [download] + │ + └──recording──▶ MP4 ──▶ [download] ``` ## Prerequisites