diff --git a/video/src/vonage_video/models/audio_connector.py b/video/src/vonage_video/models/audio_connector.py new file mode 100644 index 00000000..469bedc3 --- /dev/null +++ b/video/src/vonage_video/models/audio_connector.py @@ -0,0 +1,41 @@ +from typing import Optional + +from pydantic import BaseModel, Field +from vonage_video.models.enums import AudioSampleRate + + +class AudioConnectorWebsocket(BaseModel): + """The audio connector websocket options. + + Args: + uri (str): The URI. + streams (list): The streams. + headers (dict): The headers. + audio_rate (AudioSampleRate): The audio sample rate. + """ + + uri: str + streams: Optional[list] = None + headers: Optional[dict] = None + audio_rate: Optional[AudioSampleRate] = Field(None, serialization_alias='audioRate') + + +class AudioConnectorOptions(BaseModel): + """Options for the audio connector. + + Args: + session_id (str): The session ID. + token (str): The token. + websocket (AudioConnectorWebsocket): The audio connector websocket. + """ + + session_id: str = Field(..., serialization_alias='sessionId') + token: str + websocket: AudioConnectorWebsocket + + +class AudioConnectorData(BaseModel): + """Class containing audio connector ID and audio captioning session ID.""" + + id: Optional[str] = None + captions_id: Optional[str] = Field(None, serialization_alias='captionsId') diff --git a/video/src/vonage_video/models/captions.py b/video/src/vonage_video/models/captions.py index 3dd4ae77..483da18b 100644 --- a/video/src/vonage_video/models/captions.py +++ b/video/src/vonage_video/models/captions.py @@ -10,7 +10,7 @@ class CaptionsOptions(BaseModel): Args: session_id (str): The session ID. - token (str): The token. + token (str): A valid token with moderation privileges. language_code (LanguageCode, Optional): The language code. max_duration (int, Optional): The maximum duration. partial_captions (bool, Optional): The partial captions. @@ -23,9 +23,19 @@ class CaptionsOptions(BaseModel): None, serialization_alias='languageCode' ) max_duration: Optional[int] = Field( - None, le=300, ge=14400, serialization_alias='maxDuration' + None, ge=300, le=14400, serialization_alias='maxDuration' ) partial_captions: Optional[bool] = Field(None, serialization_alias='partialCaptions') status_callback_url: Optional[str] = Field( None, min_length=15, max_length=2048, serialization_alias='statusCallbackUrl' ) + + +class CaptionsData(BaseModel): + """Class containing captions ID. + + Args: + captions_id (str): The captions ID. + """ + + captions_id: str = Field(..., serialization_alias='captionsId') diff --git a/video/src/vonage_video/models/enums.py b/video/src/vonage_video/models/enums.py index 7abb6659..3326828a 100644 --- a/video/src/vonage_video/models/enums.py +++ b/video/src/vonage_video/models/enums.py @@ -37,3 +37,8 @@ class LanguageCode(str, Enum): KO_KR = 'ko-KR' PT_BR = 'pt-BR' TH_TH = 'th-TH' + + +class AudioSampleRate(str, Enum): + KHZ_8 = 8000 + KHZ_16 = 16000 diff --git a/video/src/vonage_video/models/session.py b/video/src/vonage_video/models/session.py index 5729ef25..356dd718 100644 --- a/video/src/vonage_video/models/session.py +++ b/video/src/vonage_video/models/session.py @@ -41,6 +41,8 @@ def set_p2p_preference_if_archive_mode_set(self): class VideoSession(BaseModel): + """The new session ID and options specified in the request.""" + session_id: str archive_mode: Optional[ArchiveMode] = None media_mode: Optional[MediaMode] = None diff --git a/video/src/vonage_video/models/stream.py b/video/src/vonage_video/models/stream.py index 436ec480..a3135201 100644 --- a/video/src/vonage_video/models/stream.py +++ b/video/src/vonage_video/models/stream.py @@ -4,6 +4,15 @@ class StreamInfo(BaseModel): + """The stream information. + + Args: + id (str): The stream ID. + video_type (str): The video type. + name (str): The name. + layout_class_list (list(str)): The layout class list. + """ + id: Optional[str] = Field(None, validation_alias='id') video_type: Optional[str] = Field(None, validation_alias='videoType') name: Optional[str] = Field(None, validation_alias='name') @@ -13,9 +22,18 @@ class StreamInfo(BaseModel): class StreamLayout(BaseModel): + """The stream layout. + + Args: + id (str): The stream ID. + layout_class_list (list): The layout class list. + """ + id: str layout_class_list: List[str] = Field(..., serialization_alias='layoutClassList') class StreamLayoutOptions(BaseModel): + """The options for the stream layout.""" + items: List[StreamLayout] diff --git a/video/src/vonage_video/video.py b/video/src/vonage_video/video.py index 2a55943a..81369c26 100644 --- a/video/src/vonage_video/video.py +++ b/video/src/vonage_video/video.py @@ -2,7 +2,7 @@ from pydantic import validate_call from vonage_http_client.http_client import HttpClient -from vonage_video.models.captions import CaptionsOptions +from vonage_video.models.captions import CaptionsData, CaptionsOptions from vonage_video.models.session import SessionOptions, VideoSession from vonage_video.models.signal import SignalData from vonage_video.models.stream import StreamInfo, StreamLayoutOptions @@ -207,19 +207,49 @@ def _toggle_mute_all_streams(self, session_id: str, params: dict) -> None: ) @validate_call - def enable_captions(self, options: CaptionsOptions) -> str: + def start_captions(self, options: CaptionsOptions) -> CaptionsData: """Enables captions in a session using the Vonage Video API. Args: options (CaptionsOptions): Options for the captions. Returns: - str: The captions stream ID. + CaptionsData: Class containing captions ID. """ response = self._http_client.post( self._http_client.video_host, f'/v2/project/{self._http_client.auth.application_id}/captions', - options.model_dump(exclude_none=True), + options.model_dump(exclude_none=True, by_alias=True), ) - return response['captionsId'] + return CaptionsData(captions_id=response['captionsId']) + + @validate_call + def stop_captions(self, captions: CaptionsData) -> None: + """Disables captions in a session using the Vonage Video API. + + Args: + captions (CaptionsData): The captions data. + """ + self._http_client.post( + self._http_client.video_host, + f'/v2/project/{self._http_client.auth.application_id}/captions/{captions.captions_id}/stop', + ) + + @validate_call + def start_audio_connector(self, options: AudioConnectorOptions) -> AudioConnectorData: + """Starts an audio connector in a session using the Vonage Video API. + + Args: + options (AudioConnectorOptions): Options for the audio connector. + + Returns: + AudioConnectorData: Class containing audio connector ID. + """ + response = self._http_client.post( + self._http_client.video_host, + f'/v2/project/{self._http_client.auth.application_id}/connect', + options.model_dump(exclude_none=True, by_alias=True), + ) + + return AudioConnectorData(audio_connector_id=response['audioConnectorId']) diff --git a/video/tests/data/captions_error_already_enabled.json b/video/tests/data/captions_error_already_enabled.json new file mode 100644 index 00000000..b056d73b --- /dev/null +++ b/video/tests/data/captions_error_already_enabled.json @@ -0,0 +1,5 @@ +{ + "code": 60003, + "message": "Audio captioning is already enabled", + "description": "Audio captioning is already enabled" +} \ No newline at end of file diff --git a/video/tests/data/start_captions.json b/video/tests/data/start_captions.json new file mode 100644 index 00000000..c14cb562 --- /dev/null +++ b/video/tests/data/start_captions.json @@ -0,0 +1,3 @@ +{ + "captionsId": "bc01a6b7-0e8e-4aa0-bb4e-2390f7cb18a1" +} \ No newline at end of file diff --git a/video/tests/test_captions.py b/video/tests/test_captions.py index ded1a130..875eb5b1 100644 --- a/video/tests/test_captions.py +++ b/video/tests/test_captions.py @@ -1,8 +1,12 @@ from os.path import abspath import responses +from pytest import raises from vonage_http_client import HttpClient -from vonage_video.models.captions import CaptionsOptions +from vonage_http_client.errors import HttpRequestError +from vonage_video.models.captions import CaptionsData, CaptionsOptions +from vonage_video.models.enums import LanguageCode, TokenRole +from vonage_video.models.token import TokenOptions from vonage_video.video import Video from testutils import build_response, get_mock_jwt_auth @@ -13,6 +17,26 @@ video = Video(HttpClient(get_mock_jwt_auth())) +def test_captions_options_model(): + options = CaptionsOptions( + session_id='test_session_id', + token='test_token', + language_code=LanguageCode.EN_GB, + max_duration=300, + partial_captions=True, + status_callback_url='example.com/status', + ) + + assert options.model_dump(by_alias=True) == { + 'sessionId': 'test_session_id', + 'token': 'test_token', + 'languageCode': 'en-GB', + 'maxDuration': 300, + 'partialCaptions': True, + 'statusCallbackUrl': 'example.com/status', + } + + @responses.activate def test_start_captions(): build_response( @@ -23,4 +47,54 @@ def test_start_captions(): 202, ) - options = CaptionsOptions() + session_id = 'test_session_id' + options = CaptionsOptions( + session_id=session_id, + token=video.generate_client_token( + TokenOptions(session_id=session_id, role=TokenRole.MODERATOR) + ), + language_code=LanguageCode.EN_GB, + max_duration=300, + partial_captions=True, + status_callback_url='https://example.com/status', + ) + captions = video.start_captions(options) + + assert captions.captions_id == 'bc01a6b7-0e8e-4aa0-bb4e-2390f7cb18a1' + + +@responses.activate +def test_start_captions_error_already_enabled(): + build_response( + path, + 'POST', + 'https://video.api.vonage.com/v2/project/test_application_id/captions', + 'captions_error_already_enabled.json', + 409, + ) + + session_id = 'test_session_id' + options = CaptionsOptions( + session_id=session_id, + token=video.generate_client_token( + TokenOptions(session_id=session_id, role=TokenRole.MODERATOR) + ), + ) + + with raises(HttpRequestError) as e: + video.start_captions(options) + assert 'Audio captioning is already enabled' in e.value.message + + +@responses.activate +def test_stop_captions(): + build_response( + path, + 'POST', + 'https://video.api.vonage.com/v2/project/test_application_id/captions/test_captions_id/stop', + status_code=202, + ) + + video.stop_captions(CaptionsData(captions_id='test_captions_id')) + + assert responses.calls[0].response.status_code == 202