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