Skip to content

Commit dddb259

Browse files
committed
updating changelog
2 parents 0dfb379 + 4b4d82f commit dddb259

File tree

8 files changed

+264
-8
lines changed

8 files changed

+264
-8
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Release v3.4.0
2-
- Support for end-to-end session encryption (E2EE)
2+
- Support for Audio Connector API via `connect_audio_to_websocket` method
33

44
# Release v3.3.0
55

README.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,33 @@ For more information about OpenTok live streaming broadcasts, see the
555555
`Broadcast developer guide <https://tokbox.com/developer/guides/broadcast/>`_.
556556

557557

558+
Connecting audio to a WebSocket
559+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
560+
You can send audio to a WebSocket with the ``opentok.connect_audio_to_websocket`` method.
561+
For more information, see the
562+
`Audio Connector developer guide <https://tokbox.com/developer/guides/audio-connector/>`_.
563+
564+
.. code:: python
565+
566+
websocket_options = {"uri": "wss://service.com/ws-endpoint"}
567+
websocket_audio_connection = opentok.connect_audio_to_websocket(session_id, opentok_token, websocket_options)
568+
569+
Additionally, you can list only the specific streams you want to send to the WebSocket, and/or the additional headers that are sent,
570+
by adding these fields to the ``websocket_options`` object.
571+
572+
.. code:: python
573+
574+
websocket_options = {
575+
"uri": "wss://service.com/ws-endpoint",
576+
"streams": [
577+
"streamId-1",
578+
"streamId-2"
579+
],
580+
"headers": {
581+
"headerKey": "headerValue"
582+
}
583+
}
584+
558585
Configuring Timeout
559586
-------------------
560587
Timeout is passed in the Client constructor:

opentok/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
from .sip_call import SipCall
1616
from .broadcast import Broadcast, BroadcastStreamModes
1717
from .render import Render, RenderList
18+
from .websocket_audio_connection import WebSocketAudioConnection

opentok/endpoints.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,9 @@ def get_render_url(self, render_id: str = None):
208208
url += "/" + render_id
209209

210210
return url
211+
212+
def get_audio_connector_url(self):
213+
"""Returns URLs for working with the Audio Connector API."""
214+
url = self.api_url + "/v2/project/" + self.api_key + "/connect"
215+
216+
return url

opentok/exceptions.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,22 @@ class BroadcastStreamModeError(OpenTokException):
107107
Indicates that the broadcast is configured with a streamMode that does not support stream manipulation.
108108
"""
109109

110-
pass
111-
112110

113111
class BroadcastHLSOptionsError(OpenTokException):
114112
"""
115113
Indicates that HLS options have been set incorrectly.
116114
117115
dvr and lowLatency modes cannot both be set to true in a broadcast.
118116
"""
117+
118+
119+
class InvalidWebSocketOptionsError(OpenTokException):
120+
"""
121+
Indicates that the WebSocket options selected are invalid.
122+
"""
123+
124+
125+
class InvalidMediaModeError(OpenTokException):
126+
"""
127+
Indicates that the media mode selected was not valid for the type of request made.
128+
"""

opentok/opentok.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import random # _create_jwt_auth_header
1717
import logging # logging
1818
import warnings # Native. Used for notifying deprecations
19-
import os
2019

2120

2221
# compat
@@ -33,6 +32,7 @@
3332
from .streamlist import StreamList
3433
from .sip_call import SipCall
3534
from .broadcast import Broadcast, BroadcastStreamModes
35+
from .websocket_audio_connection import WebSocketAudioConnection
3636
from .exceptions import (
3737
ArchiveStreamModeError,
3838
BroadcastHLSOptionsError,
@@ -48,8 +48,9 @@
4848
SipDialError,
4949
SetStreamClassError,
5050
BroadcastError,
51-
DTMFError
52-
51+
DTMFError,
52+
InvalidWebSocketOptionsError,
53+
InvalidMediaModeError
5354
)
5455

5556

@@ -1659,7 +1660,7 @@ def start_render(self, session_id, opentok_token, url, max_duration=7200, resolu
16591660
`Experience Composer developer guide <https://tokbox.com/developer/guides/experience-composer>`_.
16601661
16611662
:param String 'session_id': The session ID of the OpenTok session that will include the Experience Composer stream.
1662-
:param String 'token': A valid OpenTok token with a Publisher role and (optionally) connection data to be associated with the output stream.
1663+
:param String 'opentok_token': A valid OpenTok token with a Publisher role and (optionally) connection data to be associated with the output stream.
16631664
:param String 'url': A publically reachable URL controlled by the customer and capable of generating the content to be rendered without user intervention.
16641665
:param Integer 'maxDuration' Optional: The maximum time allowed for the Experience Composer, in seconds. After this time, it is stopped automatically, if it is still running. The maximum value is 36000 (10 hours), the minimum value is 60 (1 minute), and the default value is 7200 (2 hours). When the Experience Composer ends, its stream is unpublished and an event is posted to the callback URL, if configured in the Account Portal.
16651666
:param String 'resolution' Optional: The resolution of the Experience Composer, either "640x480" (SD landscape), "480x640" (SD portrait), "1280x720" (HD landscape), "720x1280" (HD portrait), "1920x1080" (FHD landscape), or "1080x1920" (FHD portrait). By default, this resolution is "1280x720" (HD landscape, the default).
@@ -1705,7 +1706,6 @@ def start_render(self, session_id, opentok_token, url, max_duration=7200, resolu
17051706
else:
17061707
raise RequestError("An unexpected error occurred", response.status_code)
17071708

1708-
17091709
def get_render(self, render_id):
17101710
"""
17111711
This method allows you to see the status of a render, which can be one of the following:
@@ -1810,6 +1810,64 @@ def list_renders(self, offset=0, count=50):
18101810
else:
18111811
raise RequestError("An unexpected error occurred", response.status_code)
18121812

