Skip to content

Commit 8aef118

Browse files
committed
test streaming methods, add signaling
1 parent c5a93da commit 8aef118

File tree

14 files changed

+180
-61
lines changed

14 files changed

+180
-61
lines changed

http_client/src/vonage_http_client/auth.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@ def application_id(self):
6868
def create_jwt_auth_string(self):
6969
return b'Bearer ' + self.generate_application_jwt()
7070

71-
def generate_application_jwt(self, claims: dict = {}) -> bytes:
71+
def generate_application_jwt(self, claims: dict = None) -> bytes:
72+
if claims is None:
73+
claims = {}
7274
try:
73-
return self._jwt_client.generate_application_jwt(claims)
75+
token = self._jwt_client.generate_application_jwt(claims)
76+
return token
7477
except AttributeError as err:
7578
raise JWTGenerationError(
7679
'JWT generation failed. Check that you passed in valid values for "application_id" and "private_key".'

http_client/tests/test_http_client.py

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_create_http_client_options():
4141
client_options = {
4242
'api_host': 'api.nexmo.com',
4343
'rest_host': 'rest.nexmo.com',
44+
'video_host': 'video.api.vonage.com',
4445
'timeout': 30,
4546
'pool_connections': 5,
4647
'pool_maxsize': 12,

jwt/src/vonage_jwt/jwt.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,28 @@ def __init__(self, application_id: str, private_key: str):
2424
'Both of "application_id" and "private_key" are required.'
2525
)
2626

27-
def generate_application_jwt(self, jwt_options: dict = {}) -> bytes:
27+
def generate_application_jwt(self, jwt_options: dict = None) -> bytes:
2828
"""Generates a JWT for the specified Vonage application.
2929
3030
You can override values for application_id and private_key on the JWTClient object by
3131
specifying them in the `jwt_options` dict if required.
32+
33+
Args:
34+
jwt_options (dict): The options to include in the JWT.
35+
36+
Returns:
37+
bytes: The generated JWT.
3238
"""
39+
if jwt_options is None:
40+
jwt_options = {}
3341

3442
iat = int(time())
3543

3644
payload = jwt_options
3745
payload["application_id"] = self._application_id
38-
payload.setdefault("iat", iat)
39-
payload.setdefault("jti", str(uuid4()))
40-
payload.setdefault("exp", iat + (15 * 60))
46+
payload['iat'] = payload.get("iat", iat)
47+
payload["jti"] = payload.get("jti", str(uuid4()))
48+
payload["exp"] = payload.get("exp", payload["iat"] + (15 * 60))
4149

4250
headers = {'alg': 'RS256', 'typ': 'JWT'}
4351

video/src/vonage_video/models/session.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ class VideoSession(BaseModel):
4545
archive_mode: Optional[ArchiveMode] = None
4646
media_mode: Optional[MediaMode] = None
4747
location: Optional[str] = None
48+
e2ee: Optional[bool] = None
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class SignalData(BaseModel):
5+
"""The data to send in a signal."""
6+
7+
type: str
8+
data: str = Field(None, max_length=8192)

video/src/vonage_video/models/stream.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class StreamInfo(BaseModel):
1414

1515
class StreamLayout(BaseModel):
1616
id: str
17-
layout_class_list: List[str] = Field(..., validation_alias='layoutClassList')
17+
layout_class_list: List[str] = Field(..., serialization_alias='layoutClassList')
1818

1919

2020
class StreamLayoutOptions(BaseModel):

video/src/vonage_video/models/token.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Literal, Optional
33
from uuid import uuid4
44

5-
from pydantic import BaseModel, field_validator, model_validator
5+
from pydantic import BaseModel, Field, field_validator, model_validator
66

77
from ..errors import TokenExpiryError
88
from .enums import TokenRole
@@ -29,8 +29,8 @@ class TokenOptions(BaseModel):
2929
connection_data: Optional[str] = None
3030
initial_layout_class_list: Optional[List[str]] = None
3131
exp: Optional[int] = None
32-
jti: str = str(uuid4())
33-
iat: int = int(time())
32+
jti: str = Field(default_factory=lambda: str(uuid4()))
33+
iat: int = Field(default_factory=lambda: int(time()))
3434
subject: Literal['video'] = 'video'
3535
scope: Literal['session.connect'] = 'session.connect'
3636
acl: dict = {'paths': {'/session/**': {}}}

video/src/vonage_video/video.py

+27-49
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pydantic import validate_call
44
from vonage_http_client.http_client import HttpClient
55
from vonage_video.models.session import SessionOptions, VideoSession
6+
from vonage_video.models.signal import SignalData
67
from vonage_video.models.stream import StreamInfo, StreamLayoutOptions
78
from vonage_video.models.token import TokenOptions
89

@@ -44,7 +45,7 @@ def create_session(self, options: SessionOptions = None) -> VideoSession:
4445
options (SessionOptions): The options for the session.
4546
4647
Returns:
47-
VideoSession: The new session.
48+
VideoSession: The new session ID, plus the config options specified in `options`.
4849
"""
4950

5051
response = self._http_client.post(
@@ -61,25 +62,6 @@ def create_session(self, options: SessionOptions = None) -> VideoSession:
6162

6263
return VideoSession(**session_response)
6364

64-
@validate_call
65-
def get_stream(self, session_id: str, stream_id: str) -> StreamInfo:
66-
"""Gets a stream from the Vonage Video API.
67-
68-
Args:
69-
session_id (str): The session ID.
70-
stream_id (str): The stream ID.
71-
72-
Returns:
73-
StreamInfo: Information about the video stream.
74-
"""
75-
76-
response = self._http_client.get(
77-
self._http_client.video_host,
78-
f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/stream/{stream_id}',
79-
)
80-
81-
return StreamInfo(**response)
82-
8365
@validate_call
8466
def list_streams(self, session_id: str) -> List[StreamInfo]:
8567
"""Lists the streams in a session from the Vonage Video API.
@@ -99,22 +81,6 @@ def list_streams(self, session_id: str) -> List[StreamInfo]:
9981
return [StreamInfo(**stream) for stream in response['items']]
10082

10183
@validate_call
102-
def change_stream_layout(
103-
self, session_id: str, stream_layout_options: StreamLayoutOptions
104-
) -> None:
105-
"""Changes the layout of a stream in a session in the Vonage Video API.
106-
107-
Args:
108-
session_id (str): The session ID.
109-
stream_layout_options (StreamLayoutOptions): The options for the stream layout.
110-
"""
111-
112-
self._http_client.put(
113-
self._http_client.video_host,
114-
f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/stream',
115-
stream_layout_options.model_dump(by_alias=True, exclude_none=True),
116-
)
117-
11884
def get_stream(self, session_id: str, stream_id: str) -> StreamInfo:
11985
"""Gets a stream from the Vonage Video API.
12086
@@ -133,35 +99,47 @@ def get_stream(self, session_id: str, stream_id: str) -> StreamInfo:
13399

134100
return StreamInfo(**response)
135101

136-
def list_streams(self, session_id: str) -> List[StreamInfo]:
137-
"""Lists the streams in a session from the Vonage Video API.
102+
@validate_call
103+
def change_stream_layout(
104+
self, session_id: str, stream_layout_options: StreamLayoutOptions
105+
) -> List[StreamInfo]:
106+
"""Changes the layout of a stream in a session in the Vonage Video API.
138107
139108
Args:
140109
session_id (str): The session ID.
110+
stream_layout_options (StreamLayoutOptions): The options for the stream layout.
141111
142112
Returns:
143113
List[StreamInfo]: Information about the video streams.
144114
"""
145115

146-
response = self._http_client.get(
116+
response = self._http_client.put(
147117
self._http_client.video_host,
148118
f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/stream',
119+
stream_layout_options.model_dump(by_alias=True, exclude_none=True),
149120
)
150121

151122
return [StreamInfo(**stream) for stream in response['items']]
152123

153-
def change_stream_layout(
154-
self, session_id: str, stream_layout_options: StreamLayoutOptions
124+
@validate_call
125+
def send_signal(
126+
self, session_id: str, data: SignalData, connection_id: str = None
155127
) -> None:
156-
"""Changes the layout of a stream in a session in the Vonage Video API.
128+
"""Sends a signal to a session in the Vonage Video API. If `connection_id` is not provided,
129+
the signal will be sent to all connections in the session.
157130
158131
Args:
159132
session_id (str): The session ID.
160-
stream_layout_options (StreamLayoutOptions): The options for the stream layout.
133+
data (SignalData): The data to send in the signal.
134+
connection_id (str, Optional): The connection ID to send the signal to.
161135
"""
162-
163-
self._http_client.put(
164-
self._http_client.video_host,
165-
f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/stream',
166-
stream_layout_options.model_dump(by_alias=True, exclude_none=True),
167-
)
136+
if connection_id is not None:
137+
url = f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/connection/{connection_id}/signal'
138+
else:
139+
url = (
140+
f'/v2/project/{self._http_client.auth.application_id}/session/{session_id}/signal',
141+
)
142+
143+
self._http_client.post(
144+
self._http_client.video_host, url, data.model_dump(exclude_none=True)
145+
)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"count": 1,
3+
"items": [
4+
{
5+
"id": "e08ff3f4-d04b-4363-bd6c-31bd29648ec8",
6+
"videoType": "camera",
7+
"name": "",
8+
"layoutClassList": [
9+
"full"
10+
]
11+
}
12+
]
13+
}

video/tests/data/get_stream.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": "e08ff3f4-d04b-4363-bd6c-31bd29648ec8",
3+
"videoType": "camera",
4+
"name": "",
5+
"layoutClassList": []
6+
}

video/tests/data/list_streams.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"count": 1,
3+
"items": [
4+
{
5+
"id": "e08ff3f4-d04b-4363-bd6c-31bd29648ec8",
6+
"videoType": "camera",
7+
"name": "",
8+
"layoutClassList": []
9+
}
10+
]
11+
}

video/tests/test_session.py

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_create_session():
5555
assert session.archive_mode is None
5656
assert session.media_mode is None
5757
assert session.location is None
58+
assert session.e2ee is None
5859

5960
build_response(
6061
path,
@@ -78,3 +79,4 @@ def test_create_session():
7879
assert session.archive_mode == ArchiveMode.ALWAYS
7980
assert session.media_mode == MediaMode.ROUTED
8081
assert session.location == '192.168.0.1'
82+
assert session.e2ee is True

video/tests/test_stream.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from os.path import abspath
2+
3+
import responses
4+
from vonage_http_client import HttpClient
5+
from vonage_video.models.stream import StreamLayout, StreamLayoutOptions
6+
from vonage_video.video import Video
7+
8+
from testutils import build_response, get_mock_jwt_auth
9+
10+
path = abspath(__file__)
11+
12+
13+
video = Video(HttpClient(get_mock_jwt_auth()))
14+
15+
16+
def test_stream_layout_model():
17+
stream_layout = StreamLayout(
18+
id='e08ff3f4-d04b-4363-bd6c-31bd29648ec8', layout_class_list=['full']
19+
)
20+
21+
stream_layout_options = StreamLayoutOptions(items=[stream_layout])
22+
23+
assert stream_layout.id == 'e08ff3f4-d04b-4363-bd6c-31bd29648ec8'
24+
assert stream_layout.layout_class_list == ['full']
25+
assert stream_layout_options.items == [stream_layout]
26+
27+
28+
@responses.activate
29+
def test_list_streams():
30+
build_response(
31+
path,
32+
'GET',
33+
'https://video.api.vonage.com/v2/project/test_application_id/session/test_session_id/stream',
34+
'list_streams.json',
35+
)
36+
37+
streams = video.list_streams(session_id='test_session_id')
38+
39+
assert len(streams) == 1
40+
assert streams[0].id == 'e08ff3f4-d04b-4363-bd6c-31bd29648ec8'
41+
assert streams[0].video_type == 'camera'
42+
assert streams[0].name == ''
43+
assert streams[0].layout_class_list == []
44+
45+
46+
@responses.activate
47+
def test_get_stream():
48+
build_response(
49+
path,
50+
'GET',
51+
'https://video.api.vonage.com/v2/project/test_application_id/session/test_session_id/stream/e08ff3f4-d04b-4363-bd6c-31bd29648ec8',
52+
'get_stream.json',
53+
)
54+
55+
stream = video.get_stream(
56+
session_id='test_session_id', stream_id='e08ff3f4-d04b-4363-bd6c-31bd29648ec8'
57+
)
58+
59+
assert stream.id == 'e08ff3f4-d04b-4363-bd6c-31bd29648ec8'
60+
assert stream.video_type == 'camera'
61+
assert stream.name == ''
62+
assert stream.layout_class_list == []
63+
64+
65+
@responses.activate
66+
def test_change_stream_layout():
67+
build_response(
68+
path,
69+
'PUT',
70+
'https://video.api.vonage.com/v2/project/test_application_id/session/test_session_id/stream',
71+
'change_stream_layout.json',
72+
)
73+
74+
layout = StreamLayoutOptions(
75+
items=[
76+
StreamLayout(
77+
id='e08ff3f4-d04b-4363-bd6c-31bd29648ec8', layout_class_list=['full']
78+
)
79+
]
80+
)
81+
82+
streams = video.change_stream_layout(
83+
session_id='test_session_id', stream_layout_options=layout
84+
)
85+
86+
assert len(streams) == 1
87+
assert streams[0].id == 'e08ff3f4-d04b-4363-bd6c-31bd29648ec8'
88+
assert streams[0].video_type == 'camera'
89+
assert streams[0].name == ''
90+
assert streams[0].layout_class_list == ['full']

video/tests/test_token.py

-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ def test_generate_token():
5757
)
5858
)
5959

60-
print(token)
61-
6260
assert (
6361
token
6462
== b'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoic2Vzc2lvbi1pZCIsInJvbGUiOiJwdWJsaXNoZXIiLCJjb25uZWN0aW9uX2RhdGEiOiJjb25uZWN0aW9uLWRhdGEiLCJpbml0aWFsX2xheW91dF9jbGFzc19saXN0IjpbImZvY3VzIl0sImV4cCI6MTIzNDU3Njg5LCJqdGkiOiI0Y2FiODljYS1iNjM3LTQxYzgtYjYyZi03YjljZTEwYzM5NzEiLCJpYXQiOjEyMzQ1Njc4OSwic3ViamVjdCI6InZpZGVvIiwic2NvcGUiOiJzZXNzaW9uLmNvbm5lY3QiLCJhY2wiOnsicGF0aHMiOnsiL3Nlc3Npb24vKioiOnt9fX0sImFwcGxpY2F0aW9uX2lkIjoidGVzdF9hcHBsaWNhdGlvbl9pZCJ9.DL-b9AJxZIKb0gmc_NGrD8fvIpg_ILX5FBMXpR56CgSdI63wS04VuaAKCTRojSJrqpzENv_GLR2HYY4-d1Qm1pyj1tM1yFRDk8z_vun30DWavYkCFW1T5FenK1VUjg0P9pbdGiPvq0Ku-taMuLyqXzQqHsbEGOovo-JMIag6wD6JPrPIKaYXsqGpXYaJ_BCcuIpg0NquQgJXA004Q415CxguCkQLdv0d7xTyfPw44Sj-_JfRdBdqDjyiDsmYmh7Yt5TrqRqZ1SwxNhNP7MSx8KDake3VqkQB9Iyys43MJBHZtRDrtE6VedLt80RpCz9Yo8F8CIjStwQPOfMjbV-iEA'

0 commit comments

Comments
 (0)