From e942db602ff643deb87801b37db74fe2f256b9ff Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 16 Aug 2023 22:25:07 +0100 Subject: [PATCH 1/5] add maxBitrate and hlsStatus to Broadcast API --- README.rst | 13 ++-- opentok/broadcast.py | 40 +++++++----- opentok/exceptions.py | 12 +++- opentok/opentok.py | 134 ++++++++++++++-------------------------- tests/test_broadcast.py | 121 +++++++++++++++++++----------------- 5 files changed, 154 insertions(+), 166 deletions(-) diff --git a/README.rst b/README.rst index f93badf..851c60b 100644 --- a/README.rst +++ b/README.rst @@ -438,8 +438,9 @@ The live streaming broadcast can target one HLS endpoint and up to five RTMP ser 'stylesheet': 'the layout stylesheet (only used with type == custom)' }, 'maxDuration': 5400, - 'hasAudio': True - 'hasVideo': True + 'hasAudio': True, + 'hasVideo': True, + 'maxBitrate': 2000000, 'outputs': { 'hls': {}, 'rtmp': [{ @@ -466,6 +467,8 @@ You can specify the following broadcast resolutions: * "1920x1080" (FHD landscape) * "1080x1920" (FHD portrait) +You can specify a maximum bitrate between 100000 and 6000000. + .. code:: python session_id = 'SESSIONID' @@ -476,6 +479,7 @@ You can specify the following broadcast resolutions: 'stylesheet': 'the layout stylesheet (only used with type == custom)' }, 'maxDuration': 5400, + 'maxBitrate': 2000000, 'outputs': { 'hls': {}, 'rtmp': [{ @@ -508,8 +512,9 @@ to ``False`` as required. These fields are ``True`` by default. 'stylesheet': 'the layout stylesheet (only used with type == custom)' }, 'maxDuration': 5400, - 'hasAudio': True - 'hasVideo': False + 'hasAudio': True, + 'hasVideo': False, + 'maxBitrate': 2000000, 'outputs': { 'hls': {}, 'rtmp': [{ diff --git a/opentok/broadcast.py b/opentok/broadcast.py index d44de86..a0620c1 100644 --- a/opentok/broadcast.py +++ b/opentok/broadcast.py @@ -24,7 +24,7 @@ class Broadcast(object): :ivar resolution: The resolution of the broadcast (either "640x480", "1280x720", "1920x1080", "480x640", "720x1280", or "1920x1080"). - + :ivar status: The status of the broadcast. @@ -34,29 +34,36 @@ class Broadcast(object): :ivar hasVideo: Whether the broadcast has video. + :ivar 'maxBitrate' optional: + The maximum bitrate (bits per second) used by the broadcast. + :ivar broadcastUrls: An object containing details about the HLS and RTMP broadcasts. - - If you specified an HLS endpoint, the object includes an hls property, which is set to the URL for the HLS broadcast. - Note this HLS broadcast URL points to an index file, an .M3U8-formatted playlist that contains a list of URLs + + If you specified an HLS endpoint, the object includes an hls property, which is set to the URL for the HLS broadcast. + Note this HLS broadcast URL points to an index file, an .M3U8-formatted playlist that contains a list of URLs to .ts media segment files (MPEG-2 transport stream files). While the URLs of both the playlist index file and media segment files are provided as soon as the HTTP response - is returned, these URLs should not be accessed until 15 - 20 seconds later, - after the initiation of the HLS broadcast, due to the delay between the HLS broadcast and the live streams - in the OpenTok session. - See https://developer.apple.com/library/ios/technotes/tn2288/_index.html for more information about the playlist index + is returned, these URLs should not be accessed until 15 - 20 seconds later, + after the initiation of the HLS broadcast, due to the delay between the HLS broadcast and the live streams + in the OpenTok session. + See https://developer.apple.com/library/ios/technotes/tn2288/_index.html for more information about the playlist index file and media segment files for HLS. - If you specified RTMP stream endpoints, the object includes an rtmp property. - This is an array of objects that include information on each of the RTMP streams. - Each of these objects has the following properties: id (the ID you assigned to the RTMP stream), - serverUrl (the server URL), streamName (the stream name), and status property (which is set to "connecting"). - You can call the OpenTok REST method to check for status updates for the broadcast: + If you specified an HLS endpoint, the object will also include an "hlsStatus" property with + information about the HLS broadcast. This will have one of the following values: + ["connecting", "ready", "live", "ended", "error"]. + + If you specified RTMP stream endpoints, the object includes an rtmp property. + This is an array of objects that include information on each of the RTMP streams. + Each of these objects has the following properties: id (the ID you assigned to the RTMP stream), + serverUrl (the server URL), streamName (the stream name), and status property (which is set to "connecting"). + You can call the OpenTok REST method to check for status updates for the broadcast: https://tokbox.com/developer/rest/#get_info_broadcast :ivar streamMode: Whether streams included in the broadcast are selected automatically - ("auto", the default) or manually ("manual"). + ("auto", the default) or manually ("manual"). :ivar streams: A list of streams currently being broadcasted. This is only set for a broadcast with @@ -71,6 +78,8 @@ def __init__(self, kwargs): self.updatedAt = kwargs.get("updatedAt") self.hasAudio = kwargs.get("hasAudio") self.hasVideo = kwargs.get("hasVideo") + self.maxBitrate = kwargs.get("maxBitrate") + self.maxDuration = kwargs.get("maxDuration") self.resolution = kwargs.get("resolution") self.status = kwargs.get("status") self.broadcastUrls = kwargs.get("broadcastUrls") @@ -83,8 +92,9 @@ def json(self): """ return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + class BroadcastStreamModes(Enum): - """"List of valid settings for the stream_mode parameter of the OpenTok.start_broadcast() + """ "List of valid settings for the stream_mode parameter of the OpenTok.start_broadcast() method.""" auto = u("auto") diff --git a/opentok/exceptions.py b/opentok/exceptions.py index 2a0b9ee..bece5af 100644 --- a/opentok/exceptions.py +++ b/opentok/exceptions.py @@ -89,11 +89,13 @@ class BroadcastError(OpenTokException): pass + class DTMFError(OpenTokException): """ Indicates that one of the properties digits, session_id or connection_id is invalid """ + class ArchiveStreamModeError(OpenTokException): """ Indicates that the archive is configured with a streamMode that does not support stream manipulation. @@ -110,12 +112,18 @@ class BroadcastStreamModeError(OpenTokException): class BroadcastHLSOptionsError(OpenTokException): """ - Indicates that HLS options have been set incorrectly. - + Indicates that HLS options have been set incorrectly. + dvr and lowLatency modes cannot both be set to true in a broadcast. """ +class BroadcastOptionsError(OpenTokException): + """ + Indicates that broadcast options have been set incorrectly. + """ + + class InvalidWebSocketOptionsError(OpenTokException): """ Indicates that the WebSocket options selected are invalid. diff --git a/opentok/opentok.py b/opentok/opentok.py index fec4fbd..d4320f2 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -35,6 +35,7 @@ from .websocket_audio_connection import WebSocketAudioConnection from .exceptions import ( ArchiveStreamModeError, + BroadcastOptionsError, BroadcastHLSOptionsError, BroadcastStreamModeError, OpenTokException, @@ -219,9 +220,7 @@ def generate_token( expire_time = int(expire_time) except (ValueError, TypeError): raise OpenTokException( - u("Cannot generate token, invalid expire time {0}").format( - expire_time - ) + u("Cannot generate token, invalid expire time {0}").format(expire_time) ) else: expire_time = int(time.time()) + (60 * 60 * 24) # 1 day @@ -229,38 +228,28 @@ def generate_token( # validations if not text_type(session_id): raise OpenTokException( - u("Cannot generate token, session_id was not valid {0}").format( - session_id - ) + u("Cannot generate token, session_id was not valid {0}").format(session_id) ) if not isinstance(role, Roles): - raise OpenTokException( - u("Cannot generate token, {0} is not a valid role").format(role) - ) + raise OpenTokException(u("Cannot generate token, {0} is not a valid role").format(role)) now = int(time.time()) if expire_time < now: raise OpenTokException( - u("Cannot generate token, expire_time is not in the future {0}").format( - expire_time - ) + u("Cannot generate token, expire_time is not in the future {0}").format(expire_time) ) if expire_time > now + (60 * 60 * 24 * 30): # 30 days raise OpenTokException( - u( - "Cannot generate token, expire_time is not in the next 30 days {0}" - ).format(expire_time) + u("Cannot generate token, expire_time is not in the next 30 days {0}").format( + expire_time + ) ) if data and len(data) > 1000: raise OpenTokException( u("Cannot generate token, data must be less than 1000 characters") ) - if initial_layout_class_list and not all( - text_type(c) for c in initial_layout_class_list - ): + if initial_layout_class_list and not all(text_type(c) for c in initial_layout_class_list): raise OpenTokException( - u( - "Cannot generate token, all items in initial_layout_class_list must be strings" - ) + u("Cannot generate token, all items in initial_layout_class_list must be strings") ) initial_layout_class_list_serialized = u(" ").join(initial_layout_class_list) if len(initial_layout_class_list_serialized) > 1000: @@ -281,9 +270,7 @@ def generate_token( parts = decoded_session_id.decode("utf-8").split(u("~")) except Exception as e: raise OpenTokException( - u("Cannot generate token, the session_id {0} was not valid").format( - session_id - ) + u("Cannot generate token, the session_id {0} was not valid").format(session_id) ) if self.api_key not in parts: raise OpenTokException( @@ -402,21 +389,15 @@ def create_session( options = {} if not isinstance(media_mode, MediaModes): raise OpenTokException( - u("Cannot create session, {0} is not a valid media mode").format( - media_mode - ) + u("Cannot create session, {0} is not a valid media mode").format(media_mode) ) if not isinstance(archive_mode, ArchiveModes): raise OpenTokException( - u("Cannot create session, {0} is not a valid archive mode").format( - archive_mode - ) + u("Cannot create session, {0} is not a valid archive mode").format(archive_mode) ) if archive_mode == ArchiveModes.always and media_mode != MediaModes.routed: raise OpenTokException( - u( - "A session with always archive mode must also have the routed media mode." - ) + u("A session with always archive mode must also have the routed media mode.") ) if archive_name is not None: @@ -497,9 +478,7 @@ def create_session( ) ) - session_id = ( - dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue - ) + session_id = dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue return Session( self, session_id, @@ -617,16 +596,12 @@ def start_archive( """ if not isinstance(output_mode, OutputModes): raise OpenTokException( - u("Cannot start archive, {0} is not a valid output mode").format( - output_mode - ) + u("Cannot start archive, {0} is not a valid output mode").format(output_mode) ) if resolution and output_mode == OutputModes.individual: raise OpenTokException( - u( - "Invalid parameters: Resolution cannot be supplied for individual output mode." - ) + u("Invalid parameters: Resolution cannot be supplied for individual output mode.") ) payload = { @@ -888,9 +863,7 @@ def add_archive_stream( else: raise RequestError("An unexpected error occurred.", response.status_code) - def remove_archive_stream( - self, archive_id: str, stream_id: str - ) -> requests.Response: + def remove_archive_stream(self, archive_id: str, stream_id: str) -> requests.Response: """ This method will remove streams from the archive with removeStream. @@ -1113,9 +1086,7 @@ def force_disconnect(self, session_id, connection_id): else: raise RequestError("An unexpected error occurred", response.status_code) - def set_archive_layout( - self, archive_id, layout_type, stylesheet=None, screenshare_type=None - ): + def set_archive_layout(self, archive_id, layout_type, stylesheet=None, screenshare_type=None): """ Use this method to change the layout of videos in an OpenTok archive @@ -1354,9 +1325,7 @@ class names (Strings) to apply to the stream. For example: else: raise RequestError("OpenTok server error.", response.status_code) - def start_broadcast( - self, session_id, options, stream_mode=BroadcastStreamModes.auto - ): + def start_broadcast(self, session_id, options, stream_mode=BroadcastStreamModes.auto): """ Use this method to start a live streaming broadcast for an OpenTok session. This broadcasts the session to an HLS (HTTP live streaming) or to RTMP streams. To successfully start @@ -1367,11 +1336,11 @@ def start_broadcast( :param String session_id: The session ID of the OpenTok session you want to broadcast - :param Boolean optional hasAudio: Whether the stream is broadcast with audio. + :param Dictionary options, with the following properties: - :param Boolean optional hasVideo: Whether the stream is broadcast with video. + :param Boolean optional hasAudio: Whether the stream is broadcast with audio. - :param Dictionary options, with the following properties: + :param Boolean optional hasVideo: Whether the stream is broadcast with video. Dictionary 'layout' optional: Specify this to assign the initial layout type for the broadcast. @@ -1392,6 +1361,9 @@ def start_broadcast( set the maximum duration to a value from 60 (60 seconds) to 36000 (10 hours). The default maximum duration is 4 hours (14,400 seconds) + Integer 'maxBitrate' optional: The maximum bitrate (bits per second) used by the broadcast. + Value must be between 100_000 and 6_000_000. + Dictionary 'outputs': This object defines the types of broadcast streams you want to start (both HLS and RTMP). You can include HLS, RTMP, or both as broadcast streams. If you include RTMP streaming, you can specify up to five target RTMP streams. For @@ -1436,10 +1408,7 @@ def start_broadcast( """ if "hls" in options["outputs"]: - if ( - "lowLatency" in options["outputs"]["hls"] - and "dvr" in options["outputs"]["hls"] - ): + if "lowLatency" in options["outputs"]["hls"] and "dvr" in options["outputs"]["hls"]: if ( options["outputs"]["hls"]["lowLatency"] == True and options["outputs"]["hls"]["dvr"] == True @@ -1448,6 +1417,16 @@ def start_broadcast( 'HLS options "lowLatency" and "dvr" cannot both be set to "True".' ) + if "maxBitrate" in options: + if ( + type(options["maxBitrate"]) != int + or options["maxBitrate"] < 100000 + or options["maxBitrate"] > 6000000 + ): + raise BroadcastOptionsError( + "maxBitrate must be an integer between 100000 and 6000000." + ) + payload = {"sessionId": session_id, "streamMode": stream_mode.value} payload.update(options) @@ -1523,8 +1502,7 @@ def stop_broadcast(self, broadcast_id): raise AuthError("Authentication error.") elif response.status_code == 409: raise BroadcastError( - "The broadcast (with the specified ID) was not found or it has already " - "stopped." + "The broadcast (with the specified ID) was not found or it has already " "stopped." ) else: raise RequestError("OpenTok server error.", response.status_code) @@ -1582,9 +1560,7 @@ def add_broadcast_stream( else: raise RequestError("An unexpected error occurred.", response.status_code) - def remove_broadcast_stream( - self, broadcast_id: str, stream_id: str - ) -> requests.Response: + def remove_broadcast_stream(self, broadcast_id: str, stream_id: str) -> requests.Response: """ This method will remove streams from the broadcast with removeStream. @@ -1947,16 +1923,12 @@ def connect_audio_to_websocket( 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.") def _sign_string(self, string, secret): - return hmac.new( - secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1 - ).hexdigest() + return hmac.new(secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1).hexdigest() def _create_jwt_auth_header(self): payload = { @@ -1997,9 +1969,7 @@ def mute_all( else: options = {"active": True, "excludedStreams": []} - response = requests.post( - url, headers=self.get_headers(), data=json.dumps(options) - ) + response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) if response: return response @@ -2034,9 +2004,7 @@ def disable_force_mute(self, session_id: str) -> requests.Response: options = {"active": False} url = self.endpoints.get_mute_all_url(session_id) - response = requests.post( - url, headers=self.get_headers(), data=json.dumps(options) - ) + response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) try: if response: @@ -2114,9 +2082,7 @@ def play_dtmf( url = self.endpoints.get_dtmf_specific_url(session_id, connection_id) payload = {"digits": digits} - response = requests.post( - url, headers=self.get_json_headers(), data=json.dumps(payload) - ) + response = requests.post(url, headers=self.get_json_headers(), data=json.dumps(payload)) if response.status_code == 200: return response @@ -2185,9 +2151,7 @@ def mute_all( else: options = {"active": True, "excludedStreams": []} - response = requests.post( - url, headers=self.get_headers(), data=json.dumps(options) - ) + response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) if response: return response @@ -2220,9 +2184,7 @@ def disable_force_mute(self, session_id: str) -> requests.Response: options = {"active": False} url = self.endpoints.get_mute_all_url(session_id) - response = requests.post( - url, headers=self.get_headers(), data=json.dumps(options) - ) + response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) try: if response: @@ -2294,9 +2256,7 @@ def play_dtmf( url = self.endpoints.get_dtmf_specific_url(session_id, connection_id) payload = {"digits": digits} - response = requests.post( - url, headers=self.get_json_headers(), data=json.dumps(payload) - ) + response = requests.post(url, headers=self.get_json_headers(), data=json.dumps(payload)) if response.status_code == 200: return response diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py index 599d932..c56028e 100644 --- a/tests/test_broadcast.py +++ b/tests/test_broadcast.py @@ -41,8 +41,13 @@ def test_start_broadcast(self): "updatedAt": 1437676551000, "resolution": "640x480", "status": "started", + "hasAudio": true, + "hasVideo": true, + "maxBitrate": 1000000, + "maxDuration": 5400, "broadcastUrls": { "hls" : "http://server/fakepath/playlist.m3u8", + "hlsStatus": "ready", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -68,6 +73,7 @@ def test_start_broadcast(self): "stylesheet": "the layout stylesheet (only used with type == custom)", }, "maxDuration": 5400, + "maxBitrate": 1000000, "outputs": { "hls": {}, "rtmp": [ @@ -91,9 +97,7 @@ def test_start_broadcast(self): 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")) - ) + 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) @@ -102,9 +106,7 @@ def test_start_broadcast(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -113,7 +115,12 @@ def test_start_broadcast(self): expect(broadcast).to(have_property(u("updatedAt"), 1437676551000)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast).to(have_property("maxDuration", 5400)) + expect(broadcast).to(have_property("hasAudio", True)) + expect(broadcast).to(have_property("hasVideo", True)) + expect(broadcast).to(have_property("maxBitrate", 1000000)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("ready")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -135,8 +142,13 @@ def test_start_broadcast_only_one_rtmp(self): "updatedAt": 1437676551000, "resolution": "640x480", "status": "started", + "hasAudio": true, + "hasVideo": true, + "maxBitrate": 1000000, + "maxDuration": 5400, "broadcastUrls": { - "hls" : "http://server/fakepath/playlist.m3u8", + "hls": "http://server/fakepath/playlist.m3u8", + "hlsStatus": "connecting", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -178,13 +190,9 @@ def test_start_broadcast_only_one_rtmp(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -193,7 +201,8 @@ def test_start_broadcast_only_one_rtmp(self): expect(broadcast).to(have_property(u("updatedAt"), 1437676551000)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("connecting")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -215,8 +224,13 @@ def test_start_broadcast_with_screenshare_type(self): "updatedAt": 1437676551000, "resolution": "640x480", "status": "started", + "hasAudio": true, + "hasVideo": true, + "maxBitrate": 1000000, + "maxDuration": 5400, "broadcastUrls": { "hls" : "http://server/fakepath/playlist.m3u8", + "hlsStatus": "ready", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -262,9 +276,7 @@ def test_start_broadcast_with_screenshare_type(self): 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")) - ) + 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) @@ -276,9 +288,7 @@ def test_start_broadcast_with_screenshare_type(self): expect(body["layout"]).to(have_key("screenshareType")) expect(body["layout"]["screenshareType"]).to(equal("verticalPresentation")) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -287,7 +297,8 @@ def test_start_broadcast_with_screenshare_type(self): expect(broadcast).to(have_property(u("updatedAt"), 1437676551000)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("ready")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -311,8 +322,13 @@ def test_start_broadcast_audio_only(self): "status": "started", "hasAudio": true, "hasVideo": false, + "maxBitrate": 1000000, + "maxDuration": 5400, + "hasAudio": true, + "hasVideo": false, "broadcastUrls": { - "hls" : "http://server/fakepath/playlist.m3u8", + "hls": "http://server/fakepath/playlist.m3u8", + "hlsStatus": "live", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -363,9 +379,7 @@ def test_start_broadcast_audio_only(self): 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")) - ) + 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) @@ -374,9 +388,7 @@ def test_start_broadcast_audio_only(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -387,7 +399,8 @@ def test_start_broadcast_audio_only(self): expect(broadcast).to(have_property(u("hasVideo"), False)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("live")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -411,8 +424,13 @@ def test_start_broadcast_video_only(self): "status": "started", "hasAudio": false, "hasVideo": true, + "maxBitrate": 1000000, + "maxDuration": 5400, + "hasAudio": false, + "hasVideo": true, "broadcastUrls": { - "hls" : "http://server/fakepath/playlist.m3u8", + "hls": "http://server/fakepath/playlist.m3u8", + "hlsStatus": "ready", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -463,9 +481,7 @@ def test_start_broadcast_video_only(self): 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")) - ) + 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) @@ -474,9 +490,7 @@ def test_start_broadcast_video_only(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -487,7 +501,8 @@ def test_start_broadcast_video_only(self): expect(broadcast).to(have_property(u("hasVideo"), True)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("ready")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -637,13 +652,9 @@ def test_stop_broadcast(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to( - have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) - ) + expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -675,7 +686,8 @@ def test_get_broadcast(self): "updatedAt": 1437676551000, "resolution": "640x480", "broadcastUrls": { - "hls" : "http://server/fakepath/playlist.m3u8", + "hls": "http://server/fakepath/playlist.m3u8", + "hlsStatus": "live", "rtmp": { "foo": { "serverUrl": "rtmp://myfooserver/myfooapp", @@ -703,9 +715,7 @@ def test_get_broadcast(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) expect(broadcast).to(be_an(Broadcast)) expect(broadcast).to(have_property(u("id"), broadcast_id)) expect(broadcast).to( @@ -716,7 +726,8 @@ def test_get_broadcast(self): expect(broadcast).to(have_property(u("updatedAt"), 1437676551000)) expect(broadcast).to(have_property(u("resolution"), u("640x480"))) expect(broadcast).to(have_property(u("status"), u("started"))) - expect(list(broadcast.broadcastUrls)).to(have_length(2)) + expect(broadcast.broadcastUrls["hlsStatus"]).to(equal("live")) + expect(list(broadcast.broadcastUrls)).to(have_length(3)) expect(list(broadcast.broadcastUrls["rtmp"])).to(have_length(2)) @httpretty.activate @@ -739,9 +750,7 @@ def test_set_broadcast_layout(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) @httpretty.activate def test_set_broadcast_layout_with_screenshare_type(self): @@ -765,9 +774,7 @@ def test_set_broadcast_layout_with_screenshare_type(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) if PY2: body = json.loads(httpretty.last_request().body) if PY3: @@ -801,9 +808,7 @@ def test_set_custom_broadcast_layout(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) @httpretty.activate def test_set_broadcast_layout_throws_exception(self): From ff0314772c0ffec093e92f10ceeb419a51e8aa16 Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 16 Aug 2023 23:27:45 +0100 Subject: [PATCH 2/5] add individual streams option to Dial API, add missing methods and values --- README.rst | 11 +++++---- opentok/opentok.py | 51 ++++++++++++++---------------------------- tests/test_sip_call.py | 24 +++++++------------- 3 files changed, 32 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index 851c60b..ba7ca91 100644 --- a/README.rst +++ b/README.rst @@ -386,7 +386,8 @@ Your application server can disconnect a client from an OpenTok session by calli Working with SIP Interconnect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can connect your SIP platform to an OpenTok session, the audio from your end of the SIP call is added to the OpenTok session as an audio-only stream. The OpenTok Media Router mixes audio from other streams in the session and sends the mixed audio to your SIP endpoint. +You can connect your SIP platform to an OpenTok session, the audio from your end of the SIP call is added to the OpenTok session as an audio-only stream. +The OpenTok Media Router mixes audio from other streams in the session and sends the mixed audio to your SIP endpoint. .. code:: python @@ -397,8 +398,7 @@ You can connect your SIP platform to an OpenTok session, the audio from your end # call the method with the required parameters sip_call = opentok.dial(session_id, token, sip_uri) - # the method also support aditional options to establish the sip call - + # the method also supports aditional options to establish the sip call options = { 'from': 'from@example.com', 'headers': { @@ -408,7 +408,10 @@ You can connect your SIP platform to an OpenTok session, the audio from your end 'username': 'username', 'password': 'password' }, - 'secure': True + 'secure': True, + 'video': True, + 'observeForceMute': True, + 'streams': ['stream-id-1', 'stream-id-2'] } # call the method with aditional options diff --git a/opentok/opentok.py b/opentok/opentok.py index d4320f2..4476960 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -1141,7 +1141,7 @@ def set_archive_layout(self, archive_id, layout_type, stylesheet=None, screensha else: raise RequestError("OpenTok server error.", response.status_code) - def dial(self, session_id, token, sip_uri, options=[]): + def dial(self, session_id, token, sip_uri, options={}): """ Use this method to connect a SIP platform to an OpenTok session. The audio from the end of the SIP call is added to the OpenTok session as an audio-only stream. The OpenTok Media @@ -1184,27 +1184,30 @@ def dial(self, session_id, token, sip_uri, options=[]): in the OpenTok stream that is sent to the OpenTok session. The SIP client will receive a single composed video of the published streams in the OpenTok session. - This is an example of what the payload POST data body could look like: + List 'streams': An array of stream IDs for streams to include in the SIP call. + If you do not set this property, all streams in the session are included in the call. + + This is an example of what the payload POST data dictionary could look like: { "sessionId": "Your OpenTok session ID", "token": "Your valid OpenTok token", "sip": { - "uri": "sip:user@sip.partner.com;transport=tls", - "from": "from@example.com", - "headers": { - "headerKey": "headerValue" - }, + "uri": "sip:user@sip.partner.com;transport=tls", + "from": "from@example.com", + "headers": { + "headerKey": "headerValue" + }, "auth": { "username": "username", "password": "password" }, - "secure": true|false, - "observeForceMute": true|false, - "video": true|false - } + "secure": True, + "video": True, + "observeForceMute": True, + "streams": ["stream-id-1", "stream-id-2"] } - + } :rtype: A SipCall object, which contains data of the SIP call: id, connectionId and streamId. This is what the response body should look like after returning with a status code of 200: @@ -1217,29 +1220,9 @@ def dial(self, session_id, token, sip_uri, options=[]): Note: Your response will have a different: id, connectionId and streamId """ - payload = {"sessionId": session_id, "token": token, "sip": {"uri": sip_uri}} - observeForceMute = False - video = False - - if "from" in options: - payload["sip"]["from"] = options["from"] - - if "headers" in options: - payload["sip"]["headers"] = options["headers"] - if "auth" in options: - payload["sip"]["auth"] = options["auth"] - - if "secure" in options: - payload["sip"]["secure"] = options["secure"] - - if "observeForceMute" in options: - observeForceMute = True - payload["sip"]["observeForceMute"] = options["observeForceMute"] - - if "video" in options: - video = True - payload["sip"]["video"] = options["video"] + payload = {"sessionId": session_id, "token": token, "sip": {"uri": sip_uri}} + payload.update(options) endpoint = self.endpoints.dial_url() diff --git a/tests/test_sip_call.py b/tests/test_sip_call.py index 0420fea..dd56f71 100644 --- a/tests/test_sip_call.py +++ b/tests/test_sip_call.py @@ -55,14 +55,10 @@ def test_sip_call_with_required_parameters(self): 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) expect(sip_call_response).to(be_an(SipCall)) expect(sip_call_response).to(have_property(u("id"), sip_call.id)) - expect(sip_call_response).to( - have_property(u("connectionId"), sip_call.connectionId) - ) + expect(sip_call_response).to(have_property(u("connectionId"), sip_call.connectionId)) expect(sip_call_response).to(have_property(u("streamId"), sip_call.streamId)) @httpretty.activate @@ -103,22 +99,18 @@ def test_sip_call_with_aditional_options(self): "auth": {"username": "username", "password": "password"}, "secure": True, "observeForceMute": True, - "video": True + "video": True, + "streams": ["stream-id-1", "stream-id-2"], } - sip_call_response = self.opentok.dial( - self.session_id, self.token, self.sip_uri, options - ) + sip_call_response = self.opentok.dial(self.session_id, self.token, self.sip_uri, 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")) - ) + expect(httpretty.last_request().headers[u("content-type")]).to(equal(u("application/json"))) expect(sip_call_response).to(be_an(SipCall)) expect(sip_call_response).to(have_property(u("id"), sip_call.id)) - expect(sip_call_response).to( - have_property(u("connectionId"), sip_call.connectionId) - ) + expect(sip_call_response).to(have_property(u("connectionId"), sip_call.connectionId)) expect(sip_call_response).to(have_property(u("streamId"), sip_call.streamId)) + assert b'"streams": ["stream-id-1", "stream-id-2"]}' in httpretty.last_request().body From 7da26992cfc744df9c1ba455aa492ba8a70de01e Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 16 Aug 2023 23:31:03 +0100 Subject: [PATCH 3/5] updated changelog --- CHANGES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 863b716..47157d9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +# Release v3.7.0 +- Added the `maxBitrate` parameter to the `Client.start_broadcast` method +- Added the `hlsStatus` parameter to the `Broadcast object` +- Added the `streams` parameter so specific streams can be chosen to be included in a SIP call when using the `Client.dial` method + # Release v3.6.1 - Fixed broken `opentok.Client.add_archive_stream`, `opentok.Client.remove_archive_stream`, `opentok.Client.add_broadcast_stream` and `opentok.Client.remove_broadcast_stream` methods and tests - Fixed `opentok.Endpoints.get_archive_stream` and `opentok.Endpoints.get_broadcast_stream` methods From 7c3415f9eb18e279992b4a932a90c25dcab36e6e Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 16 Aug 2023 23:32:05 +0100 Subject: [PATCH 4/5] =?UTF-8?q?Bump=20version:=203.6.1=20=E2=86=92=203.7.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 d4106da..72e62e2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.6.1 +current_version = 3.7.0 commit = True tag = False diff --git a/opentok/version.py b/opentok/version.py index 08fcda6..58727e6 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.6.1" +__version__ = "3.7.0" From 1f23e2da92ee85ec588965ab72671b49b64379ff Mon Sep 17 00:00:00 2001 From: maxkahan Date: Wed, 16 Aug 2023 23:55:19 +0100 Subject: [PATCH 5/5] reverting black to 88 lines --- opentok/opentok.py | 122 +++++++++++++++++++++++++++++----------- tests/test_broadcast.py | 64 +++++++++++++++------ tests/test_sip_call.py | 25 ++++++-- 3 files changed, 157 insertions(+), 54 deletions(-) diff --git a/opentok/opentok.py b/opentok/opentok.py index 4476960..e693eaa 100644 --- a/opentok/opentok.py +++ b/opentok/opentok.py @@ -220,7 +220,9 @@ def generate_token( expire_time = int(expire_time) except (ValueError, TypeError): raise OpenTokException( - u("Cannot generate token, invalid expire time {0}").format(expire_time) + u("Cannot generate token, invalid expire time {0}").format( + expire_time + ) ) else: expire_time = int(time.time()) + (60 * 60 * 24) # 1 day @@ -228,28 +230,38 @@ def generate_token( # validations if not text_type(session_id): raise OpenTokException( - u("Cannot generate token, session_id was not valid {0}").format(session_id) + u("Cannot generate token, session_id was not valid {0}").format( + session_id + ) ) if not isinstance(role, Roles): - raise OpenTokException(u("Cannot generate token, {0} is not a valid role").format(role)) + raise OpenTokException( + u("Cannot generate token, {0} is not a valid role").format(role) + ) now = int(time.time()) if expire_time < now: raise OpenTokException( - u("Cannot generate token, expire_time is not in the future {0}").format(expire_time) + u("Cannot generate token, expire_time is not in the future {0}").format( + expire_time + ) ) if expire_time > now + (60 * 60 * 24 * 30): # 30 days raise OpenTokException( - u("Cannot generate token, expire_time is not in the next 30 days {0}").format( - expire_time - ) + u( + "Cannot generate token, expire_time is not in the next 30 days {0}" + ).format(expire_time) ) if data and len(data) > 1000: raise OpenTokException( u("Cannot generate token, data must be less than 1000 characters") ) - if initial_layout_class_list and not all(text_type(c) for c in initial_layout_class_list): + if initial_layout_class_list and not all( + text_type(c) for c in initial_layout_class_list + ): raise OpenTokException( - u("Cannot generate token, all items in initial_layout_class_list must be strings") + u( + "Cannot generate token, all items in initial_layout_class_list must be strings" + ) ) initial_layout_class_list_serialized = u(" ").join(initial_layout_class_list) if len(initial_layout_class_list_serialized) > 1000: @@ -270,7 +282,9 @@ def generate_token( parts = decoded_session_id.decode("utf-8").split(u("~")) except Exception as e: raise OpenTokException( - u("Cannot generate token, the session_id {0} was not valid").format(session_id) + u("Cannot generate token, the session_id {0} was not valid").format( + session_id + ) ) if self.api_key not in parts: raise OpenTokException( @@ -389,15 +403,21 @@ def create_session( options = {} if not isinstance(media_mode, MediaModes): raise OpenTokException( - u("Cannot create session, {0} is not a valid media mode").format(media_mode) + u("Cannot create session, {0} is not a valid media mode").format( + media_mode + ) ) if not isinstance(archive_mode, ArchiveModes): raise OpenTokException( - u("Cannot create session, {0} is not a valid archive mode").format(archive_mode) + u("Cannot create session, {0} is not a valid archive mode").format( + archive_mode + ) ) if archive_mode == ArchiveModes.always and media_mode != MediaModes.routed: raise OpenTokException( - u("A session with always archive mode must also have the routed media mode.") + u( + "A session with always archive mode must also have the routed media mode." + ) ) if archive_name is not None: @@ -478,7 +498,9 @@ def create_session( ) ) - session_id = dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue + session_id = ( + dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue + ) return Session( self, session_id, @@ -596,12 +618,16 @@ def start_archive( """ if not isinstance(output_mode, OutputModes): raise OpenTokException( - u("Cannot start archive, {0} is not a valid output mode").format(output_mode) + u("Cannot start archive, {0} is not a valid output mode").format( + output_mode + ) ) if resolution and output_mode == OutputModes.individual: raise OpenTokException( - u("Invalid parameters: Resolution cannot be supplied for individual output mode.") + u( + "Invalid parameters: Resolution cannot be supplied for individual output mode." + ) ) payload = { @@ -863,7 +889,9 @@ def add_archive_stream( else: raise RequestError("An unexpected error occurred.", response.status_code) - def remove_archive_stream(self, archive_id: str, stream_id: str) -> requests.Response: + def remove_archive_stream( + self, archive_id: str, stream_id: str + ) -> requests.Response: """ This method will remove streams from the archive with removeStream. @@ -1086,7 +1114,9 @@ def force_disconnect(self, session_id, connection_id): else: raise RequestError("An unexpected error occurred", response.status_code) - def set_archive_layout(self, archive_id, layout_type, stylesheet=None, screenshare_type=None): + def set_archive_layout( + self, archive_id, layout_type, stylesheet=None, screenshare_type=None + ): """ Use this method to change the layout of videos in an OpenTok archive @@ -1308,7 +1338,9 @@ class names (Strings) to apply to the stream. For example: else: raise RequestError("OpenTok server error.", response.status_code) - def start_broadcast(self, session_id, options, stream_mode=BroadcastStreamModes.auto): + def start_broadcast( + self, session_id, options, stream_mode=BroadcastStreamModes.auto + ): """ Use this method to start a live streaming broadcast for an OpenTok session. This broadcasts the session to an HLS (HTTP live streaming) or to RTMP streams. To successfully start @@ -1391,7 +1423,10 @@ def start_broadcast(self, session_id, options, stream_mode=BroadcastStreamModes. """ if "hls" in options["outputs"]: - if "lowLatency" in options["outputs"]["hls"] and "dvr" in options["outputs"]["hls"]: + if ( + "lowLatency" in options["outputs"]["hls"] + and "dvr" in options["outputs"]["hls"] + ): if ( options["outputs"]["hls"]["lowLatency"] == True and options["outputs"]["hls"]["dvr"] == True @@ -1485,7 +1520,8 @@ def stop_broadcast(self, broadcast_id): raise AuthError("Authentication error.") elif response.status_code == 409: raise BroadcastError( - "The broadcast (with the specified ID) was not found or it has already " "stopped." + "The broadcast (with the specified ID) was not found or it has already " + "stopped." ) else: raise RequestError("OpenTok server error.", response.status_code) @@ -1539,11 +1575,15 @@ def add_broadcast_stream( "Your broadcast is configured with a streamMode that does not support stream manipulation." ) elif response.status_code == 409: - raise BroadcastError("The broadcast has already started for the session.") + raise BroadcastError( + "The broadcast has already started for the session." + ) else: raise RequestError("An unexpected error occurred.", response.status_code) - def remove_broadcast_stream(self, broadcast_id: str, stream_id: str) -> requests.Response: + def remove_broadcast_stream( + self, broadcast_id: str, stream_id: str + ) -> requests.Response: """ This method will remove streams from the broadcast with removeStream. @@ -1580,7 +1620,9 @@ def remove_broadcast_stream(self, broadcast_id: str, stream_id: str) -> requests "Your broadcast is configured with a streamMode that does not support stream manipulation." ) elif response.status_code == 409: - raise BroadcastError("The broadcast has already started for the session.") + raise BroadcastError( + "The broadcast has already started for the session." + ) else: raise RequestError("OpenTok server error.", response.status_code) @@ -1906,12 +1948,16 @@ def connect_audio_to_websocket( 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.") def _sign_string(self, string, secret): - return hmac.new(secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1).hexdigest() + return hmac.new( + secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha1 + ).hexdigest() def _create_jwt_auth_header(self): payload = { @@ -1952,7 +1998,9 @@ def mute_all( else: options = {"active": True, "excludedStreams": []} - response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) + response = requests.post( + url, headers=self.get_headers(), data=json.dumps(options) + ) if response: return response @@ -1987,7 +2035,9 @@ def disable_force_mute(self, session_id: str) -> requests.Response: options = {"active": False} url = self.endpoints.get_mute_all_url(session_id) - response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) + response = requests.post( + url, headers=self.get_headers(), data=json.dumps(options) + ) try: if response: @@ -2065,7 +2115,9 @@ def play_dtmf( url = self.endpoints.get_dtmf_specific_url(session_id, connection_id) payload = {"digits": digits} - response = requests.post(url, headers=self.get_json_headers(), data=json.dumps(payload)) + response = requests.post( + url, headers=self.get_json_headers(), data=json.dumps(payload) + ) if response.status_code == 200: return response @@ -2134,7 +2186,9 @@ def mute_all( else: options = {"active": True, "excludedStreams": []} - response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) + response = requests.post( + url, headers=self.get_headers(), data=json.dumps(options) + ) if response: return response @@ -2167,7 +2221,9 @@ def disable_force_mute(self, session_id: str) -> requests.Response: options = {"active": False} url = self.endpoints.get_mute_all_url(session_id) - response = requests.post(url, headers=self.get_headers(), data=json.dumps(options)) + response = requests.post( + url, headers=self.get_headers(), data=json.dumps(options) + ) try: if response: @@ -2239,7 +2295,9 @@ def play_dtmf( url = self.endpoints.get_dtmf_specific_url(session_id, connection_id) payload = {"digits": digits} - response = requests.post(url, headers=self.get_json_headers(), data=json.dumps(payload)) + response = requests.post( + url, headers=self.get_json_headers(), data=json.dumps(payload) + ) if response.status_code == 200: return response diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py index c56028e..359b8ff 100644 --- a/tests/test_broadcast.py +++ b/tests/test_broadcast.py @@ -97,7 +97,9 @@ def test_start_broadcast(self): 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"))) + 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) @@ -106,7 +108,9 @@ def test_start_broadcast(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -190,9 +194,13 @@ def test_start_broadcast_only_one_rtmp(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -276,7 +284,9 @@ def test_start_broadcast_with_screenshare_type(self): 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"))) + 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) @@ -288,7 +298,9 @@ def test_start_broadcast_with_screenshare_type(self): expect(body["layout"]).to(have_key("screenshareType")) expect(body["layout"]["screenshareType"]).to(equal("verticalPresentation")) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -379,7 +391,9 @@ def test_start_broadcast_audio_only(self): 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"))) + 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) @@ -388,7 +402,9 @@ def test_start_broadcast_audio_only(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -481,7 +497,9 @@ def test_start_broadcast_video_only(self): 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"))) + 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) @@ -490,7 +508,9 @@ def test_start_broadcast_video_only(self): expect(body).to(have_key(u("layout"))) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -652,9 +672,13 @@ def test_stop_broadcast(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) expect(broadcast).to(be_an(Broadcast)) - expect(broadcast).to(have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734"))) + expect(broadcast).to( + have_property(u("id"), u("1748b7070a81464c9759c46ad10d3734")) + ) expect(broadcast).to( have_property(u("sessionId"), u("2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4")) ) @@ -715,7 +739,9 @@ def test_get_broadcast(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) expect(broadcast).to(be_an(Broadcast)) expect(broadcast).to(have_property(u("id"), broadcast_id)) expect(broadcast).to( @@ -750,7 +776,9 @@ def test_set_broadcast_layout(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) @httpretty.activate def test_set_broadcast_layout_with_screenshare_type(self): @@ -774,7 +802,9 @@ def test_set_broadcast_layout_with_screenshare_type(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) if PY2: body = json.loads(httpretty.last_request().body) if PY3: @@ -808,7 +838,9 @@ def test_set_custom_broadcast_layout(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) @httpretty.activate def test_set_broadcast_layout_throws_exception(self): diff --git a/tests/test_sip_call.py b/tests/test_sip_call.py index dd56f71..8e3acc3 100644 --- a/tests/test_sip_call.py +++ b/tests/test_sip_call.py @@ -55,10 +55,14 @@ def test_sip_call_with_required_parameters(self): 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) expect(sip_call_response).to(be_an(SipCall)) expect(sip_call_response).to(have_property(u("id"), sip_call.id)) - expect(sip_call_response).to(have_property(u("connectionId"), sip_call.connectionId)) + expect(sip_call_response).to( + have_property(u("connectionId"), sip_call.connectionId) + ) expect(sip_call_response).to(have_property(u("streamId"), sip_call.streamId)) @httpretty.activate @@ -103,14 +107,23 @@ def test_sip_call_with_aditional_options(self): "streams": ["stream-id-1", "stream-id-2"], } - sip_call_response = self.opentok.dial(self.session_id, self.token, self.sip_uri, options) + sip_call_response = self.opentok.dial( + self.session_id, self.token, self.sip_uri, 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"))) + expect(httpretty.last_request().headers[u("content-type")]).to( + equal(u("application/json")) + ) expect(sip_call_response).to(be_an(SipCall)) expect(sip_call_response).to(have_property(u("id"), sip_call.id)) - expect(sip_call_response).to(have_property(u("connectionId"), sip_call.connectionId)) + expect(sip_call_response).to( + have_property(u("connectionId"), sip_call.connectionId) + ) expect(sip_call_response).to(have_property(u("streamId"), sip_call.streamId)) - assert b'"streams": ["stream-id-1", "stream-id-2"]}' in httpretty.last_request().body + assert ( + b'"streams": ["stream-id-1", "stream-id-2"]}' + in httpretty.last_request().body + )