1813+
def connect_audio_to_websocket(self, session_id: str, opentok_token: str, websocket_options: dict):
1814+
"""
1815+
Connects audio streams to a specified WebSocket URI.
1816+
For more information, see the `Audio Connector developer guide <https://tokbox.com/developer/guides/audio-streamer/>`.
1817+
1818+
:param String 'session_id': The OpenTok session ID that includes the OpenTok streams you want to include in the WebSocket stream. The Audio Connector feature is only supported in routed sessions (sessions that use the OpenTok Media Router).
1819+
:param String 'opentok_token': The OpenTok token to be used for the Audio Connector connection to the OpenTok session.
1820+
:param Dictionary 'websocket_options': Included options for the WebSocket.
1821+
String 'uri': A publicly reachable WebSocket URI to be used for the destination of the audio stream (such as "wss://example.com/ws-endpoint").
1822+
List 'streams' Optional: A list of stream IDs for the OpenTok streams you want to include in the WebSocket audio. If you omit this property, all streams in the session will be included.
1823+
Dictionary 'headers' Optional: An object of key-value pairs of headers to be sent to your WebSocket server with each message, with a maximum length of 512 bytes.
1824+
"""
1825+
self.validate_websocket_options(websocket_options)
1826+
1827+
payload = {
1828+
"sessionId": session_id,
1829+
"token": opentok_token,
1830+
"websocket": websocket_options
1831+
}
1832+
1833+
logger.debug(
1834+
"POST to %r with params %r, headers %r, proxies %r",
1835+
self.endpoints.get_audio_connector_url(),
1836+
json.dumps(payload),
1837+
self.get_json_headers(),
1838+
self.proxies,
1839+
)
1840+
1841+
response = requests.post(
1842+
self.endpoints.get_audio_connector_url(),
1843+
json=payload,
1844+
headers=self.get_json_headers(),
1845+
proxies=self.proxies,
1846+
timeout=self.timeout,
1847+
)
1848+
1849+
if response and response.status_code == 200:
1850+
return WebSocketAudioConnection(response.json())
1851+
elif response.status_code == 400:
1852+
"""
1853+
The HTTP response has a 400 status code in the following cases:
1854+
You did not pass in a session ID or you pass in an invalid session ID.
1855+
You specified an invalid value for input parameters.
1856+
"""
1857+
raise RequestError(response.json().get("message"))
1858+
elif response.status_code == 403:
1859+
raise AuthError("You passed in an invalid OpenTok API key or JWT token.")
1860+
elif response.status_code == 409:
1861+
raise InvalidMediaModeError("Only routed sessions are allowed to initiate Audio Connector WebSocket connections.")
1862+
else:
1863+
raise RequestError("An unexpected error occurred", response.status_code)
1864+
1865+
def validate_websocket_options(self, options):
1866+
if type(options) is not dict:
1867+
raise InvalidWebSocketOptionsError('Must pass WebSocket options as a dictionary.')
1868+
if 'uri' not in options:
1869+
raise InvalidWebSocketOptionsError('Provide a WebSocket URI.')
1870+
18131871
def _sign_string(self, string, secret):
18141872
return hmac.new(
18151873
secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1

opentok/websocket_audio_connection.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import json
2+
from six import iteritems
3+
4+
class WebSocketAudioConnection:
5+
"""Represents information about the audio connection of an OpenTok session to a WebSocket."""
6+
7+
def __init__(self, kwargs):
8+
self.id = kwargs.get("id")
9+
self.connectionId = kwargs.get("connectionId")
10+
11+
def json(self):
12+
"""Returns a JSON representation of the WebSocket audio connection information."""
13+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
14+
15+
def attrs(self):
16+
"""
17+
Returns a dictionary of the WebSocket audio connection's attributes.
18+
"""
19+
return dict((k, v) for k, v in iteritems(self.__dict__))

tests/test_audio_connector.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import unittest
2+
import textwrap
3+
import httpretty
4+
import json
5+
from sure import expect
6+
7+
from six import u, PY2, PY3
8+
from expects import *
9+
from opentok import Client, WebSocketAudioConnection, __version__
10+
from opentok.exceptions import InvalidWebSocketOptionsError, InvalidMediaModeError
11+
from .validate_jwt import validate_jwt_header
12+
13+
14+
class OpenTokAudioConnectorLiteTest(unittest.TestCase):
15+
def setUp(self):
16+
self.api_key = u("123456")
17+
self.api_secret = u("1234567890abcdef1234567890abcdef1234567890")
18+
self.opentok = Client(self.api_key, self.api_secret)
19+
self.session_id = u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")
20+
self.token = u("1234-5678-9012")
21+
self.response_body = textwrap.dedent(
22+
u(
23+
""" \
24+
{
25+
"id": "b0a5a8c7-dc38-459f-a48d-a7f2008da853",
26+
"connectionId": "e9f8c166-6c67-440d-994a-04fb6dfed007"
27+
}
28+
"""
29+
)
30+
)
31+
32+
@httpretty.activate
33+
def test_connect_audio_to_websocket(self):
34+
httpretty.register_uri(
35+
httpretty.POST,
36+
u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"),
37+
body=self.response_body,
38+
status=200,
39+
content_type=u("application/json"),
40+
)
41+
42+
websocket_options = {"uri": "wss://service.com/ws-endpoint"}
43+
44+
websocket_audio_connection = self.opentok.connect_audio_to_websocket(
45+
self.session_id, self.token, websocket_options
46+
)
47+
48+
validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")])
49+
expect(httpretty.last_request().headers[u("user-agent")]).to(contain(u("OpenTok-Python-SDK/") + __version__))
50+
expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json")))
51+
# non-deterministic json encoding. have to decode to test it properly
52+
if PY2:
53+
body = json.loads(httpretty.last_request().body)
54+
if PY3:
55+
body = json.loads(httpretty.last_request().body.decode("utf-8"))
56+
57+
expect(body).to(have_key(u("token")))
58+
expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection))
59+
expect(websocket_audio_connection).to(have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")))
60+
expect(websocket_audio_connection).to(
61+
have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007"))
62+
)
63+
64+
@httpretty.activate
65+
def test_connect_audio_to_websocket_custom_options(self):
66+
httpretty.register_uri(
67+
httpretty.POST,
68+
u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"),
69+
body=self.response_body,
70+
status=200,
71+
content_type=u("application/json"),
72+
)
73+
74+
websocket_options = {
75+
"uri": "wss://service.com/ws-endpoint",
76+
"streams": ["stream-id-1", "stream-id-2"],
77+
"headers": {"WebSocketHeader": "Sent via Audio Connector API"},
78+
}
79+
80+
websocket_audio_connection = self.opentok.connect_audio_to_websocket(
81+
self.session_id, self.token, websocket_options
82+
)
83+
84+
validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")])
85+
expect(httpretty.last_request().headers[u("user-agent")]).to(contain(u("OpenTok-Python-SDK/") + __version__))
86+
expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json")))
87+
# non-deterministic json encoding. have to decode to test it properly
88+
if PY2:
89+
body = json.loads(httpretty.last_request().body)
90+
if PY3:
91+
body = json.loads(httpretty.last_request().body.decode("utf-8"))
92+
93+
expect(body).to(have_key(u("token")))
94+
expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection))
95+
expect(websocket_audio_connection).to(have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")))
96+
expect(websocket_audio_connection).to(
97+
have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007"))
98+
)
99+
100+
@httpretty.activate
101+
def test_connect_audio_to_websocket_media_mode_error(self):
102+
httpretty.register_uri(
103+
httpretty.POST,
104+
u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"),
105+
body={},
106+
status=409,
107+
content_type=u("application/json"),
108+
)
109+
110+
websocket_options = {"uri": "wss://service.com/ws-endpoint"}
111+
112+
session_id = "session-where-mediaMode=relayed-was-selected"
113+
114+
with self.assertRaises(InvalidMediaModeError) as context:
115+
self.opentok.connect_audio_to_websocket(session_id, self.token, websocket_options)
116+
self.assertTrue(
117+
"Only routed sessions are allowed to initiate Audio Connector WebSocket connections."
118+
in str(context.exception)
119+
)
120+
121+
validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")])
122+
expect(httpretty.last_request().headers[u("user-agent")]).to(contain(u("OpenTok-Python-SDK/") + __version__))
123+
expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json")))
124+
125+
def test_connect_audio_to_websocket_invalid_options_type_error(self):
126+
websocket_options = "wss://service.com/ws-endpoint"
127+
with self.assertRaises(InvalidWebSocketOptionsError) as context:
128+
self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options)
129+
self.assertTrue("Must pass WebSocket options as a dictionary." in str(context.exception))
130+
131+
def test_connect_audio_to_websocket_missing_uri_error(self):
132+
websocket_options = {}
133+
with self.assertRaises(InvalidWebSocketOptionsError) as context:
134+
self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options)
135+
self.assertTrue("Provide a WebSocket URI." in str(context.exception))

0 commit comments

Comments
 (0)