Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.11.1
current_version = 3.13.0
commit = True
tag = True

Expand Down
54 changes: 54 additions & 0 deletions .requirements.txt
Original file line number Diff line number Diff line change
@@ -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://[email protected]/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
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions opentok/archives.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand Down
16 changes: 16 additions & 0 deletions opentok/opentok.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ def start_archive(
layout=None,
multi_archive_tag=None,
max_bitrate=None,
quantization_parameter=None,
):
"""
Starts archiving an OpenTok session.
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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,
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion opentok/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
__version__ = "3.12.0"
__version__ = "3.13.0"

237 changes: 237 additions & 0 deletions tests/test_archive_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down