From a46013480dd5f8f540be52bccff106662340fffa Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:14:59 +0000 Subject: [PATCH 1/9] added audio streamer lite functionality and tests --- opentok/__init__.py | 1 + opentok/endpoints.py | 6 ++ opentok/exceptions.py | 12 +++ opentok/opentok.py | 68 ++++++++++++- opentok/webhook_audio_connection.py | 20 ++++ tests/test_audio_streamer.py | 152 ++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 opentok/webhook_audio_connection.py create mode 100644 tests/test_audio_streamer.py diff --git a/opentok/__init__.py b/opentok/__init__.py index b0ccd57..0b7abc6 100644 --- a/opentok/__init__.py +++ b/opentok/__init__.py @@ -15,3 +15,4 @@ from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes from .render import Render, RenderList +from .webhook_audio_connection import WebhookAudioConnection diff --git a/opentok/endpoints.py b/opentok/endpoints.py index cb3a47c..d101eb6 100644 --- a/opentok/endpoints.py +++ b/opentok/endpoints.py @@ -208,3 +208,9 @@ def get_render_url(self, render_id: str = None): url += "/" + render_id return url + + def get_audio_streamer_url(self): + """Returns URLs for working with the Audio Streamer (lite) API.""" + url = self.api_url + "/v2/project/" + self.api_key + "/connect" + + return url diff --git a/opentok/exceptions.py b/opentok/exceptions.py index 099733e..80bba1b 100644 --- a/opentok/exceptions.py +++ b/opentok/exceptions.py @@ -116,3 +116,15 @@ class BroadcastHLSOptionsError(OpenTokException): dvr and lowLatency modes cannot both be set to true in a broadcast. """ + + +class InvalidWebsocketOptionsError(OpenTokException): + """ + Indicates that the websocket options selected are invalid. + """ + + +class InvalidMediaModeError(OpenTokException): + """ + Indicates that the media mode selected was not valid for the type of request made. + """ diff --git a/opentok/opentok.py b/opentok/opentok.py index 07821a3..3365bb1 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -16,7 +16,6 @@ import random # _create_jwt_auth_header import logging # logging import warnings # Native. Used for notifying deprecations -import os # compat @@ -33,6 +32,7 @@ from .streamlist import StreamList from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes +from .webhook_audio_connection import WebhookAudioConnection from .exceptions import ( ArchiveStreamModeError, BroadcastHLSOptionsError, @@ -48,8 +48,9 @@ SipDialError, SetStreamClassError, BroadcastError, - DTMFError - + DTMFError, + InvalidWebsocketOptionsError, + InvalidMediaModeError ) @@ -1653,7 +1654,7 @@ def start_render(self, session_id, opentok_token, url, max_duration=7200, resolu `Experience Composer developer guide `_. :param String 'session_id': The session ID of the OpenTok session that will include the Experience Composer stream. - :param String 'token': A valid OpenTok token with a Publisher role and (optionally) connection data to be associated with the output stream. + :param String 'opentok_token': A valid OpenTok token with a Publisher role and (optionally) connection data to be associated with the output stream. :param String 'url': A publically reachable URL controlled by the customer and capable of generating the content to be rendered without user intervention. :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. :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). @@ -1699,7 +1700,6 @@ def start_render(self, session_id, opentok_token, url, max_duration=7200, resolu else: raise RequestError("An unexpected error occurred", response.status_code) - def get_render(self, render_id): """ This method allows you to see the status of a render, which can be one of the following: @@ -1804,6 +1804,64 @@ def list_renders(self, offset=0, count=50): else: raise RequestError("An unexpected error occurred", response.status_code) + def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websocket_options: dict): + """ + Connects audio streams to a specified webhook URI. + For more information, see the `Audio Streamer developer guide `. + + :param String 'session_id': The OpenTok session ID that includes the OpenTok streams you want to include in the WebSocket stream. The Audio Streamer feature is only supported in routed sessions (sessions that use the OpenTok Media Router). + :param String 'opentok_token': The OpenTok token to be used for the Audio Streamer connection to the OpenTok session. + :param Dictionary 'websocket_options': Included options for the websocket. + String 'uri': A publicly reachable WebSocket URI to be used for the destination of the audio stream (such as "wss://example.com/ws-endpoint"). + 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. + 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. + """ + self.validate_websocket_options(websocket_options) + + payload = { + "sessionId": session_id, + "token": opentok_token, + "websocket": websocket_options + } + + logger.debug( + "POST to %r with params %r, headers %r, proxies %r", + self.endpoints.get_audio_streamer_url(), + json.dumps(payload), + self.get_json_headers(), + self.proxies, + ) + + response = requests.post( + self.endpoints.get_audio_streamer_url(), + json=payload, + headers=self.get_json_headers(), + proxies=self.proxies, + timeout=self.timeout, + ) + + if response and response.status_code == 200: + return WebhookAudioConnection(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 pass 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 token.") + elif response.status_code == 409: + raise InvalidMediaModeError("Only routed sessions are allowed to initiate Audio Streamer WebSocket connections.") + else: + raise RequestError("An unexpected error occurred", response.status_code) + + def validate_websocket_options(self, options): + if type(options) is not dict: + raise InvalidWebsocketOptionsError('Must pass websocket options as a dictionary.') + if 'uri' not in options: + raise InvalidWebsocketOptionsError('Provide a webhook URI.') + def _sign_string(self, string, secret): return hmac.new( secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1 diff --git a/opentok/webhook_audio_connection.py b/opentok/webhook_audio_connection.py new file mode 100644 index 0000000..6a1bfb6 --- /dev/null +++ b/opentok/webhook_audio_connection.py @@ -0,0 +1,20 @@ +import json +from six import iteritems + +class WebhookAudioConnection: + """Represents information about the audio streaming of an OpenTok session to a webhook.""" + + def __init__(self, kwargs): + self.id = kwargs.get("id") + self.connectionId = kwargs.get("connectionId") + + def json(self): + """Returns a JSON representation of the webhook audio connection information.""" + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + def attrs(self): + """ + Returns a dictionary of the webhook audio connection's attributes. + """ + return dict((k, v) for k, v in iteritems(self.__dict__)) + \ No newline at end of file diff --git a/tests/test_audio_streamer.py b/tests/test_audio_streamer.py new file mode 100644 index 0000000..d6d349f --- /dev/null +++ b/tests/test_audio_streamer.py @@ -0,0 +1,152 @@ +import unittest +import textwrap +import httpretty +import json +from sure import expect + +from six import u, PY2, PY3 +from expects import * +from opentok import Client, WebhookAudioConnection, __version__ +from opentok.exceptions import InvalidWebsocketOptionsError, InvalidMediaModeError +from .validate_jwt import validate_jwt_header + + +class OpenTokAudioStreamerLiteTest(unittest.TestCase): + def setUp(self): + self.api_key = u("123456") + self.api_secret = u("1234567890abcdef1234567890abcdef1234567890") + self.opentok = Client(self.api_key, self.api_secret) + self.session_id = u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4") + self.token = u("1234-5678-9012") + self.response_body = textwrap.dedent( + u( + """ \ + { + "id": "b0a5a8c7-dc38-459f-a48d-a7f2008da853", + "connectionId": "e9f8c166-6c67-440d-994a-04fb6dfed007" + } + """ + ) + ) + + @httpretty.activate + def test_stream_audio_to_websocket(self): + httpretty.register_uri( + httpretty.POST, + u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), + body=self.response_body, + status=200, + content_type=u("application/json"), + ) + + websocket_options = { + "uri": "wss://service.com/ws-endpoint" + } + + webhook_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + + 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("token"))) + expect(webhook_audio_connection).to(be_a(WebhookAudioConnection)) + expect(webhook_audio_connection).to( + have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) + ) + expect(webhook_audio_connection).to( + have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) + ) + + @httpretty.activate + def test_stream_audio_to_websocket_custom_options(self): + httpretty.register_uri( + httpretty.POST, + u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), + body=self.response_body, + status=200, + content_type=u("application/json"), + ) + + websocket_options = { + "uri": "wss://service.com/ws-endpoint", + "streams": ["stream-id-1", "stream-id-2"], + "headers": { + "websocketHeader": "Sent via Audio Streamer API" + } + } + + webhook_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + + 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("token"))) + expect(webhook_audio_connection).to(be_a(WebhookAudioConnection)) + expect(webhook_audio_connection).to( + have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) + ) + expect(webhook_audio_connection).to( + have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) + ) + + @httpretty.activate + def test_stream_audio_to_websocket_media_mode_error(self): + httpretty.register_uri( + httpretty.POST, + u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), + body={}, + status=409, + content_type=u("application/json"), + ) + + websocket_options = { + "uri": "wss://service.com/ws-endpoint" + } + + session_id = "session-where-mediaMode=relayed-was-selected" + + with self.assertRaises(InvalidMediaModeError) as context: + self.opentok.stream_audio_to_websocket(session_id, self.token, websocket_options) + self.assertTrue("Only routed sessions are allowed to initiate Audio Streamer WebSocket connections." in str(context.exception)) + + 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")) + ) + + def test_stream_audio_to_websocket_invalid_options_type_error(self): + websocket_options = "wss://service.com/ws-endpoint" + with self.assertRaises(InvalidWebsocketOptionsError) as context: + self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + self.assertTrue("Must pass websocket options as a dictionary." in str(context.exception)) + + def test_stream_audio_to_websocket_missing_uri_error(self): + websocket_options = {} + with self.assertRaises(InvalidWebsocketOptionsError) as context: + self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + self.assertTrue("Provide a webhook URI." in str(context.exception)) From bf7849efbb2dad473fcc14f8806d14706cbc2dda Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:33:03 +0000 Subject: [PATCH 2/9] renaming webhook -> websocket --- README.rst | 9 +++++++++ opentok/__init__.py | 2 +- opentok/opentok.py | 8 ++++---- opentok/webhook_audio_connection.py | 20 -------------------- tests/test_audio_streamer.py | 20 ++++++++++---------- 5 files changed, 24 insertions(+), 35 deletions(-) delete mode 100644 opentok/webhook_audio_connection.py diff --git a/README.rst b/README.rst index cb1462c..26fb1f0 100644 --- a/README.rst +++ b/README.rst @@ -551,6 +551,15 @@ For more information about OpenTok live streaming broadcasts, see the `Broadcast developer guide `_. +Streaming audio to a websocket +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can stream audio to a websocket with the ``opentok.stream_audio_to_websocket`` method. + +.. code:: python + + websocket_options = {"uri": "wss://service.com/ws-endpoint"} + websocketopentok.stream_audio_to_websocket(session_id, opentok_token, websocket_options) + Configuring Timeout ------------------- Timeout is passed in the Client constructor: diff --git a/opentok/__init__.py b/opentok/__init__.py index 0b7abc6..8ac0848 100644 --- a/opentok/__init__.py +++ b/opentok/__init__.py @@ -15,4 +15,4 @@ from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes from .render import Render, RenderList -from .webhook_audio_connection import WebhookAudioConnection +from .websocket_audio_connection import WebsocketAudioConnection diff --git a/opentok/opentok.py b/opentok/opentok.py index 3365bb1..34a04c1 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -32,7 +32,7 @@ from .streamlist import StreamList from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes -from .webhook_audio_connection import WebhookAudioConnection +from .websocket_audio_connection import WebsocketAudioConnection from .exceptions import ( ArchiveStreamModeError, BroadcastHLSOptionsError, @@ -1806,7 +1806,7 @@ def list_renders(self, offset=0, count=50): def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websocket_options: dict): """ - Connects audio streams to a specified webhook URI. + Connects audio streams to a specified websocket URI. For more information, see the `Audio Streamer developer guide `. :param String 'session_id': The OpenTok session ID that includes the OpenTok streams you want to include in the WebSocket stream. The Audio Streamer feature is only supported in routed sessions (sessions that use the OpenTok Media Router). @@ -1841,7 +1841,7 @@ def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websock ) if response and response.status_code == 200: - return WebhookAudioConnection(response.json()) + return WebsocketAudioConnection(response.json()) elif response.status_code == 400: """ The HTTP response has a 400 status code in the following cases: @@ -1860,7 +1860,7 @@ def validate_websocket_options(self, options): if type(options) is not dict: raise InvalidWebsocketOptionsError('Must pass websocket options as a dictionary.') if 'uri' not in options: - raise InvalidWebsocketOptionsError('Provide a webhook URI.') + raise InvalidWebsocketOptionsError('Provide a websocket URI.') def _sign_string(self, string, secret): return hmac.new( diff --git a/opentok/webhook_audio_connection.py b/opentok/webhook_audio_connection.py deleted file mode 100644 index 6a1bfb6..0000000 --- a/opentok/webhook_audio_connection.py +++ /dev/null @@ -1,20 +0,0 @@ -import json -from six import iteritems - -class WebhookAudioConnection: - """Represents information about the audio streaming of an OpenTok session to a webhook.""" - - def __init__(self, kwargs): - self.id = kwargs.get("id") - self.connectionId = kwargs.get("connectionId") - - def json(self): - """Returns a JSON representation of the webhook audio connection information.""" - return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) - - def attrs(self): - """ - Returns a dictionary of the webhook audio connection's attributes. - """ - return dict((k, v) for k, v in iteritems(self.__dict__)) - \ No newline at end of file diff --git a/tests/test_audio_streamer.py b/tests/test_audio_streamer.py index d6d349f..6a98c51 100644 --- a/tests/test_audio_streamer.py +++ b/tests/test_audio_streamer.py @@ -6,7 +6,7 @@ from six import u, PY2, PY3 from expects import * -from opentok import Client, WebhookAudioConnection, __version__ +from opentok import Client, WebsocketAudioConnection, __version__ from opentok.exceptions import InvalidWebsocketOptionsError, InvalidMediaModeError from .validate_jwt import validate_jwt_header @@ -43,7 +43,7 @@ def test_stream_audio_to_websocket(self): "uri": "wss://service.com/ws-endpoint" } - webhook_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + websocket_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) expect(httpretty.last_request().headers[u("user-agent")]).to( @@ -60,11 +60,11 @@ def test_stream_audio_to_websocket(self): body = json.loads(httpretty.last_request().body.decode("utf-8")) expect(body).to(have_key(u("token"))) - expect(webhook_audio_connection).to(be_a(WebhookAudioConnection)) - expect(webhook_audio_connection).to( + expect(websocket_audio_connection).to(be_a(WebsocketAudioConnection)) + expect(websocket_audio_connection).to( have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) ) - expect(webhook_audio_connection).to( + expect(websocket_audio_connection).to( have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) ) @@ -86,7 +86,7 @@ def test_stream_audio_to_websocket_custom_options(self): } } - webhook_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + websocket_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) expect(httpretty.last_request().headers[u("user-agent")]).to( @@ -103,11 +103,11 @@ def test_stream_audio_to_websocket_custom_options(self): body = json.loads(httpretty.last_request().body.decode("utf-8")) expect(body).to(have_key(u("token"))) - expect(webhook_audio_connection).to(be_a(WebhookAudioConnection)) - expect(webhook_audio_connection).to( + expect(websocket_audio_connection).to(be_a(WebsocketAudioConnection)) + expect(websocket_audio_connection).to( have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) ) - expect(webhook_audio_connection).to( + expect(websocket_audio_connection).to( have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) ) @@ -149,4 +149,4 @@ def test_stream_audio_to_websocket_missing_uri_error(self): websocket_options = {} with self.assertRaises(InvalidWebsocketOptionsError) as context: self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) - self.assertTrue("Provide a webhook URI." in str(context.exception)) + self.assertTrue("Provide a websocket URI." in str(context.exception)) From 595c5d7629b43bf38c8ef517b62840b641fd52ce Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:35:11 +0000 Subject: [PATCH 3/9] renaming webhook_audio_connection module -> websocket_audio_connection --- opentok/websocket_audio_connection.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 opentok/websocket_audio_connection.py diff --git a/opentok/websocket_audio_connection.py b/opentok/websocket_audio_connection.py new file mode 100644 index 0000000..2ac1d5a --- /dev/null +++ b/opentok/websocket_audio_connection.py @@ -0,0 +1,20 @@ +import json +from six import iteritems + +class WebsocketAudioConnection: + """Represents information about the audio streaming of an OpenTok session to a websocket.""" + + def __init__(self, kwargs): + self.id = kwargs.get("id") + self.connectionId = kwargs.get("connectionId") + + def json(self): + """Returns a JSON representation of the websocket audio connection information.""" + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + def attrs(self): + """ + Returns a dictionary of the websocket audio connection's attributes. + """ + return dict((k, v) for k, v in iteritems(self.__dict__)) + \ No newline at end of file From c16d71542fb7fcda7168871e8ea40e954e897587 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:42:03 +0000 Subject: [PATCH 4/9] adding audio streamer docs --- README.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 26fb1f0..56dc014 100644 --- a/README.rst +++ b/README.rst @@ -554,11 +554,29 @@ For more information about OpenTok live streaming broadcasts, see the Streaming audio to a websocket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can stream audio to a websocket with the ``opentok.stream_audio_to_websocket`` method. +For more information, see the +`Audio Streamer developer guide `_. .. code:: python websocket_options = {"uri": "wss://service.com/ws-endpoint"} - websocketopentok.stream_audio_to_websocket(session_id, opentok_token, websocket_options) + websocket_audio_connection = opentok.stream_audio_to_websocket(session_id, opentok_token, websocket_options) + +Additionally, you can specify only the streams you want to send to the websocket, and/or the additional headers that are sent, +by adding these fields to the ``websocket_options`` object. + +.. code:: python + + websocket_options = { + "uri": "wss://service.com/ws-endpoint", + "streams": [ + "streamId-1", + "streamId-2" + ], + "headers": { + "headerKey": "headerValue" + } + } Configuring Timeout ------------------- From b73ec699d3a9b06c36a45f50c1533304a8f3b172 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:52:19 +0000 Subject: [PATCH 5/9] rephrasing doc sentence --- README.rst | 2 +- opentok/websocket_audio_connection.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 56dc014..9387dbb 100644 --- a/README.rst +++ b/README.rst @@ -562,7 +562,7 @@ For more information, see the websocket_options = {"uri": "wss://service.com/ws-endpoint"} websocket_audio_connection = opentok.stream_audio_to_websocket(session_id, opentok_token, websocket_options) -Additionally, you can specify only the streams you want to send to the websocket, and/or the additional headers that are sent, +Additionally, you can list only the specific streams you want to send to the websocket, and/or the additional headers that are sent, by adding these fields to the ``websocket_options`` object. .. code:: python diff --git a/opentok/websocket_audio_connection.py b/opentok/websocket_audio_connection.py index 2ac1d5a..d54f9d4 100644 --- a/opentok/websocket_audio_connection.py +++ b/opentok/websocket_audio_connection.py @@ -17,4 +17,3 @@ def attrs(self): Returns a dictionary of the websocket audio connection's attributes. """ return dict((k, v) for k, v in iteritems(self.__dict__)) - \ No newline at end of file From b931db522563f2c0c2a1e2c549da4050ed75bca0 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:52:23 +0000 Subject: [PATCH 6/9] =?UTF-8?q?Bump=20version:=203.3.0=20=E2=86=92=203.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- opentok/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index fc3cc3a..7215d9a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.3.0 +current_version = 3.4.0 commit = True tag = False diff --git a/opentok/version.py b/opentok/version.py index 2f29f36..44d145a 100644 --- a/opentok/version.py +++ b/opentok/version.py @@ -1,3 +1,3 @@ # see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers -__version__ = "3.3.0" +__version__ = "3.4.0" From 4065a01558afc0426c0f5249a639ac8cd8af5138 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 8 Nov 2022 18:54:18 +0000 Subject: [PATCH 7/9] updated CHANGES.md --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 2120a1b..d7c8049 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# Release v3.4.0 + +- Support for Audio Streamer (lite) API via `stream_audio_to_websocket` method + # Release v3.3.0 - Support for Experience Composer (Render) API From 576e6ea9cb9a9a8acb149604c5ecd413cb591857 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Sat, 12 Nov 2022 00:54:22 +0000 Subject: [PATCH 8/9] changing audio streamer -> audio connector, fixing typos --- CHANGES.md | 2 +- README.rst | 10 +++---- opentok/__init__.py | 2 +- opentok/endpoints.py | 4 +-- opentok/exceptions.py | 6 ++-- opentok/opentok.py | 28 +++++++++---------- opentok/websocket_audio_connection.py | 8 +++--- tests/test_audio_streamer.py | 40 +++++++++++++-------------- 8 files changed, 49 insertions(+), 51 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d7c8049..45e7b7a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Release v3.4.0 -- Support for Audio Streamer (lite) API via `stream_audio_to_websocket` method +- Support for Audio Connector API via `connect_audio_to_websocket` method # Release v3.3.0 diff --git a/README.rst b/README.rst index 9387dbb..3d8afe6 100644 --- a/README.rst +++ b/README.rst @@ -551,18 +551,18 @@ For more information about OpenTok live streaming broadcasts, see the `Broadcast developer guide `_. -Streaming audio to a websocket +Streaming audio to a WebSocket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can stream audio to a websocket with the ``opentok.stream_audio_to_websocket`` method. +You can stream audio to a WebSocket with the ``opentok.connect_audio_to_websocket`` method. For more information, see the -`Audio Streamer developer guide `_. +`Audio Connector developer guide `_. .. code:: python websocket_options = {"uri": "wss://service.com/ws-endpoint"} - websocket_audio_connection = opentok.stream_audio_to_websocket(session_id, opentok_token, websocket_options) + websocket_audio_connection = opentok.connect_audio_to_websocket(session_id, opentok_token, websocket_options) -Additionally, you can list only the specific streams you want to send to the websocket, and/or the additional headers that are sent, +Additionally, you can list only the specific streams you want to send to the WebSocket, and/or the additional headers that are sent, by adding these fields to the ``websocket_options`` object. .. code:: python diff --git a/opentok/__init__.py b/opentok/__init__.py index 8ac0848..8900677 100644 --- a/opentok/__init__.py +++ b/opentok/__init__.py @@ -15,4 +15,4 @@ from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes from .render import Render, RenderList -from .websocket_audio_connection import WebsocketAudioConnection +from .websocket_audio_connection import WebSocketAudioConnection diff --git a/opentok/endpoints.py b/opentok/endpoints.py index d101eb6..ddbb497 100644 --- a/opentok/endpoints.py +++ b/opentok/endpoints.py @@ -209,8 +209,8 @@ def get_render_url(self, render_id: str = None): return url - def get_audio_streamer_url(self): - """Returns URLs for working with the Audio Streamer (lite) API.""" + def get_audio_connector_url(self): + """Returns URLs for working with the Audio Connector API.""" url = self.api_url + "/v2/project/" + self.api_key + "/connect" return url diff --git a/opentok/exceptions.py b/opentok/exceptions.py index 80bba1b..2a0b9ee 100644 --- a/opentok/exceptions.py +++ b/opentok/exceptions.py @@ -107,8 +107,6 @@ class BroadcastStreamModeError(OpenTokException): Indicates that the broadcast is configured with a streamMode that does not support stream manipulation. """ - pass - class BroadcastHLSOptionsError(OpenTokException): """ @@ -118,9 +116,9 @@ class BroadcastHLSOptionsError(OpenTokException): """ -class InvalidWebsocketOptionsError(OpenTokException): +class InvalidWebSocketOptionsError(OpenTokException): """ - Indicates that the websocket options selected are invalid. + Indicates that the WebSocket options selected are invalid. """ diff --git a/opentok/opentok.py b/opentok/opentok.py index 34a04c1..09f62fa 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -32,7 +32,7 @@ from .streamlist import StreamList from .sip_call import SipCall from .broadcast import Broadcast, BroadcastStreamModes -from .websocket_audio_connection import WebsocketAudioConnection +from .websocket_audio_connection import WebSocketAudioConnection from .exceptions import ( ArchiveStreamModeError, BroadcastHLSOptionsError, @@ -49,7 +49,7 @@ SetStreamClassError, BroadcastError, DTMFError, - InvalidWebsocketOptionsError, + InvalidWebSocketOptionsError, InvalidMediaModeError ) @@ -1804,14 +1804,14 @@ def list_renders(self, offset=0, count=50): else: raise RequestError("An unexpected error occurred", response.status_code) - def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websocket_options: dict): + def connect_audio_to_websocket(self, session_id: str, opentok_token: str, websocket_options: dict): """ - Connects audio streams to a specified websocket URI. - For more information, see the `Audio Streamer developer guide `. + Connects audio streams to a specified WebSocket URI. + For more information, see the `Audio Connector developer guide `. - :param String 'session_id': The OpenTok session ID that includes the OpenTok streams you want to include in the WebSocket stream. The Audio Streamer feature is only supported in routed sessions (sessions that use the OpenTok Media Router). - :param String 'opentok_token': The OpenTok token to be used for the Audio Streamer connection to the OpenTok session. - :param Dictionary 'websocket_options': Included options for the websocket. + :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). + :param String 'opentok_token': The OpenTok token to be used for the Audio Connector connection to the OpenTok session. + :param Dictionary 'websocket_options': Included options for the WebSocket. String 'uri': A publicly reachable WebSocket URI to be used for the destination of the audio stream (such as "wss://example.com/ws-endpoint"). 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. 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. @@ -1826,14 +1826,14 @@ def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websock logger.debug( "POST to %r with params %r, headers %r, proxies %r", - self.endpoints.get_audio_streamer_url(), + self.endpoints.get_audio_connector_url(), json.dumps(payload), self.get_json_headers(), self.proxies, ) response = requests.post( - self.endpoints.get_audio_streamer_url(), + self.endpoints.get_audio_connector_url(), json=payload, headers=self.get_json_headers(), proxies=self.proxies, @@ -1841,7 +1841,7 @@ def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websock ) if response and response.status_code == 200: - return WebsocketAudioConnection(response.json()) + return WebSocketAudioConnection(response.json()) elif response.status_code == 400: """ The HTTP response has a 400 status code in the following cases: @@ -1852,15 +1852,15 @@ def stream_audio_to_websocket(self, session_id: str, opentok_token: str, websock elif response.status_code == 403: raise AuthError("You passed in an invalid OpenTok API key or JWT token.") elif response.status_code == 409: - raise InvalidMediaModeError("Only routed sessions are allowed to initiate Audio Streamer WebSocket connections.") + raise InvalidMediaModeError("Only routed sessions are allowed to initiate Audio Connector WebSocket connections.") else: raise RequestError("An unexpected error occurred", response.status_code) def validate_websocket_options(self, options): if type(options) is not dict: - raise InvalidWebsocketOptionsError('Must pass websocket options as a dictionary.') + raise InvalidWebSocketOptionsError('Must pass WebSocket options as a dictionary.') if 'uri' not in options: - raise InvalidWebsocketOptionsError('Provide a websocket URI.') + raise InvalidWebSocketOptionsError('Provide a WebSocket URI.') def _sign_string(self, string, secret): return hmac.new( diff --git a/opentok/websocket_audio_connection.py b/opentok/websocket_audio_connection.py index d54f9d4..4d4290e 100644 --- a/opentok/websocket_audio_connection.py +++ b/opentok/websocket_audio_connection.py @@ -1,19 +1,19 @@ import json from six import iteritems -class WebsocketAudioConnection: - """Represents information about the audio streaming of an OpenTok session to a websocket.""" +class WebSocketAudioConnection: + """Represents information about the audio connection of an OpenTok session to a WebSocket.""" def __init__(self, kwargs): self.id = kwargs.get("id") self.connectionId = kwargs.get("connectionId") def json(self): - """Returns a JSON representation of the websocket audio connection information.""" + """Returns a JSON representation of the WebSocket audio connection information.""" return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) def attrs(self): """ - Returns a dictionary of the websocket audio connection's attributes. + Returns a dictionary of the WebSocket audio connection's attributes. """ return dict((k, v) for k, v in iteritems(self.__dict__)) diff --git a/tests/test_audio_streamer.py b/tests/test_audio_streamer.py index 6a98c51..02f3747 100644 --- a/tests/test_audio_streamer.py +++ b/tests/test_audio_streamer.py @@ -6,8 +6,8 @@ from six import u, PY2, PY3 from expects import * -from opentok import Client, WebsocketAudioConnection, __version__ -from opentok.exceptions import InvalidWebsocketOptionsError, InvalidMediaModeError +from opentok import Client, WebSocketAudioConnection, __version__ +from opentok.exceptions import InvalidWebSocketOptionsError, InvalidMediaModeError from .validate_jwt import validate_jwt_header @@ -30,7 +30,7 @@ def setUp(self): ) @httpretty.activate - def test_stream_audio_to_websocket(self): + def test_connect_audio_to_websocket(self): httpretty.register_uri( httpretty.POST, u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), @@ -43,7 +43,7 @@ def test_stream_audio_to_websocket(self): "uri": "wss://service.com/ws-endpoint" } - websocket_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + websocket_audio_connection = self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) expect(httpretty.last_request().headers[u("user-agent")]).to( @@ -60,7 +60,7 @@ def test_stream_audio_to_websocket(self): body = json.loads(httpretty.last_request().body.decode("utf-8")) expect(body).to(have_key(u("token"))) - expect(websocket_audio_connection).to(be_a(WebsocketAudioConnection)) + expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection)) expect(websocket_audio_connection).to( have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) ) @@ -69,7 +69,7 @@ def test_stream_audio_to_websocket(self): ) @httpretty.activate - def test_stream_audio_to_websocket_custom_options(self): + def test_connect_audio_to_websocket_custom_options(self): httpretty.register_uri( httpretty.POST, u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), @@ -82,11 +82,11 @@ def test_stream_audio_to_websocket_custom_options(self): "uri": "wss://service.com/ws-endpoint", "streams": ["stream-id-1", "stream-id-2"], "headers": { - "websocketHeader": "Sent via Audio Streamer API" + "WebSocketHeader": "Sent via Audio Connector API" } } - websocket_audio_connection = self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) + websocket_audio_connection = self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) expect(httpretty.last_request().headers[u("user-agent")]).to( @@ -103,7 +103,7 @@ def test_stream_audio_to_websocket_custom_options(self): body = json.loads(httpretty.last_request().body.decode("utf-8")) expect(body).to(have_key(u("token"))) - expect(websocket_audio_connection).to(be_a(WebsocketAudioConnection)) + expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection)) expect(websocket_audio_connection).to( have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) ) @@ -112,7 +112,7 @@ def test_stream_audio_to_websocket_custom_options(self): ) @httpretty.activate - def test_stream_audio_to_websocket_media_mode_error(self): + def test_connect_audio_to_websocket_media_mode_error(self): httpretty.register_uri( httpretty.POST, u(f"https://api.opentok.com/v2/project/{self.api_key}/connect"), @@ -128,8 +128,8 @@ def test_stream_audio_to_websocket_media_mode_error(self): session_id = "session-where-mediaMode=relayed-was-selected" with self.assertRaises(InvalidMediaModeError) as context: - self.opentok.stream_audio_to_websocket(session_id, self.token, websocket_options) - self.assertTrue("Only routed sessions are allowed to initiate Audio Streamer WebSocket connections." in str(context.exception)) + self.opentok.connect_audio_to_websocket(session_id, self.token, websocket_options) + self.assertTrue("Only routed sessions are allowed to initiate Audio Connector WebSocket connections." in str(context.exception)) validate_jwt_header(self, httpretty.last_request().headers[u("x-opentok-auth")]) expect(httpretty.last_request().headers[u("user-agent")]).to( @@ -139,14 +139,14 @@ def test_stream_audio_to_websocket_media_mode_error(self): equal(u("application/json")) ) - def test_stream_audio_to_websocket_invalid_options_type_error(self): + def test_connect_audio_to_websocket_invalid_options_type_error(self): websocket_options = "wss://service.com/ws-endpoint" - with self.assertRaises(InvalidWebsocketOptionsError) as context: - self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) - self.assertTrue("Must pass websocket options as a dictionary." in str(context.exception)) + with self.assertRaises(InvalidWebSocketOptionsError) as context: + self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) + self.assertTrue("Must pass WebSocket options as a dictionary." in str(context.exception)) - def test_stream_audio_to_websocket_missing_uri_error(self): + def test_connect_audio_to_websocket_missing_uri_error(self): websocket_options = {} - with self.assertRaises(InvalidWebsocketOptionsError) as context: - self.opentok.stream_audio_to_websocket(self.session_id, self.token, websocket_options) - self.assertTrue("Provide a websocket URI." in str(context.exception)) + with self.assertRaises(InvalidWebSocketOptionsError) as context: + self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) + self.assertTrue("Provide a WebSocket URI." in str(context.exception)) From 285b39c2e84a2b1b31c9bd87be48df12f468890d Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 15 Feb 2023 12:06:44 +0000 Subject: [PATCH 9/9] renamed audio streamer instances to audio connector --- README.rst | 6 +- ...io_streamer.py => test_audio_connector.py} | 69 +++++++------------ 2 files changed, 29 insertions(+), 46 deletions(-) rename tests/{test_audio_streamer.py => test_audio_connector.py} (78%) diff --git a/README.rst b/README.rst index 3d8afe6..2bf36fa 100644 --- a/README.rst +++ b/README.rst @@ -551,11 +551,11 @@ For more information about OpenTok live streaming broadcasts, see the `Broadcast developer guide `_. -Streaming audio to a WebSocket +Connecting audio to a WebSocket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can stream audio to a WebSocket with the ``opentok.connect_audio_to_websocket`` method. +You can send audio to a WebSocket with the ``opentok.connect_audio_to_websocket`` method. For more information, see the -`Audio Connector developer guide `_. +`Audio Connector developer guide `_. .. code:: python diff --git a/tests/test_audio_streamer.py b/tests/test_audio_connector.py similarity index 78% rename from tests/test_audio_streamer.py rename to tests/test_audio_connector.py index 02f3747..fba29da 100644 --- a/tests/test_audio_streamer.py +++ b/tests/test_audio_connector.py @@ -11,7 +11,7 @@ from .validate_jwt import validate_jwt_header -class OpenTokAudioStreamerLiteTest(unittest.TestCase): +class OpenTokAudioConnectorLiteTest(unittest.TestCase): def setUp(self): self.api_key = u("123456") self.api_secret = u("1234567890abcdef1234567890abcdef1234567890") @@ -39,20 +39,15 @@ def test_connect_audio_to_websocket(self): content_type=u("application/json"), ) - websocket_options = { - "uri": "wss://service.com/ws-endpoint" - } + websocket_options = {"uri": "wss://service.com/ws-endpoint"} - websocket_audio_connection = self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) - - 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")) - + websocket_audio_connection = self.opentok.connect_audio_to_websocket( + self.session_id, self.token, websocket_options ) + + 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) @@ -61,9 +56,7 @@ def test_connect_audio_to_websocket(self): expect(body).to(have_key(u("token"))) expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection)) - expect(websocket_audio_connection).to( - have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) - ) + expect(websocket_audio_connection).to(have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853"))) expect(websocket_audio_connection).to( have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) ) @@ -81,21 +74,16 @@ def test_connect_audio_to_websocket_custom_options(self): websocket_options = { "uri": "wss://service.com/ws-endpoint", "streams": ["stream-id-1", "stream-id-2"], - "headers": { - "WebSocketHeader": "Sent via Audio Connector API" - } + "headers": {"WebSocketHeader": "Sent via Audio Connector API"}, } - websocket_audio_connection = self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) - - 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")) - + websocket_audio_connection = self.opentok.connect_audio_to_websocket( + self.session_id, self.token, websocket_options ) + + 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) @@ -104,9 +92,7 @@ def test_connect_audio_to_websocket_custom_options(self): expect(body).to(have_key(u("token"))) expect(websocket_audio_connection).to(be_a(WebSocketAudioConnection)) - expect(websocket_audio_connection).to( - have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853")) - ) + expect(websocket_audio_connection).to(have_property(u("id"), u("b0a5a8c7-dc38-459f-a48d-a7f2008da853"))) expect(websocket_audio_connection).to( have_property(u("connectionId"), u("e9f8c166-6c67-440d-994a-04fb6dfed007")) ) @@ -121,30 +107,27 @@ def test_connect_audio_to_websocket_media_mode_error(self): content_type=u("application/json"), ) - websocket_options = { - "uri": "wss://service.com/ws-endpoint" - } + websocket_options = {"uri": "wss://service.com/ws-endpoint"} session_id = "session-where-mediaMode=relayed-was-selected" with self.assertRaises(InvalidMediaModeError) as context: self.opentok.connect_audio_to_websocket(session_id, self.token, websocket_options) - self.assertTrue("Only routed sessions are allowed to initiate Audio Connector WebSocket connections." in str(context.exception)) - - 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")) + self.assertTrue( + "Only routed sessions are allowed to initiate Audio Connector WebSocket connections." + in str(context.exception) ) + 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"))) + def test_connect_audio_to_websocket_invalid_options_type_error(self): websocket_options = "wss://service.com/ws-endpoint" with self.assertRaises(InvalidWebSocketOptionsError) as context: self.opentok.connect_audio_to_websocket(self.session_id, self.token, websocket_options) self.assertTrue("Must pass WebSocket options as a dictionary." in str(context.exception)) - + def test_connect_audio_to_websocket_missing_uri_error(self): websocket_options = {} with self.assertRaises(InvalidWebSocketOptionsError) as context: