Skip to content

Commit 814b319

Browse files
authored
Merge pull request #198 from opentok/3.x-beta
* SIP Video Outbound and Play DTMF * Selective Stream API (for Archive/broadcast) * observeMute API
2 parents 9b98b44 + 66c5c53 commit 814b319

File tree

7 files changed

+218
-2
lines changed

7 files changed

+218
-2
lines changed

opentok/endpoints.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,42 @@ def get_broadcast_url(self, broadcast_id=None, stop=False, layout=False):
132132
url = url + "/layout"
133133
return url
134134

135+
def get_mute_all_url(self, session_id):
136+
""" this method returns the urls for muting every stream in a session """
137+
url = (
138+
self.api_url
139+
+ "/v2/project/"
140+
+ self.api_key
141+
+ "/session/"
142+
+ session_id
143+
+ "/mute"
144+
145+
)
146+
147+
def get_dtmf_all_url(self, session_id):
148+
""" this method returns the url for Play DTMF to all clients in the session """
149+
url = (
150+
self.api_url
151+
+ "/v2/project/"
152+
+ self.api_key
153+
+ "/session/"
154+
+ session_id
155+
+ "/play-dtmf"
156+
)
157+
158+
def get_dtmf_specific_url(self, session_id, connection_id):
159+
""" this method returns the url for Play DTMF to a specific client connection"""
160+
url = (
161+
self.api_url
162+
+ "/v2/project/"
163+
+ self.api_key
164+
+ "/session/"
165+
+ session_id
166+
+ "/connection/"
167+
+ connection_id
168+
+ "/play-dtmf"
169+
)
170+
135171
def get_archive_stream(self, archive_id=None):
136172
""" this method returns urls for working with streamModes in archives """
137173
url = (

opentok/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ class BroadcastError(OpenTokException):
8989

9090
pass
9191

92+
class DTMFError(OpenTokException):
93+
"""
94+
Indicates that one of the properties digits, session_id or connection_id is invalid
95+
"""
96+
9297
class ArchiveStreamModeError(OpenTokException):
9398
"""
9499
Indicates that the archive is configured with a streamMode that does not support stream manipulation.

opentok/opentok.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
SipDialError,
4646
SetStreamClassError,
4747
BroadcastError,
48+
DTMFError
4849
)
4950

5051

@@ -1120,9 +1121,51 @@ def dial(self, session_id, token, sip_uri, options=[]):
11201121
Boolean 'secure': A Boolean flag that indicates whether the media must be transmitted
11211122
encrypted (true) or not (false, the default)
11221123
1123-
:rtype: A SipCall object, which contains data of the SIP call: id, connectionId and streamId
1124+
Boolean 'observeForceMute': A Boolean flag that determines whether the SIP endpoint should
1125+
honor the force mute action. The force mute action allows a moderator to force clients to
1126+
mute audio in streams they publish. It defaults to False if moderator does not want to observe
1127+
force mute a stream and set to True if the moderator wants to observe force mute a stream.
1128+
1129+
Boolean 'video': A Boolean flag that indicates whether the SIP call will include video(true)
1130+
or not(false, which is the default). With video included, the SIP client's video is included
1131+
in the OpenTok stream that is sent to the OpenTok session. The SIP client will receive a single
1132+
composed video of the published streams in the OpenTok session.
1133+
1134+
This is an example of what the payload POST data body could look like:
1135+
1136+
{
1137+
"sessionId": "Your OpenTok session ID",
1138+
"token": "Your valid OpenTok token",
1139+
"sip": {
1140+
"uri": "sip:[email protected];transport=tls",
1141+
"from": "[email protected]",
1142+
"headers": {
1143+
"headerKey": "headerValue"
1144+
},
1145+
"auth": {
1146+
"username": "username",
1147+
"password": "password"
1148+
},
1149+
"secure": true|false,
1150+
"observeForceMute": true|false,
1151+
"video": true|false
1152+
}
1153+
}
1154+
1155+
:rtype: A SipCall object, which contains data of the SIP call: id, connectionId and streamId.
1156+
This is what the response body should look like after returning with a status code of 200:
1157+
1158+
{
1159+
"id": "b0a5a8c7-dc38-459f-a48d-a7f2008da853",
1160+
"connectionId": "e9f8c166-6c67-440d-994a-04fb6dfed007",
1161+
"streamId": "482bce73-f882-40fd-8ca5-cb74ff416036",
1162+
}
1163+
1164+
Note: Your response will have a different: id, connectionId and streamId
11241165
"""
11251166
payload = {"sessionId": session_id, "token": token, "sip": {"uri": sip_uri}}
1167+
observeForceMute = False
1168+
video = False
11261169

11271170
if "from" in options:
11281171
payload["sip"]["from"] = options["from"]
@@ -1136,6 +1179,14 @@ def dial(self, session_id, token, sip_uri, options=[]):
11361179
if "secure" in options:
11371180
payload["sip"]["secure"] = options["secure"]
11381181

1182+
if "observeForceMute" in options:
1183+
observeForceMute = True
1184+
payload["sip"]["observeForceMute"] = options["observeForceMute"]
1185+
1186+
if "video" in options:
1187+
video = True
1188+
payload["sip"]["video"] = options["video"]
1189+
11391190
endpoint = self.endpoints.dial_url()
11401191

11411192
logger.debug(
@@ -1588,4 +1639,81 @@ def __init__(
15881639
app_version=app_version
15891640
)
15901641

1642+
1643+
1644+
def mute(self, session_id: str, stream_id: str= "", options: dict = {}) -> requests.Response:
1645+
"""
1646+
Use this method so the moderator can mute all streams or a specific stream for OpenTok.
1647+
Please note that a client is able to unmute themselves.
1648+
This function stays in the OpenTok class and inherits from the Client class.
1649+
1650+
:param session_id gets the session id from another function called get_session()
1651+
1652+
:param stream_id gets the stream id from another function called get_stream(). Note
1653+
that this variable is set to an empty string in the function definition as a specific
1654+
stream may not be chosen.
15911655
1656+
"""
1657+
1658+
try:
1659+
if not stream_id:
1660+
url = self.endpoints.get_mute_all_url(session_id)
1661+
data = {'excludedStream': stream_id}
1662+
else:
1663+
url = self.endpoints.get_stream_url(session_id, stream_id) + "/mute"
1664+
data = None
1665+
1666+
1667+
response = requests.post(url, headers=self.get_headers(), data=data)
1668+
1669+
if response:
1670+
return response
1671+
elif response.status_code == 400:
1672+
raise GetStreamError("Invalid request. This response may indicate that data in your request data is invalid JSON. Or it may indicate that you do not pass in a session ID or you passed in an invalid stream ID.")
1673+
elif response.status_code == 403:
1674+
raise AuthError("Failed to create session, invalid credentials")
1675+
elif response.status_code == 404:
1676+
raise NotFoundError("Mute not found")
1677+
except Exception as e:
1678+
raise OpenTokException(
1679+
("There was an error thrown by the OpenTok SDK, please check that your session_id {0} and stream_id (if exists) {1} are valid").format(
1680+
session_id, stream_id))
1681+
1682+
def play_dtmf(self, session_id: str, connection_id: str, digits: str, options: dict = {}) -> requests.Response:
1683+
"""
1684+
Plays a DTMF string into a session or to a specific connection
1685+
1686+
:param session_id The ID of the OpenTok session that the participant being called
1687+
will join
1688+
1689+
:param connection_id An optional parameter used to send the DTMF tones to a specific
1690+
connectoiin in a session.
1691+
1692+
:param digits DTMF digits to play
1693+
Valid DTMF digits are 0-9, p, #, and * digits. 'p' represents a 500ms pause if a delay is
1694+
needed during the input process.
1695+
1696+
"""
1697+
1698+
try:
1699+
if not connection_id:
1700+
url = self.endpoints.get_dtmf_all_url(session_id)
1701+
payload = {"digits": digits}
1702+
else:
1703+
url = self.endpoints.get_dtmf_specific_url(session_id, connection_id)
1704+
payload = {"digits": digits}
1705+
1706+
response = requests.post(url, headers=self.get_json_headers(), data=json.dumps(payload))
1707+
1708+
if response.status_code == 200:
1709+
return response
1710+
elif response.status_code == 400:
1711+
raise DTMFError("One of the properties digits, sessionId or connectionId is invalid.")
1712+
elif response.status_code == 403:
1713+
raise AuthError("Failed to create session, invalid credentials. Please check your OpenTok API Key or JSON web token")
1714+
elif response.status_code == 404:
1715+
raise NotFoundError("The session does not exists or the client specified by the connection_id is not connected to the session")
1716+
except Exception as e:
1717+
raise OpenTokException(
1718+
(f"There was an error thrown by the OpenTok SDK, please check that your session_id: {session_id}, connection_id (if exists): {connection_id} and digits: {digits} are valid"))
1719+

opentok/version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
22
__version__ = "3.2.0"
3+

test_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ expects
44
wheel
55
twine
66
bump2version
7-
sure
7+
sure

tests/test_opentok.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def setUp(self):
2121
self.api_key = "123456"
2222
self.api_secret = "1234567890abcdef1234567890abcdef1234567890"
2323
self.session_id = "SESSIONID"
24+
self.connection_id = "CONNECTIONID"
25+
2426
self.opentok = Client(self.api_key, self.api_secret)
2527
token = string.ascii_letters+string.digits
2628
self.jwt_token_string = ''.join(random.choice(token[:100]))
@@ -73,3 +75,45 @@ def test_mute_stream_response(self):
7375
response.headers["x-opentok-auth"].should.equal(self.jwt_token_string)
7476
response.headers["Content-Type"].should.equal("application/json")
7577

78+
@httpretty.activate
79+
def test_dtmf_all_clients(self):
80+
self.url = f"https://api.opentok.com/v2/project/{self.api_key}/session/{self.session_id}/play-dtmf"
81+
82+
httpretty.register_uri(httpretty.POST,
83+
self.url,
84+
responses=[
85+
httpretty.Response(body="Testing text matches inside of the JSON file",
86+
content_type="application/json",
87+
adding_headers= {"x-opentok-auth": self.jwt_token_string},
88+
status=200)
89+
])
90+
91+
92+
response = requests.post(self.url)
93+
94+
response.status_code.should.equal(200)
95+
response.text.should.equal("Testing text matches inside of the JSON file")
96+
response.headers["x-opentok-auth"].should.equal(self.jwt_token_string)
97+
response.headers["Content-Type"].should.equal("application/json")
98+
99+
@httpretty.activate
100+
def test_dtmf_specific_client(self):
101+
self.url = f"https://api.opentok.com/v2/project/{self.api_key}/session/{self.session_id}/connection/{self.connection_id}/play-dtmf"
102+
103+
httpretty.register_uri(httpretty.POST,
104+
self.url,
105+
responses=[
106+
httpretty.Response(body="Testing text matches inside of the JSON file",
107+
content_type="application/json",
108+
adding_headers= {"x-opentok-auth": self.jwt_token_string},
109+
status=200)
110+
])
111+
112+
113+
response = requests.post(self.url)
114+
115+
response.status_code.should.equal(200)
116+
response.text.should.equal("Testing text matches inside of the JSON file")
117+
response.headers["x-opentok-auth"].should.equal(self.jwt_token_string)
118+
response.headers["Content-Type"].should.equal("application/json")
119+

tests/test_sip_call.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ def test_sip_call_with_aditional_options(self):
102102
"headers": {"headerKey": "headerValue"},
103103
"auth": {"username": "username", "password": "password"},
104104
"secure": True,
105+
"observeForceMute": True,
106+
"video": True
105107
}
106108

107109
sip_call_response = self.opentok.dial(

0 commit comments

Comments
 (0)