From 0826deb631f4c957a2ab7d28573d6da32c862806 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 19 Sep 2025 11:16:50 -0400 Subject: [PATCH] feat: Add quantization parameter for video archives --- video/src/vonage_video/_version.py | 2 +- video/src/vonage_video/models/archive.py | 21 +++++- video/tests/data/archive.json | 3 +- video/tests/test_archive.py | 83 ++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/video/src/vonage_video/_version.py b/video/src/vonage_video/_version.py index 96e3ce8d..77f1c8e6 100644 --- a/video/src/vonage_video/_version.py +++ b/video/src/vonage_video/_version.py @@ -1 +1 @@ -__version__ = '1.4.0' +__version__ = '1.5.0' diff --git a/video/src/vonage_video/models/archive.py b/video/src/vonage_video/models/archive.py index 7745e900..7efb2dc4 100644 --- a/video/src/vonage_video/models/archive.py +++ b/video/src/vonage_video/models/archive.py @@ -71,6 +71,9 @@ class Archive(BaseModel): transcription (Transcription, Optional): Transcription options for the archive. max_bitrate (int, Optional): The maximum video bitrate of the archive, in bits per second. This is only valid for composed archives. + quantization_parameter (int, Optional): Quantization parameter (QP) for video encoding, + smaller values generate higher quality and larger archives, larger values generate + lower quality and smaller archives. Range: 15-40. Only valid for composed archives. """ id: Optional[str] = None @@ -97,6 +100,9 @@ class Archive(BaseModel): url: Optional[str] = None transcription: Optional[Transcription] = None max_bitrate: Optional[int] = Field(None, validation_alias='maxBitrate') + quantization_parameter: Optional[int] = Field( + None, validation_alias='quantizationParameter' + ) class CreateArchiveRequest(BaseModel): @@ -119,9 +125,12 @@ class CreateArchiveRequest(BaseModel): automatically ("auto", the default) or manually ("manual"). max_bitrate (int, Optional): The maximum video bitrate of the archive, in bits per second. This is only valid for composed archives. + quantization_parameter (int, Optional): Quantization parameter (QP) for video encoding, + smaller values generate higher quality and larger archives, larger values generate + lower quality and smaller archives. Range: 15-40. Only valid for composed archives. Raises: NoAudioOrVideoError: If neither `has_audio` nor `has_video` is set. - IndividualArchivePropertyError: If `resolution` or `layout` is set for individual archives + IndividualArchivePropertyError: If `resolution`, `layout`, or `quantization_parameter` is set for individual archives or if `has_transcription` is set for composed archives. """ @@ -140,6 +149,9 @@ class CreateArchiveRequest(BaseModel): max_bitrate: Optional[int] = Field( None, ge=100_000, le=6_000_000, serialization_alias='maxBitrate' ) + quantization_parameter: Optional[int] = Field( + None, ge=15, le=40, serialization_alias='quantizationParameter' + ) @model_validator(mode='after') def validate_audio_or_video(self): @@ -159,6 +171,13 @@ def no_layout_or_resolution_for_individual_archives(self): raise IndividualArchivePropertyError( 'The `layout` property cannot be set for `archive_mode: \'individual\'`.' ) + if ( + self.output_mode == OutputMode.INDIVIDUAL + and self.quantization_parameter is not None + ): + raise IndividualArchivePropertyError( + 'The `quantization_parameter` property cannot be set for `archive_mode: \'individual\'`.' + ) return self @model_validator(mode='after') diff --git a/video/tests/data/archive.json b/video/tests/data/archive.json index ee1490cc..7871653a 100644 --- a/video/tests/data/archive.json +++ b/video/tests/data/archive.json @@ -20,5 +20,6 @@ "event": "archive", "resolution": "1280x720", "url": null, - "maxBitrate": 2000000 + "maxBitrate": 2000000, + "quantizationParameter": 25 } \ No newline at end of file diff --git a/video/tests/test_archive.py b/video/tests/test_archive.py index 65e9f18a..5baa2a6c 100644 --- a/video/tests/test_archive.py +++ b/video/tests/test_archive.py @@ -89,6 +89,87 @@ def test_create_archive_request_composed_output_mode_with_transcription_error(): ) +def test_create_archive_request_valid_quantization_parameter(): + """Test that quantization_parameter is accepted for composed archives with valid + values.""" + request = CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + has_video=True, + output_mode=OutputMode.COMPOSED, + quantization_parameter=25, + ) + assert request.quantization_parameter == 25 + + +def test_create_archive_request_quantization_parameter_boundary_values(): + """Test that quantization_parameter accepts boundary values (15 and 40).""" + # Test minimum value + request_min = CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + quantization_parameter=15, + ) + assert request_min.quantization_parameter == 15 + + # Test maximum value + request_max = CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + quantization_parameter=40, + ) + assert request_max.quantization_parameter == 40 + + +def test_create_archive_request_quantization_parameter_invalid_low(): + """Test that quantization_parameter rejects values below 15.""" + with raises(ValueError): + CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + quantization_parameter=14, + ) + + +def test_create_archive_request_quantization_parameter_invalid_high(): + """Test that quantization_parameter rejects values above 40.""" + with raises(ValueError): + CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + quantization_parameter=41, + ) + + +def test_create_archive_request_individual_output_mode_with_quantization_parameter(): + """Test that quantization_parameter is rejected for individual archives.""" + with raises(IndividualArchivePropertyError): + CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + output_mode=OutputMode.INDIVIDUAL, + quantization_parameter=25, + ) + + +def test_create_archive_request_serialization_with_quantization_parameter(): + """Test that quantization_parameter is properly serialized with the correct alias.""" + request = CreateArchiveRequest( + session_id="1_MX40NTY3NjYzMn5-MTQ4MTY3NjYzMn5", + has_audio=True, + has_video=True, + output_mode=OutputMode.COMPOSED, + quantization_parameter=30, + ) + + serialized = request.model_dump(by_alias=True, exclude_unset=True) + assert 'quantizationParameter' in serialized + assert serialized['quantizationParameter'] == 30 + assert ( + 'quantization_parameter' not in serialized + ) # Ensure Python field name is not used + + def test_layout_custom_without_stylesheet(): with raises(LayoutStylesheetError): ComposedLayout(type=LayoutType.CUSTOM) @@ -194,6 +275,7 @@ def test_start_archive(): assert archive.name == 'first archive test' assert archive.resolution == '1280x720' assert archive.max_bitrate == 2_000_000 + assert archive.quantization_parameter == 25 @responses.activate @@ -215,6 +297,7 @@ def test_get_archive(): assert archive.status == 'started' assert archive.name == 'first archive test' assert archive.resolution == '1280x720' + assert archive.quantization_parameter == 25 @responses.activate