Fishjam Python Server SDK

Ad Ad Ad

Fishjam Python Server SDK

Python server SDK for the Fishjam.

Read the docs here

Installation

pip install fishjam-server-sdk

Usage

The SDK exports two main classes for interacting with Fishjam server: FishjamClient and FishjamNotifier.

FishjamClient wraps http REST api calls, while FishjamNotifier is responsible for receiving real-time updates from the server.

FishjamClient

Create a FishjamClient instance, providing the fishjam server address and api token

from fishjam import FishjamClient

fishjam_client = FishjamClient(
    fishjam_id="<fishjam_id>", management_token="<management_token>"
)

You can use it to interact with Fishjam to manage rooms and peers

# Create a room
options = RoomOptions(video_codec="h264", webhook_url="http://localhost:5000/webhook")
room = fishjam_client.create_room(options=options)

# Room(components=[], config=RoomConfig(max_peers=None, video_codec=<RoomConfigVideoCodec.H264: 'h264'>, webhook_url='http://localhost:5000/webhook'), id='1d905478-ccfc-44d6-a6e7-8ccb1b38d955', peers=[])

# Add peer to the room
peer, token = fishjam_client.create_peer(room.id)

# Peer(id='b1232c7e-c969-4450-acdf-ea24f3cdd7f6', status=<PeerStatus.DISCONNECTED: 'disconnected'>, type='webrtc'), 'M8TUGhj-L11KpyG-2zBPIo'

All methods in FishjamClient may raise one of the exceptions deriving from fishjam.errors.HTTPError. They are defined in fishjam.errors.

FishjamNotifier

FishjamNotifier allows for receiving real-time updates from the Fishjam Server.

You can read more about notifications in the Fishjam Docs.

Create FishjamNotifier instance

from fishjam import FishjamNotifier

fishjam_notifier = FishjamNotifier(
    fishjam_id="<fishjam_id>", management_token="<management_token>"
)

Then define a handler for incoming messages

@notifier.on_server_notification
def handle_notification(server_notification):
    print(f"Received a notification: {server_notification}")

After that you can start the notifier

async def test_notifier():
    notifier_task = asyncio.create_task(fishjam_notifier.connect())

    # Wait for notifier to be ready to receive messages
    await fishjam_notifier.wait_ready()

    # Create a room to trigger a server notification
    fishjam_client = FishjamClient()
    fishjam_client.create_room()

    await notifier_task


asyncio.run(test_notifier())

# Received a notification: ServerMessageRoomCreated(room_id='69a3fd1a-6a4d-47bc-ae54-0c72b0d05e29')

License

Licensed under the Apache License, Version 2.0

Fishjam is created by Software Mansion

Since 2012 Software Mansion is a software agency with experience in building web and mobile apps. We are Core React Native Contributors and experts in dealing with all kinds of React Native issues. We can help you build your next dream product – Hire us.

Software Mansion

 1""".. include:: ../README.md"""
 2
 3# pylint: disable=locally-disabled, no-name-in-module, import-error
 4
 5# Exceptions and Server Messages
 6
 7# API
 8# pylint: disable=locally-disabled, no-name-in-module, import-error
 9
10# Exceptions and Server Messages
11from fishjam import (
12    agent,
13    composition,
14    errors,
15    events,
16    integrations,
17    peer,
18    recording,
19    room,
20    version,
21)
22from fishjam._fishjam_openapi_client.models import PeerMetadata
23
24# API
25from fishjam._webhook_notifier import (
26    decode_server_notifications,
27    receive_binary,
28    verify_webhook_signature,
29)
30from fishjam._ws_notifier import FishjamNotifier
31from fishjam.api._composition_client import (
32    CompositionClient,
33    Mp4InputDurations,
34    WhipInputTarget,
35)
36from fishjam.api._fishjam_client import (
37    AgentOptions,
38    AgentOutputOptions,
39    FishjamClient,
40    MoqAccess,
41    Peer,
42    PeerOptions,
43    PeerOptionsVapi,
44    Recording,
45    Room,
46    RoomOptions,
47)
48from fishjam.errors import (
49    InvalidFishjamCredentialsError,
50    MissingFishjamIdError,
51    StaleSdkError,
52)
53
54__version__ = version.__version__
55
56__all__ = [
57    "FishjamClient",
58    "CompositionClient",
59    "WhipInputTarget",
60    "Mp4InputDurations",
61    "FishjamNotifier",
62    "decode_server_notifications",
63    "receive_binary",
64    "verify_webhook_signature",
65    "PeerMetadata",
66    "PeerOptions",
67    "PeerOptionsVapi",
68    "RoomOptions",
69    "AgentOptions",
70    "AgentOutputOptions",
71    "Room",
72    "Peer",
73    "Recording",
74    "MoqAccess",
75    "MissingFishjamIdError",
76    "InvalidFishjamCredentialsError",
77    "StaleSdkError",
78    "events",
79    "errors",
80    "room",
81    "peer",
82    "recording",
83    "agent",
84    "integrations",
85    "composition",
86]
87
88
89__docformat__ = "restructuredtext"
class FishjamClient(fishjam.api._client.Client):
210class FishjamClient(Client):
211    """Allows for managing rooms."""
212
213    def __init__(
214        self,
215        fishjam_id: str,
216        management_token: str,
217    ):
218        """Create a FishjamClient instance.
219
220        Does not contact the Fishjam backend — use :meth:`create_and_verify`
221        or :meth:`check_credentials` to verify credentials live.
222
223        Args:
224            fishjam_id: The unique identifier for the Fishjam instance.
225            management_token: The token used for authenticating management operations.
226        """
227        super().__init__(fishjam_id=fishjam_id, management_token=management_token)
228
229    @classmethod
230    def create_and_verify(
231        cls, *, fishjam_id: str, management_token: str
232    ) -> "FishjamClient":
233        """Construct a FishjamClient and verify its credentials against the backend.
234
235        Args:
236            fishjam_id: The unique identifier for the Fishjam instance.
237            management_token: The token used for authenticating management operations.
238
239        Returns:
240            FishjamClient: A client whose credentials have been verified.
241
242        Raises:
243            InvalidFishjamCredentialsError: If the token is rejected.
244        """
245        client = cls(fishjam_id=fishjam_id, management_token=management_token)
246        client.check_credentials()
247        return client
248
249    def check_credentials(self) -> None:
250        """Verify the management token via a single ``/validate`` call.
251
252        Raises:
253            InvalidFishjamCredentialsError: If the token is rejected.
254        """
255        response = credentials_validate_credentials.sync_detailed(client=self.client)
256        self._handle_deprecation_header(response.headers)
257
258        if response.status_code == HTTPStatus.NOT_FOUND:
259            raise InvalidFishjamCredentialsError("Invalid Fishjam credentials")
260
261    def create_peer(
262        self,
263        room_id: str,
264        options: PeerOptions | None = None,
265    ) -> tuple[Peer, str]:
266        """Creates a peer in the room.
267
268        Args:
269            room_id: The ID of the room where the peer will be created.
270            options: Configuration options for the peer. Defaults to None.
271
272        Returns:
273            A tuple containing:
274                - Peer: The created peer object.
275                - str: The peer token needed to authenticate to Fishjam.
276        """
277        options = options or PeerOptions()
278
279        peer_metadata = self.__parse_peer_metadata(options.metadata)
280        peer_options = PeerOptionsWebRTC(
281            metadata=peer_metadata,
282            subscribe_mode=SubscribeMode(options.subscribe_mode),
283        )
284        body = PeerConfigWebRTC(type_=PeerConfigWebRTCType.WEBRTC, options=peer_options)
285
286        resp = cast(
287            PeerDetailsResponse,
288            self._request(room_add_peer, room_id=room_id, body=body),
289        )
290
291        return (resp.data.peer, resp.data.token)
292
293    def create_agent(self, room_id: str, options: AgentOptions | None = None):
294        """Creates an agent in the room.
295
296        Args:
297            room_id: The ID of the room where the agent will be created.
298            options: Configuration options for the agent. Defaults to None.
299
300        Returns:
301            Agent: The created agent instance initialized with peer ID, room ID, token,
302                and Fishjam URL.
303        """
304        options = options or AgentOptions()
305        body = PeerConfigAgent(
306            type_=PeerConfigAgentType.AGENT,
307            options=PeerOptionsAgent(
308                output=AgentOutput(
309                    audio_format=AudioFormat(options.output.audio_format),
310                    audio_sample_rate=AudioSampleRate(options.output.audio_sample_rate),
311                ),
312                subscribe_mode=SubscribeMode(options.subscribe_mode),
313            ),
314        )
315
316        resp = cast(
317            PeerDetailsResponse,
318            self._request(room_add_peer, room_id=room_id, body=body),
319        )
320
321        socket_base_url = self._peer_socket_base_url(resp.data.peer_websocket_url)
322        return Agent(resp.data.peer.id, room_id, resp.data.token, socket_base_url)
323
324    def _peer_socket_base_url(self, peer_websocket_url: str | Unset) -> str:
325        if isinstance(peer_websocket_url, Unset) or not peer_websocket_url:
326            return self._fishjam_url
327
328        url = peer_websocket_url
329        if "://" not in url:
330            url = f"https://{url}"
331        for suffix in ("/socket/peer/websocket", "/socket/agent/websocket"):
332            url = url.removesuffix(suffix)
333        return url
334
335    def create_vapi_agent(
336        self,
337        room_id: str,
338        options: PeerOptionsVapi,
339    ) -> Peer:
340        """Creates a vapi agent in the room.
341
342        Args:
343            room_id: The ID of the room where the vapi agent will be created.
344            options: Configuration options for the vapi peer.
345
346        Returns:
347            - Peer: The created peer object.
348        """
349        body = PeerConfigVAPI(type_=PeerConfigVAPIType.VAPI, options=options)
350
351        resp = cast(
352            PeerDetailsResponse,
353            self._request(room_add_peer, room_id=room_id, body=body),
354        )
355
356        return resp.data.peer
357
358    def create_room(self, options: RoomOptions | None = None) -> Room:
359        """Creates a new room.
360
361        Args:
362            options: Configuration options for the room. Defaults to None.
363
364        Returns:
365            Room: The created Room object.
366        """
367        options = options or RoomOptions()
368
369        if options.video_codec is None:
370            codec = UNSET
371        else:
372            codec = VideoCodec(options.video_codec)
373
374        config = RoomConfig(
375            max_peers=options.max_peers,
376            video_codec=codec,
377            webhook_url=options.webhook_url,
378            room_type=RoomType(options.room_type),
379            public=options.public,
380            batch_webhook_notifications=options.batch_webhook_notifications,
381        )
382
383        room = cast(
384            RoomCreateDetailsResponse, self._request(room_create_room, body=config)
385        ).data.room
386
387        return _to_room(room)
388
389    def get_all_rooms(self) -> list[Room]:
390        """Returns list of all rooms.
391
392        Returns:
393            list[Room]: A list of all available Room objects.
394        """
395        rooms = cast(RoomsListingResponse, self._request(room_get_all_rooms)).data
396
397        return [_to_room(room) for room in rooms]
398
399    def get_room(self, room_id: str) -> Room:
400        """Returns room with the given id.
401
402        Args:
403            room_id: The ID of the room to retrieve.
404
405        Returns:
406            Room: The Room object corresponding to the given ID.
407        """
408        room = cast(
409            RoomDetailsResponse, self._request(room_get_room, room_id=room_id)
410        ).data
411
412        return _to_room(room)
413
414    def delete_peer(self, room_id: str, peer_id: str) -> None:
415        """Deletes a peer from a room.
416
417        Args:
418            room_id: The ID of the room the peer belongs to.
419            peer_id: The ID of the peer to delete.
420        """
421        return self._request(room_delete_peer, id=peer_id, room_id=room_id)
422
423    def delete_room(self, room_id: str) -> None:
424        """Deletes a room.
425
426        Args:
427            room_id: The ID of the room to delete.
428        """
429        return self._request(room_delete_room, room_id=room_id)
430
431    def refresh_peer_token(self, room_id: str, peer_id: str) -> str:
432        """Refreshes a peer token.
433
434        Args:
435            room_id: The ID of the room.
436            peer_id: The ID of the peer whose token needs refreshing.
437
438        Returns:
439            str: The new peer token.
440        """
441        response = cast(
442            PeerRefreshTokenResponse,
443            self._request(room_refresh_token, id=peer_id, room_id=room_id),
444        )
445
446        return response.data.token
447
448    def forward_room_tracks(self, room_id: str, composition_url: str) -> None:
449        """Forwards every track published in the room into a composition.
450
451        The composition composes them into its outputs. Pass the composition's
452        address, as returned by
453        `fishjam.CompositionClient.composition_url`.
454
455        Args:
456            room_id: The ID of the room to forward tracks from.
457            composition_url: The address of the composition to forward tracks to.
458        """
459        self._request(
460            track_forwardings_create,
461            room_id=room_id,
462            body=TrackForwarding(composition_url=composition_url),
463        )
464
465    def livestream_whip_url(self) -> str:
466        """Where to publish a livestream.
467
468        Pair it with a token from
469        `fishjam.FishjamClient.create_livestream_streamer_token`. A composition
470        reaches viewers by sending a WHIP output here.
471
472        Returns:
473            str: The address a WHIP publisher sends the livestream to.
474        """
475        return get_livestream_whip_url(self._fishjam_id)
476
477    def livestream_whep_url(self) -> str:
478        """Where to watch a livestream.
479
480        Pair it with a token from
481        `fishjam.FishjamClient.create_livestream_viewer_token`, sent as a bearer
482        token by the WHEP player.
483
484        Returns:
485            str: The address a WHEP viewer plays the livestream from.
486        """
487        return get_livestream_whep_url(self._fishjam_id)
488
489    def create_livestream_viewer_token(self, room_id: str) -> str:
490        """Generates a viewer token for livestream rooms.
491
492        Args:
493            room_id: The ID of the livestream room.
494
495        Returns:
496            str: The generated viewer token.
497        """
498        response = cast(
499            ViewerToken, self._request(viewer_generate_viewer_token, room_id=room_id)
500        )
501
502        return response.token
503
504    def create_livestream_streamer_token(self, room_id: str) -> str:
505        """Generates a streamer token for livestream rooms.
506
507        Args:
508            room_id: The ID of the livestream room.
509
510        Returns:
511            str: The generated streamer token.
512        """
513        response = cast(
514            StreamerToken,
515            self._request(streamer_generate_streamer_token, room_id=room_id),
516        )
517
518        return response.token
519
520    def create_moq_access(
521        self,
522        publish_path: str | None = None,
523        subscribe_path: str | None = None,
524        ttl: int | None = None,
525    ) -> MoqAccess:
526        """Generates MoQ relay connection details.
527
528        Args:
529            publish_path: Path the access grants publish access to.
530            subscribe_path: Path the access grants subscribe access to.
531            ttl: Token time to live in seconds. Defaults to 3600 (1 hour),
532                maximum is 604800 (7 days).
533
534        Returns:
535            MoqAccess: The relay connection details, containing the
536            ``connection_url`` (with the JWT embedded as a ``?jwt=`` query
537            parameter) and the ``token`` itself.
538        """
539        config = MoqAccessConfig(
540            publish_path=publish_path,
541            subscribe_path=subscribe_path,
542            ttl=ttl if ttl is not None else UNSET,
543        )
544        response = cast(
545            MoqAccess,
546            self._request(moq_create_access, body=config),
547        )
548
549        return response
550
551    def create_recording(
552        self,
553        source: CompositionSource,
554        metadata: dict[str, Any] | None = None,
555    ) -> Recording:
556        """Creates a new recording.
557
558        Capturing starts synchronously, so the returned recording is `active`.
559
560        Args:
561            source: The source of the recording.
562            metadata: Free-form metadata used to organize and filter recordings.
563
564        Returns:
565            Recording: The started recording details.
566        """
567        warnings.warn(
568            "create_recording is deprecated; use create_composition_recording instead.",
569            DeprecationWarning,
570            stacklevel=2,
571        )
572        config = RecordingConfig(
573            source=source, metadata=self.__parse_recording_metadata(metadata)
574        )
575
576        resp = cast(
577            RecordingDetailsResponse,
578            self._request(recording_create_recording, body=config),
579        )
580
581        return resp.data
582
583    def create_composition_recording(
584        self,
585        source: CompositionSource,
586        metadata: dict[str, Any] | None = None,
587    ) -> Recording:
588        """Creates a new recording.
589
590        Capturing starts synchronously, so the returned recording is `active`.
591
592        Args:
593            source: The source of the recording.
594            metadata: Free-form metadata used to organize and filter recordings.
595
596        Returns:
597            Recording: The started recording details.
598        """
599        config = RecordingConfig(
600            source=source, metadata=self.__parse_recording_metadata(metadata)
601        )
602
603        resp = cast(
604            RecordingDetailsResponse,
605            self._request(recording_create_recording, body=config),
606        )
607
608        return resp.data
609
610    def create_template_recording(
611        self,
612        source: TemplateSource,
613        template: FileSource,
614        metadata: dict[str, Any] | None = None,
615    ) -> Recording:
616        """Starts a new recording that renders its own scene from a template.
617
618        The template bundle can weigh at most 1 MiB; only valid template
619        bundles are accepted.
620
621        Args:
622            source: The source of the recording.
623            template: The bundle to render, as bytes or a path to read them from.
624            metadata: Free-form metadata used to organize and filter recordings.
625
626        Returns:
627            Recording: The created recording.
628        """
629        config = RecordingConfig(
630            source=source, metadata=self.__parse_recording_metadata(metadata)
631        )
632
633        resp = cast(
634            RecordingDetailsResponse,
635            self._request(
636                recording_create_recording,
637                body=CreateRecordingFilesBody(
638                    config=config, template=_to_file(template, "template.js")
639                ),
640            ),
641        )
642
643        return resp.data
644
645    def get_recording(self, recording_id: str) -> Recording:
646        """Returns the recording with the given id.
647
648        Args:
649            recording_id: The ID of the recording to retrieve.
650
651        Returns:
652            Recording: The recording corresponding to the given ID.
653        """
654        resp = cast(
655            RecordingDetailsResponse,
656            self._request(recording_get_recording, recording_id=recording_id),
657        )
658
659        return resp.data
660
661    def get_all_recordings(
662        self, metadata: dict[str, Any] | None = None
663    ) -> list[Recording]:
664        """Returns a list of all recordings, optionally filtered by metadata.
665
666        Args:
667            metadata: If given, only recordings whose metadata contains all
668                the given key-value pairs are returned. Nested dicts match
669                nested metadata keys.
670
671        Returns:
672            list[Recording]: A list of all matching recordings.
673        """
674        # the API expects the deepObject query format
675        # (`metadata[key]=value`, `metadata[key][nested]=value`), but the
676        # generated client serializes the filter keys at the top level, so
677        # prefix and flatten them here
678        if metadata is None:
679            metadata_query = UNSET
680        else:
681            metadata_query = ListRecordingsMetadata()
682            self.__flatten_metadata_filter(
683                "metadata", metadata, metadata_query.additional_properties
684            )
685
686        resp = cast(
687            RecordingListResponse,
688            self._request(recording_list_recordings, metadata=metadata_query),
689        )
690
691        return resp.data
692
693    def stop_recording(self, recording_id: str) -> Recording:
694        """Stops an active recording.
695
696        Finalization is asynchronous: the recording stays `active` until the
697        capture is finalized, then becomes `finished`. Stopping a recording
698        that is no longer active is a no-op.
699
700        Args:
701            recording_id: The ID of the recording to stop.
702
703        Returns:
704            Recording: The stopped recording.
705        """
706        resp = cast(
707            RecordingDetailsResponse,
708            self._request(recording_stop_recording, recording_id=recording_id),
709        )
710
711        return resp.data
712
713    def delete_recording(self, recording_id: str) -> None:
714        """Deletes a recording. Its stored media is removed asynchronously.
715
716        A recording that is still `active` cannot be deleted — stop it first
717        or wait for it to finish.
718
719        Args:
720            recording_id: The ID of the recording to delete.
721        """
722        self._request(recording_delete_recording, recording_id=recording_id)
723
724    def subscribe_peer(self, room_id: str, peer_id: str, target_peer_id: str):
725        """Subscribes a peer to all tracks of another peer.
726
727        Args:
728            room_id: The ID of the room.
729            peer_id: The ID of the subscribing peer.
730            target_peer_id: The ID of the peer to subscribe to.
731        """
732        self._request(
733            room_subscribe_peer,
734            room_id=room_id,
735            id=peer_id,
736            peer_id=target_peer_id,
737        )
738
739    def subscribe_tracks(self, room_id: str, peer_id: str, track_ids: list[str]):
740        """Subscribes a peer to specific tracks of another peer.
741
742        Args:
743            room_id: The ID of the room.
744            peer_id: The ID of the subscribing peer.
745            track_ids: A list of track IDs to subscribe to.
746        """
747        self._request(
748            room_subscribe_tracks,
749            room_id=room_id,
750            id=peer_id,
751            body=SubscribeTracksBody(track_ids=track_ids),
752        )
753
754    def __flatten_metadata_filter(
755        self, prefix: str, metadata: dict[str, Any], params: dict[str, Any]
756    ) -> None:
757        for key, value in metadata.items():
758            param_key = f"{prefix}[{key}]"
759            if isinstance(value, dict):
760                self.__flatten_metadata_filter(param_key, value, params)
761            elif value is None:
762                # the generated client drops `None` params; the API compares
763                # values as JSON strings, so send the JSON representation
764                params[param_key] = "null"
765            else:
766                params[param_key] = value
767
768    def __parse_recording_metadata(
769        self, metadata: dict[str, Any] | None
770    ) -> RecordingConfigMetadataType0 | Unset:
771        if metadata is None:
772            return UNSET
773
774        config_metadata = RecordingConfigMetadataType0()
775        for key, value in metadata.items():
776            config_metadata.additional_properties[key] = value
777
778        return config_metadata
779
780    def __parse_peer_metadata(self, metadata: dict | None) -> WebRTCMetadata:
781        peer_metadata = WebRTCMetadata()
782
783        if not metadata:
784            return peer_metadata
785
786        for key, value in metadata.items():
787            peer_metadata.additional_properties[key] = value
788
789        return peer_metadata

