Skip to content

Commit

Permalink
Merge pull request #228 from opentok/add-captions-api
Browse files Browse the repository at this point in the history
Add captions api
  • Loading branch information
maxkahan authored Sep 7, 2023
2 parents 98f05f7 + d28fc9a commit 0043355
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 36 deletions.
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.7.1
current_version = 3.8.0
commit = True
tag = False

Expand Down
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Release v3.8.0
- Added support for the [Captions API](https://tokbox.com/developer/guides/live-captions/)

# Release v3.7.1
- Fixed an issue with end-to-end encryption not being called correctly when creating a new session

Expand Down
30 changes: 30 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,36 @@ by adding these fields to the ``websocket_options`` object.
}
}
Using the Live Captions API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can enable live captioning for an OpenTok session with the ``opentok.start_captions`` method.
For more information, see the
`Live Captions API developer guide <https://tokbox.com/developer/guides/live-captions/>`.

.. code:: python
captions = opentok.start_captions(session_id, opentok_token)
You can also specify optional parameters, as shown below.

.. code:: python
captions = opentok.start_captions(
session_id,
opentok_token,
language_code='en-GB',
max_duration=10000,
partial_captions=False,
status_callback_url='https://example.com',
)
You can stop an ongoing live captioning session by calling the ``opentok.stop_captions`` method.

.. code:: python
opentok.stop_captions(captions_id)
Configuring Timeout
-------------------
Timeout is passed in the Client constructor:
Expand Down
12 changes: 12 additions & 0 deletions opentok/captions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import json


class Captions:
"""Represents information about a captioning session."""

def __init__(self, kwargs):
self.captions_id = kwargs.get("captionsId")

def json(self):
"""Returns a JSON representation of the captioning session information."""
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
16 changes: 12 additions & 4 deletions opentok/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def get_dtmf_specific_url(self, session_id, connection_id):
return url

def get_archive_stream(self, archive_id=None):
"""this method returns urls for working with streamModes in archives"""
"""this method returns the url for working with streamModes in archives"""
url = (
self.api_url
+ "/v2/project/"
Expand All @@ -187,7 +187,7 @@ def get_archive_stream(self, archive_id=None):
return url

def get_broadcast_stream(self, broadcast_id=None):
"""this method returns urls for working with streamModes in broadcasts"""
"""this method returns the url for working with streamModes in broadcasts"""
url = (
self.api_url
+ "/v2/project/"
Expand All @@ -200,15 +200,23 @@ def get_broadcast_stream(self, broadcast_id=None):
return url

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

return url

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

return url

def get_captions_url(self, captions_id: str = None):
"""Returns the URL for working with the Captions API."""
url = self.api_url + '/v2/project/' + self.api_key + '/captions'
if captions_id:
url += f'/{captions_id}/stop'

return url
32 changes: 7 additions & 25 deletions opentok/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
class OpenTokException(Exception):
"""Defines exceptions thrown by the OpenTok SDK."""

pass


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

pass


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

pass


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

pass


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

pass


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

pass


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

pass


class ForceDisconnectError(OpenTokException):
"""
Expand All @@ -57,8 +43,6 @@ class ForceDisconnectError(OpenTokException):
is not connected to the session
"""

pass


class SipDialError(OpenTokException):
"""
Expand All @@ -67,17 +51,13 @@ class SipDialError(OpenTokException):
that does not use the OpenTok Media Router.
"""

pass


class SetStreamClassError(OpenTokException):
"""
Indicates that there is invalid data in the JSON request.
It may also indicate that invalid layout options have been passed
It may also indicate that invalid layout options have been passed.
"""

pass


class BroadcastError(OpenTokException):
"""
Expand All @@ -87,8 +67,6 @@ class BroadcastError(OpenTokException):
Or The broadcast has already started for the session
"""

pass


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

pass


class BroadcastStreamModeError(OpenTokException):
"""
Expand Down Expand Up @@ -134,3 +110,9 @@ class InvalidMediaModeError(OpenTokException):
"""
Indicates that the media mode selected was not valid for the type of request made.
"""


class CaptioningAlreadyInProgressError(OpenTokException):
"""
Indicates that captioning was requested for an OpenTok session where live captions have already started.
"""
109 changes: 104 additions & 5 deletions opentok/opentok.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .endpoints import Endpoints
from .session import Session
from .archives import Archive, ArchiveList, OutputModes, StreamModes
from .captions import Captions
from .render import Render, RenderList
from .stream import Stream
from .streamlist import StreamList
Expand All @@ -52,6 +53,7 @@
DTMFError,
InvalidWebSocketOptionsError,
InvalidMediaModeError,
CaptioningAlreadyInProgressError,
)


Expand Down Expand Up @@ -1730,7 +1732,6 @@ def start_render(
url,
max_duration=7200,
resolution="1280x720",
status_callback_url=None,
properties: dict = None,
):
"""
Expand Down Expand Up @@ -1776,7 +1777,7 @@ def start_render(
elif response.status_code == 400:
"""
The HTTP response has a 400 status code in the following cases:
You do not pass in a session ID or you pass in an invalid session ID.
You did not pass in a session ID or you passed in an invalid session ID.
You specify an invalid value for input parameters.
"""
raise RequestError(response.json().get("message"))
Expand Down Expand Up @@ -1810,7 +1811,7 @@ def get_render(self, render_id):
return Render(response.json())
elif response.status_code == 400:
raise RequestError(
"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."
"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."
)
elif response.status_code == 403:
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
Expand Down Expand Up @@ -1843,7 +1844,7 @@ def stop_render(self, render_id):
return response
elif response.status_code == 400:
raise RequestError(
"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."
"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."
)
elif response.status_code == 403:
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
Expand Down Expand Up @@ -1932,7 +1933,7 @@ def connect_audio_to_websocket(
elif response.status_code == 400:
"""
The HTTP response has a 400 status code in the following cases:
You did not pass in a session ID or you pass in an invalid session ID.
You did not pass in a session ID or you passed in an invalid session ID.
You specified an invalid value for input parameters.
"""
raise RequestError(response.json().get("message"))
Expand All @@ -1953,6 +1954,104 @@ def validate_websocket_options(self, options):
if "uri" not in options:
raise InvalidWebSocketOptionsError("Provide a WebSocket URI.")

def start_captions(
self,
session_id: str,
opentok_token: str,
language_code: str = "en-US",
max_duration: int = 14400,
partial_captions: bool = True,
status_callback_url: str = None,
):
"""
Starts real-time Live Captions for an OpenTok Session. The maximum allowed duration is 4 hours, after which the audio
captioning will stop without any effect on the ongoing OpenTok Session.
An event will be posted to your callback URL if provided when starting the captions.
Each OpenTok Session supports only one audio captioning session. For more information about the Live Captions feature,
see the Live Captions developer guide <https://tokbox.com/developer/guides/live-captions/>.
:param String 'session_id': The OpenTok session ID. The audio from participants publishing into this session will be used to generate the captions.
:param String 'opentok_token': A valid OpenTok token with role set to Moderator.
:param String 'language_code' Optional: The BCP-47 code for a spoken language used on this call.
:param Integer 'max_duration' Optional: The maximum duration for the audio captioning, in seconds.
:param Boolean 'partial_captions' Optional: Whether to enable this to faster captioning at the cost of some inaccuracies.
: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.
"""

payload = {
"sessionId": session_id,
"token": opentok_token,
"languageCode": language_code,
"maxDuration": max_duration,
"partialCaptions": partial_captions,
"statusCallbackUrl": status_callback_url,
}

logger.debug(
"POST to %r with params %r, headers %r, proxies %r",
self.endpoints.get_captions_url(),
json.dumps(payload),
self.get_json_headers(),
self.proxies,
)

response = requests.post(
self.endpoints.get_captions_url(),
json=payload,
headers=self.get_json_headers(),
proxies=self.proxies,
timeout=self.timeout,
)

if response and response.status_code == 200:
return Captions(response.json())
elif response.status_code == 400:
"""
The HTTP response has a 400 status code in the following cases:
You did not pass in a session ID or you passed in an invalid session ID.
You specified an invalid value for input parameters.
"""
raise RequestError(response.json().get("message"))
elif response.status_code == 403:
raise AuthError("You passed in an invalid OpenTok API key or JWT.")
elif response.status_code == 409:
raise CaptioningAlreadyInProgressError(
"Live captions have already started for this OpenTok Session."
)
else:
raise RequestError("An unexpected error occurred", response.status_code)

def stop_captions(self, captions_id: str):
"""
Stops live captioning for the specified captioning session.
:param String captions_id: The ID of the captioning session to stop.
"""

logger.debug(
"POST to %r with headers %r, proxies %r",
self.endpoints.get_captions_url(captions_id),
self.get_json_headers(),
self.proxies,
)

response = requests.post(
self.endpoints.get_captions_url(captions_id),
headers=self.get_json_headers(),
proxies=self.proxies,
timeout=self.timeout,
)

if response and response.status_code == 202:
return None
elif response.status_code == 403:
raise AuthError("You passed in an invalid OpenTok API key or JWT.")
elif response.status_code == 404:
raise NotFoundError("No matching captionsId was found.")
else:
raise RequestError("An unexpected error occurred", response.status_code)

def _sign_string(self, string, secret):
return hmac.new(
secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1
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.7.1"
__version__ = "3.8.0"

Loading

0 comments on commit 0043355

Please sign in to comment.