Skip to content

Commit 0043355

Browse files
authored
Merge pull request #228 from opentok/add-captions-api
Add captions api
2 parents 98f05f7 + d28fc9a commit 0043355

9 files changed

+239
-36
lines changed

.bumpversion.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.7.1
2+
current_version = 3.8.0
33
commit = True
44
tag = False
55

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Release v3.8.0
2+
- Added support for the [Captions API](https://tokbox.com/developer/guides/live-captions/)
3+
14
# Release v3.7.1
25
- Fixed an issue with end-to-end encryption not being called correctly when creating a new session
36

README.rst

+30
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,36 @@ by adding these fields to the ``websocket_options`` object.
640640
}
641641
}
642642
643+
644+
Using the Live Captions API
645+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
646+
You can enable live captioning for an OpenTok session with the ``opentok.start_captions`` method.
647+
For more information, see the
648+
`Live Captions API developer guide <https://tokbox.com/developer/guides/live-captions/>`.
649+
650+
.. code:: python
651+
652+
captions = opentok.start_captions(session_id, opentok_token)
653+
654+
You can also specify optional parameters, as shown below.
655+
656+
.. code:: python
657+
658+
captions = opentok.start_captions(
659+
session_id,
660+
opentok_token,
661+
language_code='en-GB',
662+
max_duration=10000,
663+
partial_captions=False,
664+
status_callback_url='https://example.com',
665+
)
666+
667+
You can stop an ongoing live captioning session by calling the ``opentok.stop_captions`` method.
668+
669+
.. code:: python
670+
671+
opentok.stop_captions(captions_id)
672+
643673
Configuring Timeout
644674
-------------------
645675
Timeout is passed in the Client constructor:

opentok/captions.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import json
2+
3+
4+
class Captions:
5+
"""Represents information about a captioning session."""
6+
7+
def __init__(self, kwargs):
8+
self.captions_id = kwargs.get("captionsId")
9+
10+
def json(self):
11+
"""Returns a JSON representation of the captioning session information."""
12+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

opentok/endpoints.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def get_dtmf_specific_url(self, session_id, connection_id):
174174
return url
175175

176176
def get_archive_stream(self, archive_id=None):
177-
"""this method returns urls for working with streamModes in archives"""
177+
"""this method returns the url for working with streamModes in archives"""
178178
url = (
179179
self.api_url
180180
+ "/v2/project/"
@@ -187,7 +187,7 @@ def get_archive_stream(self, archive_id=None):
187187
return url
188188

189189
def get_broadcast_stream(self, broadcast_id=None):
190-
"""this method returns urls for working with streamModes in broadcasts"""
190+
"""this method returns the url for working with streamModes in broadcasts"""
191191
url = (
192192
self.api_url
193193
+ "/v2/project/"
@@ -200,15 +200,23 @@ def get_broadcast_stream(self, broadcast_id=None):
200200
return url
201201

202202
def get_render_url(self, render_id: str = None):
203-
"Returns URLs for working with the Render API." ""
203+
"Returns the URL for working with the Render API." ""
204204
url = self.api_url + "/v2/project/" + self.api_key + "/render"
205205
if render_id:
206206
url += "/" + render_id
207207

208208
return url
209209

210210
def get_audio_connector_url(self):
211-
"""Returns URLs for working with the Audio Connector API."""
211+
"""Returns the URL for working with the Audio Connector API."""
212212
url = self.api_url + "/v2/project/" + self.api_key + "/connect"
213213

214214
return url
215+
216+
def get_captions_url(self, captions_id: str = None):
217+
"""Returns the URL for working with the Captions API."""
218+
url = self.api_url + '/v2/project/' + self.api_key + '/captions'
219+
if captions_id:
220+
url += f'/{captions_id}/stop'
221+
222+
return url

opentok/exceptions.py

+7-25
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,40 @@
11
class OpenTokException(Exception):
22
"""Defines exceptions thrown by the OpenTok SDK."""
33

4-
pass
5-
64

75
class RequestError(OpenTokException):
86
"""Indicates an error during the request. Most likely an error connecting
97
to the OpenTok API servers. (HTTP 500 error).
108
"""
119

12-
pass
13-
1410

1511
class AuthError(OpenTokException):
1612
"""Indicates that the problem was likely with credentials. Check your API
1713
key and API secret and try again.
1814
"""
1915

20-
pass
21-
2216

2317
class NotFoundError(OpenTokException):
2418
"""Indicates that the element requested was not found. Check the parameters
2519
of the request.
2620
"""
2721

28-
pass
29-
3022

3123
class ArchiveError(OpenTokException):
3224
"""Indicates that there was a archive specific problem, probably the status
3325
of the requested archive is invalid.
3426
"""
3527

36-
pass
37-
3828

3929
class SignalingError(OpenTokException):
4030
"""Indicates that there was a signaling specific problem, one of the parameter
4131
is invalid or the type|data string doesn't have a correct size"""
4232

43-
pass
44-
4533

4634
class GetStreamError(OpenTokException):
4735
"""Indicates that the data in the request is invalid, or the session_id or stream_id
4836
are invalid"""
4937

50-
pass
51-
5238

5339
class ForceDisconnectError(OpenTokException):
5440
"""
@@ -57,8 +43,6 @@ class ForceDisconnectError(OpenTokException):
5743
is not connected to the session
5844
"""
5945

60-
pass
61-
6246

6347
class SipDialError(OpenTokException):
6448
"""
@@ -67,17 +51,13 @@ class SipDialError(OpenTokException):
6751
that does not use the OpenTok Media Router.
6852
"""
6953

70-
pass
71-
7254

7355
class SetStreamClassError(OpenTokException):
7456
"""
7557
Indicates that there is invalid data in the JSON request.
76-
It may also indicate that invalid layout options have been passed
58+
It may also indicate that invalid layout options have been passed.
7759
"""
7860

79-
pass
80-
8161

8262
class BroadcastError(OpenTokException):
8363
"""
@@ -87,8 +67,6 @@ class BroadcastError(OpenTokException):
8767
Or The broadcast has already started for the session
8868
"""
8969

90-
pass
91-
9270

9371
class DTMFError(OpenTokException):
9472
"""
@@ -101,8 +79,6 @@ class ArchiveStreamModeError(OpenTokException):
10179
Indicates that the archive is configured with a streamMode that does not support stream manipulation.
10280
"""
10381

104-
pass
105-
10682

10783
class BroadcastStreamModeError(OpenTokException):
10884
"""
@@ -134,3 +110,9 @@ class InvalidMediaModeError(OpenTokException):
134110
"""
135111
Indicates that the media mode selected was not valid for the type of request made.
136112
"""
113+
114+
115+
class CaptioningAlreadyInProgressError(OpenTokException):
116+
"""
117+
Indicates that captioning was requested for an OpenTok session where live captions have already started.
118+
"""

opentok/opentok.py

+104-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .endpoints import Endpoints
2828
from .session import Session
2929
from .archives import Archive, ArchiveList, OutputModes, StreamModes
30+
from .captions import Captions
3031
from .render import Render, RenderList
3132
from .stream import Stream
3233
from .streamlist import StreamList
@@ -52,6 +53,7 @@
5253
DTMFError,
5354
InvalidWebSocketOptionsError,
5455
InvalidMediaModeError,
56+
CaptioningAlreadyInProgressError,
5557
)
5658

5759

@@ -1730,7 +1732,6 @@ def start_render(
17301732
url,
17311733
max_duration=7200,
17321734
resolution="1280x720",
1733-
status_callback_url=None,
17341735
properties: dict = None,
17351736
):
17361737
"""
@@ -1776,7 +1777,7 @@ def start_render(
17761777
elif response.status_code == 400:
17771778
"""
17781779
The HTTP response has a 400 status code in the following cases:
1779-
You do not pass in a session ID or you pass in an invalid session ID.
1780+
You did not pass in a session ID or you passed in an invalid session ID.
17801781
You specify an invalid value for input parameters.
17811782
"""
17821783
raise RequestError(response.json().get("message"))
@@ -1810,7 +1811,7 @@ def get_render(self, render_id):
18101811
return Render(response.json())
18111812
elif response.status_code == 400:
18121813
raise RequestError(
1813-
"Invalid request. This response may indicate that data in your request is invalid JSON. Or it may indicate that you do not pass in a session ID."
1814+
"Invalid request. This response may indicate that data in your request is invalid JSON. Or it may indicate that you did not pass in a session ID."
18141815
)
18151816
elif response.status_code == 403:
18161817
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
@@ -1843,7 +1844,7 @@ def stop_render(self, render_id):
18431844
return response
18441845
elif response.status_code == 400:
18451846
raise RequestError(
1846-
"Invalid request. This response may indicate that data in your request is invalid JSON. Or it may indicate that you do not pass in a session ID."
1847+
"Invalid request. This response may indicate that data in your request is invalid JSON. Or it may indicate that you did not pass in a session ID."
18471848
)
18481849
elif response.status_code == 403:
18491850
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
@@ -1932,7 +1933,7 @@ def connect_audio_to_websocket(
19321933
elif response.status_code == 400:
19331934
"""
19341935
The HTTP response has a 400 status code in the following cases:
1935-
You did not pass in a session ID or you pass in an invalid session ID.
1936+
You did not pass in a session ID or you passed in an invalid session ID.
19361937
You specified an invalid value for input parameters.
19371938
"""
19381939
raise RequestError(response.json().get("message"))
@@ -1953,6 +1954,104 @@ def validate_websocket_options(self, options):
19531954
if "uri" not in options:
19541955
raise InvalidWebSocketOptionsError("Provide a WebSocket URI.")
19551956

1957+
def start_captions(
1958+
self,
1959+
session_id: str,
1960+
opentok_token: str,
1961+
language_code: str = "en-US",
1962+
max_duration: int = 14400,
1963+
partial_captions: bool = True,
1964+
status_callback_url: str = None,
1965+
):
1966+
"""
1967+
Starts real-time Live Captions for an OpenTok Session. The maximum allowed duration is 4 hours, after which the audio
1968+
captioning will stop without any effect on the ongoing OpenTok Session.
1969+
An event will be posted to your callback URL if provided when starting the captions.
1970+
1971+
Each OpenTok Session supports only one audio captioning session. For more information about the Live Captions feature,
1972+
see the Live Captions developer guide <https://tokbox.com/developer/guides/live-captions/>.
1973+
1974+
:param String 'session_id': The OpenTok session ID. The audio from participants publishing into this session will be used to generate the captions.
1975+
:param String 'opentok_token': A valid OpenTok token with role set to Moderator.
1976+
:param String 'language_code' Optional: The BCP-47 code for a spoken language used on this call.
1977+
:param Integer 'max_duration' Optional: The maximum duration for the audio captioning, in seconds.
1978+
:param Boolean 'partial_captions' Optional: Whether to enable this to faster captioning at the cost of some inaccuracies.
1979+
:param String 'status_callback_url' Optional: A publicly reachable URL controlled by the customer and capable of generating the content to be rendered without user intervention. The minimum length of the URL is 15 characters and the maximum length is 2048 characters.
1980+
"""
1981+
1982+
payload = {
1983+
"sessionId": session_id,
1984+
"token": opentok_token,
1985+
"languageCode": language_code,
1986+
"maxDuration": max_duration,
1987+
"partialCaptions": partial_captions,
1988+
"statusCallbackUrl": status_callback_url,
1989+
}
1990+
1991+
logger.debug(
1992+
"POST to %r with params %r, headers %r, proxies %r",
1993+
self.endpoints.get_captions_url(),
1994+
json.dumps(payload),
1995+
self.get_json_headers(),
1996+
self.proxies,
1997+
)
1998+
1999+
response = requests.post(
2000+
self.endpoints.get_captions_url(),
2001+
json=payload,
2002+
headers=self.get_json_headers(),
2003+
proxies=self.proxies,
2004+
timeout=self.timeout,
2005+
)
2006+
2007+
if response and response.status_code == 200:
2008+
return Captions(response.json())
2009+
elif response.status_code == 400:
2010+
"""
2011+
The HTTP response has a 400 status code in the following cases:
2012+
You did not pass in a session ID or you passed in an invalid session ID.
2013+
You specified an invalid value for input parameters.
2014+
"""
2015+
raise RequestError(response.json().get("message"))
2016+
elif response.status_code == 403:
2017+
raise AuthError("You passed in an invalid OpenTok API key or JWT.")
2018+
elif response.status_code == 409:
2019+
raise CaptioningAlreadyInProgressError(
2020+
"Live captions have already started for this OpenTok Session."
2021+
)
2022+
else:
2023+
raise RequestError("An unexpected error occurred", response.status_code)
2024+
2025+
def stop_captions(self, captions_id: str):
2026+
"""
2027+
Stops live captioning for the specified captioning session.
2028+
2029+
:param String captions_id: The ID of the captioning session to stop.
2030+
"""
2031+
2032+
logger.debug(
2033+
"POST to %r with headers %r, proxies %r",
2034+
self.endpoints.get_captions_url(captions_id),
2035+
self.get_json_headers(),
2036+
self.proxies,
2037+
)
2038+
2039+
response = requests.post(
2040+
self.endpoints.get_captions_url(captions_id),
2041+
headers=self.get_json_headers(),
2042+
proxies=self.proxies,
2043+
timeout=self.timeout,
2044+
)
2045+
2046+
if response and response.status_code == 202:
2047+
return None
2048+
elif response.status_code == 403:
2049+
raise AuthError("You passed in an invalid OpenTok API key or JWT.")
2050+
elif response.status_code == 404:
2051+
raise NotFoundError("No matching captionsId was found.")
2052+
else:
2053+
raise RequestError("An unexpected error occurred", response.status_code)
2054+
19562055
def _sign_string(self, string, secret):
19572056
return hmac.new(
19582057
secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1

opentok/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
2-
__version__ = "3.7.1"
2+
__version__ = "3.8.0"
33

0 commit comments

Comments
 (0)