diff --git a/fishjam/_composition_openapi_client/api/inputs/register_input.py b/fishjam/_composition_openapi_client/api/inputs/register_input.py index 263486d..6799b9b 100644 --- a/fishjam/_composition_openapi_client/api/inputs/register_input.py +++ b/fishjam/_composition_openapi_client/api/inputs/register_input.py @@ -79,6 +79,11 @@ def _parse_response( return response_500 + if response.status_code == 503: + response_503 = ApiError.from_dict(response.json()) + + return response_503 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: diff --git a/fishjam/_composition_openapi_client/models/register_input_response.py b/fishjam/_composition_openapi_client/models/register_input_response.py index da66255..cb4961d 100644 --- a/fishjam/_composition_openapi_client/models/register_input_response.py +++ b/fishjam/_composition_openapi_client/models/register_input_response.py @@ -18,14 +18,14 @@ class RegisterInputResponse: endpoint_route (None | str | Unset): video_duration_ms (int | None | Unset): audio_duration_ms (int | None | Unset): - port (int | None | Unset): + publish_url (None | str | Unset): """ bearer_token: None | str | Unset = UNSET endpoint_route: None | str | Unset = UNSET video_duration_ms: int | None | Unset = UNSET audio_duration_ms: int | None | Unset = UNSET - port: int | None | Unset = UNSET + publish_url: None | str | Unset = UNSET def to_dict(self) -> dict[str, Any]: bearer_token: None | str | Unset @@ -52,11 +52,11 @@ def to_dict(self) -> dict[str, Any]: else: audio_duration_ms = self.audio_duration_ms - port: int | None | Unset - if isinstance(self.port, Unset): - port = UNSET + publish_url: None | str | Unset + if isinstance(self.publish_url, Unset): + publish_url = UNSET else: - port = self.port + publish_url = self.publish_url field_dict: dict[str, Any] = {} @@ -69,8 +69,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["video_duration_ms"] = video_duration_ms if audio_duration_ms is not UNSET: field_dict["audio_duration_ms"] = audio_duration_ms - if port is not UNSET: - field_dict["port"] = port + if publish_url is not UNSET: + field_dict["publish_url"] = publish_url return field_dict @@ -114,21 +114,21 @@ def _parse_audio_duration_ms(data: object) -> int | None | Unset: audio_duration_ms = _parse_audio_duration_ms(d.pop("audio_duration_ms", UNSET)) - def _parse_port(data: object) -> int | None | Unset: + def _parse_publish_url(data: object) -> None | str | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(int | None | Unset, data) + return cast(None | str | Unset, data) - port = _parse_port(d.pop("port", UNSET)) + publish_url = _parse_publish_url(d.pop("publish_url", UNSET)) register_input_response = cls( bearer_token=bearer_token, endpoint_route=endpoint_route, video_duration_ms=video_duration_ms, audio_duration_ms=audio_duration_ms, - port=port, + publish_url=publish_url, ) return register_input_response diff --git a/fishjam/api/_composition_client.py b/fishjam/api/_composition_client.py index 261d2af..433b26e 100644 --- a/fishjam/api/_composition_client.py +++ b/fishjam/api/_composition_client.py @@ -433,23 +433,38 @@ def register_mp4_input( def register_rtmp_input( self, composition_id: str, input_id: str, *, stream_key: str - ) -> None: + ) -> str: """Register an input that an RTMP publisher pushes media into. - The stream key identifies the input; the address to publish to belongs to the - composition, not to this call. + 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. """ - self.register_input( + response = self.register_input( composition_id, input_id, RtmpInput(type_=RtmpInputType.RTMP_SERVER, stream_key=stream_key), ) + publish_url = _or_none(response.publish_url) + if not publish_url: + raise InternalServerError( + f'Registering RTMP input "{input_id}" returned no publishing address, ' + "so it cannot be published to" + ) + + return publish_url + def unregister_input( self, composition_id: str, diff --git a/tests/test_composition.py b/tests/test_composition.py index c5efc15..f89ad7c 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -35,6 +35,7 @@ IMAGE_ID = "logo" LOCAL_URL = "http://localhost:8000" FONT_PATH = Path(__file__).parent / "fixtures" / "font.ttf" +RTMP_PUBLISH_URL = "rtmps://rtmp.example.com:443/key" def client(composition_url: str | None = None) -> CompositionClient: @@ -211,11 +212,24 @@ def test_sends_the_mp4_discriminant(self): } def test_sends_the_rtmp_discriminant(self): - with mock_response() as requests: + with mock_response({"publish_url": RTMP_PUBLISH_URL}) as requests: client().register_rtmp_input(COMPOSITION_ID, INPUT_ID, stream_key="key") assert sent_json(requests) == {"type": "rtmp_server", "stream_key": "key"} + def test_returns_the_rtmp_publishing_address_the_server_chose(self): + with mock_response({"publish_url": RTMP_PUBLISH_URL}): + url = client().register_rtmp_input( + COMPOSITION_ID, INPUT_ID, stream_key="key" + ) + + assert url == RTMP_PUBLISH_URL + + def test_raises_when_no_rtmp_publishing_address_is_available(self): + with mock_response(): + with pytest.raises(InternalServerError): + client().register_rtmp_input(COMPOSITION_ID, INPUT_ID, stream_key="key") + def test_returns_the_durations_of_an_mp4_input(self): with mock_response({"video_duration_ms": 1000, "audio_duration_ms": 2000}): durations = client().register_mp4_input(