diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f6048cf..6e15a25 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.11.1 +current_version = 3.13.0 commit = True tag = True diff --git a/.requirements.txt b/.requirements.txt new file mode 100644 index 0000000..a79a597 --- /dev/null +++ b/.requirements.txt @@ -0,0 +1,54 @@ +astroid==3.3.10 +black==25.1.0 +bump2version==1.0.1 +certifi==2025.4.26 +cffi==1.17.1 +charset-normalizer==3.4.2 +click==8.2.1 +coverage==7.9.0 +cryptography==45.0.4 +dill==0.4.0 +docutils==0.21.2 +expects==0.9.0 +httpretty==1.1.4 +id==1.5.0 +idna==3.10 +iniconfig==2.1.0 +isort==6.0.1 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.1.0 +keyring==25.6.0 +markdown-it-py==3.0.0 +mccabe==0.7.0 +mdurl==0.1.2 +mock==5.2.0 +more-itertools==10.7.0 +mypy_extensions==1.1.0 +nh3==0.2.21 +-e git+ssh://git@github.com/opentok/Opentok-Python-SDK.git@1ad6e1763b04f4897c1bd1a77ff884cf85e63caf#egg=opentok +packaging==25.0 +pathspec==0.12.1 +platformdirs==4.3.8 +pluggy==1.6.0 +pyasn1==0.6.1 +pycparser==2.22 +Pygments==2.19.1 +PyJWT==2.10.1 +pylint==3.3.7 +pytest==8.4.0 +pytest-cov==6.2.1 +pytz==2025.2 +readme_renderer==44.0 +requests==2.32.4 +requests-toolbelt==1.0.0 +rfc3986==2.0.0 +rich==14.0.0 +rsa==4.9.1 +setuptools==80.9.0 +six==1.17.0 +sure==2.0.1 +tomlkit==0.13.3 +twine==6.1.0 +urllib3==2.4.0 +wheel==0.45.1 diff --git a/CHANGES.md b/CHANGES.md index b7109a0..50053f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# Release 3.12.0 +- Add new `quantization_parameter` option for archives to control video encoding quality. Values between 15-40, where smaller values generate higher quality and larger archives, larger values generate lower quality and smaller archives. QP uses variable bitrate (VBR). + # Release 3.11.0 - OpenTok SDK now accepts Vonage credentials so it's possible to use the existing OpenTok SDK with the Vonage Video API - Add additional headers to some requests diff --git a/opentok/archives.py b/opentok/archives.py index a358b5b..d0993fd 100644 --- a/opentok/archives.py +++ b/opentok/archives.py @@ -112,6 +112,8 @@ class Archive(object): 10 minutes. To generate a new URL, call the Archive.listArchives() or OpenTok.getArchive() method. :ivar max_bitrate: The maximum video bitrate for the archive, in bits per second. The minimum value is 100,000 and the maximum is 6,000,000. + + :ivar quantization_parameter: The quantization parameter (QP) for video encoding quality. Values between 15-40, where smaller values generate higher quality and larger archives. """ def __init__(self, sdk, values): @@ -139,6 +141,7 @@ def __init__(self, sdk, values): self.url = values.get("url") self.resolution = values.get("resolution") self.max_bitrate = values.get("maxBitrate") + self.quantization_parameter = values.get("quantizationParameter") def stop(self): """ diff --git a/opentok/opentok.py b/opentok/opentok.py index 9d94e39..0f8a1d2 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -648,6 +648,7 @@ def start_archive( layout=None, multi_archive_tag=None, max_bitrate=None, + quantization_parameter=None, ): """ Starts archiving an OpenTok session. @@ -708,6 +709,8 @@ def start_archive( :param String max_bitrate (Optional): The maximum video bitrate for the archive, in bits per second. The minimum value is 100,000 and the maximum is 6,000,000. + :param Number quantization_parameter (Optional): The quantization parameter (QP) for video encoding quality. Values between 15-40, where smaller values generate higher quality and larger archives, larger values generate lower quality and smaller archives. QP uses variable bitrate (VBR). + :rtype: The Archive object, which includes properties defining the archive, including the archive ID. """ @@ -725,6 +728,16 @@ def start_archive( ) ) + if quantization_parameter is not None: + if not isinstance(quantization_parameter, (int, float)): + raise OpenTokException( + u("quantization_parameter must be a number") + ) + if quantization_parameter < 15 or quantization_parameter > 40: + raise OpenTokException( + u("quantization_parameter must be between 15 and 40") + ) + payload = { "name": name, "sessionId": session_id, @@ -737,6 +750,9 @@ def start_archive( "maxBitrate": max_bitrate, } + if quantization_parameter is not None: + payload["quantizationParameter"] = quantization_parameter + if layout is not None: payload["layout"] = layout diff --git a/opentok/version.py b/opentok/version.py index 4cddec2..d88fb31 100644 --- a/opentok/version.py +++ b/opentok/version.py @@ -1,3 +1,3 @@ # see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers -__version__ = "3.12.0" +__version__ = "3.13.0" diff --git a/tests/test_archive_api.py b/tests/test_archive_api.py index 6a6c072..e8f2e30 100644 --- a/tests/test_archive_api.py +++ b/tests/test_archive_api.py @@ -1552,6 +1552,243 @@ def test_start_archive_with_streammode_manual(self): response.json().should.equal({"streamMode": "manual"}) response.headers["Content-Type"].should.equal("application/json") + @httpretty.activate + def test_start_archive_with_quantization_parameter(self): + """Test start archive with quantization parameter""" + httpretty.register_uri( + httpretty.POST, + u("https://api.opentok.com/v2/project/{0}/archive").format(self.api_key), + body=textwrap.dedent( + u( + """\ + { + "createdAt" : 1395183243556, + "duration" : 0, + "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9", + "name" : "ARCHIVE NAME", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "hasAudio": true, + "hasVideo": true, + "outputMode": "composed", + "url" : null, + "quantizationParameter": 25 + } + """ + ) + ), + status=200, + content_type=u("application/json"), + ) + + archive = self.opentok.start_archive( + self.session_id, name=u("ARCHIVE NAME"), quantization_parameter=25 + ) + + validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) + expect(httpretty.last_request().headers[u("user-agent")]).to( + contain(u("OpenTok-Python-SDK/") + __version__) + ) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) + # non-deterministic json encoding. have to decode to test it properly + if PY2: + body = json.loads(httpretty.last_request().body) + if PY3: + body = json.loads(httpretty.last_request().body.decode("utf-8")) + expect(body).to(have_key(u("sessionId"), u("SESSIONID"))) + expect(body).to(have_key(u("name"), u("ARCHIVE NAME"))) + expect(body).to(have_key(u("quantizationParameter"), 25)) + expect(archive).to(be_an(Archive)) + expect(archive).to( + have_property(u("id"), u("30b3ebf1-ba36-4f5b-8def-6f70d9986fe9")) + ) + expect(archive).to(have_property(u("name"), ("ARCHIVE NAME"))) + expect(archive).to(have_property(u("quantization_parameter"), 25)) + + def test_start_archive_with_invalid_quantization_parameter_type(self): + """Test start archive with invalid quantization parameter type""" + with pytest.raises(OpenTokException) as excinfo: + self.opentok.start_archive( + self.session_id, quantization_parameter="invalid" + ) + expect(str(excinfo.value)).to(contain("quantization_parameter must be a number")) + + def test_start_archive_with_quantization_parameter_too_low(self): + """Test start archive with quantization parameter below minimum""" + with pytest.raises(OpenTokException) as excinfo: + self.opentok.start_archive( + self.session_id, quantization_parameter=14 + ) + expect(str(excinfo.value)).to(contain("quantization_parameter must be between 15 and 40")) + + def test_start_archive_with_quantization_parameter_too_high(self): + """Test start archive with quantization parameter above maximum""" + with pytest.raises(OpenTokException) as excinfo: + self.opentok.start_archive( + self.session_id, quantization_parameter=41 + ) + expect(str(excinfo.value)).to(contain("quantization_parameter must be between 15 and 40")) + + @httpretty.activate + def test_start_archive_with_quantization_parameter_boundary_values(self): + """Test start archive with quantization parameter boundary values""" + httpretty.register_uri( + httpretty.POST, + u("https://api.opentok.com/v2/project/{0}/archive").format(self.api_key), + body=textwrap.dedent( + u( + """\ + { + "createdAt" : 1395183243556, + "duration" : 0, + "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9", + "name" : "", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "hasAudio": true, + "hasVideo": true, + "outputMode": "composed", + "url" : null, + "quantizationParameter": 15 + } + """ + ) + ), + status=200, + content_type=u("application/json"), + ) + + # Test minimum value (15) + archive = self.opentok.start_archive(self.session_id, quantization_parameter=15) + expect(archive).to(be_an(Archive)) + expect(archive).to(have_property(u("quantization_parameter"), 15)) + + # Test maximum value (40) + httpretty.reset() + httpretty.register_uri( + httpretty.POST, + u("https://api.opentok.com/v2/project/{0}/archive").format(self.api_key), + body=textwrap.dedent( + u( + """\ + { + "createdAt" : 1395183243556, + "duration" : 0, + "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9", + "name" : "", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "hasAudio": true, + "hasVideo": true, + "outputMode": "composed", + "url" : null, + "quantizationParameter": 40 + } + """ + ) + ), + status=200, + content_type=u("application/json"), + ) + + archive = self.opentok.start_archive(self.session_id, quantization_parameter=40) + expect(archive).to(be_an(Archive)) + expect(archive).to(have_property(u("quantization_parameter"), 40)) + + @httpretty.activate + def test_start_archive_with_float_quantization_parameter(self): + """Test start archive with float quantization parameter""" + httpretty.register_uri( + httpretty.POST, + u("https://api.opentok.com/v2/project/{0}/archive").format(self.api_key), + body=textwrap.dedent( + u( + """\ + { + "createdAt" : 1395183243556, + "duration" : 0, + "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9", + "name" : "", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "hasAudio": true, + "hasVideo": true, + "outputMode": "composed", + "url" : null, + "quantizationParameter": 25.5 + } + """ + ) + ), + status=200, + content_type=u("application/json"), + ) + + archive = self.opentok.start_archive(self.session_id, quantization_parameter=25.5) + + if PY2: + body = json.loads(httpretty.last_request().body) + if PY3: + body = json.loads(httpretty.last_request().body.decode("utf-8")) + expect(body).to(have_key(u("quantizationParameter"), 25.5)) + expect(archive).to(be_an(Archive)) + expect(archive).to(have_property(u("quantization_parameter"), 25.5)) + + @httpretty.activate + def test_start_archive_without_quantization_parameter(self): + """Test start archive without quantization parameter (should not include in payload)""" + httpretty.register_uri( + httpretty.POST, + u("https://api.opentok.com/v2/project/{0}/archive").format(self.api_key), + body=textwrap.dedent( + u( + """\ + { + "createdAt" : 1395183243556, + "duration" : 0, + "id" : "30b3ebf1-ba36-4f5b-8def-6f70d9986fe9", + "name" : "", + "partnerId" : 123456, + "reason" : "", + "sessionId" : "SESSIONID", + "size" : 0, + "status" : "started", + "hasAudio": true, + "hasVideo": true, + "outputMode": "composed", + "url" : null + } + """ + ) + ), + status=200, + content_type=u("application/json"), + ) + + archive = self.opentok.start_archive(self.session_id) + + if PY2: + body = json.loads(httpretty.last_request().body) + if PY3: + body = json.loads(httpretty.last_request().body.decode("utf-8")) + expect(body).to_not(have_key(u("quantizationParameter"))) + expect(archive).to(be_an(Archive)) + expect(archive).to(have_property(u("quantization_parameter"), None)) + @httpretty.activate def test_set_archive_layout_throws_exception(self): """Test invalid request in set archive layout"""