diff --git a/video/src/vonage_video/models/__init__.py b/video/src/vonage_video/models/__init__.py index 83697351..13071441 100644 --- a/video/src/vonage_video/models/__init__.py +++ b/video/src/vonage_video/models/__init__.py @@ -3,6 +3,7 @@ AudioConnectorData, AudioConnectorOptions, AudioConnectorWebSocket, + AudioTransportConfiguration, ) from .broadcast import ( Broadcast, @@ -22,6 +23,8 @@ ArchiveMode, ArchiveStatus, AudioSampleRate, + AudioTransportEncoding, + AudioTransportTransport, ExperienceComposerStatus, LanguageCode, LayoutType, @@ -48,6 +51,9 @@ "AudioConnectorData", "AudioConnectorOptions", "AudioConnectorWebSocket", + "AudioTransportConfiguration", + "AudioTransportEncoding", + "AudioTransportTransport", "Archive", "ListArchivesFilter", "Transcription", diff --git a/video/src/vonage_video/models/audio_connector.py b/video/src/vonage_video/models/audio_connector.py index 4c8fc060..def7e76f 100644 --- a/video/src/vonage_video/models/audio_connector.py +++ b/video/src/vonage_video/models/audio_connector.py @@ -1,7 +1,35 @@ from typing import Optional -from pydantic import BaseModel, Field -from vonage_video.models.enums import AudioSampleRate +from pydantic import BaseModel, Field, model_validator +from vonage_video.models.enums import ( + AudioSampleRate, + AudioTransportEncoding, + AudioTransportTransport, +) + + +class AudioTransportConfiguration(BaseModel): + """The audio transport configuration. + + Args: + transport (AudioTransportTransport): 'binary' (raw PCM16, the default) or 'json'. + encoding (AudioTransportEncoding): Required when transport is 'json'. Set to 'base64'. + audio_field (str): The JSON key for the outbound audio data. Defaults to 'audio'. + receive_audio_field (str): The JSON key for inbound audio data (when bidirectional is enabled). Defaults to the same value as audio_field. + static_fields (dict): A dictionary of extra key-value pairs included in every outbound JSON audio message. + """ + + transport: Optional[AudioTransportTransport] = None + encoding: Optional[AudioTransportEncoding] = None + audio_field: Optional[str] = None + receive_audio_field: Optional[str] = None + static_fields: Optional[dict] = None + + @model_validator(mode='after') + def encoding_must_be_specified_for_json_transport(self): + if self.transport == AudioTransportTransport.JSON and not self.encoding: + raise ValueError("encoding must be specified when transport is JSON") + return self class AudioConnectorWebSocket(BaseModel): @@ -13,6 +41,7 @@ class AudioConnectorWebSocket(BaseModel): headers (dict): The headers to send to your WebSocket server. audio_rate (AudioSampleRate): The audio sample rate in Hertz. bidirectional (bool): Whether the websocket is bidirectional. + audio_transport (AudioTransportConfiguration): The audio transport configuration. Configures how audio is serialized on the WebSocket wire. By default, audio is sent as raw binary PCM 16-bit frames. """ uri: str @@ -22,6 +51,9 @@ class AudioConnectorWebSocket(BaseModel): bidirectional: Optional[bool] = Field( None, description="Whether the websocket is bidirectional." ) + audio_transport: Optional[AudioTransportConfiguration] = Field( + None, serialization_alias='audioTransport' + ) def model_dump(self, *args, **kwargs): data = super().model_dump(*args, **kwargs) diff --git a/video/src/vonage_video/models/enums.py b/video/src/vonage_video/models/enums.py index 24e594ac..9ec91602 100644 --- a/video/src/vonage_video/models/enums.py +++ b/video/src/vonage_video/models/enums.py @@ -55,6 +55,19 @@ class AudioSampleRate(int, Enum): KHZ_16 = 16000 +class AudioTransportEncoding(str, Enum): + """Audio encoding type when using JSON transport.""" + + BASE64 = 'base64' + + +class AudioTransportTransport(str, Enum): + """Audio transport type for the WebSocket connection.""" + + BINARY = 'binary' + JSON = 'json' + + class VideoResolution(str, Enum): """The resolution of the archive or broadcast. diff --git a/video/tests/test_audio_connector.py b/video/tests/test_audio_connector.py index 32ee4ad0..9cade010 100644 --- a/video/tests/test_audio_connector.py +++ b/video/tests/test_audio_connector.py @@ -1,6 +1,8 @@ from os.path import abspath +import pytest import responses +from pydantic import ValidationError from vonage_http_client import HttpClient from vonage_video import ( AudioConnectorOptions, @@ -10,6 +12,8 @@ TokenRole, Video, ) +from vonage_video.models.audio_connector import AudioTransportConfiguration +from vonage_video.models.enums import AudioTransportEncoding, AudioTransportTransport from testutils import build_response, get_mock_jwt_auth @@ -44,7 +48,7 @@ def test_audio_connector_options_model(): ), ) - actual = options.model_dump(by_alias=True) + actual = options.model_dump(by_alias=True, exclude_none=True) expected = { 'sessionId': 'test_session_id', 'token': 'test_token', @@ -59,6 +63,54 @@ def test_audio_connector_options_model(): assert actual == expected +def test_audio_connector_options_model_with_audio_transport(): + options = AudioConnectorOptions( + session_id='test_session_id', + token='test_token', + websocket=AudioConnectorWebSocket( + uri='test_uri', + streams=['test_stream_id'], + headers={'test_header': 'test_value'}, + audio_rate=AudioSampleRate.KHZ_16, + bidirectional=True, + audio_transport=AudioTransportConfiguration( + transport=AudioTransportTransport.JSON, + encoding=AudioTransportEncoding.BASE64, + audio_field='audio', + receive_audio_field='audio', + static_fields={'foo': 'bar'}, + ), + ), + ) + + actual = options.model_dump(by_alias=True, exclude_none=True) + expected = { + 'sessionId': 'test_session_id', + 'token': 'test_token', + 'websocket': { + 'uri': 'test_uri', + 'streams': ['test_stream_id'], + 'headers': {'test_header': 'test_value'}, + 'audioRate': 16000, + 'bidirectional': True, + 'audioTransport': { + 'transport': 'json', + 'encoding': 'base64', + 'audio_field': 'audio', + 'receive_audio_field': 'audio', + 'static_fields': {'foo': 'bar'}, + }, + }, + } + assert actual == expected + + +def test_audio_transport_configuration_model_with_json_transport_and_encoding_not_set(): + with pytest.raises(ValidationError) as err: + config = AudioTransportConfiguration(transport=AudioTransportTransport.JSON) + assert "encoding must be specified when transport is JSON" in str(err.value) + + @responses.activate def test_start_audio_connector(): build_response(