From 3b714ddcd6aa5e74ab0e339b8312f10b888eea6a Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Wed, 25 Sep 2024 16:32:14 +0200 Subject: [PATCH] Update API --- fishjam/__init__.py | 76 +------- fishjam/_ws_notifier.py | 14 +- fishjam/api/_base_api.py | 13 +- fishjam/api/_recording_api.py | 37 ---- fishjam/api/_room_api.py | 293 +++++++++++------------------- tests/support/peer_socket.py | 8 +- tests/test_notifier.py | 95 +++------- tests/test_recording_api.py | 31 ---- tests/test_room_api.py | 325 ++++++---------------------------- 9 files changed, 212 insertions(+), 680 deletions(-) delete mode 100644 fishjam/api/_recording_api.py delete mode 100644 tests/test_recording_api.py diff --git a/fishjam/__init__.py b/fishjam/__init__.py index 4290ef1..eb79325 100644 --- a/fishjam/__init__.py +++ b/fishjam/__init__.py @@ -7,79 +7,13 @@ # Exceptions and Server Messages from fishjam import errors, events -# Models -from fishjam._openapi_client.models import ( - ComponentFile, - ComponentHLS, - ComponentOptionsFile, - ComponentOptionsHLS, - ComponentOptionsHLSSubscribeMode, - ComponentOptionsRecording, - ComponentOptionsRecordingSubscribeMode, - ComponentOptionsRTSP, - ComponentOptionsSIP, - ComponentPropertiesFile, - ComponentPropertiesHLS, - ComponentPropertiesHLSSubscribeMode, - ComponentPropertiesRecording, - ComponentPropertiesRecordingSubscribeMode, - ComponentPropertiesRTSP, - ComponentPropertiesSIP, - ComponentPropertiesSIPSIPCredentials, - ComponentRecording, - ComponentRTSP, - ComponentSIP, - Peer, - PeerOptionsWebRTC, - PeerStatus, - Room, - RoomConfig, - RoomConfigVideoCodec, - S3Credentials, - SIPCredentials, -) - # API from fishjam._webhook_notifier import receive_binary from fishjam._ws_notifier import Notifier -from fishjam.api._recording_api import RecordingApi -from fishjam.api._room_api import RoomApi +from fishjam.api._room_api import ( + RoomApi, + RoomOptions, + PeerOptions, +) -__all__ = [ - "RoomApi", - "RecordingApi", - "Notifier", - "receive_binary", - "Room", - "RoomConfig", - "RoomConfigVideoCodec", - "Peer", - "PeerOptionsWebRTC", - "PeerStatus", - "ComponentHLS", - "ComponentOptionsHLS", - "ComponentOptionsHLSSubscribeMode", - "ComponentPropertiesHLS", - "ComponentPropertiesHLSSubscribeMode", - "ComponentSIP", - "ComponentOptionsSIP", - "ComponentPropertiesSIP", - "ComponentPropertiesSIPSIPCredentials", - "ComponentFile", - "ComponentRTSP", - "ComponentOptionsRTSP", - "ComponentPropertiesRTSP", - "ComponentFile", - "ComponentOptionsFile", - "ComponentPropertiesFile", - "events", - "errors", - "SIPCredentials", - "ComponentRecording", - "ComponentOptionsRecording", - "ComponentOptionsRecordingSubscribeMode", - "ComponentPropertiesRecording", - "ComponentPropertiesRecordingSubscribeMode", - "S3Credentials", -] __docformat__ = "restructuredtext" diff --git a/fishjam/_ws_notifier.py b/fishjam/_ws_notifier.py index bafe08b..a2da35b 100644 --- a/fishjam/_ws_notifier.py +++ b/fishjam/_ws_notifier.py @@ -25,20 +25,16 @@ class Notifier: Allows for receiving WebSocket messages from Fishjam. """ - def __init__( - self, - server_address: str = "localhost:5002", - server_api_token: str = "development", - secure: bool = False, - ): + def __init__(self, fishjam_url: str, management_token: str): """ Create Notifier instance, providing the fishjam address and api token. Set secure to `True` for `wss` and `False` for `ws` connection (default). """ - protocol = "wss" if secure else "ws" - self._server_address = f"{protocol}://{server_address}/socket/server/websocket" - self._server_api_token = server_api_token + self._server_address = ( + f"{fishjam_url.replace('http', 'ws')}/socket/server/websocket" + ) + self._server_api_token = management_token self._websocket = None self._ready = False diff --git a/fishjam/api/_base_api.py b/fishjam/api/_base_api.py index 0fea83a..610938a 100644 --- a/fishjam/api/_base_api.py +++ b/fishjam/api/_base_api.py @@ -5,17 +5,8 @@ class BaseApi: - def __init__( - self, - server_address: str = "localhost:5002", - server_api_token: str = "development", - secure: bool = False, - ): - protocol = "https" if secure else "http" - - self.client = AuthenticatedClient( - f"{protocol}://{server_address}", token=server_api_token - ) + def __init__(self, fishjam_url: str, management_token: str): + self.client = AuthenticatedClient(f"{fishjam_url}", token=management_token) def _request(self, method, **kwargs): response: Response = method.sync_detailed(client=self.client, **kwargs) diff --git a/fishjam/api/_recording_api.py b/fishjam/api/_recording_api.py deleted file mode 100644 index 1bec10a..0000000 --- a/fishjam/api/_recording_api.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -RecordingApi used to manage rooms -""" - -from fishjam._openapi_client.api.recording import delete_recording, get_recordings -from fishjam.api._base_api import BaseApi - - -class RecordingApi(BaseApi): - """Allows for managing recordings""" - - def __init__( - self, - server_address: str = "localhost:5002", - server_api_token: str = "development", - secure: bool = False, - ): - """ - Create RecordingApi instance, providing the fishjam address and api token. - Set secure to `True` for `https` and `False` for `http` connection (default). - """ - - super().__init__( - server_address=server_address, - server_api_token=server_api_token, - secure=secure, - ) - - def get_list(self) -> list: - """Returns a list of available recordings""" - - return self._request(get_recordings).data - - def delete(self, recording_id: str): - """Deletes recording with given id""" - - return self._request(delete_recording, recording_id=recording_id) diff --git a/fishjam/api/_room_api.py b/fishjam/api/_room_api.py index e05b901..bbb4380 100644 --- a/fishjam/api/_room_api.py +++ b/fishjam/api/_room_api.py @@ -2,236 +2,159 @@ RoomApi used to manage rooms """ -from typing import List, Literal, Tuple, Union +from dataclasses import dataclass +from typing import Tuple, NewType, List, Literal + -from fishjam._openapi_client.api.room import add_component as room_add_component from fishjam._openapi_client.api.room import add_peer as room_add_peer from fishjam._openapi_client.api.room import create_room as room_create_room -from fishjam._openapi_client.api.room import delete_component as room_delete_component from fishjam._openapi_client.api.room import delete_peer as room_delete_peer from fishjam._openapi_client.api.room import delete_room as room_delete_room from fishjam._openapi_client.api.room import get_all_rooms as room_get_all_rooms from fishjam._openapi_client.api.room import get_room as room_get_room -from fishjam._openapi_client.api.room import subscribe_to -from fishjam._openapi_client.api.sip import dial as sip_dial -from fishjam._openapi_client.api.sip import end_call as sip_end_call from fishjam._openapi_client.models import ( - AddComponentJsonBody, AddPeerJsonBody, - ComponentFile, - ComponentHLS, - ComponentOptionsFile, - ComponentOptionsHLS, - ComponentOptionsRecording, - ComponentOptionsRTSP, - ComponentOptionsSIP, - ComponentRecording, - ComponentRTSP, - ComponentSIP, - DialConfig, - PeerDetailsResponseData, - PeerOptionsWebRTC, - Room, RoomConfig, - RoomConfigVideoCodec, - SubscriptionConfig, + Peer, + PeerOptionsWebRTC, +) +from fishjam._openapi_client.models.room_config_video_codec import RoomConfigVideoCodec +from fishjam._openapi_client.models.peer_options_web_rtc_metadata import ( + PeerOptionsWebRTCMetadata, ) + from fishjam.api._base_api import BaseApi +PeerToken = NewType("PeerToken", str) + + +@dataclass +class Room: + """Description of the room state""" + + config: RoomConfig + """Room configuration""" + id: str + """Room ID""" + peers: List[Peer] + """List of all peers""" + + +@dataclass +class RoomOptions: + """Description of a room options""" + + max_peers: int = None + """Maximum amount of peers allowed into the room""" + peer_disconnected_timeout: int = None + """Duration (in seconds) after which the peer will be removed if it is disconnected. If not provided, this feature is disabled.""" + peerless_purge_timeout: int = None + """Duration (in seconds) after which the room will be removed if no peers are connected. If not provided, this feature is disabled.""" + room_id: str = None + """Custom id used for identifying room within Fishjam. Must be unique across all rooms. If not provided, random UUID is generated.""" + video_codec: Literal["h264", "vp8"] = None + """Enforces video codec for each peer in the room""" + webhook_url: str = None + """URL where Fishjam notifications will be sent""" + + +@dataclass +class PeerOptions: + """Options specific to the Peer""" + + enable_simulcast: bool = True + """Enables the peer to use simulcast""" + metadata: dict = None + """Peer metadata""" + class RoomApi(BaseApi): """Allows for managing rooms""" - def __init__( - self, - server_address: str = "localhost:5002", - server_api_token: str = "development", - secure: bool = False, - ): + def __init__(self, fishjam_url: str, management_token: str): """ - Create RoomApi instance, providing the fishjam address and api token. - Set secure to `True` for `https` and `False` for `http` connection (default). + Create RoomApi instance, providing the fishjam url and managment token. """ - super().__init__( - server_address=server_address, - server_api_token=server_api_token, - secure=secure, - ) + super().__init__(fishjam_url=fishjam_url, management_token=management_token) - def create_room( - self, - room_id: str = None, - max_peers: int = None, - video_codec: Literal["h264", "vp8"] = None, - webhook_url: str = None, - peerless_purge_timeout: int = None, - peer_disconnected_timeout: int = None, - ) -> Tuple[str, Room]: + def create_peer( + self, room_id: str, options: PeerOptions = PeerOptions() + ) -> Tuple[Peer, PeerToken]: """ - Creates a new room + Creates peer in the room - Returns a tuple (`fishjam_address`, `Room`) - the address of the Fishjam - in which the room has been created and the created `Room` + Returns a tuple (`Peer`, `PeerToken`) - the token is needed by Peer + to authenticate to Fishjam. - The returned address may be different from the current `RoomApi` instance. - In such case, a new `RoomApi` instance has to be created using - the returned address in order to interact with the room. + The possible options to pass for peer are `PeerOptions`. """ - if video_codec is not None: - video_codec = RoomConfigVideoCodec(video_codec) - else: - video_codec = None - - room_config = RoomConfig( - room_id=room_id, - max_peers=max_peers, - video_codec=video_codec, - webhook_url=webhook_url, - peerless_purge_timeout=peerless_purge_timeout, - peer_disconnected_timeout=peer_disconnected_timeout, + peer_type = "webrtc" + peer_metadata = self.__parse_peer_metadata(options.metadata) + peer_options = PeerOptionsWebRTC( + enable_simulcast=options.enable_simulcast, metadata=peer_metadata ) + json_body = AddPeerJsonBody(type=peer_type, options=peer_options) - resp = self._request(room_create_room, json_body=room_config) - return (resp.data.fishjam_address, resp.data.room) - - def delete_room(self, room_id: str) -> None: - """Deletes a room""" - - return self._request(room_delete_room, room_id=room_id) + resp = self._request(room_add_peer, room_id=room_id, json_body=json_body) - def get_all_rooms(self) -> list: - """Returns list of all rooms""" + return (resp.data.peer, resp.data.token) - return self._request(room_get_all_rooms).data + def create_room(self, options: RoomOptions = RoomOptions()) -> Room: + """ + Creates a new room + Returns the created `Room` + """ - def get_room(self, room_id: str) -> Room: - """Returns room with the given id""" + codec = None + if options.video_codec: + codec = RoomConfigVideoCodec(options.video_codec) + + config = RoomConfig( + max_peers=options.max_peers, + peer_disconnected_timeout=options.peer_disconnected_timeout, + peerless_purge_timeout=options.peerless_purge_timeout, + room_id=options.room_id, + video_codec=codec, + webhook_url=options.webhook_url, + ) + room = self._request(room_create_room, json_body=config).data.room - return self._request(room_get_room, room_id=room_id).data + return Room(config=room.config, id=room.id, peers=room.peers) - def add_peer( - self, room_id: str, options: PeerOptionsWebRTC - ) -> PeerDetailsResponseData: - """ - Creates peer in the room + def get_all_rooms(self) -> list[Room]: + """Returns list of all rooms""" - Currently only `webrtc` peer is supported + rooms = self._request(room_get_all_rooms).data - Returns a tuple (`peer_token`, `Peer`) - the token needed by Peer - to authenticate to Fishjam and the new `Peer`. + return [ + Room(config=room.config, id=room.id, peers=room.peers) for room in rooms + ] - The possible options to pass for peer are `PeerOptionsWebRTC`. - """ + def get_room(self, room_id: str) -> Room: + """Returns room with the given id""" - peer_type = "webrtc" - json_body = AddPeerJsonBody(type=peer_type, options=options) + room = self._request(room_get_room, room_id=room_id).data - resp = self._request(room_add_peer, room_id=room_id, json_body=json_body) - return PeerDetailsResponseData( - peer=resp.data.peer, - token=resp.data.token, - peer_websocket_url=resp.data.peer_websocket_url, - ) + return Room(config=room.config, id=room.id, peers=room.peers) def delete_peer(self, room_id: str, peer_id: str) -> None: """Deletes peer""" return self._request(room_delete_peer, id=peer_id, room_id=room_id) - def add_component( - self, - room_id: str, - options: Union[ - ComponentOptionsFile, - ComponentOptionsHLS, - ComponentOptionsRecording, - ComponentOptionsRTSP, - ComponentOptionsSIP, - ], - ) -> Union[ - ComponentFile, ComponentHLS, ComponentRecording, ComponentRTSP, ComponentSIP - ]: - """ - Creates component in the room. - Currently there are 4 different components: - * File Component for which the options are `ComponentOptionsFile` - * HLS Component which options are `ComponentOptionsHLS` - * Recording Component which options are `ComponentOptionsRecording` - * RTSP Component which options are `ComponentOptionsRTSP` - * SIP Component which options are `ComponentOptionsSIP` - """ - - if isinstance(options, ComponentOptionsFile): - component_type = "file" - elif isinstance(options, ComponentOptionsHLS): - component_type = "hls" - elif isinstance(options, ComponentOptionsRecording): - component_type = "recording" - elif isinstance(options, ComponentOptionsRTSP): - component_type = "rtsp" - elif isinstance(options, ComponentOptionsSIP): - component_type = "sip" - else: - raise ValueError( - "options must be ComponentOptionsFile, ComponentOptionsHLS," - "ComponentOptionsRTSP, ComponentOptionsRecording or ComponentOptionsSIP" - ) - - json_body = AddComponentJsonBody(type=component_type, options=options) - - return self._request( - room_add_component, room_id=room_id, json_body=json_body - ).data - - def delete_component(self, room_id: str, component_id: str) -> None: - """Deletes component""" - - return self._request(room_delete_component, id=component_id, room_id=room_id) - - def subscribe(self, room_id: str, component_id: str, origins: List[str]): - """ - In order to subscribe the component to peers/components, - the component should be initialized with the subscribe_mode set to manual. - This mode proves beneficial when you do not wish to record or stream - all the available streams within a room. - It allows for selective addition instead – - you can manually select specific streams. - For instance, you could opt to record only the stream of an event's host. - """ - - return self._request( - subscribe_to, - room_id=room_id, - component_id=component_id, - json_body=SubscriptionConfig(origins=origins), - ) - - def sip_dial(self, room_id: str, component_id: str, phone_number: str): - """ - Starts a phone call from a specified component to a provided phone number. + def delete_room(self, room_id: str) -> None: + """Deletes a room""" - This is asynchronous operation. - In case of providing incorrect phone number you will receive - notification `ComponentCrashed`. - """ + return self._request(room_delete_room, room_id=room_id) - return self._request( - sip_dial, - room_id=room_id, - component_id=component_id, - json_body=DialConfig(phone_number=phone_number), - ) + def __parse_peer_metadata(self, metadata: dict) -> PeerOptionsWebRTCMetadata: + peer_metadata = PeerOptionsWebRTCMetadata() - def sip_end_call(self, room_id: str, component_id: str): - """ - End a phone call on a specified SIP component. + if not metadata: + return peer_metadata - This is asynchronous operation. - """ + for key, value in metadata.items(): + peer_metadata.additional_properties[key] = value - return self._request( - sip_end_call, - room_id=room_id, - component_id=component_id, - ) + return peer_metadata diff --git a/tests/support/peer_socket.py b/tests/support/peer_socket.py index 1651b4c..589572e 100644 --- a/tests/support/peer_socket.py +++ b/tests/support/peer_socket.py @@ -14,15 +14,17 @@ class PeerSocket: - def __init__(self, socket_address, auto_close=False): - self._socket_address = socket_address + def __init__(self, fishjam_url, auto_close=False): + self._socket_address = ( + f"{fishjam_url.replace('http', 'ws')}/socket/peer/websocket" + ) self._ready = False self._ready_event = None self._auto_close = auto_close async def connect(self, token): - async with client.connect(f"ws://{self._socket_address}") as websocket: + async with client.connect(self._socket_address) as websocket: msg = PeerMessage(auth_request=PeerMessageAuthRequest(token=token)) await websocket.send(bytes(msg)) diff --git a/tests/test_notifier.py b/tests/test_notifier.py index b84ee7a..a1898ad 100644 --- a/tests/test_notifier.py +++ b/tests/test_notifier.py @@ -9,7 +9,8 @@ import pytest import requests -from fishjam import ComponentOptionsFile, Notifier, PeerOptionsWebRTC, RoomApi +from fishjam import Notifier, RoomApi, RoomOptions + from fishjam.events import ( ServerMessageMetricsReport, ServerMessagePeerAdded, @@ -18,23 +19,18 @@ ServerMessagePeerDisconnected, ServerMessageRoomCreated, ServerMessageRoomDeleted, - ServerMessageTrackAdded, - ServerMessageTrackRemoved, ) from tests.support.asyncio_utils import assert_events, assert_metrics, cancel from tests.support.peer_socket import PeerSocket from tests.support.webhook_notifier import run_server HOST = "fishjam" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -SERVER_ADDRESS = f"{HOST}:5002" +FISHJAM_URL = f"http://{HOST}:5002" SERVER_API_TOKEN = "development" WEBHOOK_ADDRESS = "test" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" WEBHOOK_URL = f"http://{WEBHOOK_ADDRESS}:5000/webhook" queue = Queue() -CODEC_H264 = "h264" -FILE_OPTIONS = ComponentOptionsFile(file_path="video.h264") - @pytest.fixture(scope="session", autouse=True) def start_server(): @@ -61,9 +57,7 @@ def start_server(): class TestConnectingToServer: @pytest.mark.asyncio async def test_valid_credentials(self): - notifier = Notifier( - server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN - ) + notifier = Notifier(fishjam_url=FISHJAM_URL, management_token=SERVER_API_TOKEN) notifier_task = asyncio.create_task(notifier.connect()) await notifier.wait_ready() @@ -74,9 +68,7 @@ async def test_valid_credentials(self): @pytest.mark.asyncio async def test_invalid_credentials(self): - notifier = Notifier( - server_address=SERVER_ADDRESS, server_api_token="wrong_token" - ) + notifier = Notifier(fishjam_url=FISHJAM_URL, management_token="wrong_token") task = asyncio.create_task(notifier.connect()) @@ -86,14 +78,12 @@ async def test_invalid_credentials(self): @pytest.fixture def room_api(): - return RoomApi(server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN) + return RoomApi(fishjam_url=FISHJAM_URL, management_token=SERVER_API_TOKEN) @pytest.fixture def notifier(): - notifier = Notifier( - server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN - ) + notifier = Notifier(fishjam_url=FISHJAM_URL, management_token=SERVER_API_TOKEN) return notifier @@ -107,7 +97,8 @@ async def test_room_created_deleted(self, room_api: RoomApi, notifier: Notifier) notifier_task = asyncio.create_task(notifier.connect()) await notifier.wait_ready() - _, room = room_api.create_room(webhook_url=WEBHOOK_URL) + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) room_api.delete_room(room.id) @@ -134,17 +125,16 @@ async def test_peer_connected_disconnected( notifier_task = asyncio.create_task(notifier.connect()) await notifier.wait_ready() - _, room = room_api.create_room(webhook_url=WEBHOOK_URL) + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) - result = room_api.add_peer(room.id, options=PeerOptionsWebRTC()) - - peer_socket = PeerSocket(socket_address=result.peer_websocket_url) - peer_task = asyncio.create_task(peer_socket.connect(result.token)) + peer, token = room_api.create_peer(room.id) + peer_socket = PeerSocket(fishjam_url=FISHJAM_URL) + peer_task = asyncio.create_task(peer_socket.connect(token)) await peer_socket.wait_ready() - room_api.delete_peer(room.id, result.peer.id) - + room_api.delete_peer(room.id, peer.id) room_api.delete_room(room.id) await assert_task @@ -172,18 +162,17 @@ async def test_peer_connected_disconnected_deleted( notifier_task = asyncio.create_task(notifier.connect()) await notifier.wait_ready() - _, room = room_api.create_room( + options = RoomOptions( webhook_url=WEBHOOK_URL, peerless_purge_timeout=2, peer_disconnected_timeout=1, ) + room = room_api.create_room(options=options) - result = room_api.add_peer(room.id, options=PeerOptionsWebRTC()) + _peer, token = room_api.create_peer(room.id) - peer_socket = PeerSocket( - socket_address=result.peer_websocket_url, auto_close=True - ) - peer_task = asyncio.create_task(peer_socket.connect(result.token)) + peer_socket = PeerSocket(fishjam_url=FISHJAM_URL, auto_close=True) + peer_task = asyncio.create_task(peer_socket.connect(token)) await peer_socket.wait_ready() @@ -210,11 +199,12 @@ async def test_peer_connected_room_deleted( notifier_task = asyncio.create_task(notifier.connect()) await notifier.wait_ready() - _, room = room_api.create_room(webhook_url=WEBHOOK_URL) - result = room_api.add_peer(room.id, options=PeerOptionsWebRTC()) + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) + _peer, token = room_api.create_peer(room.id) - peer_socket = PeerSocket(socket_address=result.peer_websocket_url) - peer_task = asyncio.create_task(peer_socket.connect(result.token)) + peer_socket = PeerSocket(fishjam_url=FISHJAM_URL) + peer_task = asyncio.create_task(peer_socket.connect(token)) await peer_socket.wait_ready() @@ -227,33 +217,6 @@ async def test_peer_connected_room_deleted( for event in event_checks: self.assert_event(event) - @pytest.mark.asyncio - @pytest.mark.file_component_sources - async def test_file_component_connected_room_deleted( - self, room_api: RoomApi, notifier: Notifier - ): - event_checks = [ - ServerMessageRoomCreated, - ServerMessageTrackAdded, - ServerMessageTrackRemoved, - ServerMessageRoomDeleted, - ] - assert_task = asyncio.create_task(assert_events(notifier, event_checks.copy())) - - notifier_task = asyncio.create_task(notifier.connect()) - await notifier.wait_ready() - - _, room = room_api.create_room(webhook_url=WEBHOOK_URL) - room_api.add_component(room.id, options=FILE_OPTIONS) - - room_api.delete_room(room.id) - - await assert_task - await cancel(notifier_task) - - for event in event_checks: - self.assert_event(event) - def assert_event(self, event): data = queue.get(timeout=2.5) assert data == event or isinstance(data, event) @@ -262,11 +225,11 @@ def assert_event(self, event): class TestReceivingMetrics: @pytest.mark.asyncio async def test_metrics_with_one_peer(self, room_api: RoomApi, notifier: Notifier): - _, room = room_api.create_room() - result = room_api.add_peer(room.id, PeerOptionsWebRTC()) + room = room_api.create_room() + _peer, token = room_api.create_peer(room.id) - peer_socket = PeerSocket(socket_address=result.peer_websocket_url) - peer_task = asyncio.create_task(peer_socket.connect(result.token)) + peer_socket = PeerSocket(fishjam_url=FISHJAM_URL) + peer_task = asyncio.create_task(peer_socket.connect(token)) await peer_socket.wait_ready() diff --git a/tests/test_recording_api.py b/tests/test_recording_api.py deleted file mode 100644 index ee358de..0000000 --- a/tests/test_recording_api.py +++ /dev/null @@ -1,31 +0,0 @@ -# pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring - -import os - -import pytest - -from fishjam import RecordingApi -from fishjam.errors import NotFoundError - -HOST = "fishjam" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -SERVER_ADDRESS = f"{HOST}:5002" -SERVER_API_TOKEN = "development" - - -@pytest.fixture -def recording_api(): - return RecordingApi( - server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN - ) - - -class TestGetList: - def test_valid(self, recording_api: RecordingApi): - all_rooms = recording_api.get_list() - assert isinstance(all_rooms, list) - - -class TestDelete: - def test_invalid_recording(self, recording_api: RecordingApi): - with pytest.raises(NotFoundError): - recording_api.delete("invalid-id") diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 061e073..1b71a12 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -3,40 +3,19 @@ import os import uuid -from dataclasses import dataclass import pytest from fishjam import ( - ComponentFile, - ComponentHLS, - ComponentOptionsFile, - ComponentOptionsHLS, - ComponentOptionsHLSSubscribeMode, - ComponentOptionsRecording, - ComponentOptionsRecordingSubscribeMode, - ComponentOptionsRTSP, - ComponentOptionsSIP, - ComponentPropertiesFile, - ComponentPropertiesHLS, - ComponentPropertiesHLSSubscribeMode, - ComponentPropertiesRecording, - ComponentPropertiesRecordingSubscribeMode, - ComponentPropertiesRTSP, - ComponentPropertiesSIP, - ComponentPropertiesSIPSIPCredentials, - ComponentRecording, - ComponentRTSP, - ComponentSIP, - Peer, - PeerOptionsWebRTC, - PeerStatus, - Room, + PeerOptions, RoomApi, + RoomOptions, +) +from fishjam.api._room_api import Peer, Room +from fishjam._openapi_client.models import ( RoomConfig, RoomConfigVideoCodec, - S3Credentials, - SIPCredentials, + PeerStatus, ) from fishjam.errors import ( BadRequestError, @@ -46,90 +25,24 @@ ) HOST = "fishjam" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -SERVER_ADDRESS = f"{HOST}:5002" -SERVER_API_TOKEN = "development" +FISHJAM_URL = f"http://{HOST}:5002" +MANAGEMENT_TOKEN = "development" MAX_PEERS = 10 CODEC_H264 = "h264" -HLS_OPTIONS = ComponentOptionsHLS() -HLS_PROPERTIES = ComponentPropertiesHLS( - low_latency=False, - persistent=False, - playable=False, - subscribe_mode=ComponentPropertiesHLSSubscribeMode("auto"), - target_window_duration=None, -) -HLS_PROPERTIES.additional_properties = {"s3": None} - -RTSP_OPTIONS = ComponentOptionsRTSP( - source_uri="rtsp://ef36c6dff23ecc5bbe311cc880d95dc8.se:2137/does/not/matter" -) -RTSP_PROPERTIES = ComponentPropertiesRTSP( - source_uri=RTSP_OPTIONS.source_uri, - keep_alive_interval=15000, - reconnect_delay=15000, - rtp_port=20000, - pierce_nat=True, -) - -SIP_PHONE_NUMBER = "1234" - -SIP_CREDENTIALS = SIPCredentials( - address="my-sip-registrar.net", username="user-name", password="pass-word" -) - -SIP_OPTIONS = ComponentOptionsSIP(registrar_credentials=SIP_CREDENTIALS) - -SIP_PROPERTIES = ComponentPropertiesSIP( - registrar_credentials=ComponentPropertiesSIPSIPCredentials( - address="my-sip-registrar.net", username="user-name", password="pass-word" - ) -) - -FILE_OPTIONS = ComponentOptionsFile(file_path="video.h264") -FILE_PROPERTIES = ComponentPropertiesFile( - file_path=FILE_OPTIONS.file_path, framerate=30 -) - -RECORDING_OPTIONS = ComponentOptionsRecording( - path_prefix="prefix", - credentials=S3Credentials( - bucket="bucket", - region="region", - secret_access_key="secret", - access_key_id="access", - ), - subscribe_mode=ComponentOptionsRecordingSubscribeMode.AUTO, -) -RECORDING_PROPERTIES = ComponentPropertiesRecording( - subscribe_mode=ComponentPropertiesRecordingSubscribeMode("auto"), -) - class TestAuthentication: def test_invalid_token(self): - room_api = RoomApi(server_address=SERVER_ADDRESS, server_api_token="invalid") + room_api = RoomApi(fishjam_url=FISHJAM_URL, management_token="invalid") with pytest.raises(UnauthorizedError): room_api.create_room() def test_valid_token(self): - room_api = RoomApi( - server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN - ) - - _, room = room_api.create_room() - - all_rooms = room_api.get_all_rooms() - - assert room in all_rooms - - def test_default_api_token(self): - room_api = RoomApi(server_address=SERVER_ADDRESS) - - _, room = room_api.create_room() + room_api = RoomApi(fishjam_url=FISHJAM_URL, management_token=MANAGEMENT_TOKEN) + room = room_api.create_room() all_rooms = room_api.get_all_rooms() assert room in all_rooms @@ -137,15 +50,14 @@ def test_default_api_token(self): @pytest.fixture def room_api(): - return RoomApi(server_address=SERVER_ADDRESS, server_api_token=SERVER_API_TOKEN) + return RoomApi(fishjam_url=FISHJAM_URL, management_token=MANAGEMENT_TOKEN) class TestCreateRoom: def test_no_params(self, room_api): - _, room = room_api.create_room() + room = room_api.create_room() assert room == Room( - components=[], config=RoomConfig( room_id=room.id, max_peers=None, @@ -161,12 +73,10 @@ def test_no_params(self, room_api): assert room in room_api.get_all_rooms() def test_valid_params(self, room_api): - _, room = room_api.create_room( - max_peers=MAX_PEERS, video_codec=RoomConfigVideoCodec(CODEC_H264) - ) + options = RoomOptions(max_peers=MAX_PEERS, video_codec=CODEC_H264) + room = room_api.create_room(options) assert room == Room( - components=[], config=RoomConfig( room_id=room.id, max_peers=MAX_PEERS, @@ -178,24 +88,25 @@ def test_valid_params(self, room_api): id=room.id, peers=[], ) + assert room in room_api.get_all_rooms() def test_invalid_max_peers(self, room_api): + options = RoomOptions(max_peers="10") + with pytest.raises(BadRequestError): - room_api.create_room( - max_peers="10", video_codec=CODEC_H264, webhook_url=None - ) + room_api.create_room(options) def test_invalid_video_codec(self, room_api): with pytest.raises(ValueError): - room_api.create_room(max_peers=MAX_PEERS, video_codec="h420") + options = RoomOptions(video_codec="h420") + room_api.create_room(options) def test_valid_room_id(self, room_api): - room_id = str(uuid.uuid4()) - _, room = room_api.create_room(room_id=room_id) + options = RoomOptions(room_id=str(uuid.uuid4())) + room = room_api.create_room(options) assert room == Room( - components=[], config=RoomConfig( room_id=room.id, max_peers=None, @@ -204,27 +115,27 @@ def test_valid_room_id(self, room_api): peerless_purge_timeout=None, peer_disconnected_timeout=None, ), - id=room_id, + id=options.room_id, peers=[], ) assert room in room_api.get_all_rooms() def test_duplicated_room_id(self, room_api): - room_id = str(uuid.uuid4()) - _, room = room_api.create_room(room_id=room_id) + options = RoomOptions(room_id=str(uuid.uuid4())) + _room = room_api.create_room(options) with pytest.raises(BadRequestError) as exception_info: - _, room = room_api.create_room(room_id=room_id) + _room = room_api.create_room(options) assert ( str(exception_info.value) - == f'Cannot add room with id "{room_id}" - room already exists' + == f'Cannot add room with id "{options.room_id}" - room already exists' ) class TestDeleteRoom: def test_valid(self, room_api): - _, room = room_api.create_room() + room = room_api.create_room() room_api.delete_room(room.id) assert room not in room_api.get_all_rooms() @@ -236,19 +147,18 @@ def test_invalid(self, room_api): class TestGetAllRooms: def test_valid(self, room_api): - _, room = room_api.create_room() - + room = room_api.create_room() all_rooms = room_api.get_all_rooms() + assert isinstance(all_rooms, list) assert room in all_rooms class TestGetRoom: def test_valid(self, room_api: RoomApi): - _, room = room_api.create_room() + room = room_api.create_room() assert Room( - components=[], peers=[], id=room.id, config=RoomConfig( @@ -266,184 +176,65 @@ def test_invalid(self, room_api: RoomApi): room_api.get_room("invalid_id") -@dataclass -class ComponentTestData: - component: any - type: str - options: any - properties: any - - -class TestAddComponent: - def test_with_options_hls(self, room_api): - data = ComponentTestData(ComponentHLS, "hls", HLS_OPTIONS, HLS_PROPERTIES) - self._test_component(room_api, data) - - def test_with_options_rtsp(self, room_api): - data = ComponentTestData(ComponentRTSP, "rtsp", RTSP_OPTIONS, RTSP_PROPERTIES) - self._test_component(room_api, data) - - def test_with_options_sip(self, room_api): - data = ComponentTestData(ComponentSIP, "sip", SIP_OPTIONS, SIP_PROPERTIES) - self._test_component(room_api, data) - - def test_with_options_recording(self, room_api): - data = ComponentTestData( - ComponentRecording, "recording", RECORDING_OPTIONS, RECORDING_PROPERTIES - ) - self._test_component(room_api, data) - - @pytest.mark.file_component_sources - def test_with_options_file(self, room_api): - data = ComponentTestData(ComponentFile, "file", FILE_OPTIONS, FILE_PROPERTIES) - self._test_component(room_api, data) - - def test_invalid_type(self, room_api: RoomApi): - _, room = room_api.create_room(video_codec=CODEC_H264) - - with pytest.raises(ValueError): - room_api.add_component(room.id, options=PeerOptionsWebRTC()) - - def _test_component(self, room_api: RoomApi, test_data: ComponentTestData): - _, room = room_api.create_room(video_codec=CODEC_H264) - - response = room_api.add_component(room.id, options=test_data.options) - component = room_api.get_room(room.id).components[0] - - component = test_data.component( - id=component.id, - type=test_data.type, - properties=test_data.properties, - tracks=[], - ) - - assert response == component - assert component == component - - -class TestDeleteComponent: - def test_valid_component(self, room_api: RoomApi): - _, room = room_api.create_room(video_codec=CODEC_H264) - component = room_api.add_component(room.id, options=HLS_OPTIONS) - - room_api.delete_component(room.id, component.id) - assert [] == room_api.get_room(room.id).components - - def test_invalid_component(self, room_api: RoomApi): - _, room = room_api.create_room() - - with pytest.raises(NotFoundError): - room_api.delete_component(room.id, "invalid_id") - - -class TestHLSSubscribe: - def test_valid_subscription(self, room_api: RoomApi): - _, room = room_api.create_room(video_codec=CODEC_H264) - hls_component = room_api.add_component( - room.id, - options=ComponentOptionsHLS( - subscribe_mode=ComponentOptionsHLSSubscribeMode("manual") - ), - ) - recording_component = room_api.add_component( - room.id, - options=ComponentOptionsRecording( - path_prefix="prefix", - credentials=S3Credentials( - bucket="bucket", - region="region", - secret_access_key="secret", - access_key_id="access", - ), - subscribe_mode=ComponentOptionsRecordingSubscribeMode("manual"), - ), - ) - - for component in [hls_component, recording_component]: - assert room_api.subscribe(room.id, component.id, ["peer-id"]) is None - - def test_invalid_subscription_in_auto_mode(self, room_api: RoomApi): - _, room = room_api.create_room(video_codec=CODEC_H264) - hls_component = room_api.add_component(room.id, options=HLS_OPTIONS) - recording_component = room_api.add_component(room.id, options=RECORDING_OPTIONS) - - for component in [hls_component, recording_component]: - with pytest.raises(BadRequestError) as exception_info: - room_api.subscribe(room.id, component.id, ["component-id"]) - - assert ( - str(exception_info.value) - == f"Component {component.id} option `subscribe_mode` is set to :auto" - ) - - -class TestSIPCall: - def test_happy_path(self, room_api: RoomApi): - _, room = room_api.create_room(video_codec=CODEC_H264) - component = room_api.add_component( - room.id, - options=ComponentOptionsSIP(registrar_credentials=SIP_CREDENTIALS), - ) - assert room_api.sip_dial(room.id, component.id, SIP_PHONE_NUMBER) is None +class TestCreateParticipant: + def _assert_peer_created( + self, room_api, webrtc_peer, room_id, server_metadata=None + ): + server_metadata = server_metadata or {} - assert room_api.sip_end_call(room.id, component.id) is None - - -class TestAddPeer: - def _assert_peer_created(self, room_api, webrtc_peer, room_id): peer = Peer( id=webrtc_peer.id, type="webrtc", status=PeerStatus("disconnected"), tracks=[], - metadata={"peer": {}, "server": {}}, + metadata={"peer": {}, "server": server_metadata}, ) room = room_api.get_room(room_id) assert peer in room.peers def test_with_specified_options(self, room_api: RoomApi): - _, room = room_api.create_room() + options = PeerOptions(enable_simulcast=True) - peer = room_api.add_peer( - room.id, options=PeerOptionsWebRTC(enable_simulcast=True) - ).peer + room = room_api.create_room() + peer, _token = room_api.create_peer(room.id, options=options) self._assert_peer_created(room_api, peer, room.id) - def test_default_options(self, room_api: RoomApi): - _, room = room_api.create_room() + def test_with_metadata(self, room_api: RoomApi): + options = PeerOptions(metadata={"is_test": True}) + room = room_api.create_room() + peer, _token = room_api.create_peer(room.id, options=options) - peer = room_api.add_peer(room.id, options=PeerOptionsWebRTC()).peer + self._assert_peer_created(room_api, peer, room.id, {"is_test": True}) + + def test_default_options(self, room_api: RoomApi): + room = room_api.create_room() + peer, _token = room_api.create_peer(room.id) self._assert_peer_created(room_api, peer, room.id) def test_peer_limit_reached(self, room_api: RoomApi): - _, room = room_api.create_room(max_peers=1) - - peer = room_api.add_peer(room.id, options=PeerOptionsWebRTC()).peer + config = RoomOptions(max_peers=1) + room = room_api.create_room(config) + peer, _token = room_api.create_peer(room.id) self._assert_peer_created(room_api, peer, room.id) with pytest.raises(ServiceUnavailableError): - room_api.add_peer(room.id, options=PeerOptionsWebRTC()) + room_api.create_peer(room.id) -class TestDeletePeer: +class TestDeleteParticipant: def test_valid(self, room_api: RoomApi): - _, room = room_api.create_room() - result = room_api.add_peer( - room.id, options=PeerOptionsWebRTC(enable_simulcast=True) - ) - - peer = result.peer - + room = room_api.create_room() + peer, _token = room_api.create_peer(room.id) room_api.delete_peer(room.id, peer.id) assert [] == room_api.get_room(room.id).peers def test_invalid(self, room_api: RoomApi): - _, room = room_api.create_room() + room = room_api.create_room() with pytest.raises(NotFoundError): room_api.delete_peer(room.id, peer_id="invalid_peer_id")