Allows for managing rooms.

FishjamClient(fishjam_id: str, management_token: str)
213    def __init__(
214        self,
215        fishjam_id: str,
216        management_token: str,
217    ):
218        """Create a FishjamClient instance.
219
220        Does not contact the Fishjam backend — use :meth:`create_and_verify`
221        or :meth:`check_credentials` to verify credentials live.
222
223        Args:
224            fishjam_id: The unique identifier for the Fishjam instance.
225            management_token: The token used for authenticating management operations.
226        """
227        super().__init__(fishjam_id=fishjam_id, management_token=management_token)

Create a FishjamClient instance.

Does not contact the Fishjam backend — use create_and_verify() or check_credentials() to verify credentials live.

Args:

  • fishjam_id: The unique identifier for the Fishjam instance.
  • management_token: The token used for authenticating management operations.
@classmethod
def create_and_verify( cls, *, fishjam_id: str, management_token: str) -> FishjamClient:
229    @classmethod
230    def create_and_verify(
231        cls, *, fishjam_id: str, management_token: str
232    ) -> "FishjamClient":
233        """Construct a FishjamClient and verify its credentials against the backend.
234
235        Args:
236            fishjam_id: The unique identifier for the Fishjam instance.
237            management_token: The token used for authenticating management operations.
238
239        Returns:
240            FishjamClient: A client whose credentials have been verified.
241
242        Raises:
243            InvalidFishjamCredentialsError: If the token is rejected.
244        """
245        client = cls(fishjam_id=fishjam_id, management_token=management_token)
246        client.check_credentials()
247        return client

Construct a FishjamClient and verify its credentials against the backend.

Args:

  • fishjam_id: The unique identifier for the Fishjam instance.
  • management_token: The token used for authenticating management operations.

Returns:

  • FishjamClient: A client whose credentials have been verified.

Raises:

  • InvalidFishjamCredentialsError: If the token is rejected.
def check_credentials(self) -> None:
249    def check_credentials(self) -> None:
250        """Verify the management token via a single ``/validate`` call.
251
252        Raises:
253            InvalidFishjamCredentialsError: If the token is rejected.
254        """
255        response = credentials_validate_credentials.sync_detailed(client=self.client)
256        self._handle_deprecation_header(response.headers)
257
258        if response.status_code == HTTPStatus.NOT_FOUND:
259            raise InvalidFishjamCredentialsError("Invalid Fishjam credentials")

Verify the management token via a single /validate call.

Raises:

  • InvalidFishjamCredentialsError: If the token is rejected.
def create_peer( self, room_id: str, options: PeerOptions | None = None) -> tuple[Peer, str]:
261    def create_peer(
262        self,
263        room_id: str,
264        options: PeerOptions | None = None,
265    ) -> tuple[Peer, str]:
266        """Creates a peer in the room.
267
268        Args:
269            room_id: The ID of the room where the peer will be created.
270            options: Configuration options for the peer. Defaults to None.
271
272        Returns:
273            A tuple containing:
274                - Peer: The created peer object.
275                - str: The peer token needed to authenticate to Fishjam.
276        """
277        options = options or PeerOptions()
278
279        peer_metadata = self.__parse_peer_metadata(options.metadata)
280        peer_options = PeerOptionsWebRTC(
281            metadata=peer_metadata,
282            subscribe_mode=SubscribeMode(options.subscribe_mode),
283        )
284        body = PeerConfigWebRTC(type_=PeerConfigWebRTCType.WEBRTC, options=peer_options)
285
286        resp = cast(
287            PeerDetailsResponse,
288            self._request(room_add_peer, room_id=room_id, body=body),
289        )
290
291        return (resp.data.peer, resp.data.token)

Creates a peer in the room.

Args:

  • room_id: The ID of the room where the peer will be created.
  • options: Configuration options for the peer. Defaults to None.

Returns:

  • A tuple containing:
    • Peer: The created peer object.
    • str: The peer token needed to authenticate to Fishjam.
def create_agent( self, room_id: str, options: AgentOptions | None = None):
293    def create_agent(self, room_id: str, options: AgentOptions | None = None):
294        """Creates an agent in the room.
295
296        Args:
297            room_id: The ID of the room where the agent will be created.
298            options: Configuration options for the agent. Defaults to None.
299
300        Returns:
301            Agent: The created agent instance initialized with peer ID, room ID, token,
302                and Fishjam URL.
303        """
304        options = options or AgentOptions()
305        body = PeerConfigAgent(
306            type_=PeerConfigAgentType.AGENT,
307            options=PeerOptionsAgent(
308                output=AgentOutput(
309                    audio_format=AudioFormat(options.output.audio_format),
310                    audio_sample_rate=AudioSampleRate(options.output.audio_sample_rate),
311                ),
312                subscribe_mode=SubscribeMode(options.subscribe_mode),
313            ),
314        )
315
316        resp = cast(
317            PeerDetailsResponse,
318            self._request(room_add_peer, room_id=room_id, body=body),
319        )
320
321        socket_base_url = self._peer_socket_base_url(resp.data.peer_websocket_url)
322        return Agent(resp.data.peer.id, room_id, resp.data.token, socket_base_url)

Creates an agent in the room.

Args:

  • room_id: The ID of the room where the agent will be created.
  • options: Configuration options for the agent. Defaults to None.

Returns:

  • Agent: The created agent instance initialized with peer ID, room ID, token, and Fishjam URL.
def create_vapi_agent( self, room_id: str, options: PeerOptionsVapi) -> Peer:
335    def create_vapi_agent(
336        self,
337        room_id: str,
338        options: PeerOptionsVapi,
339    ) -> Peer:
340        """Creates a vapi agent in the room.
341
342        Args:
343            room_id: The ID of the room where the vapi agent will be created.
344            options: Configuration options for the vapi peer.
345
346        Returns:
347            - Peer: The created peer object.
348        """
349        body = PeerConfigVAPI(type_=PeerConfigVAPIType.VAPI, options=options)
350
351        resp = cast(
352            PeerDetailsResponse,
353            self._request(room_add_peer, room_id=room_id, body=body),
354        )
355
356        return resp.data.peer

Creates a vapi agent in the room.

Args:

  • room_id: The ID of the room where the vapi agent will be created.
  • options: Configuration options for the vapi peer.

- - Peer: The created peer object.

def create_room( self, options: RoomOptions | None = None) -> Room:
358    def create_room(self, options: RoomOptions | None = None) -> Room:
359        """Creates a new room.
360
361        Args:
362            options: Configuration options for the room. Defaults to None.
363
364        Returns:
365            Room: The created Room object.
366        """
367        options = options or RoomOptions()
368
369        if options.video_codec is None:
370            codec = UNSET
371        else:
372            codec = VideoCodec(options.video_codec)
373
374        config = RoomConfig(
375            max_peers=options.max_peers,
376            video_codec=codec,
377            webhook_url=options.webhook_url,
378            room_type=RoomType(options.room_type),
379            public=options.public,
380            batch_webhook_notifications=options.batch_webhook_notifications,
381        )
382
383        room = cast(
384            RoomCreateDetailsResponse, self._request(room_create_room, body=config)
385        ).data.room
386
387        return _to_room(room)

Creates a new room.

Args:

  • options: Configuration options for the room. Defaults to None.

Returns:

  • Room: The created Room object.
def get_all_rooms(self) -> list[Room]:
389    def get_all_rooms(self) -> list[Room]:
390        """Returns list of all rooms.
391
392        Returns:
393            list[Room]: A list of all available Room objects.
394        """
395        rooms = cast(RoomsListingResponse, self._request(room_get_all_rooms)).data
396
397        return [_to_room(room) for room in rooms]

Returns list of all rooms.

Returns:

  • list[Room]: A list of all available Room objects.
def get_room(self, room_id: str) -> Room:
399    def get_room(self, room_id: str) -> Room:
400        """Returns room with the given id.
401
402        Args:
403            room_id: The ID of the room to retrieve.
404
405        Returns:
406            Room: The Room object corresponding to the given ID.
407        """
408        room = cast(
409            RoomDetailsResponse, self._request(room_get_room, room_id=room_id)
410        ).data
411
412        return _to_room(room)

Returns room with the given id.

Args:

  • room_id: The ID of the room to retrieve.

Returns:

  • Room: The Room object corresponding to the given ID.
def delete_peer(self, room_id: str, peer_id: str) -> None:
414    def delete_peer(self, room_id: str, peer_id: str) -> None:
415        """Deletes a peer from a room.
416
417        Args:
418            room_id: The ID of the room the peer belongs to.
419            peer_id: The ID of the peer to delete.
420        """
421        return self._request(room_delete_peer, id=peer_id, room_id=room_id)

Deletes a peer from a room.

Args:

  • room_id: The ID of the room the peer belongs to.
  • peer_id: The ID of the peer to delete.
def delete_room(self, room_id: str) -> None:
423    def delete_room(self, room_id: str) -> None:
424        """Deletes a room.
425
426        Args:
427            room_id: The ID of the room to delete.
428        """
429        return self._request(room_delete_room, room_id=room_id)

Deletes a room.

Args:

  • room_id: The ID of the room to delete.
def refresh_peer_token(self, room_id: str, peer_id: str) -> str:
431    def refresh_peer_token(self, room_id: str, peer_id: str) -> str:
432        """Refreshes a peer token.
433
434        Args:
435            room_id: The ID of the room.
436            peer_id: The ID of the peer whose token needs refreshing.
437
438        Returns:
439            str: The new peer token.
440        """
441        response = cast(
442            PeerRefreshTokenResponse,
443            self._request(room_refresh_token, id=peer_id, room_id=room_id),
444        )
445
446        return response.data.token

Refreshes a peer token.

Args:

  • room_id: The ID of the room.
  • peer_id: The ID of the peer whose token needs refreshing.

Returns:

  • str: The new peer token.
def forward_room_tracks(self, room_id: str, composition_url: str) -> None:
448    def forward_room_tracks(self, room_id: str, composition_url: str) -> None:
449        """Forwards every track published in the room into a composition.
450
451        The composition composes them into its outputs. Pass the composition's
452        address, as returned by
453        `fishjam.CompositionClient.composition_url`.
454
455        Args:
456            room_id: The ID of the room to forward tracks from.
457            composition_url: The address of the composition to forward tracks to.
458        """
459        self._request(
460            track_forwardings_create,
461            room_id=room_id,
462            body=TrackForwarding(composition_url=composition_url),
463        )

Forwards every track published in the room into a composition.

The composition composes them into its outputs. Pass the composition's address, as returned by CompositionClient.composition_url.

Args:

  • room_id: The ID of the room to forward tracks from.
  • composition_url: The address of the composition to forward tracks to.
def livestream_whip_url(self) -> str:
465    def livestream_whip_url(self) -> str:
466        """Where to publish a livestream.
467
468        Pair it with a token from
469        `fishjam.FishjamClient.create_livestream_streamer_token`. A composition
470        reaches viewers by sending a WHIP output here.
471
472        Returns:
473            str: The address a WHIP publisher sends the livestream to.
474        """
475        return get_livestream_whip_url(self._fishjam_id)

Where to publish a livestream.

Pair it with a token from FishjamClient.create_livestream_streamer_token. A composition reaches viewers by sending a WHIP output here.

Returns:

  • str: The address a WHIP publisher sends the livestream to.
def livestream_whep_url(self) -> str:
477    def livestream_whep_url(self) -> str:
478        """Where to watch a livestream.
479
480        Pair it with a token from
481        `fishjam.FishjamClient.create_livestream_viewer_token`, sent as a bearer
482        token by the WHEP player.
483
484        Returns:
485            str: The address a WHEP viewer plays the livestream from.
486        """
487        return get_livestream_whep_url(self._fishjam_id)

Where to watch a livestream.

Pair it with a token from FishjamClient.create_livestream_viewer_token, sent as a bearer token by the WHEP player.

Returns:

  • str: The address a WHEP viewer plays the livestream from.
def create_livestream_viewer_token(self, room_id: str) -> str:
489    def create_livestream_viewer_token(self, room_id: str) -> str:
490        """Generates a viewer token for livestream rooms.
491
492        Args:
493            room_id: The ID of the livestream room.
494
495        Returns:
496            str: The generated viewer token.
497        """
498        response = cast(
499            ViewerToken, self._request(viewer_generate_viewer_token, room_id=room_id)
500        )
501
502        return response.token

Generates a viewer token for livestream rooms.

Args:

  • room_id: The ID of the livestream room.

Returns:

  • str: The generated viewer token.
def create_livestream_streamer_token(self, room_id: str) -> str:
504    def create_livestream_streamer_token(self, room_id: str) -> str:
505        """Generates a streamer token for livestream rooms.
506
507        Args:
508            room_id: The ID of the livestream room.
509
510        Returns:
511            str: The generated streamer token.
512        """
513        response = cast(
514            StreamerToken,
515            self._request(streamer_generate_streamer_token, room_id=room_id),
516        )
517
518        return response.token

Generates a streamer token for livestream rooms.

Args:

  • room_id: The ID of the livestream room.

Returns:

  • str: The generated streamer token.
def create_moq_access( self, publish_path: str | None = None, subscribe_path: str | None = None, ttl: int | None = None) -> MoqAccess:
520    def create_moq_access(
521        self,
522        publish_path: str | None = None,
523        subscribe_path: str | None = None,
524        ttl: int | None = None,
525    ) -> MoqAccess:
526        """Generates MoQ relay connection details.
527
528        Args:
529            publish_path: Path the access grants publish access to.
530            subscribe_path: Path the access grants subscribe access to.
531            ttl: Token time to live in seconds. Defaults to 3600 (1 hour),
532                maximum is 604800 (7 days).
533
534        Returns:
535            MoqAccess: The relay connection details, containing the
536            ``connection_url`` (with the JWT embedded as a ``?jwt=`` query
537            parameter) and the ``token`` itself.
538        """
539        config = MoqAccessConfig(
540            publish_path=publish_path,
541            subscribe_path=subscribe_path,
542            ttl=ttl if ttl is not None else UNSET,
543        )
544        response = cast(
545            MoqAccess,
546            self._request(moq_create_access, body=config),
547        )
548
549        return response

Generates MoQ relay connection details.

Args:

  • publish_path: Path the access grants publish access to.
  • subscribe_path: Path the access grants subscribe access to.
  • ttl: Token time to live in seconds. Defaults to 3600 (1 hour), maximum is 604800 (7 days).

Returns:

  • MoqAccess: The relay connection details, containing the
  • connection_url (with the JWT embedded as a ?jwt= query
  • parameter) and the token itself.
def create_recording( self, source: fishjam._fishjam_openapi_client.models.composition_source.CompositionSource, metadata: dict[str, Any] | None = None) -> Recording:
551    def create_recording(
552        self,
553        source: CompositionSource,
554        metadata: dict[str, Any] | None = None,
555    ) -> Recording:
556        """Creates a new recording.
557
558        Capturing starts synchronously, so the returned recording is `active`.
559
560        Args:
561            source: The source of the recording.
562            metadata: Free-form metadata used to organize and filter recordings.
563
564        Returns:
565            Recording: The started recording details.
566        """
567        warnings.warn(
568            "create_recording is deprecated; use create_composition_recording instead.",
569            DeprecationWarning,
570            stacklevel=2,
571        )
572        config = RecordingConfig(
573            source=source, metadata=self.__parse_recording_metadata(metadata)
574        )
575
576        resp = cast(
577            RecordingDetailsResponse,
578            self._request(recording_create_recording, body=config),
579        )
580
581        return resp.data

Creates a new recording.

Capturing starts synchronously, so the returned recording is active.

Args:

  • source: The source of the recording.
  • metadata: Free-form metadata used to organize and filter recordings.

Returns:

  • Recording: The started recording details.
def create_composition_recording( self, source: fishjam._fishjam_openapi_client.models.composition_source.CompositionSource, metadata: dict[str, Any] | None = None) -> Recording:
583    def create_composition_recording(
584        self,
585        source: CompositionSource,
586        metadata: dict[str, Any] | None = None,
587    ) -> Recording:
588        """Creates a new recording.
589
590        Capturing starts synchronously, so the returned recording is `active`.
591
592        Args:
593            source: The source of the recording.
594            metadata: Free-form metadata used to organize and filter recordings.
595
596        Returns:
597            Recording: The started recording details.
598        """
599        config = RecordingConfig(
600            source=source, metadata=self.__parse_recording_metadata(metadata)
601        )
602
603        resp = cast(
604            RecordingDetailsResponse,
605            self._request(recording_create_recording, body=config),
606        )
607
608        return resp.data

Creates a new recording.

Capturing starts synchronously, so the returned recording is active.

Args:

  • source: The source of the recording.
  • metadata: Free-form metadata used to organize and filter recordings.

Returns:

  • Recording: The started recording details.
def create_template_recording( self, source: fishjam._fishjam_openapi_client.models.template_source.TemplateSource, template: bytes | str | pathlib.Path, metadata: dict[str, Any] | None = None) -> Recording:
610    def create_template_recording(
611        self,
612        source: TemplateSource,
613        template: FileSource,
614        metadata: dict[str, Any] | None = None,
615    ) -> Recording:
616        """Starts a new recording that renders its own scene from a template.
617
618        The template bundle can weigh at most 1 MiB; only valid template
619        bundles are accepted.
620
621        Args:
622            source: The source of the recording.
623            template: The bundle to render, as bytes or a path to read them from.
624            metadata: Free-form metadata used to organize and filter recordings.
625
626        Returns:
627            Recording: The created recording.
628        """
629        config = RecordingConfig(
630            source=source, metadata=self.__parse_recording_metadata(metadata)
631        )
632
633        resp = cast(
634            RecordingDetailsResponse,
635            self._request(
636                recording_create_recording,
637                body=CreateRecordingFilesBody(
638                    config=config, template=_to_file(template, "template.js")
639                ),
640            ),
641        )
642
643        return resp.data

Starts a new recording that renders its own scene from a template.

The template bundle can weigh at most 1 MiB; only valid template bundles are accepted.

Args:

  • source: The source of the recording.
  • template: The bundle to render, as bytes or a path to read them from.
  • metadata: Free-form metadata used to organize and filter recordings.

Returns:

  • Recording: The created recording.
def get_recording( self, recording_id: str) -> Recording:
645    def get_recording(self, recording_id: str) -> Recording:
646        """Returns the recording with the given id.
647
648        Args:
649            recording_id: The ID of the recording to retrieve.
650
651        Returns:
652            Recording: The recording corresponding to the given ID.
653        """
654        resp = cast(
655            RecordingDetailsResponse,
656            self._request(recording_get_recording, recording_id=recording_id),
657        )
658
659        return resp.data

Returns the recording with the given id.

Args:

  • recording_id: The ID of the recording to retrieve.

Returns:

  • Recording: The recording corresponding to the given ID.
def get_all_recordings( self, metadata: dict[str, Any] | None = None) -> list[Recording]:
661    def get_all_recordings(
662        self, metadata: dict[str, Any] | None = None
663    ) -> list[Recording]:
664        """Returns a list of all recordings, optionally filtered by metadata.
665
666        Args:
667            metadata: If given, only recordings whose metadata contains all
668                the given key-value pairs are returned. Nested dicts match
669                nested metadata keys.
670
671        Returns:
672            list[Recording]: A list of all matching recordings.
673        """
674        # the API expects the deepObject query format
675        # (`metadata[key]=value`, `metadata[key][nested]=value`), but the
676        # generated client serializes the filter keys at the top level, so
677        # prefix and flatten them here
678        if metadata is None:
679            metadata_query = UNSET
680        else:
681            metadata_query = ListRecordingsMetadata()
682            self.__flatten_metadata_filter(
683                "metadata", metadata, metadata_query.additional_properties
684            )
685
686        resp = cast(
687            RecordingListResponse,
688            self._request(recording_list_recordings, metadata=metadata_query),
689        )
690
691        return resp.data

Returns a list of all recordings, optionally filtered by metadata.

Args:

  • metadata: If given, only recordings whose metadata contains all the given key-value pairs are returned. Nested dicts match nested metadata keys.

Returns:

  • list[Recording]: A list of all matching recordings.
def stop_recording( self, recording_id: str) -> Recording:
693    def stop_recording(self, recording_id: str) -> Recording:
694        """Stops an active recording.
695
696        Finalization is asynchronous: the recording stays `active` until the
697        capture is finalized, then becomes `finished`. Stopping a recording
698        that is no longer active is a no-op.
699
700        Args:
701            recording_id: The ID of the recording to stop.
702
703        Returns:
704            Recording: The stopped recording.
705        """
706        resp = cast(
707            RecordingDetailsResponse,
708            self._request(recording_stop_recording, recording_id=recording_id),
709        )
710
711        return resp.data

Stops an active recording.

Finalization is asynchronous: the recording stays active until the capture is finalized, then becomes finished. Stopping a recording that is no longer active is a no-op.

Args:

  • recording_id: The ID of the recording to stop.

Returns:

  • Recording: The stopped recording.
def delete_recording(self, recording_id: str) -> None:
713    def delete_recording(self, recording_id: str) -> None:
714        """Deletes a recording. Its stored media is removed asynchronously.
715
716        A recording that is still `active` cannot be deleted — stop it first
717        or wait for it to finish.
718
719        Args:
720            recording_id: The ID of the recording to delete.
721        """
722        self._request(recording_delete_recording, recording_id=recording_id)

Deletes a recording. Its stored media is removed asynchronously.

A recording that is still active cannot be deleted — stop it first or wait for it to finish.

Args:

  • recording_id: The ID of the recording to delete.
def subscribe_peer(self, room_id: str, peer_id: str, target_peer_id: str):
724    def subscribe_peer(self, room_id: str, peer_id: str, target_peer_id: str):
725        """Subscribes a peer to all tracks of another peer.
726
727        Args:
728            room_id: The ID of the room.
729            peer_id: The ID of the subscribing peer.
730            target_peer_id: The ID of the peer to subscribe to.
731        """
732        self._request(
733            room_subscribe_peer,
734            room_id=room_id,
735            id=peer_id,
736            peer_id=target_peer_id,
737        )

Subscribes a peer to all tracks of another peer.

Args:

  • room_id: The ID of the room.
  • peer_id: The ID of the subscribing peer.
  • target_peer_id: The ID of the peer to subscribe to.
def subscribe_tracks(self, room_id: str, peer_id: str, track_ids: list[str]):
739    def subscribe_tracks(self, room_id: str, peer_id: str, track_ids: list[str]):
740        """Subscribes a peer to specific tracks of another peer.
741
742        Args:
743            room_id: The ID of the room.
744            peer_id: The ID of the subscribing peer.
745            track_ids: A list of track IDs to subscribe to.
746        """
747        self._request(
748            room_subscribe_tracks,
749            room_id=room_id,
750            id=peer_id,
751            body=SubscribeTracksBody(track_ids=track_ids),
752        )

Subscribes a peer to specific tracks of another peer.

Args:

  • room_id: The ID of the room.
  • peer_id: The ID of the subscribing peer.
  • track_ids: A list of track IDs to subscribe to.
Inherited Members
fishjam.api._client.Client
client
warnings_shown
class CompositionClient:
183class CompositionClient:
184    """Client class that allows to manage compositions.
185
186    A composition is a real-time video compositing session of a Fishjam App. It
187    requires the management token that can be retrieved from the Fishjam Dashboard,
188    the same one used by `fishjam.FishjamClient`.
189
190    Example usage:
191    ```python
192    client = CompositionClient(management_token="your-management-token")
193    ```
194    """
195
196    def __init__(self, management_token: str, composition_url: str | None = None):
197        """Create a client talking to the Composition API.
198
199        Args:
200            management_token: Secret token authorizing to perform actions on your
201                account. It is the same token `fishjam.FishjamClient` is configured
202                with. Never share this token with anyone.
203            composition_url: Address of the Composition API. Only needs setting when
204                running against a deployment other than production.
205        """
206        self._url = get_composition_url(composition_url)
207        self.client = AuthenticatedClient(
208            self._url,
209            token=management_token,
210            headers={"x-fishjam-api-client": f"python-server/{get_version()}"},
211            raise_on_unexpected_status=True,
212        )
213
214    def _request(
215        self,
216        method,
217        not_found: type[HTTPError] = CompositionNotFoundError,
218        **kwargs,
219    ):
220        try:
221            response = method.sync_detailed(client=self.client, **kwargs)
222        except UnexpectedStatus as status:
223            raise error_for_status(
224                status.status_code,
225                status.content.decode(errors="replace"),
226                not_found,
227            ) from status
228        except ValueError as error:
229            raise InternalServerError(
230                f"The Composition API answered with a status the client cannot "
231                f"interpret: {error}"
232            ) from error
233
234        if isinstance(response.parsed, ApiError):
235            raise _to_error(response.status_code, response.parsed, not_found)
236
237        return response.parsed
238
239    def composition_url(self, composition_id: str) -> str:
240        """The address of a composition, as other services refer to it.
241
242        Fishjam needs it to forward a room's tracks with
243        `fishjam.FishjamClient.forward_room_tracks`.
244
245        Args:
246            composition_id: ID of the composition.
247
248        Returns:
249            The address of the composition.
250        """
251        return f"{self._url}/api/composition/{composition_id}"
252
253    def create_composition(
254        self, config: CreateCompositionRequest | None = None
255    ) -> CompositionCreatedResponse:
256        """Create a new composition.
257
258        Inputs registered on it are composed into the scenes its outputs render.
259
260        Args:
261            config: Configuration of the composition.
262
263        Returns:
264            The created composition.
265        """
266        return cast(
267            CompositionCreatedResponse,
268            self._request(
269                compositions_create, body=config or CreateCompositionRequest()
270            ),
271        )
272
273    def start_composition(self, composition_id: str) -> None:
274        """Start a composition created with `autostart` disabled.
275
276        Its outputs begin producing audio and video.
277
278        Args:
279            composition_id: ID of the composition.
280        """
281        self._request(compositions_start, composition_id=composition_id)
282
283    def reset_composition(self, composition_id: str) -> None:
284        """Reset a composition, tearing down its scene but keeping it alive.
285
286        Args:
287            composition_id: ID of the composition.
288        """
289        self._request(compositions_reset, composition_id=composition_id)
290
291    def delete_composition(self, composition_id: str) -> None:
292        """Delete an existing composition. Its inputs and outputs are torn down with it.
293
294        Args:
295            composition_id: ID of the composition.
296        """
297        self._request(compositions_delete, composition_id=composition_id)
298
299    def register_input(
300        self,
301        composition_id: str,
302        input_id: str,
303        input_: RegisterInput,
304    ) -> RegisterInputResponse:
305        """Register a media source on a composition.
306
307        Prefer the variant methods, such as
308        `CompositionClient.register_whip_input`, which return what that input
309        type produces.
310
311        Args:
312            composition_id: ID of the composition.
313            input_id: ID to register the input under.
314            input_: Configuration of the input.
315
316        Returns:
317            Whatever the input type produces on registration.
318        """
319        return cast(
320            RegisterInputResponse,
321            self._request(
322                inputs_register,
323                composition_id=composition_id,
324                input_id=input_id,
325                body=input_,
326            ),
327        )
328
329    def register_whip_input(
330        self,
331        composition_id: str,
332        input_id: str,
333        *,
334        bearer_token: str | None = None,
335        video: bool | None = None,
336    ) -> WhipInputTarget:
337        """Register an input that a WHIP publisher pushes media into.
338
339        Args:
340            composition_id: ID of the composition.
341            input_id: ID to register the input under.
342            bearer_token: Token the publisher authenticates with. The server picks one
343                when it is not given.
344            video: Whether the input accepts an h264-encoded video track.
345
346        Returns:
347            The address and token to publish with.
348
349        Raises:
350            InternalServerError: When neither the caller nor the server provides a
351                token, leaving the input impossible to publish to.
352        """
353        response = self.register_input(
354            composition_id,
355            input_id,
356            WhipInput(
357                type_=WhipInputType.WHIP_SERVER,
358                bearer_token=_or_unset(bearer_token),
359                video=_or_unset(video),
360            ),
361        )
362
363        token = _or_none(response.bearer_token) or bearer_token
364        if not token:
365            raise InternalServerError(
366                f'Registering WHIP input "{input_id}" returned no bearer token, '
367                "so it cannot be published to"
368            )
369
370        route = _or_none(response.endpoint_route) or f"/whip/{quote(input_id, safe='')}"
371
372        return WhipInputTarget(
373            url=f"{self.composition_url(composition_id)}{route}", bearer_token=token
374        )
375
376    def register_whep_input(
377        self,
378        composition_id: str,
379        input_id: str,
380        *,
381        endpoint_url: str,
382        bearer_token: str | None = None,
383        video: bool | None = None,
384    ) -> None:
385        """Register an input that pulls media from a WHEP endpoint.
386
387        Args:
388            composition_id: ID of the composition.
389            input_id: ID to register the input under.
390            endpoint_url: Address of the WHEP endpoint to pull from.
391            bearer_token: Token to authenticate with.
392            video: Whether the input accepts an h264-encoded video track.
393        """
394        self.register_input(
395            composition_id,
396            input_id,
397            WhepInput(
398                type_=WhepInputType.WHEP_CLIENT,
399                endpoint_url=endpoint_url,
400                bearer_token=_or_unset(bearer_token),
401                video=_or_unset(video),
402            ),
403        )
404
405    def register_mp4_input(
406        self,
407        composition_id: str,
408        input_id: str,
409        *,
410        url: str,
411        loop: bool | None = None,
412    ) -> Mp4InputDurations:
413        """Register an input that plays an MP4 file.
414
415        Args:
416            composition_id: ID of the composition.
417            input_id: ID to register the input under.
418            url: Address of the file to play.
419            loop: Whether the file restarts when it ends.
420
421        Returns:
422            How much media the file holds.
423        """
424        response = self.register_input(
425            composition_id,
426            input_id,
427            Mp4Input(type_=Mp4InputType.MP4, url=url, loop=_or_unset(loop)),
428        )
429
430        return Mp4InputDurations(
431            video_duration_ms=_or_none(response.video_duration_ms),
432            audio_duration_ms=_or_none(response.audio_duration_ms),
433        )
434
435    def register_rtmp_input(
436        self, composition_id: str, input_id: str, *, stream_key: str
437    ) -> str:
438        """Register an input that an RTMP publisher pushes media into.
439
440        The stream key identifies the input and is carried in the returned address.
441
442        Args:
443            composition_id: ID of the composition.
444            input_id: ID to register the input under.
445            stream_key: Key the publisher identifies the input with.
446
447        Returns:
448            The address to publish the RTMP stream to.
449
450        Raises:
451            InternalServerError: When the server reports no address, leaving the input
452                impossible to publish to.
453        """
454        response = self.register_input(
455            composition_id,
456            input_id,
457            RtmpInput(type_=RtmpInputType.RTMP_SERVER, stream_key=stream_key),
458        )
459
460        publish_url = _or_none(response.publish_url)
461        if not publish_url:
462            raise InternalServerError(
463                f'Registering RTMP input "{input_id}" returned no publishing address, '
464                "so it cannot be published to"
465            )
466
467        return publish_url
468
469    def unregister_input(
470        self,
471        composition_id: str,
472        input_id: str,
473        options: UnregisterInput | None = None,
474    ) -> None:
475        """Unregister an input. Scenes referencing it stop receiving its media.
476
477        Args:
478            composition_id: ID of the composition.
479            input_id: ID of the input.
480            options: When to unregister the input.
481        """
482        self._request(
483            inputs_unregister,
484            not_found=InputNotFoundError,
485            composition_id=composition_id,
486            input_id=input_id,
487            body=options or UnregisterInput(),
488        )
489
490    def register_output(
491        self, composition_id: str, output_id: str, output: RegisterOutput
492    ) -> None:
493        """Register an output, the destination the composed result is sent to.
494
495        Args:
496            composition_id: ID of the composition.
497            output_id: ID to register the output under.
498            output: Configuration of the output, carrying the scene to render.
499        """
500        self._request(
501            outputs_register,
502            composition_id=composition_id,
503            output_id=output_id,
504            body=output,
505        )
506
507    def register_template_output(
508        self,
509        composition_id: str,
510        output_id: str,
511        config: RegisterOutput,
512        template: FileSource,
513    ) -> None:
514        """Register an output rendering a template bundle.
515
516        The bundle is built by `@fishjam-cloud/composition-cli`. Never pass a path taken
517        from untrusted input, since its contents are uploaded.
518
519        A template rebuilds both scenes from React, so `video.initial` and
520        `audio.initial` are ignored. Omitting `audio` entirely still means no audio
521        track at all, so pass an audio option with an empty scene when the output
522        should carry audio.
523
524        Args:
525            composition_id: ID of the composition.
526            output_id: ID to register the output under.
527            config: Configuration of the output.
528            template: The bundle to render, as bytes or a path to read them from.
529        """
530        self._request(
531            outputs_register_template,
532            composition_id=composition_id,
533            output_id=output_id,
534            body=RegisterTemplateOutputBody(
535                config=config, template=_to_file(template, "template.js")
536            ),
537        )
538
539    def register_whip_output(
540        self,
541        composition_id: str,
542        output_id: str,
543        *,
544        endpoint_url: str,
545        bearer_token: str | None = None,
546        video: OutputWhipVideoOptions | None = None,
547        audio: OutputWhipAudioOptions | None = None,
548    ) -> None:
549        """Register an output sending the composed result to a WHIP endpoint.
550
551        Args:
552            composition_id: ID of the composition.
553            output_id: ID to register the output under.
554            endpoint_url: Address of the WHIP endpoint to publish to.
555            bearer_token: Token to authenticate with.
556            video: Video options, carrying the scene to render.
557            audio: Audio options, carrying the scene to mix.
558        """
559        self.register_output(
560            composition_id,
561            output_id,
562            WhipOutput(
563                type_=WhipOutputType.WHIP_CLIENT,
564                endpoint_url=endpoint_url,
565                bearer_token=_or_unset(bearer_token),
566                video=_or_unset(video),
567                audio=_or_unset(audio),
568            ),
569        )
570
571    def register_rtmp_output(
572        self,
573        composition_id: str,
574        output_id: str,
575        *,
576        url: str,
577        video: OutputRtmpClientVideoOptions | None = None,
578        audio: OutputRtmpClientAudioOptions | None = None,
579    ) -> None:
580        """Register an output sending the composed result to an RTMP endpoint.
581
582        Args:
583            composition_id: ID of the composition.
584            output_id: ID to register the output under.
585            url: Address of the RTMP endpoint to publish to.
586            video: Video options, carrying the scene to render.
587            audio: Audio options, carrying the scene to mix.
588        """
589        self.register_output(
590            composition_id,
591            output_id,
592            RtmpOutput(
593                type_=RtmpOutputType.RTMP_CLIENT,
594                url=url,
595                video=_or_unset(video),
596                audio=_or_unset(audio),
597            ),
598        )
599
600    def unregister_output(
601        self,
602        composition_id: str,
603        output_id: str,
604        options: UnregisterOutput | None = None,
605    ) -> None:
606        """Unregister an output. It stops producing audio and video.
607
608        Args:
609            composition_id: ID of the composition.
610            output_id: ID of the output.
611            options: When to unregister the output.
612        """
613        self._request(
614            outputs_unregister,
615            not_found=OutputNotFoundError,
616            composition_id=composition_id,
617            output_id=output_id,
618            body=options or UnregisterOutput(),
619        )
620
621    def update_output(
622        self,
623        composition_id: str,
624        output_id: str,
625        update: UpdateOutputRequest | None = None,
626        *,
627        video: VideoScene | None = None,
628        audio: AudioScene | None = None,
629    ) -> None:
630        """Replace the scenes an output renders.
631
632        An update has to mirror the registration: whatever the output was
633        registered with, video, audio or both, has to be given here too, and
634        whatever it was registered without cannot be.
635
636        Args:
637            composition_id: ID of the composition.
638            output_id: ID of the output.
639            update: The update to apply.
640            video: The video scene to render, when no full update is given.
641            audio: The audio scene to mix, when no full update is given.
642        """
643        self._request(
644            outputs_update,
645            composition_id=composition_id,
646            output_id=output_id,
647            body=update
648            or UpdateOutputRequest(video=_or_unset(video), audio=_or_unset(audio)),
649        )
650
651    def request_keyframe(self, composition_id: str, output_id: str) -> None:
652        """Ask an output to emit a keyframe.
653
654        A viewer joining mid-stream then renders a full picture sooner.
655
656        Args:
657            composition_id: ID of the composition.
658            output_id: ID of the output.
659        """
660        self._request(
661            outputs_request_keyframe,
662            composition_id=composition_id,
663            output_id=output_id,
664        )
665
666    def register_image(
667        self, composition_id: str, image_id: str, image: ImageSpec
668    ) -> None:
669        """Register an image that scenes can reference by its renderer ID.
670
671        Args:
672            composition_id: ID of the composition.
673            image_id: ID to register the image under.
674            image: Where to fetch the image from and how to decode it.
675        """
676        self._request(
677            renderers_register_image,
678            composition_id=composition_id,
679            image_id=image_id,
680            body=image,
681        )
682
683    def register_font(self, composition_id: str, font: FileSource) -> None:
684        """Register a font that scenes can render text with.
685
686        Never pass a path taken from untrusted input, since its contents are uploaded.
687
688        Args:
689            composition_id: ID of the composition.
690            font: The font to upload, as bytes or a path to read them from.
691        """
692        self._request(
693            renderers_register_font,
694            composition_id=composition_id,
695            body=RegisterFontBody(font=_to_file(font, "font")),
696        )
697
698    def unregister_image(
699        self,
700        composition_id: str,
701        image_id: str,
702        options: UnregisterRenderer | None = None,
703    ) -> None:
704        """Unregister a previously registered image.
705
706        Args:
707            composition_id: ID of the composition.
708            image_id: ID of the image.
709            options: When to unregister the image.
710        """
711        self._request(
712            renderers_unregister_image,
713            not_found=RendererNotFoundError,
714            composition_id=composition_id,
715            image_id=image_id,
716            body=options or UnregisterRenderer(),
717        )
718
719    def send_event(
720        self, composition_id: str, event_name: str, data: Any = None
721    ) -> None:
722        """Deliver an event to the templates rendered by the composition's outputs.
723
724        Args:
725            composition_id: ID of the composition.
726            event_name: Name the template listens for.
727            data: Payload the template receives with the event.
728        """
729        self._request(
730            events_send,
731            composition_id=composition_id,
732            body=SendCompositionEventBody(event_name=event_name, data=_or_unset(data)),
733        )

Client class that allows to manage compositions.

A composition is a real-time video compositing session of a Fishjam App. It requires the management token that can be retrieved from the Fishjam Dashboard, the same one used by FishjamClient.

Example usage:

client = CompositionClient(management_token="your-management-token")
CompositionClient(management_token: str, composition_url: str | None = None)
196    def __init__(self, management_token: str, composition_url: str | None = None):
197        """Create a client talking to the Composition API.
198
199        Args:
200            management_token: Secret token authorizing to perform actions on your
201                account. It is the same token `fishjam.FishjamClient` is configured
202                with. Never share this token with anyone.
203            composition_url: Address of the Composition API. Only needs setting when
204                running against a deployment other than production.
205        """
206        self._url = get_composition_url(composition_url)
207        self.client = AuthenticatedClient(
208            self._url,
209            token=management_token,
210            headers={"x-fishjam-api-client": f"python-server/{get_version()}"},
211            raise_on_unexpected_status=True,
212        )

Create a client talking to the Composition API.

Args:

  • management_token: Secret token authorizing to perform actions on your account. It is the same token FishjamClient is configured with. Never share this token with anyone.
  • composition_url: Address of the Composition API. Only needs setting when running against a deployment other than production.
client
def composition_url(self, composition_id: str) -> str:
239    def composition_url(self, composition_id: str) -> str:
240        """The address of a composition, as other services refer to it.
241
242        Fishjam needs it to forward a room's tracks with
243        `fishjam.FishjamClient.forward_room_tracks`.
244
245        Args:
246            composition_id: ID of the composition.
247
248        Returns:
249            The address of the composition.
250        """
251        return f"{self._url}/api/composition/{composition_id}"

The address of a composition, as other services refer to it.

Fishjam needs it to forward a room's tracks with FishjamClient.forward_room_tracks.

Args:

  • composition_id: ID of the composition.

Returns:

  • The address of the composition.
def create_composition( self, config: fishjam._composition_openapi_client.models.create_composition_request.CreateCompositionRequest | None = None) -> fishjam._composition_openapi_client.models.composition_created_response.CompositionCreatedResponse:
253    def create_composition(
254        self, config: CreateCompositionRequest | None = None
255    ) -> CompositionCreatedResponse:
256        """Create a new composition.
257
258        Inputs registered on it are composed into the scenes its outputs render.
259
260        Args:
261            config: Configuration of the composition.
262
263        Returns:
264            The created composition.
265        """
266        return cast(
267            CompositionCreatedResponse,
268            self._request(
269                compositions_create, body=config or CreateCompositionRequest()
270            ),
271        )

Create a new composition.

Inputs registered on it are composed into the scenes its outputs render.

Args:

  • config: Configuration of the composition.

Returns:

  • The created composition.
def start_composition(self, composition_id: str) -> None:
273    def start_composition(self, composition_id: str) -> None:
274        """Start a composition created with `autostart` disabled.
275
276        Its outputs begin producing audio and video.
277
278        Args:
279            composition_id: ID of the composition.
280        """
281        self._request(compositions_start, composition_id=composition_id)

Start a composition created with autostart disabled.

Its outputs begin producing audio and video.

Args:

  • composition_id: ID of the composition.
def reset_composition(self, composition_id: str) -> None:
283    def reset_composition(self, composition_id: str) -> None:
284        """Reset a composition, tearing down its scene but keeping it alive.
285
286        Args:
287            composition_id: ID of the composition.
288        """
289        self._request(compositions_reset, composition_id=composition_id)

Reset a composition, tearing down its scene but keeping it alive.

Args:

  • composition_id: ID of the composition.
def delete_composition(self, composition_id: str) -> None:
291    def delete_composition(self, composition_id: str) -> None:
292        """Delete an existing composition. Its inputs and outputs are torn down with it.
293
294        Args:
295            composition_id: ID of the composition.
296        """
297        self._request(compositions_delete, composition_id=composition_id)

Delete an existing composition. Its inputs and outputs are torn down with it.

Args:

  • composition_id: ID of the composition.
def register_input( self, composition_id: str, input_id: str, input_: fishjam._composition_openapi_client.models.mp_4_input.Mp4Input | fishjam._composition_openapi_client.models.rtmp_input.RtmpInput | fishjam._composition_openapi_client.models.whep_input.WhepInput | fishjam._composition_openapi_client.models.whip_input.WhipInput) -> fishjam._composition_openapi_client.models.register_input_response.RegisterInputResponse:
299    def register_input(
300        self,
301        composition_id: str,
302        input_id: str,
303        input_: RegisterInput,
304    ) -> RegisterInputResponse:
305        """Register a media source on a composition.
306
307        Prefer the variant methods, such as
308        `CompositionClient.register_whip_input`, which return what that input
309        type produces.
310
311        Args:
312            composition_id: ID of the composition.
313            input_id: ID to register the input under.
314            input_: Configuration of the input.
315
316        Returns:
317            Whatever the input type produces on registration.
318        """
319        return cast(
320            RegisterInputResponse,
321            self._request(
322                inputs_register,
323                composition_id=composition_id,
324                input_id=input_id,
325                body=input_,
326            ),
327        )

Register a media source on a composition.

Prefer the variant methods, such as CompositionClient.register_whip_input, which return what that input type produces.

Args:

  • composition_id: ID of the composition.
  • input_id: ID to register the input under.
  • input_: Configuration of the input.

Returns:

  • Whatever the input type produces on registration.
def register_whip_input( self, composition_id: str, input_id: str, *, bearer_token: str | None = None, video: bool | None = None) -> WhipInputTarget:
329    def register_whip_input(
330        self,
331        composition_id: str,
332        input_id: str,
333        *,
334        bearer_token: str | None = None,
335        video: bool | None = None,
336    ) -> WhipInputTarget:
337        """Register an input that a WHIP publisher pushes media into.
338
339        Args:
340            composition_id: ID of the composition.
341            input_id: ID to register the input under.
342            bearer_token: Token the publisher authenticates with. The server picks one
343                when it is not given.
344            video: Whether the input accepts an h264-encoded video track.
345
346        Returns:
347            The address and token to publish with.
348
349        Raises:
350            InternalServerError: When neither the caller nor the server provides a
351                token, leaving the input impossible to publish to.
352        """
353        response = self.register_input(
354            composition_id,
355            input_id,
356            WhipInput(
357                type_=WhipInputType.WHIP_SERVER,
358                bearer_token=_or_unset(bearer_token),
359                video=_or_unset(video),
360            ),
361        )
362
363        token = _or_none(response.bearer_token) or bearer_token
364        if not token:
365            raise InternalServerError(
366                f'Registering WHIP input "{input_id}" returned no bearer token, '
367                "so it cannot be published to"
368            )
369
370        route = _or_none(response.endpoint_route) or f"/whip/{quote(input_id, safe='')}"
371
372        return WhipInputTarget(
373            url=f"{self.composition_url(composition_id)}{route}", bearer_token=token
374        )

Register an input that a WHIP publisher pushes media into.

Args:

  • composition_id: ID of the composition.
  • input_id: ID to register the input under.
  • bearer_token: Token the publisher authenticates with. The server picks one when it is not given.
  • video: Whether the input accepts an h264-encoded video track.

Returns:

  • The address and token to publish with.

Raises:

  • InternalServerError: When neither the caller nor the server provides a token, leaving the input impossible to publish to.
def register_whep_input( self, composition_id: str, input_id: str, *, endpoint_url: str, bearer_token: str | None = None, video: bool | None = None) -> None:
376    def register_whep_input(
377        self,
378        composition_id: str,
379        input_id: str,
380        *,
381        endpoint_url: str,
382        bearer_token: str | None = None,
383        video: bool | None = None,
384    ) -> None:
385        """Register an input that pulls media from a WHEP endpoint.
386
387        Args:
388            composition_id: ID of the composition.
389            input_id: ID to register the input under.
390            endpoint_url: Address of the WHEP endpoint to pull from.
391            bearer_token: Token to authenticate with.
392            video: Whether the input accepts an h264-encoded video track.
393        """
394        self.register_input(
395            composition_id,
396            input_id,
397            WhepInput(
398                type_=WhepInputType.WHEP_CLIENT,
399                endpoint_url=endpoint_url,
400                bearer_token=_or_unset(bearer_token),
401                video=_or_unset(video),
402            ),
403        )

Register an input that pulls media from a WHEP endpoint.

Args:

  • composition_id: ID of the composition.
  • input_id: ID to register the input under.
  • endpoint_url: Address of the WHEP endpoint to pull from.
  • bearer_token: Token to authenticate with.
  • video: Whether the input accepts an h264-encoded video track.
def register_mp4_input( self, composition_id: str, input_id: str, *, url: str, loop: bool | None = None) -> Mp4InputDurations:
405    def register_mp4_input(
406        self,
407        composition_id: str,
408        input_id: str,
409        *,
410        url: str,
411        loop: bool | None = None,
412    ) -> Mp4InputDurations:
413        """Register an input that plays an MP4 file.
414
415        Args:
416            composition_id: ID of the composition.
417            input_id: ID to register the input under.
418            url: Address of the file to play.
419            loop: Whether the file restarts when it ends.
420
421        Returns:
422            How much media the file holds.
423        """
424        response = self.register_input(
425            composition_id,
426            input_id,
427            Mp4Input(type_=Mp4InputType.MP4, url=url, loop=_or_unset(loop)),
428        )
429
430        return Mp4InputDurations(
431            video_duration_ms=_or_none(response.video_duration_ms),
432            audio_duration_ms=_or_none(response.audio_duration_ms),
433        )

Register an input that plays an MP4 file.

Args:

  • composition_id: ID of the composition.
  • input_id: ID to register the input under.
  • url: Address of the file to play.
  • loop: Whether the file restarts when it ends.

Returns:

  • How much media the file holds.
def register_rtmp_input(self, composition_id: str, input_id: str, *, stream_key: str) -> str:
435    def register_rtmp_input(
436        self, composition_id: str, input_id: str, *, stream_key: str
437    ) -> str:
438        """Register an input that an RTMP publisher pushes media into.
439
440        The stream key identifies the input and is carried in the returned address.
441
442        Args:
443            composition_id: ID of the composition.
444            input_id: ID to register the input under.
445            stream_key: Key the publisher identifies the input with.
446
447        Returns:
448            The address to publish the RTMP stream to.
449
450        Raises:
451            InternalServerError: When the server reports no address, leaving the input
452                impossible to publish to.
453        """
454        response = self.register_input(
455            composition_id,
456            input_id,
457            RtmpInput(type_=RtmpInputType.RTMP_SERVER, stream_key=stream_key),
458        )
459
460        publish_url = _or_none(response.publish_url)
461        if not publish_url:
462            raise InternalServerError(
463                f'Registering RTMP input "{input_id}" returned no publishing address, '
464                "so it cannot be published to"
465            )
466
467        return publish_url

Register an input that an RTMP publisher pushes media into.

The stream key identifies the input and is carried in the returned address.

Args:

  • composition_id: ID of the composition.
  • input_id: ID to register the input under.
  • stream_key: Key the publisher identifies the input with.

Returns:

  • The address to publish the RTMP stream to.

Raises:

  • InternalServerError: When the server reports no address, leaving the input impossible to publish to.
def unregister_input( self, composition_id: str, input_id: str, options: fishjam._composition_openapi_client.models.unregister_input.UnregisterInput | None = None) -> None:
469    def unregister_input(
470        self,
471        composition_id: str,
472        input_id: str,
473        options: UnregisterInput | None = None,
474    ) -> None:
475        """Unregister an input. Scenes referencing it stop receiving its media.
476
477        Args:
478            composition_id: ID of the composition.
479            input_id: ID of the input.
480            options: When to unregister the input.
481        """
482        self._request(
483            inputs_unregister,
484            not_found=InputNotFoundError,
485            composition_id=composition_id,
486            input_id=input_id,
487            body=options or UnregisterInput(),
488        )

Unregister an input. Scenes referencing it stop receiving its media.

Args:

  • composition_id: ID of the composition.
  • input_id: ID of the input.
  • options: When to unregister the input.
def register_output( self, composition_id: str, output_id: str, output: fishjam._composition_openapi_client.models.rtmp_output.RtmpOutput | fishjam._composition_openapi_client.models.whip_output.WhipOutput) -> None:
490    def register_output(
491        self, composition_id: str, output_id: str, output: RegisterOutput
492    ) -> None:
493        """Register an output, the destination the composed result is sent to.
494
495        Args:
496            composition_id: ID of the composition.
497            output_id: ID to register the output under.
498            output: Configuration of the output, carrying the scene to render.
499        """
500        self._request(
501            outputs_register,
502            composition_id=composition_id,
503            output_id=output_id,
504            body=output,
505        )

Register an output, the destination the composed result is sent to.

Args:

  • composition_id: ID of the composition.
  • output_id: ID to register the output under.
  • output: Configuration of the output, carrying the scene to render.
def register_template_output( self, composition_id: str, output_id: str, config: fishjam._composition_openapi_client.models.rtmp_output.RtmpOutput | fishjam._composition_openapi_client.models.whip_output.WhipOutput, template: bytes | str | pathlib.Path) -> None:
507    def register_template_output(
508        self,
509        composition_id: str,
510        output_id: str,
511        config: RegisterOutput,
512        template: FileSource,
513    ) -> None:
514        """Register an output rendering a template bundle.
515
516        The bundle is built by `@fishjam-cloud/composition-cli`. Never pass a path taken
517        from untrusted input, since its contents are uploaded.
518
519        A template rebuilds both scenes from React, so `video.initial` and
520        `audio.initial` are ignored. Omitting `audio` entirely still means no audio
521        track at all, so pass an audio option with an empty scene when the output
522        should carry audio.
523
524        Args:
525            composition_id: ID of the composition.
526            output_id: ID to register the output under.
527            config: Configuration of the output.
528            template: The bundle to render, as bytes or a path to read them from.
529        """
530        self._request(
531            outputs_register_template,
532            composition_id=composition_id,
533            output_id=output_id,
534            body=RegisterTemplateOutputBody(
535                config=config, template=_to_file(template, "template.js")
536            ),
537        )

Register an output rendering a template bundle.

The bundle is built by @fishjam-cloud/composition-cli. Never pass a path taken from untrusted input, since its contents are uploaded.

A template rebuilds both scenes from React, so video.initial and audio.initial are ignored. Omitting audio entirely still means no audio track at all, so pass an audio option with an empty scene when the output should carry audio.

Args:

  • composition_id: ID of the composition.
  • output_id: ID to register the output under.
  • config: Configuration of the output.
  • template: The bundle to render, as bytes or a path to read them from.
def register_whip_output( self, composition_id: str, output_id: str, *, endpoint_url: str, bearer_token: str | None = None, video: fishjam._composition_openapi_client.models.output_whip_video_options.OutputWhipVideoOptions | None = None, audio: fishjam._composition_openapi_client.models.output_whip_audio_options.OutputWhipAudioOptions | None = None) -> None:
539    def register_whip_output(
540        self,
541        composition_id: str,
542        output_id: str,
543        *,
544        endpoint_url: str,
545        bearer_token: str | None = None,
546        video: OutputWhipVideoOptions | None = None,
547        audio: OutputWhipAudioOptions | None = None,
548    ) -> None:
549        """Register an output sending the composed result to a WHIP endpoint.
550
551        Args:
552            composition_id: ID of the composition.
553            output_id: ID to register the output under.
554            endpoint_url: Address of the WHIP endpoint to publish to.
555            bearer_token: Token to authenticate with.
556            video: Video options, carrying the scene to render.
557            audio: Audio options, carrying the scene to mix.
558        """
559        self.register_output(
560            composition_id,
561            output_id,
562            WhipOutput(
563                type_=WhipOutputType.WHIP_CLIENT,
564                endpoint_url=endpoint_url,
565                bearer_token=_or_unset(bearer_token),
566                video=_or_unset(video),
567                audio=_or_unset(audio),
568            ),
569        )

Register an output sending the composed result to a WHIP endpoint.

Args:

  • composition_id: ID of the composition.
  • output_id: ID to register the output under.
  • endpoint_url: Address of the WHIP endpoint to publish to.
  • bearer_token: Token to authenticate with.
  • video: Video options, carrying the scene to render.
  • audio: Audio options, carrying the scene to mix.
def register_rtmp_output( self, composition_id: str, output_id: str, *, url: str, video: fishjam._composition_openapi_client.models.output_rtmp_client_video_options.OutputRtmpClientVideoOptions | None = None, audio: fishjam._composition_openapi_client.models.output_rtmp_client_audio_options.OutputRtmpClientAudioOptions | None = None) -> None:
571    def register_rtmp_output(
572        self,
573        composition_id: str,
574        output_id: str,
575        *,
576        url: str,
577        video: OutputRtmpClientVideoOptions | None = None,
578        audio: OutputRtmpClientAudioOptions | None = None,
579    ) -> None:
580        """Register an output sending the composed result to an RTMP endpoint.
581
582        Args:
583            composition_id: ID of the composition.
584            output_id: ID to register the output under.
585            url: Address of the RTMP endpoint to publish to.
586            video: Video options, carrying the scene to render.
587            audio: Audio options, carrying the scene to mix.
588        """
589        self.register_output(
590            composition_id,
591            output_id,
592            RtmpOutput(
593                type_=RtmpOutputType.RTMP_CLIENT,
594                url=url,
595                video=_or_unset(video),
596                audio=_or_unset(audio),
597            ),
598        )

Register an output sending the composed result to an RTMP endpoint.

Args:

  • composition_id: ID of the composition.
  • output_id: ID to register the output under.
  • url: Address of the RTMP endpoint to publish to.
  • video: Video options, carrying the scene to render.
  • audio: Audio options, carrying the scene to mix.
def unregister_output( self, composition_id: str, output_id: str, options: fishjam._composition_openapi_client.models.unregister_output.UnregisterOutput | None = None) -> None:
600    def unregister_output(
601        self,
602        composition_id: str,
603        output_id: str,
604        options: UnregisterOutput | None = None,
605    ) -> None:
606        """Unregister an output. It stops producing audio and video.
607
608        Args:
609            composition_id: ID of the composition.
610            output_id: ID of the output.
611            options: When to unregister the output.
612        """
613        self._request(
614            outputs_unregister,
615            not_found=OutputNotFoundError,
616            composition_id=composition_id,
617            output_id=output_id,
618            body=options or UnregisterOutput(),
619        )

Unregister an output. It stops producing audio and video.

Args:

  • composition_id: ID of the composition.
  • output_id: ID of the output.
  • options: When to unregister the output.
def update_output( self, composition_id: str, output_id: str, update: fishjam._composition_openapi_client.models.update_output_request.UpdateOutputRequest | None = None, *, video: fishjam._composition_openapi_client.models.video_scene.VideoScene | None = None, audio: fishjam._composition_openapi_client.models.audio_scene.AudioScene | None = None) -> None:
621    def update_output(
622        self,
623        composition_id: str,
624        output_id: str,
625        update: UpdateOutputRequest | None = None,
626        *,
627        video: VideoScene | None = None,
628        audio: AudioScene | None = None,
629    ) -> None:
630        """Replace the scenes an output renders.
631
632        An update has to mirror the registration: whatever the output was
633        registered with, video, audio or both, has to be given here too, and
634        whatever it was registered without cannot be.
635
636        Args:
637            composition_id: ID of the composition.
638            output_id: ID of the output.
639            update: The update to apply.
640            video: The video scene to render, when no full update is given.
641            audio: The audio scene to mix, when no full update is given.
642        """
643        self._request(
644            outputs_update,
645            composition_id=composition_id,
646            output_id=output_id,
647            body=update
648            or UpdateOutputRequest(video=_or_unset(video), audio=_or_unset(audio)),
649        )

Replace the scenes an output renders.

An update has to mirror the registration: whatever the output was registered with, video, audio or both, has to be given here too, and whatever it was registered without cannot be.

Args:

  • composition_id: ID of the composition.
  • output_id: ID of the output.
  • update: The update to apply.
  • video: The video scene to render, when no full update is given.
  • audio: The audio scene to mix, when no full update is given.
def request_keyframe(self, composition_id: str, output_id: str) -> None:
651    def request_keyframe(self, composition_id: str, output_id: str) -> None:
652        """Ask an output to emit a keyframe.
653
654        A viewer joining mid-stream then renders a full picture sooner.
655
656        Args:
657            composition_id: ID of the composition.
658            output_id: ID of the output.
659        """
660        self._request(
661            outputs_request_keyframe,
662            composition_id=composition_id,
663            output_id=output_id,
664        )

Ask an output to emit a keyframe.

A viewer joining mid-stream then renders a full picture sooner.

Args:

  • composition_id: ID of the composition.
  • output_id: ID of the output.
def register_image( self, composition_id: str, image_id: str, image: fishjam._composition_openapi_client.models.image_spec_auto.ImageSpecAuto | fishjam._composition_openapi_client.models.image_spec_gif.ImageSpecGif | fishjam._composition_openapi_client.models.image_spec_jpeg.ImageSpecJpeg | fishjam._composition_openapi_client.models.image_spec_png.ImageSpecPng | fishjam._composition_openapi_client.models.image_spec_svg.ImageSpecSvg) -> None:
666    def register_image(
667        self, composition_id: str, image_id: str, image: ImageSpec
668    ) -> None:
669        """Register an image that scenes can reference by its renderer ID.
670
671        Args:
672            composition_id: ID of the composition.
673            image_id: ID to register the image under.
674            image: Where to fetch the image from and how to decode it.
675        """
676        self._request(
677            renderers_register_image,
678            composition_id=composition_id,
679            image_id=image_id,
680            body=image,
681        )

Register an image that scenes can reference by its renderer ID.

Args:

  • composition_id: ID of the composition.
  • image_id: ID to register the image under.
  • image: Where to fetch the image from and how to decode it.
def register_font(self, composition_id: str, font: bytes | str | pathlib.Path) -> None:
683    def register_font(self, composition_id: str, font: FileSource) -> None:
684        """Register a font that scenes can render text with.
685
686        Never pass a path taken from untrusted input, since its contents are uploaded.
687
688        Args:
689            composition_id: ID of the composition.
690            font: The font to upload, as bytes or a path to read them from.
691        """
692        self._request(
693            renderers_register_font,
694            composition_id=composition_id,
695            body=RegisterFontBody(font=_to_file(font, "font")),
696        )

Register a font that scenes can render text with.

Never pass a path taken from untrusted input, since its contents are uploaded.

Args:

  • composition_id: ID of the composition.
  • font: The font to upload, as bytes or a path to read them from.
def unregister_image( self, composition_id: str, image_id: str, options: fishjam._composition_openapi_client.models.unregister_renderer.UnregisterRenderer | None = None) -> None:
698    def unregister_image(
699        self,
700        composition_id: str,
701        image_id: str,
702        options: UnregisterRenderer | None = None,
703    ) -> None:
704        """Unregister a previously registered image.
705
706        Args:
707            composition_id: ID of the composition.
708            image_id: ID of the image.
709            options: When to unregister the image.
710        """
711        self._request(
712            renderers_unregister_image,
713            not_found=RendererNotFoundError,
714            composition_id=composition_id,
715            image_id=image_id,
716            body=options or UnregisterRenderer(),
717        )

Unregister a previously registered image.

Args:

  • composition_id: ID of the composition.
  • image_id: ID of the image.
  • options: When to unregister the image.
def send_event(self, composition_id: str, event_name: str, data: Any = None) -> None:
719    def send_event(
720        self, composition_id: str, event_name: str, data: Any = None
721    ) -> None:
722        """Deliver an event to the templates rendered by the composition's outputs.
723
724        Args:
725            composition_id: ID of the composition.
726            event_name: Name the template listens for.
727            data: Payload the template receives with the event.
728        """
729        self._request(
730            events_send,
731            composition_id=composition_id,
732            body=SendCompositionEventBody(event_name=event_name, data=_or_unset(data)),
733        )

Deliver an event to the templates rendered by the composition's outputs.

Args:

  • composition_id: ID of the composition.
  • event_name: Name the template listens for.
  • data: Payload the template receives with the event.
@dataclass
class WhipInputTarget:
110@dataclass
111class WhipInputTarget:
112    """Where to publish a WHIP input.
113
114    The input is registered with `CompositionClient.register_whip_input`. Hand
115    these to a WHIP publisher, such as `useLivestreamStreamer` in the React
116    client SDK.
117
118    Attributes:
119        url: Address to publish to.
120        bearer_token: Token authorizing the publisher.
121    """
122
123    url: str
124    """Address to publish to"""
125    bearer_token: str
126    """Token authorizing the publisher"""

Where to publish a WHIP input.

The input is registered with CompositionClient.register_whip_input. Hand these to a WHIP publisher, such as useLivestreamStreamer in the React client SDK.

Attributes:

  • url: Address to publish to.
  • bearer_token: Token authorizing the publisher.
WhipInputTarget(url: str, bearer_token: str)
url: str

Address to publish to

bearer_token: str

Token authorizing the publisher

@dataclass
class Mp4InputDurations:
129@dataclass
130class Mp4InputDurations:
131    """How much media an MP4 input holds.
132
133    The input is registered with `CompositionClient.register_mp4_input`.
134
135    Attributes:
136        video_duration_ms: Length of the video track, when the file has one.
137        audio_duration_ms: Length of the audio track, when the file has one.
138    """
139
140    video_duration_ms: int | None
141    """Length of the video track, when the file has one"""
142    audio_duration_ms: int | None
143    """Length of the audio track, when the file has one"""

How much media an MP4 input holds.

The input is registered with CompositionClient.register_mp4_input.

Attributes:

  • video_duration_ms: Length of the video track, when the file has one.
  • audio_duration_ms: Length of the audio track, when the file has one.
Mp4InputDurations(video_duration_ms: int | None, audio_duration_ms: int | None)
video_duration_ms: int | None

Length of the video track, when the file has one

audio_duration_ms: int | None

Length of the audio track, when the file has one

class FishjamNotifier:
 34class FishjamNotifier:
 35    """Allows for receiving WebSocket messages from Fishjam."""
 36
 37    def __init__(
 38        self,
 39        fishjam_id: str,
 40        management_token: str,
 41    ):
 42        """Create a FishjamNotifier instance with an ID and management token."""
 43        websocket_url = get_fishjam_url(fishjam_id).replace("http", "ws")
 44        self._fishjam_url = f"{websocket_url}/socket/server/websocket"
 45        self._management_token: str = management_token
 46        self._websocket: client.ClientConnection | None = None
 47        self._ready: bool = False
 48
 49        self._ready_event: asyncio.Event | None = None
 50
 51        self._notification_handler: NotificationHandler | None = None
 52
 53    def on_server_notification(self, handler: NotificationHandler):
 54        """Decorator for defining a handler for Fishjam notifications.
 55
 56        Args:
 57            handler: The function to be registered as the notification handler.
 58
 59        Returns:
 60            NotificationHandler: The original handler function (unmodified).
 61        """
 62        self._notification_handler = handler
 63        return handler
 64
 65    async def connect(self):
 66        """Connects to Fishjam and listens for all incoming messages.
 67
 68        It runs until the connection isn't closed.
 69
 70        The incoming messages are handled by the functions defined using the
 71        `on_server_notification` decorator.
 72
 73        The handler have to be defined before calling `connect`,
 74        otherwise the messages won't be received.
 75        """
 76        async with client.connect(self._fishjam_url) as websocket:
 77            try:
 78                self._websocket = websocket
 79                await self._authenticate()
 80
 81                if self._notification_handler:
 82                    await self._subscribe_event(
 83                        event=ServerMessageEventType.EVENT_TYPE_SERVER_NOTIFICATION
 84                    )
 85
 86                self._ready = True
 87                if self._ready_event:
 88                    self._ready_event.set()
 89
 90                await self._receive_loop()
 91            finally:
 92                self._websocket = None
 93
 94    async def wait_ready(self) -> None:
 95        """Waits until the notifier is connected and authenticated to Fishjam.
 96
 97        If already connected, returns immediately.
 98        """
 99        if self._ready:
100            return
101
102        if self._ready_event is None:
103            self._ready_event = asyncio.Event()
104
105        await self._ready_event.wait()
106
107    async def _authenticate(self):
108        if not self._websocket:
109            raise RuntimeError("Websocket is not connected")
110
111        msg = ServerMessage(
112            auth_request=ServerMessageAuthRequest(token=self._management_token)
113        )
114        await self._websocket.send(bytes(msg))
115
116        try:
117            message = await self._websocket.recv(decode=False)
118        except ConnectionClosed as exception:
119            if "invalid token" in str(exception):
120                raise RuntimeError("Invalid management token") from exception
121            raise
122
123        message = ServerMessage().parse(message)
124
125        _type, message = betterproto.which_one_of(message, "content")
126        assert isinstance(message, ServerMessageAuthenticated)
127
128    async def _receive_loop(self):
129        if not self._websocket:
130            raise RuntimeError("Websocket is not connected")
131        if not self._notification_handler:
132            raise RuntimeError("Notification handler is not defined")
133
134        while True:
135            message = await self._websocket.recv(decode=False)
136            message = ServerMessage().parse(message)
137            _which, message = betterproto.which_one_of(message, "content")
138
139            if isinstance(message, ALLOWED_NOTIFICATIONS):
140                validate_notification(message)
141                res = self._notification_handler(message)
142                if inspect.isawaitable(res):
143                    await res
144
145    async def _subscribe_event(self, event: ServerMessageEventType):
146        if not self._websocket:
147            raise RuntimeError("Websocket is not connected")
148
149        request = ServerMessage(subscribe_request=ServerMessageSubscribeRequest(event))
150
151        await self._websocket.send(bytes(request))
152        message = cast(bytes, await self._websocket.recv())
153        message = ServerMessage().parse(message)
154        _which, message = betterproto.which_one_of(message, "content")
155        assert isinstance(message, ServerMessageSubscribeResponse)

Allows for receiving WebSocket messages from Fishjam.

FishjamNotifier(fishjam_id: str, management_token: str)
37    def __init__(
38        self,
39        fishjam_id: str,
40        management_token: str,
41    ):
42        """Create a FishjamNotifier instance with an ID and management token."""
43        websocket_url = get_fishjam_url(fishjam_id).replace("http", "ws")
44        self._fishjam_url = f"{websocket_url}/socket/server/websocket"
45        self._management_token: str = management_token
46        self._websocket: client.ClientConnection | None = None
47        self._ready: bool = False
48
49        self._ready_event: asyncio.Event | None = None
50
51        self._notification_handler: NotificationHandler | None = None

Create a FishjamNotifier instance with an ID and management token.

def on_server_notification( self, handler: Callable[[fishjam.events.ServerMessageRoomCreated | fishjam.events.ServerMessageRoomDeleted | fishjam.events.ServerMessageRoomCrashed | fishjam.events.ServerMessagePeerAdded | fishjam.events.ServerMessagePeerDeleted | fishjam.events.ServerMessagePeerConnected | fishjam.events.ServerMessagePeerDisconnected | fishjam.events.ServerMessagePeerMetadataUpdated | fishjam.events.ServerMessagePeerCrashed | fishjam.events.ServerMessageStreamerConnected | fishjam.events.ServerMessageStreamerDisconnected | fishjam.events.ServerMessageChannelAdded | fishjam.events.ServerMessageChannelRemoved | fishjam.events.ServerMessageViewerConnected | fishjam.events.ServerMessageViewerDisconnected | fishjam.events.ServerMessageTrackAdded | fishjam.events.ServerMessageTrackRemoved | fishjam.events.ServerMessageTrackMetadataUpdated | fishjam.events.ServerMessageRecordingStatusChanged], NoneType] | Callable[[fishjam.events.ServerMessageRoomCreated | fishjam.events.ServerMessageRoomDeleted | fishjam.events.ServerMessageRoomCrashed | fishjam.events.ServerMessagePeerAdded | fishjam.events.ServerMessagePeerDeleted | fishjam.events.ServerMessagePeerConnected | fishjam.events.ServerMessagePeerDisconnected | fishjam.events.ServerMessagePeerMetadataUpdated | fishjam.events.ServerMessagePeerCrashed | fishjam.events.ServerMessageStreamerConnected | fishjam.events.ServerMessageStreamerDisconnected | fishjam.events.ServerMessageChannelAdded | fishjam.events.ServerMessageChannelRemoved | fishjam.events.ServerMessageViewerConnected | fishjam.events.ServerMessageViewerDisconnected | fishjam.events.ServerMessageTrackAdded | fishjam.events.ServerMessageTrackRemoved | fishjam.events.ServerMessageTrackMetadataUpdated | fishjam.events.ServerMessageRecordingStatusChanged], Coroutine[Any, Any, None]]):
53    def on_server_notification(self, handler: NotificationHandler):
54        """Decorator for defining a handler for Fishjam notifications.
55
56        Args:
57            handler: The function to be registered as the notification handler.
58
59        Returns:
60            NotificationHandler: The original handler function (unmodified).
61        """
62        self._notification_handler = handler
63        return handler

Decorator for defining a handler for Fishjam notifications.

Args:

  • handler: The function to be registered as the notification handler.

Returns:

  • NotificationHandler: The original handler function (unmodified).
async def connect(self):
65    async def connect(self):
66        """Connects to Fishjam and listens for all incoming messages.
67
68        It runs until the connection isn't closed.
69
70        The incoming messages are handled by the functions defined using the
71        `on_server_notification` decorator.
72
73        The handler have to be defined before calling `connect`,
74        otherwise the messages won't be received.
75        """
76        async with client.connect(self._fishjam_url) as websocket:
77            try:
78                self._websocket = websocket
79                await self._authenticate()
80
81                if self._notification_handler:
82                    await self._subscribe_event(
83                        event=ServerMessageEventType.EVENT_TYPE_SERVER_NOTIFICATION
84                    )
85
86                self._ready = True
87                if self._ready_event:
88                    self._ready_event.set()
89
90                await self._receive_loop()
91            finally:
92                self._websocket = None

Connects to Fishjam and listens for all incoming messages.

It runs until the connection isn't closed.

The incoming messages are handled by the functions defined using the on_server_notification decorator.

The handler have to be defined before calling connect, otherwise the messages won't be received.

async def wait_ready(self) -> None:
 94    async def wait_ready(self) -> None:
 95        """Waits until the notifier is connected and authenticated to Fishjam.
 96
 97        If already connected, returns immediately.
 98        """
 99        if self._ready:
100            return
101
102        if self._ready_event is None:
103            self._ready_event = asyncio.Event()
104
105        await self._ready_event.wait()

Waits until the notifier is connected and authenticated to Fishjam.

If already connected, returns immediately.

48def decode_server_notifications(binary: bytes) -> List[AllowedNotification]:
49    """Decode a received protobuf payload into a list of notifications.
50
51    Handles both single notifications and batches transparently: a single
52    notification is returned as a one-element list, a batch is unpacked into
53    its members (in order), and anything unsupported yields an empty list.
54
55    The available notifications are listed in the `fishjam.events` module.
56
57    Args:
58        binary: The raw binary data received from the webhook.
59
60    Returns:
61        list[AllowedNotification]: The decoded notifications, in order. Empty
62            when the payload carries no supported notification.
63
64    Raises:
65        fishjam.errors.StaleSdkError: When a notification carries a value this
66            SDK cannot parse, which likely means the SDK is outdated.
67    """
68    message = ServerMessage().parse(binary)
69    _which, content = betterproto.which_one_of(message, "content")
70
71    if isinstance(content, ServerMessageNotificationBatch):
72        return _unpack_batch(content)
73
74    if isinstance(content, ALLOWED_NOTIFICATIONS):
75        validate_notification(content)
76        return [content]
77
78    return []

Decode a received protobuf payload into a list of notifications.

Handles both single notifications and batches transparently: a single notification is returned as a one-element list, a batch is unpacked into its members (in order), and anything unsupported yields an empty list.

The available notifications are listed in the fishjam.events module.

Args:

  • binary: The raw binary data received from the webhook.

Returns:

  • list[AllowedNotification]: The decoded notifications, in order. Empty when the payload carries no supported notification.

Raises:

  • StaleSdkError: When a notification carries a value this SDK cannot parse, which likely means the SDK is outdated.
def receive_binary( binary: bytes) -> fishjam.events.ServerMessageRoomCreated | fishjam.events.ServerMessageRoomDeleted | fishjam.events.ServerMessageRoomCrashed | fishjam.events.ServerMessagePeerAdded | fishjam.events.ServerMessagePeerDeleted | fishjam.events.ServerMessagePeerConnected | fishjam.events.ServerMessagePeerDisconnected | fishjam.events.ServerMessagePeerMetadataUpdated | fishjam.events.ServerMessagePeerCrashed | fishjam.events.ServerMessageStreamerConnected | fishjam.events.ServerMessageStreamerDisconnected | fishjam.events.ServerMessageChannelAdded | fishjam.events.ServerMessageChannelRemoved | fishjam.events.ServerMessageViewerConnected | fishjam.events.ServerMessageViewerDisconnected | fishjam.events.ServerMessageTrackAdded | fishjam.events.ServerMessageTrackRemoved | fishjam.events.ServerMessageTrackMetadataUpdated | fishjam.events.ServerMessageRecordingStatusChanged | List[fishjam.events.ServerMessageRoomCreated | fishjam.events.ServerMessageRoomDeleted | fishjam.events.ServerMessageRoomCrashed | fishjam.events.ServerMessagePeerAdded | fishjam.events.ServerMessagePeerDeleted | fishjam.events.ServerMessagePeerConnected | fishjam.events.ServerMessagePeerDisconnected | fishjam.events.ServerMessagePeerMetadataUpdated | fishjam.events.ServerMessagePeerCrashed | fishjam.events.ServerMessageStreamerConnected | fishjam.events.ServerMessageStreamerDisconnected | fishjam.events.ServerMessageChannelAdded | fishjam.events.ServerMessageChannelRemoved | fishjam.events.ServerMessageViewerConnected | fishjam.events.ServerMessageViewerDisconnected | fishjam.events.ServerMessageTrackAdded | fishjam.events.ServerMessageTrackRemoved | fishjam.events.ServerMessageTrackMetadataUpdated | fishjam.events.ServerMessageRecordingStatusChanged] | None:
101def receive_binary(
102    binary: bytes,
103) -> Union[AllowedNotification, List[AllowedNotification], None]:
104    """Transforms a received protobuf notification into a notification instance.
105
106    .. deprecated::
107        Use `decode_server_notifications` instead, which always returns a list
108        and handles batched payloads with a single, consistent return type.
109
110    The available notifications are listed in `fishjam.events` module.
111
112    Args:
113        binary: The raw binary data received from the webhook.
114
115    Returns:
116        AllowedNotification: A single notification when the payload carries one.
117        list[AllowedNotification]: The unpacked notifications, in order, when the
118            payload is a batch (webhook batching enabled).
119        None: When the payload is not a supported notification.
120    """
121    warnings.warn(
122        "receive_binary is deprecated; use decode_server_notifications instead.",
123        DeprecationWarning,
124        stacklevel=2,
125    )
126
127    message = ServerMessage().parse(binary)
128    _which, content = betterproto.which_one_of(message, "content")
129
130    if isinstance(content, ServerMessageNotificationBatch):
131        return _unpack_batch(content)
132
133    if isinstance(content, ALLOWED_NOTIFICATIONS):
134        validate_notification(content)
135        return content
136
137    return None

Transforms a received protobuf notification into a notification instance.

Deprecated since version .

  • Use decode_server_notifications instead, which always returns a list
  • and handles batched payloads with a single, consistent return type.

The available notifications are listed in fishjam.events module.

Args:

  • binary: The raw binary data received from the webhook.

Returns:

  • AllowedNotification: A single notification when the payload carries one.
  • list[AllowedNotification]: The unpacked notifications, in order, when the payload is a batch (webhook batching enabled).
  • None: When the payload is not a supported notification.
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
81def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
82    """Verify the `x-fishjam-signature-256` header of a raw webhook body.
83
84    Accepts the `sha256=<hex>` format sent by Fishjam (the prefix is
85    optional) and compares in constant time. Call this with the raw request
86    body before passing it to `decode_server_notifications`.
87
88    Args:
89        body: The raw binary body of the webhook request.
90        signature: The value of the `x-fishjam-signature-256` header.
91        secret: The webhook secret configured in Fishjam.
92
93    Returns:
94        bool: True when the signature matches the body, False otherwise.
95    """
96    expected = hmac.new(secret.encode(), body, "sha256").hexdigest()
97    provided = signature.strip().removeprefix("sha256=")
98    return hmac.compare_digest(provided, expected)

Verify the x-fishjam-signature-256 header of a raw webhook body.

Accepts the sha256=<hex> format sent by Fishjam (the prefix is optional) and compares in constant time. Call this with the raw request body before passing it to decode_server_notifications.

Args:

  • body: The raw binary body of the webhook request.
  • signature: The value of the x-fishjam-signature-256 header.
  • secret: The webhook secret configured in Fishjam.

Returns:

  • bool: True when the signature matches the body, False otherwise.
class PeerMetadata:
13@_attrs_define
14class PeerMetadata:
15    """Custom metadata set by the peer
16
17    Example:
18        {'name': 'FishjamUser'}
19
20    """
21
22    additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
23
24    def to_dict(self) -> dict[str, Any]:
25        field_dict: dict[str, Any] = {}
26        field_dict.update(self.additional_properties)
27
28        return field_dict
29
30    @classmethod
31    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
32        d = dict(src_dict)
33        peer_metadata = cls()
34
35        peer_metadata.additional_properties = d
36        return peer_metadata
37
38    @property
39    def additional_keys(self) -> list[str]:
40        return list(self.additional_properties.keys())
41
42    def __getitem__(self, key: str) -> Any:
43        return self.additional_properties[key]
44
45    def __setitem__(self, key: str, value: Any) -> None:
46        self.additional_properties[key] = value
47
48    def __delitem__(self, key: str) -> None:
49        del self.additional_properties[key]
50
51    def __contains__(self, key: str) -> bool:
52        return key in self.additional_properties

Custom metadata set by the peer

Example:

  • {'name': 'FishjamUser'}
PeerMetadata()
23def __init__(self, ):
24    self.additional_properties = __attr_factory_additional_properties()

Method generated by attrs for class PeerMetadata.

additional_properties: dict[str, typing.Any]
def to_dict(self) -> dict[str, typing.Any]:
24    def to_dict(self) -> dict[str, Any]:
25        field_dict: dict[str, Any] = {}
26        field_dict.update(self.additional_properties)
27
28        return field_dict
@classmethod
def from_dict(cls: type[~T], src_dict: Mapping[str, typing.Any]) -> ~T:
30    @classmethod
31    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
32        d = dict(src_dict)
33        peer_metadata = cls()
34
35        peer_metadata.additional_properties = d
36        return peer_metadata
additional_keys: list[str]
38    @property
39    def additional_keys(self) -> list[str]:
40        return list(self.additional_properties.keys())
@dataclass
class PeerOptions:
168@dataclass
169class PeerOptions:
170    """Options specific to a WebRTC Peer.
171
172    Attributes:
173        metadata: Peer metadata.
174        subscribe_mode: Configuration of peer's subscribing policy.
175    """
176
177    metadata: dict[str, Any] | None = None
178    """Peer metadata"""
179    subscribe_mode: Literal["auto", "manual"] = "auto"
180    """Configuration of peer's subscribing policy"""

Options specific to a WebRTC Peer.

Attributes:

  • metadata: Peer metadata.
  • subscribe_mode: Configuration of peer's subscribing policy.
PeerOptions( metadata: dict[str, Any] | None = None, subscribe_mode: Literal['auto', 'manual'] = 'auto')
metadata: dict[str, Any] | None = None

Peer metadata

subscribe_mode: Literal['auto', 'manual'] = 'auto'

Configuration of peer's subscribing policy

class PeerOptionsVapi:
15@_attrs_define
16class PeerOptionsVapi:
17    """Options specific to the VAPI peer
18
19    Attributes:
20        api_key (str): VAPI API key
21        call_id (str): VAPI call ID
22        auto_close (bool | Unset): Ends the VAPI call when the last participant leaves the room Default: False.
23        subscribe_mode (SubscribeMode | Unset): Configuration of peer's subscribing policy
24    """
25
26    api_key: str
27    call_id: str
28    auto_close: bool | Unset = False
29    subscribe_mode: SubscribeMode | Unset = UNSET
30
31    def to_dict(self) -> dict[str, Any]:
32        api_key = self.api_key
33
34        call_id = self.call_id
35
36        auto_close = self.auto_close
37
38        subscribe_mode: str | Unset = UNSET
39        if not isinstance(self.subscribe_mode, Unset):
40            subscribe_mode = self.subscribe_mode.value
41
42        field_dict: dict[str, Any] = {}
43
44        field_dict.update({
45            "apiKey": api_key,
46            "callId": call_id,
47        })
48        if auto_close is not UNSET:
49            field_dict["autoClose"] = auto_close
50        if subscribe_mode is not UNSET:
51            field_dict["subscribeMode"] = subscribe_mode
52
53        return field_dict
54
55    @classmethod
56    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
57        d = dict(src_dict)
58        api_key = d.pop("apiKey")
59
60        call_id = d.pop("callId")
61
62        auto_close = d.pop("autoClose", UNSET)
63
64        _subscribe_mode = d.pop("subscribeMode", UNSET)
65        subscribe_mode: SubscribeMode | Unset
66        if isinstance(_subscribe_mode, Unset):
67            subscribe_mode = UNSET
68        else:
69            subscribe_mode = SubscribeMode(_subscribe_mode)
70
71        peer_options_vapi = cls(
72            api_key=api_key,
73            call_id=call_id,
74            auto_close=auto_close,
75            subscribe_mode=subscribe_mode,
76        )
77
78        return peer_options_vapi

Options specific to the VAPI peer

Attributes:

  • api_key (str): VAPI API key
  • call_id (str): VAPI call ID
  • auto_close (bool | Unset): Ends the VAPI call when the last participant leaves the room Default: False.
  • subscribe_mode (SubscribeMode | Unset): Configuration of peer's subscribing policy
PeerOptionsVapi( api_key: str, call_id: str, auto_close: bool | fishjam._fishjam_openapi_client.types.Unset = False, subscribe_mode: fishjam._fishjam_openapi_client.models.subscribe_mode.SubscribeMode | fishjam._fishjam_openapi_client.types.Unset = <fishjam._fishjam_openapi_client.types.Unset object>)
26def __init__(self, api_key, call_id, auto_close=attr_dict['auto_close'].default, subscribe_mode=attr_dict['subscribe_mode'].default):
27    self.api_key = api_key
28    self.call_id = call_id
29    self.auto_close = auto_close
30    self.subscribe_mode = subscribe_mode

Method generated by attrs for class PeerOptionsVapi.

api_key: str
call_id: str
auto_close: bool | fishjam._fishjam_openapi_client.types.Unset
subscribe_mode: fishjam._fishjam_openapi_client.models.subscribe_mode.SubscribeMode | fishjam._fishjam_openapi_client.types.Unset
def to_dict(self) -> dict[str, typing.Any]:
31    def to_dict(self) -> dict[str, Any]:
32        api_key = self.api_key
33
34        call_id = self.call_id
35
36        auto_close = self.auto_close
37
38        subscribe_mode: str | Unset = UNSET
39        if not isinstance(self.subscribe_mode, Unset):
40            subscribe_mode = self.subscribe_mode.value
41
42        field_dict: dict[str, Any] = {}
43
44        field_dict.update({
45            "apiKey": api_key,
46            "callId": call_id,
47        })
48        if auto_close is not UNSET:
49            field_dict["autoClose"] = auto_close
50        if subscribe_mode is not UNSET:
51            field_dict["subscribeMode"] = subscribe_mode
52
53        return field_dict
@classmethod
def from_dict(cls: type[~T], src_dict: Mapping[str, typing.Any]) -> ~T:
55    @classmethod
56    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
57        d = dict(src_dict)
58        api_key = d.pop("apiKey")
59
60        call_id = d.pop("callId")
61
62        auto_close = d.pop("autoClose", UNSET)
63
64        _subscribe_mode = d.pop("subscribeMode", UNSET)
65        subscribe_mode: SubscribeMode | Unset
66        if isinstance(_subscribe_mode, Unset):
67            subscribe_mode = UNSET
68        else:
69            subscribe_mode = SubscribeMode(_subscribe_mode)
70
71        peer_options_vapi = cls(
72            api_key=api_key,
73            call_id=call_id,
74            auto_close=auto_close,
75            subscribe_mode=subscribe_mode,
76        )
77
78        return peer_options_vapi
@dataclass
class RoomOptions:
131@dataclass
132class RoomOptions:
133    """Description of a room options.
134
135    Attributes:
136        max_peers: Maximum amount of peers allowed into the room.
137        video_codec: Enforces video codec for each peer in the room.
138        webhook_url: URL where Fishjam notifications will be sent.
139        room_type: The use-case of the room. If not provided, this defaults
140            to conference.
141        public: True if livestream viewers can omit specifying a token.
142        batch_webhook_notifications: If true, webhook notifications for this room
143            are coalesced into a single NotificationBatch per HTTP send instead
144            of one request per notification.
145    """
146
147    max_peers: int | None = None
148    """Maximum amount of peers allowed into the room"""
149    video_codec: Literal["h264", "vp8"] | None = None
150    """Enforces video codec for each peer in the room"""
151    webhook_url: str | None = None
152    """URL where Fishjam notifications will be sent"""
153    room_type: Literal[
154        "conference",
155        "audio_only",
156        "livestream",
157        "full_feature",
158        "broadcaster",
159        "audio_only_livestream",
160    ] = "conference"
161    """The use-case of the room. If not provided, this defaults to conference."""
162    public: bool = False
163    """True if livestream viewers can omit specifying a token."""
164    batch_webhook_notifications: bool = False
165    """Coalesce webhook notifications into a single NotificationBatch per send."""

Description of a room options.

Attributes:

  • max_peers: Maximum amount of peers allowed into the room.
  • video_codec: Enforces video codec for each peer in the room.
  • webhook_url: URL where Fishjam notifications will be sent.
  • room_type: The use-case of the room. If not provided, this defaults to conference.
  • public: True if livestream viewers can omit specifying a token.
  • batch_webhook_notifications: If true, webhook notifications for this room are coalesced into a single NotificationBatch per HTTP send instead of one request per notification.
RoomOptions( max_peers: int | None = None, video_codec: Literal['h264', 'vp8'] | None = None, webhook_url: str | None = None, room_type: Literal['conference', 'audio_only', 'livestream', 'full_feature', 'broadcaster', 'audio_only_livestream'] = 'conference', public: bool = False, batch_webhook_notifications: bool = False)
max_peers: int | None = None

Maximum amount of peers allowed into the room

video_codec: Literal['h264', 'vp8'] | None = None

Enforces video codec for each peer in the room

webhook_url: str | None = None

URL where Fishjam notifications will be sent

room_type: Literal['conference', 'audio_only', 'livestream', 'full_feature', 'broadcaster', 'audio_only_livestream'] = 'conference'

The use-case of the room. If not provided, this defaults to conference.

public: bool = False

True if livestream viewers can omit specifying a token.

batch_webhook_notifications: bool = False

Coalesce webhook notifications into a single NotificationBatch per send.

@dataclass
class AgentOptions:
196@dataclass
197class AgentOptions:
198    """Options specific to an Agent Peer.
199
200    Attributes:
201        output: Configuration for the agent's output options.
202        subscribe_mode: Configuration of peer's subscribing policy.
203    """
204
205    output: AgentOutputOptions = field(default_factory=AgentOutputOptions)
206
207    subscribe_mode: Literal["auto", "manual"] = "auto"

Options specific to an Agent Peer.

Attributes:

  • output: Configuration for the agent's output options.
  • subscribe_mode: Configuration of peer's subscribing policy.
AgentOptions( output: AgentOutputOptions = <factory>, subscribe_mode: Literal['auto', 'manual'] = 'auto')
subscribe_mode: Literal['auto', 'manual'] = 'auto'
@dataclass
class AgentOutputOptions:
183@dataclass
184class AgentOutputOptions:
185    """Options of the desired format of audio tracks going from Fishjam to the agent.
186
187    Attributes:
188        audio_format: The format of the audio stream (e.g., 'pcm16').
189        audio_sample_rate: The sample rate of the audio stream.
190    """
191
192    audio_format: Literal["pcm16"] = "pcm16"
193    audio_sample_rate: Literal[16000, 24000] = 16000

Options of the desired format of audio tracks going from Fishjam to the agent.

Attributes:

  • audio_format: The format of the audio stream (e.g., 'pcm16').
  • audio_sample_rate: The sample rate of the audio stream.
AgentOutputOptions( audio_format: Literal['pcm16'] = 'pcm16', audio_sample_rate: Literal[16000, 24000] = 16000)
audio_format: Literal['pcm16'] = 'pcm16'
audio_sample_rate: Literal[16000, 24000] = 16000
@dataclass
class Room:
109@dataclass
110class Room:
111    """Description of the room state.
112
113    Attributes:
114        config: Room configuration.
115        id: Room ID.
116        peers: List of all peers.
117        composition_info: The composition the room's tracks are forwarded into,
118            when `FishjamClient.forward_room_tracks` has linked one.
119    """
120
121    config: RoomConfig
122    """Room configuration"""
123    id: str
124    """Room ID"""
125    peers: list[Peer]
126    """List of all peers"""
127    composition_info: CompositionInfo | None = None
128    """The composition the room's tracks are forwarded into"""

Description of the room state.

Attributes:

  • config: Room configuration.
  • id: Room ID.
  • peers: List of all peers.
  • composition_info: The composition the room's tracks are forwarded into, when FishjamClient.forward_room_tracks has linked one.
Room( config: fishjam._fishjam_openapi_client.models.room_config.RoomConfig, id: str, peers: list[Peer], composition_info: fishjam._fishjam_openapi_client.models.composition_info.CompositionInfo | None = None)
config: fishjam._fishjam_openapi_client.models.room_config.RoomConfig

Room configuration

id: str

Room ID

peers: list[Peer]

List of all peers

composition_info: fishjam._fishjam_openapi_client.models.composition_info.CompositionInfo | None = None

The composition the room's tracks are forwarded into

class Peer:
 23@_attrs_define
 24class Peer:
 25    """Describes peer status
 26
 27    Attributes:
 28        id (str): Assigned peer id Example: 4a1c1164-5fb7-425d-89d7-24cdb8fff1cf.
 29        metadata (None | PeerMetadata): Custom metadata set by the peer Example: {'name': 'FishjamUser'}.
 30        status (PeerStatus): Informs about the peer status Example: disconnected.
 31        subscribe_mode (SubscribeMode): Configuration of peer's subscribing policy
 32        subscriptions (Subscriptions): Describes peer's subscriptions in manual mode
 33        tracks (list[Track]): List of all peer's tracks
 34        type_ (PeerType): Peer type Example: webrtc.
 35    """
 36
 37    id: str
 38    metadata: None | PeerMetadata
 39    status: PeerStatus
 40    subscribe_mode: SubscribeMode
 41    subscriptions: Subscriptions
 42    tracks: list[Track]
 43    type_: PeerType
 44    additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
 45
 46    def to_dict(self) -> dict[str, Any]:
 47        from ..models.peer_metadata import PeerMetadata
 48
 49        id = self.id
 50
 51        metadata: dict[str, Any] | None
 52        if isinstance(self.metadata, PeerMetadata):
 53            metadata = self.metadata.to_dict()
 54        else:
 55            metadata = self.metadata
 56
 57        status = self.status.value
 58
 59        subscribe_mode = self.subscribe_mode.value
 60
 61        subscriptions = self.subscriptions.to_dict()
 62
 63        tracks = []
 64        for tracks_item_data in self.tracks:
 65            tracks_item = tracks_item_data.to_dict()
 66            tracks.append(tracks_item)
 67
 68        type_ = self.type_.value
 69
 70        field_dict: dict[str, Any] = {}
 71        field_dict.update(self.additional_properties)
 72        field_dict.update({
 73            "id": id,
 74            "metadata": metadata,
 75            "status": status,
 76            "subscribeMode": subscribe_mode,
 77            "subscriptions": subscriptions,
 78            "tracks": tracks,
 79            "type": type_,
 80        })
 81
 82        return field_dict
 83
 84    @classmethod
 85    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
 86        from ..models.peer_metadata import PeerMetadata
 87        from ..models.subscriptions import Subscriptions
 88        from ..models.track import Track
 89
 90        d = dict(src_dict)
 91        id = d.pop("id")
 92
 93        def _parse_metadata(data: object) -> None | PeerMetadata:
 94            if data is None:
 95                return data
 96            try:
 97                if not isinstance(data, dict):
 98                    raise TypeError()
 99                componentsschemas_peer_metadata_type_0 = PeerMetadata.from_dict(data)
100
101                return componentsschemas_peer_metadata_type_0
102            except (TypeError, ValueError, AttributeError, KeyError):
103                pass
104            return cast(None | PeerMetadata, data)
105
106        metadata = _parse_metadata(d.pop("metadata"))
107
108        status = PeerStatus(d.pop("status"))
109
110        subscribe_mode = SubscribeMode(d.pop("subscribeMode"))
111
112        subscriptions = Subscriptions.from_dict(d.pop("subscriptions"))
113
114        tracks = []
115        _tracks = d.pop("tracks")
116        for tracks_item_data in _tracks:
117            tracks_item = Track.from_dict(tracks_item_data)
118
119            tracks.append(tracks_item)
120
121        type_ = PeerType(d.pop("type"))
122
123        peer = cls(
124            id=id,
125            metadata=metadata,
126            status=status,
127            subscribe_mode=subscribe_mode,
128            subscriptions=subscriptions,
129            tracks=tracks,
130            type_=type_,
131        )
132
133        peer.additional_properties = d
134        return peer
135
136    @property
137    def additional_keys(self) -> list[str]:
138        return list(self.additional_properties.keys())
139
140    def __getitem__(self, key: str) -> Any:
141        return self.additional_properties[key]
142
143    def __setitem__(self, key: str, value: Any) -> None:
144        self.additional_properties[key] = value
145
146    def __delitem__(self, key: str) -> None:
147        del self.additional_properties[key]
148
149    def __contains__(self, key: str) -> bool:
150        return key in self.additional_properties

Describes peer status

Attributes:

  • id (str): Assigned peer id Example: 4a1c1164-5fb7-425d-89d7-24cdb8fff1cf.
  • metadata (None | PeerMetadata): Custom metadata set by the peer Example: {'name': 'FishjamUser'}.
  • status (PeerStatus): Informs about the peer status Example: disconnected.
  • subscribe_mode (SubscribeMode): Configuration of peer's subscribing policy
  • subscriptions (Subscriptions): Describes peer's subscriptions in manual mode
  • tracks (list[Track]): List of all peer's tracks
  • type_ (PeerType): Peer type Example: webrtc.
Peer( id: str, metadata: None | PeerMetadata, status: fishjam._fishjam_openapi_client.models.peer_status.PeerStatus, subscribe_mode: fishjam._fishjam_openapi_client.models.subscribe_mode.SubscribeMode, subscriptions: fishjam._fishjam_openapi_client.models.subscriptions.Subscriptions, tracks: list[fishjam._fishjam_openapi_client.models.track.Track], type_: fishjam._fishjam_openapi_client.models.peer_type.PeerType)
30def __init__(self, id, metadata, status, subscribe_mode, subscriptions, tracks, type_):
31    self.id = id
32    self.metadata = metadata
33    self.status = status
34    self.subscribe_mode = subscribe_mode
35    self.subscriptions = subscriptions
36    self.tracks = tracks
37    self.type_ = type_
38    self.additional_properties = __attr_factory_additional_properties()

Method generated by attrs for class Peer.

id: str
metadata: None | PeerMetadata
status: fishjam._fishjam_openapi_client.models.peer_status.PeerStatus
subscribe_mode: fishjam._fishjam_openapi_client.models.subscribe_mode.SubscribeMode
subscriptions: fishjam._fishjam_openapi_client.models.subscriptions.Subscriptions
tracks: list[fishjam._fishjam_openapi_client.models.track.Track]
type_: fishjam._fishjam_openapi_client.models.peer_type.PeerType
additional_properties: dict[str, typing.Any]
def to_dict(self) -> dict[str, typing.Any]:
46    def to_dict(self) -> dict[str, Any]:
47        from ..models.peer_metadata import PeerMetadata
48
49        id = self.id
50
51        metadata: dict[str, Any] | None
52        if isinstance(self.metadata, PeerMetadata):
53            metadata = self.metadata.to_dict()
54        else:
55            metadata = self.metadata
56
57        status = self.status.value
58
59        subscribe_mode = self.subscribe_mode.value
60
61        subscriptions = self.subscriptions.to_dict()
62
63        tracks = []
64        for tracks_item_data in self.tracks:
65            tracks_item = tracks_item_data.to_dict()
66            tracks.append(tracks_item)
67
68        type_ = self.type_.value
69
70        field_dict: dict[str, Any] = {}
71        field_dict.update(self.additional_properties)
72        field_dict.update({
73            "id": id,
74            "metadata": metadata,
75            "status": status,
76            "subscribeMode": subscribe_mode,
77            "subscriptions": subscriptions,
78            "tracks": tracks,
79            "type": type_,
80        })
81
82        return field_dict
@classmethod
def from_dict(cls: type[~T], src_dict: Mapping[str, typing.Any]) -> ~T:
 84    @classmethod
 85    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
 86        from ..models.peer_metadata import PeerMetadata
 87        from ..models.subscriptions import Subscriptions
 88        from ..models.track import Track
 89
 90        d = dict(src_dict)
 91        id = d.pop("id")
 92
 93        def _parse_metadata(data: object) -> None | PeerMetadata:
 94            if data is None:
 95                return data
 96            try:
 97                if not isinstance(data, dict):
 98                    raise TypeError()
 99                componentsschemas_peer_metadata_type_0 = PeerMetadata.from_dict(data)
100
101                return componentsschemas_peer_metadata_type_0
102            except (TypeError, ValueError, AttributeError, KeyError):
103                pass
104            return cast(None | PeerMetadata, data)
105
106        metadata = _parse_metadata(d.pop("metadata"))
107
108        status = PeerStatus(d.pop("status"))
109
110        subscribe_mode = SubscribeMode(d.pop("subscribeMode"))
111
112        subscriptions = Subscriptions.from_dict(d.pop("subscriptions"))
113
114        tracks = []
115        _tracks = d.pop("tracks")
116        for tracks_item_data in _tracks:
117            tracks_item = Track.from_dict(tracks_item_data)
118
119            tracks.append(tracks_item)
120
121        type_ = PeerType(d.pop("type"))
122
123        peer = cls(
124            id=id,
125            metadata=metadata,
126            status=status,
127            subscribe_mode=subscribe_mode,
128            subscriptions=subscriptions,
129            tracks=tracks,
130            type_=type_,
131        )
132
133        peer.additional_properties = d
134        return peer
additional_keys: list[str]
136    @property
137    def additional_keys(self) -> list[str]:
138        return list(self.additional_properties.keys())
class Recording:
 23@_attrs_define
 24class Recording:
 25    """A recording and its current lifecycle status
 26
 27    Attributes:
 28        files (list[RecordingFile]): Media files of the recording, in playback order. Empty until the recording is
 29            `available`.
 30        id (str): Assigned recording id
 31        source (CompositionSource | TemplateSource): The source for the recording
 32        status (RecordingStatus): Lifecycle status of a recording
 33        metadata (None | RecordingMetadataType0 | Unset): Free-form, user-supplied metadata used to organize and filter
 34            recordings
 35    """
 36
 37    files: list[RecordingFile]
 38    id: str
 39    source: CompositionSource | TemplateSource
 40    status: RecordingStatus
 41    metadata: None | RecordingMetadataType0 | Unset = UNSET
 42    additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
 43
 44    def to_dict(self) -> dict[str, Any]:
 45        from ..models.composition_source import CompositionSource
 46        from ..models.recording_metadata_type_0 import RecordingMetadataType0
 47
 48        files = []
 49        for files_item_data in self.files:
 50            files_item = files_item_data.to_dict()
 51            files.append(files_item)
 52
 53        id = self.id
 54
 55        source: dict[str, Any]
 56        if isinstance(self.source, CompositionSource):
 57            source = self.source.to_dict()
 58        else:
 59            source = self.source.to_dict()
 60
 61        status = self.status.value
 62
 63        metadata: dict[str, Any] | None | Unset
 64        if isinstance(self.metadata, Unset):
 65            metadata = UNSET
 66        elif isinstance(self.metadata, RecordingMetadataType0):
 67            metadata = self.metadata.to_dict()
 68        else:
 69            metadata = self.metadata
 70
 71        field_dict: dict[str, Any] = {}
 72        field_dict.update(self.additional_properties)
 73        field_dict.update({
 74            "files": files,
 75            "id": id,
 76            "source": source,
 77            "status": status,
 78        })
 79        if metadata is not UNSET:
 80            field_dict["metadata"] = metadata
 81
 82        return field_dict
 83
 84    @classmethod
 85    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
 86        from ..models.composition_source import CompositionSource
 87        from ..models.recording_file import RecordingFile
 88        from ..models.recording_metadata_type_0 import RecordingMetadataType0
 89        from ..models.template_source import TemplateSource
 90
 91        d = dict(src_dict)
 92        files = []
 93        _files = d.pop("files")
 94        for files_item_data in _files:
 95            files_item = RecordingFile.from_dict(files_item_data)
 96
 97            files.append(files_item)
 98
 99        id = d.pop("id")
100
101        def _parse_source(data: object) -> CompositionSource | TemplateSource:
102            try:
103                if not isinstance(data, dict):
104                    raise TypeError()
105                componentsschemas_recording_source_type_0 = CompositionSource.from_dict(
106                    data
107                )
108
109                return componentsschemas_recording_source_type_0
110            except (TypeError, ValueError, AttributeError, KeyError):
111                pass
112            if not isinstance(data, dict):
113                raise TypeError()
114            componentsschemas_recording_source_type_1 = TemplateSource.from_dict(data)
115
116            return componentsschemas_recording_source_type_1
117
118        source = _parse_source(d.pop("source"))
119
120        status = RecordingStatus(d.pop("status"))
121
122        def _parse_metadata(data: object) -> None | RecordingMetadataType0 | Unset:
123            if data is None:
124                return data
125            if isinstance(data, Unset):
126                return data
127            try:
128                if not isinstance(data, dict):
129                    raise TypeError()
130                metadata_type_0 = RecordingMetadataType0.from_dict(data)
131
132                return metadata_type_0
133            except (TypeError, ValueError, AttributeError, KeyError):
134                pass
135            return cast(None | RecordingMetadataType0 | Unset, data)
136
137        metadata = _parse_metadata(d.pop("metadata", UNSET))
138
139        recording = cls(
140            files=files,
141            id=id,
142            source=source,
143            status=status,
144            metadata=metadata,
145        )
146
147        recording.additional_properties = d
148        return recording
149
150    @property
151    def additional_keys(self) -> list[str]:
152        return list(self.additional_properties.keys())
153
154    def __getitem__(self, key: str) -> Any:
155        return self.additional_properties[key]
156
157    def __setitem__(self, key: str, value: Any) -> None:
158        self.additional_properties[key] = value
159
160    def __delitem__(self, key: str) -> None:
161        del self.additional_properties[key]
162
163    def __contains__(self, key: str) -> bool:
164        return key in self.additional_properties

A recording and its current lifecycle status

Attributes:

  • files (list[RecordingFile]): Media files of the recording, in playback order. Empty until the recording is available.
  • id (str): Assigned recording id
  • source (CompositionSource | TemplateSource): The source for the recording
  • status (RecordingStatus): Lifecycle status of a recording
  • metadata (None | RecordingMetadataType0 | Unset): Free-form, user-supplied metadata used to organize and filter recordings
Recording( files: list[fishjam._fishjam_openapi_client.models.recording_file.RecordingFile], id: str, source: fishjam._fishjam_openapi_client.models.composition_source.CompositionSource | fishjam._fishjam_openapi_client.models.template_source.TemplateSource, status: fishjam._fishjam_openapi_client.models.recording_status.RecordingStatus, metadata: None | fishjam._fishjam_openapi_client.models.recording_metadata_type_0.RecordingMetadataType0 | fishjam._fishjam_openapi_client.types.Unset = <fishjam._fishjam_openapi_client.types.Unset object>)
28def __init__(self, files, id, source, status, metadata=attr_dict['metadata'].default):
29    self.files = files
30    self.id = id
31    self.source = source
32    self.status = status
33    self.metadata = metadata
34    self.additional_properties = __attr_factory_additional_properties()

Method generated by attrs for class Recording.

files: list[fishjam._fishjam_openapi_client.models.recording_file.RecordingFile]
id: str
source: fishjam._fishjam_openapi_client.models.composition_source.CompositionSource | fishjam._fishjam_openapi_client.models.template_source.TemplateSource
status: fishjam._fishjam_openapi_client.models.recording_status.RecordingStatus
metadata: None | fishjam._fishjam_openapi_client.models.recording_metadata_type_0.RecordingMetadataType0 | fishjam._fishjam_openapi_client.types.Unset
additional_properties: dict[str, typing.Any]
def to_dict(self) -> dict[str, typing.Any]:
44    def to_dict(self) -> dict[str, Any]:
45        from ..models.composition_source import CompositionSource
46        from ..models.recording_metadata_type_0 import RecordingMetadataType0
47
48        files = []
49        for files_item_data in self.files:
50            files_item = files_item_data.to_dict()
51            files.append(files_item)
52
53        id = self.id
54
55        source: dict[str, Any]
56        if isinstance(self.source, CompositionSource):
57            source = self.source.to_dict()
58        else:
59            source = self.source.to_dict()
60
61        status = self.status.value
62
63        metadata: dict[str, Any] | None | Unset
64        if isinstance(self.metadata, Unset):
65            metadata = UNSET
66        elif isinstance(self.metadata, RecordingMetadataType0):
67            metadata = self.metadata.to_dict()
68        else:
69            metadata = self.metadata
70
71        field_dict: dict[str, Any] = {}
72        field_dict.update(self.additional_properties)
73        field_dict.update({
74            "files": files,
75            "id": id,
76            "source": source,
77            "status": status,
78        })
79        if metadata is not UNSET:
80            field_dict["metadata"] = metadata
81
82        return field_dict
@classmethod
def from_dict(cls: type[~T], src_dict: Mapping[str, typing.Any]) -> ~T:
 84    @classmethod
 85    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
 86        from ..models.composition_source import CompositionSource
 87        from ..models.recording_file import RecordingFile
 88        from ..models.recording_metadata_type_0 import RecordingMetadataType0
 89        from ..models.template_source import TemplateSource
 90
 91        d = dict(src_dict)
 92        files = []
 93        _files = d.pop("files")
 94        for files_item_data in _files:
 95            files_item = RecordingFile.from_dict(files_item_data)
 96
 97            files.append(files_item)
 98
 99        id = d.pop("id")
100
101        def _parse_source(data: object) -> CompositionSource | TemplateSource:
102            try:
103                if not isinstance(data, dict):
104                    raise TypeError()
105                componentsschemas_recording_source_type_0 = CompositionSource.from_dict(
106                    data
107                )
108
109                return componentsschemas_recording_source_type_0
110            except (TypeError, ValueError, AttributeError, KeyError):
111                pass
112            if not isinstance(data, dict):
113                raise TypeError()
114            componentsschemas_recording_source_type_1 = TemplateSource.from_dict(data)
115
116            return componentsschemas_recording_source_type_1
117
118        source = _parse_source(d.pop("source"))
119
120        status = RecordingStatus(d.pop("status"))
121
122        def _parse_metadata(data: object) -> None | RecordingMetadataType0 | Unset:
123            if data is None:
124                return data
125            if isinstance(data, Unset):
126                return data
127            try:
128                if not isinstance(data, dict):
129                    raise TypeError()
130                metadata_type_0 = RecordingMetadataType0.from_dict(data)
131
132                return metadata_type_0
133            except (TypeError, ValueError, AttributeError, KeyError):
134                pass
135            return cast(None | RecordingMetadataType0 | Unset, data)
136
137        metadata = _parse_metadata(d.pop("metadata", UNSET))
138
139        recording = cls(
140            files=files,
141            id=id,
142            source=source,
143            status=status,
144            metadata=metadata,
145        )
146
147        recording.additional_properties = d
148        return recording
additional_keys: list[str]
150    @property
151    def additional_keys(self) -> list[str]:
152        return list(self.additional_properties.keys())
class MoqAccess:
13@_attrs_define
14class MoqAccess:
15    """Connection details for a MoQ relay client
16
17    Attributes:
18        connection_url (str): Relay connection URL with the JWT embedded as a `?jwt=` query parameter. Pass directly to
19            a MoQ client SDK. Example: https://relay.fishjam.io/abc123?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
20        token (str): JWT authorizing the MoQ relay connection, also embedded in `connection_url` Example: eyJhbGciOiJIUz
21            I1NiIsInR5cCI6IkpXVCJ9.eyJyb290IjoiZmlzaGphbSIsInB1dCI6WyJteS1zdHJlYW0iXSwiZ2V0IjpbXSwiaWF0IjoxNzEzMzYwMDAwLCJle
22            HAiOjE3MTMzNjM2MDB9.abc123.
23    """
24
25    connection_url: str
26    token: str
27    additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
28
29    def to_dict(self) -> dict[str, Any]:
30        connection_url = self.connection_url
31
32        token = self.token
33
34        field_dict: dict[str, Any] = {}
35        field_dict.update(self.additional_properties)
36        field_dict.update({
37            "connection_url": connection_url,
38            "token": token,
39        })
40
41        return field_dict
42
43    @classmethod
44    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
45        d = dict(src_dict)
46        connection_url = d.pop("connection_url")
47
48        token = d.pop("token")
49
50        moq_access = cls(
51            connection_url=connection_url,
52            token=token,
53        )
54
55        moq_access.additional_properties = d
56        return moq_access
57
58    @property
59    def additional_keys(self) -> list[str]:
60        return list(self.additional_properties.keys())
61
62    def __getitem__(self, key: str) -> Any:
63        return self.additional_properties[key]
64
65    def __setitem__(self, key: str, value: Any) -> None:
66        self.additional_properties[key] = value
67
68    def __delitem__(self, key: str) -> None:
69        del self.additional_properties[key]
70
71    def __contains__(self, key: str) -> bool:
72        return key in self.additional_properties

Connection details for a MoQ relay client

Attributes:

  • connection_url (str): Relay connection URL with the JWT embedded as a ?jwt= query parameter. Pass directly to a MoQ client SDK. Example: https://relay.fishjam.io/abc123?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
  • token (str): JWT authorizing the MoQ relay connection, also embedded in connection_url Example: eyJhbGciOiJIUz I1NiIsInR5cCI6IkpXVCJ9.eyJyb290IjoiZmlzaGphbSIsInB1dCI6WyJteS1zdHJlYW0iXSwiZ2V0IjpbXSwiaWF0IjoxNzEzMzYwMDAwLCJle HAiOjE3MTMzNjM2MDB9.abc123.
MoqAccess(connection_url: str, token: str)
25def __init__(self, connection_url, token):
26    self.connection_url = connection_url
27    self.token = token
28    self.additional_properties = __attr_factory_additional_properties()

Method generated by attrs for class MoqAccess.

connection_url: str
token: str
additional_properties: dict[str, typing.Any]
def to_dict(self) -> dict[str, typing.Any]:
29    def to_dict(self) -> dict[str, Any]:
30        connection_url = self.connection_url
31
32        token = self.token
33
34        field_dict: dict[str, Any] = {}
35        field_dict.update(self.additional_properties)
36        field_dict.update({
37            "connection_url": connection_url,
38            "token": token,
39        })
40
41        return field_dict
@classmethod
def from_dict(cls: type[~T], src_dict: Mapping[str, typing.Any]) -> ~T:
43    @classmethod
44    def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
45        d = dict(src_dict)
46        connection_url = d.pop("connection_url")
47
48        token = d.pop("token")
49
50        moq_access = cls(
51            connection_url=connection_url,
52            token=token,
53        )
54
55        moq_access.additional_properties = d
56        return moq_access
additional_keys: list[str]
58    @property
59    def additional_keys(self) -> list[str]:
60        return list(self.additional_properties.keys())
class MissingFishjamIdError(builtins.ValueError):
 8class MissingFishjamIdError(ValueError):
 9    def __init__(self) -> None:
10        super().__init__("Fishjam ID is required")

Inappropriate argument value (of correct type).

Inherited Members
builtins.BaseException
with_traceback
add_note
args
class InvalidFishjamCredentialsError(fishjam.errors.HTTPError):
104class InvalidFishjamCredentialsError(HTTPError):
105    def __init__(self, errors):
106        """@private"""
107        super().__init__(errors)
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class StaleSdkError(builtins.Exception):
13class StaleSdkError(Exception):
14    def __init__(self, status: int) -> None:
15        super().__init__(
16            f"Received a recording status this SDK cannot parse ({int(status)})."
17            " You are probably using an outdated version of fishjam-server-sdk"
18            " - please update it."
19        )
20        self.status = int(status)
21        """Raw wire value received from the server."""

Common base class for all non-exit exceptions.

StaleSdkError(status: int)
14    def __init__(self, status: int) -> None:
15        super().__init__(
16            f"Received a recording status this SDK cannot parse ({int(status)})."
17            " You are probably using an outdated version of fishjam-server-sdk"
18            " - please update it."
19        )
20        self.status = int(status)
21        """Raw wire value received from the server."""
status

Raw wire value received from the server.

Inherited Members
builtins.BaseException
with_traceback
add_note
args