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.
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"
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
tokenitself.
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.
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.
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.
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.
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.
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.
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.
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.
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
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")
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
FishjamClientis 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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_notificationsinstead, 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.
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-256header. - secret: The webhook secret configured in Fishjam.
Returns:
- bool: True when the signature matches the body, False otherwise.
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'}
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.
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
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.
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
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
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.
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.
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.
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_trackshas linked one.
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.
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.
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
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
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
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.
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
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
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_urlExample: eyJhbGciOiJIUz I1NiIsInR5cCI6IkpXVCJ9.eyJyb290IjoiZmlzaGphbSIsInB1dCI6WyJteS1zdHJlYW0iXSwiZ2V0IjpbXSwiaWF0IjoxNzEzMzYwMDAwLCJle HAiOjE3MTMzNjM2MDB9.abc123.
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.
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
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
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
104class InvalidFishjamCredentialsError(HTTPError): 105 def __init__(self, errors): 106 """@private""" 107 super().__init__(errors)
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
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.
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."""
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args