diff --git a/api/composition b/api/composition index 49763593..bd943a33 160000 --- a/api/composition +++ b/api/composition @@ -1 +1 @@ -Subproject commit 49763593aaa0669062c5e2aac417770f5e59e29f +Subproject commit bd943a33df4eb5fab15ca7e34bd987295dee9d73 diff --git a/api/fishjam-server b/api/fishjam-server index 339220ff..85a97f28 160000 --- a/api/fishjam-server +++ b/api/fishjam-server @@ -1 +1 @@ -Subproject commit 339220ff517c5be2e1e54fce8d1b415b59a5f67c +Subproject commit 85a97f289db4a5a34e5271228f74a0e25f3211f3 diff --git a/api/protos b/api/protos index 85ccd2bb..da96ef19 160000 --- a/api/protos +++ b/api/protos @@ -1 +1 @@ -Subproject commit 85ccd2bbb82ed5a5528c2dba836b05e64f62424f +Subproject commit da96ef19f67cefd9ebb818d5f9b8c4c5bece74fa diff --git a/api/room-manager b/api/room-manager index 669f4aa1..4b523370 160000 --- a/api/room-manager +++ b/api/room-manager @@ -1 +1 @@ -Subproject commit 669f4aa1b223bca7e257533bd0233751319d8c23 +Subproject commit 4b5233708a3c4e5d635c8786420129844eb7030a diff --git a/docs/api/reference.md b/docs/api/reference.md index a52d50a5..4cdcf079 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, peers, and [recordings](../how-to/compositions/record-a-composition), and +Fishjam Server provides a REST API for managing rooms, peers, and [recordings](../how-to/recordings/record-with-a-template), 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 65987cae..e8578a62 100644 --- a/docs/explanation/compositions.mdx +++ b/docs/explanation/compositions.mdx @@ -21,7 +21,7 @@ Compositions are built on [Smelter](https://smelter.dev), the source-available r - **Multi-party layouts**: arrange the cameras of a conference room, or several independent streams, 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. +- **Recordings**: render a template, or capture a composed stream you already publish, into an MP4 file to store, replay, or serve on demand. ## Core concepts @@ -85,7 +85,7 @@ curl -X DELETE "https://rtc.fishjam.io/api/composition/$COMPOSITION" \ ## 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. +A **recording** saves media rendered in a composition into an MP4 file that stays around after the composition is gone. A template recording renders its own scene from a React template, with no output or live stream involved; a composition recording mirrors an output the composition already publishes. 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 with a template](./../how-to/recordings/record-with-a-template) and [Record a composition output](./../how-to/recordings/record-a-composition-output) walk through making one. ## Scenes @@ -150,5 +150,6 @@ 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. +- [Record with a template](./../how-to/recordings/record-with-a-template): render a template straight into an MP4. +- [Record a composition output](./../how-to/recordings/record-a-composition-output): save an output you already publish as an MP4. - [Composition API](./../api/reference#compositions): the full REST surface. diff --git a/docs/explanation/recordings.mdx b/docs/explanation/recordings.mdx index 1bbbc36a..a8c7ffad 100644 --- a/docs/explanation/recordings.mdx +++ b/docs/explanation/recordings.mdx @@ -2,32 +2,58 @@ 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. +description: Understand recordings, the Fishjam resource that captures media rendered in a composition into an MP4 file that persists after the composition ends. --- # Recordings -_Understanding how Fishjam captures composed streams as files_ +_Understanding how Fishjam captures composed media 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. +A **recording** captures media rendered inside a [composition](./compositions) 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, and any room forwarded into it, are gone, and Fishjam keeps it for [3 months](#retention). -```text -composition ──output──▶ livestream / RTMP ──▶ [viewers] - │ - └──recording──▶ MP4 ──▶ [download] +What a recording captures is set by its **source**. There are two kinds: + +- A **template recording** renders its own scene from a React template. It is not bound to any output or live stream, and the recording is the only thing it produces. This is the default way to record. +- A **composition recording** mirrors an output that the composition already publishes, such as the one feeding a livestream. Use it to keep a copy of a live stream you are already running. + +## Template recordings + +```mermaid +flowchart LR + Inputs[inputs] --> Composition[composition] + Composition -- template --> Recording[recording] + Recording --> MP4[MP4] + MP4 --> Download[download] ``` -## Recorders and outputs +A template recording still runs inside a composition. The composition is the workspace that holds your inputs, whether you register them yourself or forward a Fishjam room into it. The template decides how those inputs are rendered into the file, and nothing is published live: no output is registered and no livestream is needed. + +The template is the same kind of React component as the [template of an output](./../how-to/compositions/write-and-deploy-a-template). You upload its bundle, at most 1 MiB, together with the request that creates the recording. + +Besides the composition URL, a template recording accepts two options: `resolution`, the size the template is rendered at (1280x720 by default), and `audio`, which can be set to `false` for a video-only file. A composition can run several template recordings at once, each with its own template. -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. +## Composition recordings -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. +```mermaid +flowchart LR + Inputs[inputs] --> Composition[composition] + Composition --> Output[output] + Output --> Livestream[livestream / RTMP] + Livestream --> Viewers[viewers] + Output --> Recording[recording] + Recording --> MP4[MP4] + MP4 --> Download[download] +``` + +A composition produces media through its outputs, and a composition recording captures one of them. To create one, 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, including every scene update, at the output's resolution unless `scaleRatio` changes it, encoded separately from the live stream. If the recorded file should differ from the live stream, use a template recording instead. + +Besides composition URL and output ID, a composition recording 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. +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 call the Composition API to record; the Server API controls the capture inside the composition on your behalf, including loading the template of a template recording. ## Lifecycle @@ -35,16 +61,20 @@ A recording has four statuses: | Status | Meaning | | ----------- | ------------------------------------------------------- | -| `active` | The output is being captured. | +| `active` | Media 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. +Capture starts as soon as the recording is created. It ends when you stop the recording explicitly, or automatically when its composition ends: when you delete the composition, or when it [cleans itself up](./compositions#cost-and-lifecycle) after five minutes without input media. A composition recording also ends when the recorded output ends. 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`. +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. To remove a recording and its files before they expire, delete the recording. A recording cannot be deleted while it is `active`. + +## Retention + +Fishjam keeps a recording and its files for 3 months after the recording is created. After that they are no longer available through the API, so download any file you need to keep for longer. For up to 6 months after creation, an expired recording can still be retrieved on request: contact us at `contact@fishjam.io`. ## Metadata @@ -52,6 +82,8 @@ A recording carries optional free-form `metadata`, set at creation and returned ## 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. +- [Record with a template](./../how-to/recordings/record-with-a-template): render a template straight into an MP4. +- [Record a composition output](./../how-to/recordings/record-a-composition-output): mirror an output you already publish. +- [Manage recordings](./../how-to/recordings/manage-recordings): stop, download, list, and delete recordings. +- [Compositions](./compositions): the sessions recordings run in. - [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 6fd95e97..330211c0 100644 --- a/docs/how-to/compositions/compose-a-fishjam-room.mdx +++ b/docs/how-to/compositions/compose-a-fishjam-room.mdx @@ -421,7 +421,7 @@ curl -X POST "https://rtc.fishjam.io/api/composition/$COMPOSITION/start" \ -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). +Viewers can now watch the composed grid through the livestream's WHEP endpoint. If you'd like to keep an MP4 recording of the room without streaming it live, simply [record it with the same template](./../recordings/record-with-a-template). ## 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 42eed74a..43672aae 100644 --- a/docs/how-to/compositions/inputs-and-outputs.mdx +++ b/docs/how-to/compositions/inputs-and-outputs.mdx @@ -72,7 +72,7 @@ Other output operations: - **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)). +- **Record** any output into an MP4 through the Fishjam Server API (see [Record a composition output](./../recordings/record-a-composition-output)). ## Update a scene diff --git a/docs/how-to/compositions/record-a-composition.mdx b/docs/how-to/compositions/record-a-composition.mdx deleted file mode 100644 index c3df1679..00000000 --- a/docs/how-to/compositions/record-a-composition.mdx +++ /dev/null @@ -1,423 +0,0 @@ ---- -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 FISHJAM_URL="https://fishjam.io/api/v1/connect/" -export TOKEN="" -export COMPOSITION="" -export STREAMER_TOKEN="" -``` - -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 "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 - -Specify the composition and the output from Step 1 in `source`: - - - - -```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. - - - - -```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="" -``` - - - - -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 - - - - -```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 -``` - - - - -```bash -curl "$FISHJAM_URL/recordings/$RECORDING" \ - -H "Authorization: Bearer $TOKEN" -``` - - - - -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 - - - - -```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) -``` - - - - -```bash -curl -X POST "$FISHJAM_URL/recordings/$RECORDING/stop" \ - -H "Authorization: Bearer $TOKEN" -``` - - - - -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. - - - - -```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"}) -``` - - - - -```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. - - - - -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: - - - - -```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) -``` - - - - -```bash -curl -X DELETE "$FISHJAM_URL/recordings/$RECORDING" \ - -H "Authorization: Bearer $TOKEN" -``` - - - - -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/how-to/recordings/_category_.json b/docs/how-to/recordings/_category_.json new file mode 100644 index 00000000..e928a9e1 --- /dev/null +++ b/docs/how-to/recordings/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Recordings", + "position": 6, + "link": { + "type": "generated-index", + "description": "Record a template or a composition output into an MP4, then download and manage the files." + } +} diff --git a/docs/how-to/recordings/manage-recordings.mdx b/docs/how-to/recordings/manage-recordings.mdx new file mode 100644 index 00000000..42273062 --- /dev/null +++ b/docs/how-to/recordings/manage-recordings.mdx @@ -0,0 +1,206 @@ +--- +type: how-to +sidebar_position: 3 +description: Check a recording's status, stop it, download the MP4, list recordings by metadata, and delete them. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Manage recordings + +Once a recording exists, you manage it through the [Fishjam Server API](./../../api/reference#server) the same way, whether it was created [with a template](./record-with-a-template) or [from a composition output](./record-a-composition-output). Every call below is a method on the `FishjamClient` of the [JS and Python server SDKs](./../backend/server-setup). + +```bash +export FISHJAM_URL="https://fishjam.io/api/v1/connect/" +export TOKEN="" +export RECORDING="" +``` + +## Check the status + + + + +```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 +``` + + + + +```bash +curl "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +A recording has four statuses: + +| Status | Meaning | +| ----------- | ------------------------------------------------------- | +| `active` | Media 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. + +## Stop the recording + + + + +```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) +``` + + + + +```bash +curl -X POST "$FISHJAM_URL/recordings/$RECORDING/stop" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +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 also ends when the composition ends, so [deleting the composition](./../../explanation/compositions#cost-and-lifecycle) finalizes its recordings. A recording of a composition output also ends when that output ends. + +## 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. + +Fishjam keeps recordings for 3 months after they are created, so download any file you need for longer. See [Retention](./../../explanation/recordings#retention). + +## 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. + + + + +```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"}) +``` + + + + +```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. + + + + +The SDK methods filter by metadata only. To filter by status as well, call the REST endpoint directly. + +## Delete a recording + +Recordings persist independently of the composition for 3 months. To remove one sooner, delete it. Deleting a recording also removes its files: + + + + +```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) +``` + + + + +```bash +curl -X DELETE "$FISHJAM_URL/recordings/$RECORDING" \ + -H "Authorization: Bearer $TOKEN" +``` + + + + +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/how-to/recordings/record-a-composition-output.mdx b/docs/how-to/recordings/record-a-composition-output.mdx new file mode 100644 index 00000000..37bbab09 --- /dev/null +++ b/docs/how-to/recordings/record-a-composition-output.mdx @@ -0,0 +1,244 @@ +--- +type: how-to +sidebar_position: 2 +description: Record an output a composition already publishes, mirroring its live stream into an MP4. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Record a composition output + +A **composition recording** mirrors an output that a composition already publishes: it captures that output's media from the moment it is created and stores it as an MP4 file. Use it to keep a copy of a live stream you are already running. To record something that is not published live, or to record a layout different from the live stream, [record with a template](./record-with-a-template) instead. + +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. + +```mermaid +flowchart LR + Composition[composition] --> Output[output] + Output --> Livestream[livestream / RTMP] + Livestream --> Viewers[viewers] + Output -- recording --> MP4[MP4] + MP4 --> Download[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 FISHJAM_URL="https://fishjam.io/api/v1/connect/" +export TOKEN="" +export COMPOSITION="" +export STREAMER_TOKEN="" +``` + +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 "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](./../compositions/compose-a-fishjam-room), skip to Step 2. See [Choose inputs and outputs](./../compositions/inputs-and-outputs) for the other output types. + +## Step 2: Start the recording + +Specify the composition and the output from Step 1 in `source`: + + + + +```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.createCompositionRecording({ + 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_composition_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. + + + + +```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="" +``` + + + + +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: Stop and download the recording + +Stopping, checking the status, downloading the MP4, and deleting the recording work the same for every recording. Continue with [Manage recordings](./manage-recordings). diff --git a/docs/how-to/recordings/record-with-a-template.mdx b/docs/how-to/recordings/record-with-a-template.mdx new file mode 100644 index 00000000..d2a8de3b --- /dev/null +++ b/docs/how-to/recordings/record-with-a-template.mdx @@ -0,0 +1,178 @@ +--- +type: how-to +sidebar_position: 1 +description: Render a React template straight into an MP4 recording, without registering an output or publishing a live stream. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Record with a template + +A **template recording** renders a React [template](./../compositions/write-and-deploy-a-template) directly into an MP4 file. The template describes what the recording shows, and the recording is the only thing it produces: there is no output to register and no live stream to publish to. + +```mermaid +flowchart LR + Inputs[inputs] --> Composition[composition] + Composition -- template --> Recording[recording] + Recording --> MP4[MP4] + MP4 --> Download[download] +``` + +The template runs inside a composition, which holds the inputs it renders. The [Recordings](./../../explanation/recordings) article explains how template recordings relate to compositions and to recordings of an existing output. + +## Prerequisites + +- A running composition with inputs. Either register inputs yourself, as in [Steps 1 and 2 of the compositions tutorial](./../../tutorials/compositions#step-1-create-a-composition), or forward a Fishjam room into it and start the composition, as in [Steps 4 and 5 of Compose a Fishjam room](./../compositions/compose-a-fishjam-room#step-4-forward-the-room-into-the-composition). A composition created with `autostart: false` renders nothing until it is started. You do not need a livestream or any other output. +- A template project scaffolded with the composition CLI (see [Write and deploy a template](./../compositions/write-and-deploy-a-template#scaffold-a-project)). + +```bash +export FISHJAM_URL="https://fishjam.io/api/v1/connect/" +export TOKEN="" +export COMPOSITION="" +``` + +Every recording call is a method on the `FishjamClient` of the [JS and Python server SDKs](./../backend/server-setup), shown in the language tabs below. + +## Step 1: Write and bundle the template + +A recording template is written exactly like the template of an output. This one tiles every input that is playing, so the recording follows inputs as they come and go: + +```tsx +// @jsx: react-jsx +// ---cut-before--- +import { + InputStream, + Rescaler, + Tiles, + View, + useInputStreams, +} from "@swmansion/smelter"; + +export default function App() { + const inputs = Object.values(useInputStreams()); + const playing = inputs.filter((input) => input.videoState === "playing"); + + return ( + + + {playing.map((input) => ( + + + + ))} + + + ); +} +``` + +The template decides everything the recording shows. It can use the room hooks from `@fishjam-cloud/composition`, such as `usePeers()`, to render a forwarded room (see [Compose a Fishjam room](./../compositions/compose-a-fishjam-room#step-2-write-a-room-aware-template)), and the `eventBus` to react to [events you send to the composition](./../compositions/drive-a-template-with-events). + +Build the bundle: + +```bash npm2yarn +npm run build +``` + +The bundle, `dist/App.js`, can weigh at most 1 MiB. + +## Step 2: Start the recording + +Pass the composition in `source` and the bundle alongside it: + + + + +```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.createTemplateRecording( + { + source: { + compositionURL: "https://rtc.fishjam.io/api/composition/", + resolution: { width: 1280, height: 720 }, + }, + metadata: { show: "weekly-standup" }, + }, + "dist/App.js", +); +``` + +The template is either a path to the bundle or a `Blob` with its contents. The response returns the recording with status `active`; capture has already started. + + + + +```python +import os + +from fishjam import FishjamClient +from fishjam.recording import TemplateSource, TemplateSourceResolution + +fishjam_client = FishjamClient( + fishjam_id=os.environ["FISHJAM_ID"], + management_token=os.environ["MANAGEMENT_TOKEN"], +) + +recording = fishjam_client.create_template_recording( + source=TemplateSource( + composition_url="https://rtc.fishjam.io/api/composition/", + resolution=TemplateSourceResolution(width=1280, height=720), + ), + template="dist/App.js", + metadata={"show": "weekly-standup"}, +) +``` + +The template is either a path to the bundle or its contents as `bytes`. The response returns the recording with status `active`; capture has already started. + + + + +A template recording is created with a `multipart/form-data` request: the configuration in a `config` part and the bundle in a `template` part. + +```bash +CONFIG=$(cat < + + +Note the following: + +- `resolution` sets the size the template is rendered at. It defaults to 1280x720, has to be even on both sides, and can be at most 3840x2160. +- `audio` controls whether the recording captures audio. It defaults to `true`; set it to `false` for a video-only file. +- A composition can run several template recordings at once, each with its own template. +- `metadata` is optional and free-form. It is returned with the recording and can be used to filter recordings later. + +## Step 3: Stop and download the recording + +A template recording captures until you stop it or its composition ends. A composition ends when you delete it, or when it [cleans itself up](./../../explanation/compositions#cost-and-lifecycle) after five minutes without input media, so keep inputs flowing for as long as you want to record. + +Stopping, checking the status, downloading the MP4, and deleting the recording work the same for every recording. Continue with [Manage recordings](./manage-recordings). diff --git a/docs/tutorials/compositions.mdx b/docs/tutorials/compositions.mdx index c9efe120..059eff90 100644 --- a/docs/tutorials/compositions.mdx +++ b/docs/tutorials/compositions.mdx @@ -536,4 +536,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. +- [Record with a template](../how-to/recordings/record-with-a-template) to render your inputs straight into an MP4. diff --git a/packages/js-server-sdk b/packages/js-server-sdk index 48eb59b6..c4459419 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit 48eb59b6264dc4347f5570db65d2bfd4254ec872 +Subproject commit c44594194272c0869ff2315780c4ca7477240fe2 diff --git a/packages/python-server-sdk b/packages/python-server-sdk index c4890146..f2214cff 160000 --- a/packages/python-server-sdk +++ b/packages/python-server-sdk @@ -1 +1 @@ -Subproject commit c4890146739fee7e77bbd5d05ac6dd302ed1d4ce +Subproject commit f2214cff01647d78b6af4f838b9289f3398779d3 diff --git a/packages/web-client-sdk b/packages/web-client-sdk index fff4e570..8d46ae7e 160000 --- a/packages/web-client-sdk +++ b/packages/web-client-sdk @@ -1 +1 @@ -Subproject commit fff4e5704db55460ea686103f3be3212df29aed8 +Subproject commit 8d46ae7e4030c7c5f6a02a73d0fca4e633ebc53f diff --git a/redirects/index.ts b/redirects/index.ts index 7f72ced0..6d9195b0 100644 --- a/redirects/index.ts +++ b/redirects/index.ts @@ -31,6 +31,17 @@ const redirectGroups: RedirectGroup[] = [ }, ], }, + { + since: "0.31.0", + description: + "Recording how-tos moved from Compositions into their own section", + rules: [ + { + from: "/how-to/compositions/record-a-composition", + to: "/how-to/recordings/record-a-composition-output", + }, + ], + }, { since: "0.28.0", description: