Skip to content

Commit 9ea4d61

Browse files
committed
add captions API
1 parent 98f05f7 commit 9ea4d61

File tree

7 files changed

+241
-37
lines changed

7 files changed

+241
-37
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
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

Lines changed: 30 additions & 0 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 12 additions & 4 deletions
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

Lines changed: 9 additions & 27 deletions
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,38 +43,30 @@ class ForceDisconnectError(OpenTokException):
5743
is not connected to the session
5844
"""
5945

60-
pass
61-
6246

6347
class SipDialError(OpenTokException):
6448
"""
6549
Indicates that there was a SIP dial specific problem:
66-
The Session ID passed in is invalid or you attempt to start a SIP call for a session
50+
The Session ID ed in is invalid or you attempt to start a SIP call for a session
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 ed
7759
"""
7860

79-
pass
80-
8161

8262
class BroadcastError(OpenTokException):
8363
"""
8464
Indicates that data in your request data is invalid JSON. It may also indicate
85-
that you passed in invalid layout options. Or you have exceeded the limit of five
65+
that you ed in invalid layout options. Or you have exceeded the limit of five
8666
simultaneous RTMP streams for an OpenTok session. Or you specified and invalid resolution.
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

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from datetime import datetime # generate_token
1+
from datetime import datetime
2+
from functools import partial # generate_token
23
from typing import List, Optional # imports List, Optional type hint
34
import calendar # generate_token
45
import base64 # generate_token
@@ -27,6 +28,7 @@
2728
from .endpoints import Endpoints
2829
from .session import Session
2930
from .archives import Archive, ArchiveList, OutputModes, StreamModes
31+
from .captions import Captions
3032
from .render import Render, RenderList
3133
from .stream import Stream
3234
from .streamlist import StreamList
@@ -52,6 +54,7 @@
5254
DTMFError,
5355
InvalidWebSocketOptionsError,
5456
InvalidMediaModeError,
57+
CaptioningAlreadyInProgressError,
5558
)
5659

5760

@@ -1730,7 +1733,6 @@ def start_render(
17301733
url,
17311734
max_duration=7200,
17321735
resolution="1280x720",
1733-
status_callback_url=None,
17341736
properties: dict = None,
17351737
):
17361738
"""
@@ -1776,7 +1778,7 @@ def start_render(
17761778
elif response.status_code == 400:
17771779
"""
17781780
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.
1781+
You did not pass in a session ID or you passed in an invalid session ID.
17801782
You specify an invalid value for input parameters.
17811783
"""
17821784
raise RequestError(response.json().get("message"))
@@ -1810,7 +1812,7 @@ def get_render(self, render_id):
18101812
return Render(response.json())
18111813
elif response.status_code == 400:
18121814
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."
1815+
"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."
18141816
)
18151817
elif response.status_code == 403:
18161818
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
@@ -1843,7 +1845,7 @@ def stop_render(self, render_id):
18431845
return response
18441846
elif response.status_code == 400:
18451847
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."
1848+
"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."
18471849
)
18481850
elif response.status_code == 403:
18491851
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
@@ -1932,7 +1934,7 @@ def connect_audio_to_websocket(
19321934
elif response.status_code == 400:
19331935
"""
19341936
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.
1937+
You did not pass in a session ID or you passed in an invalid session ID.
19361938
You specified an invalid value for input parameters.
19371939
"""
19381940
raise RequestError(response.json().get("message"))
@@ -1953,6 +1955,104 @@ def validate_websocket_options(self, options):
19531955
if "uri" not in options:
19541956
raise InvalidWebSocketOptionsError("Provide a WebSocket URI.")
19551957

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

0 commit comments

Comments
 (0)