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..1bbbc36a --- /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 persists after the composition ends. +--- + +# Recordings + +_Understanding how Fishjam captures composed streams as files_ + +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] +``` + +## Recorders and outputs + +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. + +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`. + +## Composition API and Server API + +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 has four statuses: + +| 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 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 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`, 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 + +- [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..f812aec4 --- /dev/null +++ b/docs/how-to/compositions/record-a-composition.mdx @@ -0,0 +1,334 @@ +--- +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** 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 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] + │ + └──recording──▶ MP4 ──▶ [download] +``` + +## Prerequisites + +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: 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. + +```bash +curl -X POST "$COMPOSITION_URL/api/composition/$COMPOSITION/output/main/register" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d @- < + + +```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 +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 response returns the recording 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 response returns the recording with status `active`; capture has already started. + + + + +Note the following: + +- 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: Check 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 has four statuses: + +| 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. | + +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 recording + + + + +```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 remains `active` until capture has ended, then transitions to `finished`. Stopping a recording that is no longer active has no effect. + +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 is `available`, the recording's `files` field lists its media files 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" } + } +} +``` + +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 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 +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 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. Deleting a recording also removes its files: + + + + +```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 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. 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