sora-sdk 2025.2.0.dev2__tar.gz → 2025.2.1.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sora-sdk might be problematic. Click here for more details.

Files changed (44) hide show
  1. {sora_sdk-2025.2.0.dev2/src/sora_sdk.egg-info → sora_sdk-2025.2.1.dev0}/PKG-INFO +1 -1
  2. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/pyproject.toml +3 -3
  3. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk/sora_sdk_ext.cpython-311-darwin.so +0 -0
  4. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0/src/sora_sdk.egg-info}/PKG-INFO +1 -1
  5. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk.egg-info/SOURCES.txt +1 -0
  6. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_amd_amf.py +3 -15
  7. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_apple_video_toolbox.py +9 -14
  8. sora_sdk-2025.2.1.dev0/tests/test_authz_simulcast.py +544 -0
  9. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_intel_vpl.py +5 -10
  10. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_nvidia_video_codec_sdk.py +5 -10
  11. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_openh264.py +0 -147
  12. sora_sdk-2025.2.1.dev0/tests/test_openh264_simulcast.py +434 -0
  13. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_simulcast.py +19 -23
  14. sora_sdk-2025.2.0.dev2/tests/test_authz_simulcast.py +0 -205
  15. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/LICENSE +0 -0
  16. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/MANIFEST.in +0 -0
  17. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/README.md +0 -0
  18. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/buildbase.py +0 -0
  19. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/pypath.py +0 -0
  20. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/run.py +0 -0
  21. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/setup.cfg +0 -0
  22. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/setup.py +0 -0
  23. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk/__init__.py +0 -0
  24. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk/py.typed +0 -0
  25. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk/sora_sdk_ext.pyi +0 -0
  26. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk.egg-info/dependency_links.txt +0 -0
  27. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/src/sora_sdk.egg-info/top_level.txt +0 -0
  28. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_authz.py +0 -0
  29. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_ca_cert.py +0 -0
  30. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_capability.py +0 -0
  31. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_degradation_preference.py +0 -0
  32. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_encoded_transform.py +0 -0
  33. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_messaging.py +0 -0
  34. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_messaging_header.py +0 -0
  35. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_opus.py +0 -0
  36. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_re_offer_re_answer_sdp.py +0 -0
  37. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_sendonly_recvonly.py +0 -0
  38. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_signaling.py +0 -0
  39. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_signaling_message.py +0 -0
  40. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_signaling_notify.py +0 -0
  41. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_sora_disconnect.py +0 -0
  42. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_type_disconnect.py +0 -0
  43. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_type_switched.py +0 -0
  44. {sora_sdk-2025.2.0.dev2 → sora_sdk-2025.2.1.dev0}/tests/test_vad.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sora_sdk
3
- Version: 2025.2.0.dev2
3
+ Version: 2025.2.1.dev0
4
4
  Summary: WebRTC SFU Sora Python SDK
5
5
  Home-page: https://github.com/shiguredo/sora-python-sdk
6
6
  Author-email: "Shiguredo Inc." <contact+pypi@shiguredo.jp>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "sora_sdk"
3
3
  authors = [{ name = "Shiguredo Inc.", email = "contact+pypi@shiguredo.jp" }]
4
- version = "2025.2.0.dev2"
4
+ version = "2025.2.1.dev0"
5
5
  description = "WebRTC SFU Sora Python SDK"
6
6
  readme = "README.md"
7
7
  license = "Apache-2.0"
@@ -20,14 +20,14 @@ Documentation = "https://sora-python-sdk.shiguredo.jp"
20
20
  Discord = "https://discord.gg/shiguredo"
21
21
 
22
22
  [build-system]
23
- requires = ["setuptools==78.1", "wheel==0.45.1"]
23
+ requires = ["setuptools==80.0", "wheel==0.45.1"]
24
24
  build-backend = "setuptools.build_meta"
25
25
 
26
26
  [tool.uv]
27
27
  python-preference = "only-managed"
28
28
  dev-dependencies = [
29
29
  "nanobind==2.7.0",
30
- "setuptools==78.1",
30
+ "setuptools==80.0",
31
31
  "build==1.2.2.post1",
32
32
  "wheel==0.45.1",
33
33
  "typing-extensions",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sora_sdk
3
- Version: 2025.2.0.dev2
3
+ Version: 2025.2.1.dev0
4
4
  Summary: WebRTC SFU Sora Python SDK
5
5
  Home-page: https://github.com/shiguredo/sora-python-sdk
6
6
  Author-email: "Shiguredo Inc." <contact+pypi@shiguredo.jp>
@@ -27,6 +27,7 @@ tests/test_messaging.py
27
27
  tests/test_messaging_header.py
28
28
  tests/test_nvidia_video_codec_sdk.py
29
29
  tests/test_openh264.py
30
+ tests/test_openh264_simulcast.py
30
31
  tests/test_opus.py
31
32
  tests/test_re_offer_re_answer_sdp.py
32
33
  tests/test_sendonly_recvonly.py
@@ -238,7 +238,7 @@ def test_amd_amf_simulcast(
238
238
  )
239
239
  sendonly.connect(fake_video=True)
240
240
 
241
- time.sleep(5)
241
+ time.sleep(10)
242
242
 
243
243
  sendonly_stats = sendonly.get_stats()
244
244
 
@@ -269,25 +269,13 @@ def test_amd_amf_simulcast(
269
269
  assert "qualityLimitationDurations" in s
270
270
 
271
271
  # qualityLimitationReason が none で無い場合は安定したテストができない
272
- # さらに frameWidth/frameHeight がない場合は送られてきてすらいないのでテストをスキップしてしまう
273
- if (
274
- s["qualityLimitationReason"] != "none"
275
- and "frameWidth" not in s
276
- and "frameHeight" not in s
277
- ):
272
+ if s["qualityLimitationReason"] != "none":
278
273
  pytest.skip(f"qualityLimitationReason: {s['qualityLimitationReason']}")
279
274
 
280
275
  assert s["rid"] == f"r{i}"
281
276
  # simulcast_count が 2 の場合、rid r2 の bytesSent/packetsSent は 0 or 1 になる
282
277
  # simulcast_count が 1 の場合、rid r2 と r1 の bytesSent/packetsSent は 0 or 1 になる
283
278
  if i < simulcast_count:
284
- # 1 本になると simulcastEncodingAdapter がなくなる
285
- # if simulcast_count > 1:
286
- # assert "SimulcastEncoderAdapter" in s["encoderImplementation"]
287
-
288
- # targetBitrate が指定したビットレートの 90% 以上、100% 以下に収まることを確認
289
- expected_bitrate = video_bit_rate * 1000
290
-
291
279
  assert s["bytesSent"] > 1000
292
280
  assert s["packetsSent"] > 5
293
281
 
@@ -300,7 +288,7 @@ def test_amd_amf_simulcast(
300
288
  print(
301
289
  s["rid"],
302
290
  video_codec_type,
303
- expected_bitrate,
291
+ video_bit_rate * 1000,
304
292
  s["targetBitrate"],
305
293
  expected_implementation,
306
294
  encoder_implementation,
@@ -143,16 +143,16 @@ def test_apple_video_toolbox_sendonly_recvonly(setup, video_codec_type):
143
143
  ("H264", "VideoToolbox", 960, 540, 3),
144
144
  ("H265", "VideoToolbox", 960, 540, 3),
145
145
  # 360p
146
- ("H264", "VideoToolbox", 640, 360, 2),
147
- ("H265", "VideoToolbox", 640, 360, 2),
146
+ # ("H264", "VideoToolbox", 640, 360, 2),
147
+ # ("H265", "VideoToolbox", 640, 360, 2),
148
148
  # 270p
149
- ("H264", "VideoToolbox", 480, 270, 2),
150
- ("H265", "VideoToolbox", 480, 270, 2),
149
+ # ("H264", "VideoToolbox", 480, 270, 2),
150
+ # ("H265", "VideoToolbox", 480, 270, 2),
151
151
  # 180p
152
- ("H264", "VideoToolbox", 320, 180, 1),
153
- ("H265", "VideoToolbox", 320, 180, 1),
152
+ # ("H264", "VideoToolbox", 320, 180, 1),
153
+ # ("H265", "VideoToolbox", 320, 180, 1),
154
154
  # 135p
155
- ("H265", "VideoToolbox", 240, 135, 1),
155
+ # ("H265", "VideoToolbox", 240, 135, 1),
156
156
  ],
157
157
  )
158
158
  def test_apple_video_toolbox_simulcast(
@@ -186,7 +186,7 @@ def test_apple_video_toolbox_simulcast(
186
186
  )
187
187
  sendonly.connect(fake_video=True)
188
188
 
189
- time.sleep(5)
189
+ time.sleep(10)
190
190
 
191
191
  sendonly_stats = sendonly.get_stats()
192
192
 
@@ -217,12 +217,7 @@ def test_apple_video_toolbox_simulcast(
217
217
  assert "qualityLimitationDurations" in s
218
218
 
219
219
  # qualityLimitationReason が none で無い場合は安定したテストができない
220
- # さらに frameWidth/frameHeight がない場合は送られてきてすらいないのでテストをスキップしてしまう
221
- if (
222
- s["qualityLimitationReason"] != "none"
223
- and "frameWidth" not in s
224
- and "frameHeight" not in s
225
- ):
220
+ if s["qualityLimitationReason"] != "none":
226
221
  pytest.skip(f"qualityLimitationReason: {s['qualityLimitationReason']}")
227
222
 
228
223
  assert s["rid"] == f"r{i}"
@@ -0,0 +1,544 @@
1
+ import sys
2
+ import time
3
+ import uuid
4
+
5
+ import jwt
6
+ import pytest
7
+ from client import SoraClient, SoraRole
8
+ from simulcast import default_video_bit_rate, expect_target_bitrate
9
+
10
+
11
+ @pytest.mark.parametrize(
12
+ (
13
+ "video_codec_type",
14
+ "encoder_implementation",
15
+ "video_width",
16
+ "video_height",
17
+ ),
18
+ [
19
+ # 360p
20
+ ("VP8", "libvpx", 640, 360),
21
+ ("VP9", "libvpx", 640, 360),
22
+ ("AV1", "libaom", 640, 360),
23
+ # 270p
24
+ ("VP8", "libvpx", 480, 270),
25
+ ("VP9", "libvpx", 480, 270),
26
+ ("AV1", "libaom", 480, 270),
27
+ ],
28
+ )
29
+ def test_authz_simulcast_r2_active_false(
30
+ setup,
31
+ video_codec_type,
32
+ encoder_implementation,
33
+ video_width,
34
+ video_height,
35
+ ):
36
+ signaling_urls = setup.get("signaling_urls")
37
+ channel_id_prefix = setup.get("channel_id_prefix")
38
+ secret = setup.get("secret")
39
+
40
+ channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}"
41
+
42
+ video_bit_rate = default_video_bit_rate(video_codec_type, video_width, video_height)
43
+
44
+ simulcast_encodings = [
45
+ {
46
+ "rid": "r0",
47
+ "active": True,
48
+ "scaleResolutionDownBy": 2,
49
+ "scalabilityMode": "L1T1",
50
+ },
51
+ {
52
+ "rid": "r1",
53
+ "active": True,
54
+ "scaleResolutionDownBy": 1,
55
+ "scalabilityMode": "L1T1",
56
+ },
57
+ {
58
+ "rid": "r2",
59
+ "active": False,
60
+ },
61
+ ]
62
+
63
+ access_token = jwt.encode(
64
+ {
65
+ "channel_id": channel_id,
66
+ "video": True,
67
+ "video_codec_type": video_codec_type,
68
+ "video_bit_rate": video_bit_rate,
69
+ "simulcast": True,
70
+ "simulcast_encodings": simulcast_encodings,
71
+ # 現在時刻 + 300 秒 (5分)
72
+ "exp": int(time.time()) + 300,
73
+ },
74
+ secret,
75
+ algorithm="HS256",
76
+ )
77
+
78
+ sendonly = SoraClient(
79
+ signaling_urls,
80
+ SoraRole.SENDONLY,
81
+ channel_id,
82
+ audio=False,
83
+ video=True,
84
+ metadata={"access_token": access_token},
85
+ video_width=video_width,
86
+ video_height=video_height,
87
+ )
88
+ sendonly.connect(fake_video=True)
89
+
90
+ time.sleep(10)
91
+
92
+ # "type": "offer" の SDP で Simulcast があるかどうか
93
+ assert sendonly.offer_message is not None
94
+ assert sendonly.offer_message["sdp"] is not None
95
+ assert video_codec_type in sendonly.offer_message["sdp"]
96
+ assert "a=simulcast:recv r0;r1;~r2" in sendonly.offer_message["sdp"]
97
+
98
+ assert "encodings" in sendonly.offer_message
99
+ assert len(sendonly.offer_message["encodings"]) == 3
100
+
101
+ assert sendonly.offer_message["encodings"][0]["rid"] == simulcast_encodings[0]["rid"]
102
+ assert sendonly.offer_message["encodings"][1]["rid"] == simulcast_encodings[1]["rid"]
103
+ assert sendonly.offer_message["encodings"][2]["rid"] == simulcast_encodings[2]["rid"]
104
+
105
+ assert sendonly.offer_message["encodings"][0]["active"] == simulcast_encodings[0]["active"]
106
+ assert sendonly.offer_message["encodings"][1]["active"] == simulcast_encodings[1]["active"]
107
+ assert sendonly.offer_message["encodings"][2]["active"] == simulcast_encodings[2]["active"]
108
+
109
+ assert (
110
+ sendonly.offer_message["encodings"][0]["scaleResolutionDownBy"]
111
+ == simulcast_encodings[0]["scaleResolutionDownBy"]
112
+ )
113
+ assert (
114
+ sendonly.offer_message["encodings"][1]["scaleResolutionDownBy"]
115
+ == simulcast_encodings[1]["scaleResolutionDownBy"]
116
+ )
117
+
118
+ assert (
119
+ sendonly.offer_message["encodings"][0]["scalabilityMode"]
120
+ == simulcast_encodings[0]["scalabilityMode"]
121
+ )
122
+
123
+ assert (
124
+ sendonly.offer_message["encodings"][1]["scalabilityMode"]
125
+ == simulcast_encodings[1]["scalabilityMode"]
126
+ )
127
+
128
+ # "type": "answer" の SDP で Simulcast があるかどうか
129
+ assert sendonly.answer_message is not None
130
+ assert "sdp" in sendonly.answer_message
131
+ assert "a=simulcast:send r0;r1;~r2" in sendonly.answer_message["sdp"]
132
+
133
+ sendonly_stats = sendonly.get_stats()
134
+ sendonly.disconnect()
135
+
136
+ # codec が無かったら StopIteration 例外が上がる
137
+ sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec")
138
+ assert sendonly_codec_stats["mimeType"] == f"video/{video_codec_type}"
139
+
140
+ # 複数の outbound-rtp 統計情報を取得
141
+ outbound_rtp_stats = [
142
+ s
143
+ for s in sendonly_stats
144
+ if s.get("type") == "outbound-rtp" and s.get("kind") == "video" and s.get("active") is True
145
+ ]
146
+
147
+ # active は 2 本だけ
148
+ assert len(outbound_rtp_stats) == 2
149
+
150
+ # rid でソート
151
+ sorted_stats = sorted(outbound_rtp_stats, key=lambda x: x.get("rid", ""))
152
+
153
+ for i, s in enumerate(sorted_stats):
154
+ assert s["rid"] == f"r{i}"
155
+ assert s["kind"] == "video"
156
+
157
+ assert encoder_implementation in s.get("encoderImplementation")
158
+ if video_codec_type in ["AV1"]:
159
+ assert "SimulcastEncoderAdapter" in s.get("encoderImplementation")
160
+
161
+ if s["qualityLimitationReason"] != "none":
162
+ pytest.skip(f"qualityLimitationReason: {s['qualityLimitationReason']}")
163
+
164
+ assert s["keyFramesEncoded"] > 0
165
+ assert s["bytesSent"] > 500
166
+ assert s["packetsSent"] > 5
167
+
168
+ # assert s["frameWidth"] >= video_width
169
+ # assert s["frameHeight"] >= video_height
170
+
171
+ assert s["targetBitrate"] >= expect_target_bitrate(
172
+ video_codec_type, s["frameWidth"], s["frameHeight"]
173
+ )
174
+
175
+ assert "scalabilityMode" in s
176
+ assert s["scalabilityMode"] == "L1T1"
177
+
178
+ print(
179
+ s["rid"],
180
+ video_codec_type,
181
+ s["encoderImplementation"],
182
+ s["scalabilityMode"],
183
+ video_bit_rate * 1000,
184
+ s["targetBitrate"],
185
+ s["frameWidth"],
186
+ s["frameHeight"],
187
+ s["bytesSent"],
188
+ s["packetsSent"],
189
+ )
190
+
191
+
192
+ @pytest.mark.parametrize(
193
+ (
194
+ "video_codec_type",
195
+ "encoder_implementation",
196
+ "video_width",
197
+ "video_height",
198
+ ),
199
+ [
200
+ # 180p
201
+ ("VP8", "libvpx", 320, 180),
202
+ ("VP9", "libvpx", 320, 180),
203
+ ("AV1", "libaom", 320, 180),
204
+ # 135p
205
+ ("VP8", "libvpx", 240, 135),
206
+ ("VP9", "libvpx", 240, 135),
207
+ ("AV1", "libaom", 240, 135),
208
+ ],
209
+ )
210
+ def test_authz_simulcast_r2_and_r1_active_false(
211
+ setup,
212
+ video_codec_type,
213
+ encoder_implementation,
214
+ video_width,
215
+ video_height,
216
+ ):
217
+ signaling_urls = setup.get("signaling_urls")
218
+ channel_id_prefix = setup.get("channel_id_prefix")
219
+ secret = setup.get("secret")
220
+
221
+ channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}"
222
+ video_bit_rate = default_video_bit_rate(video_codec_type, video_width, video_height)
223
+
224
+ simulcast_encodings = [
225
+ {
226
+ "rid": "r0",
227
+ "active": True,
228
+ "scaleResolutionDownBy": 1,
229
+ "scalabilityMode": "L1T1",
230
+ },
231
+ {
232
+ "rid": "r1",
233
+ "active": False,
234
+ },
235
+ {
236
+ "rid": "r2",
237
+ "active": False,
238
+ },
239
+ ]
240
+
241
+ access_token = jwt.encode(
242
+ {
243
+ "channel_id": channel_id,
244
+ "video": True,
245
+ "video_codec_type": video_codec_type,
246
+ "video_bit_rate": video_bit_rate,
247
+ "simulcast": True,
248
+ "simulcast_encodings": simulcast_encodings,
249
+ # 現在時刻 + 300 秒 (5分)
250
+ "exp": int(time.time()) + 300,
251
+ },
252
+ secret,
253
+ algorithm="HS256",
254
+ )
255
+
256
+ sendonly = SoraClient(
257
+ signaling_urls,
258
+ SoraRole.SENDONLY,
259
+ channel_id,
260
+ audio=False,
261
+ video=True,
262
+ metadata={"access_token": access_token},
263
+ video_width=video_width,
264
+ video_height=video_height,
265
+ )
266
+ sendonly.connect(fake_video=True)
267
+
268
+ time.sleep(10)
269
+
270
+ # "type": "offer" の SDP で Simulcast があるかどうか
271
+ assert sendonly.offer_message is not None
272
+ assert sendonly.offer_message["sdp"] is not None
273
+ assert video_codec_type in sendonly.offer_message["sdp"]
274
+ assert "a=simulcast:recv r0;~r1;~r2" in sendonly.offer_message["sdp"]
275
+
276
+ assert "encodings" in sendonly.offer_message
277
+ assert len(sendonly.offer_message["encodings"]) == 3
278
+
279
+ assert sendonly.offer_message["encodings"][0]["rid"] == simulcast_encodings[0]["rid"]
280
+ assert sendonly.offer_message["encodings"][1]["rid"] == simulcast_encodings[1]["rid"]
281
+ assert sendonly.offer_message["encodings"][2]["rid"] == simulcast_encodings[2]["rid"]
282
+
283
+ assert sendonly.offer_message["encodings"][0]["active"] == simulcast_encodings[0]["active"]
284
+ assert sendonly.offer_message["encodings"][1]["active"] == simulcast_encodings[1]["active"]
285
+ assert sendonly.offer_message["encodings"][2]["active"] == simulcast_encodings[2]["active"]
286
+
287
+ assert (
288
+ sendonly.offer_message["encodings"][0]["scaleResolutionDownBy"]
289
+ == simulcast_encodings[0]["scaleResolutionDownBy"]
290
+ )
291
+
292
+ assert (
293
+ sendonly.offer_message["encodings"][0]["scalabilityMode"]
294
+ == simulcast_encodings[0]["scalabilityMode"]
295
+ )
296
+
297
+ # "type": "answer" の SDP で Simulcast があるかどうか
298
+ assert sendonly.answer_message is not None
299
+ assert "sdp" in sendonly.answer_message
300
+ assert "a=simulcast:send r0;~r1;~r2" in sendonly.answer_message["sdp"]
301
+
302
+ sendonly_stats = sendonly.get_stats()
303
+ sendonly.disconnect()
304
+
305
+ # codec が無かったら StopIteration 例外が上がる
306
+ sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec")
307
+ assert sendonly_codec_stats["mimeType"] == f"video/{video_codec_type}"
308
+
309
+ # 複数の outbound-rtp 統計情報を取得
310
+ outbound_rtp_stats = [
311
+ s
312
+ for s in sendonly_stats
313
+ if s.get("type") == "outbound-rtp" and s.get("kind") == "video" and s.get("active") is True
314
+ ]
315
+
316
+ assert len(outbound_rtp_stats) == 1
317
+
318
+ s = outbound_rtp_stats[0]
319
+
320
+ assert s["rid"] == "r0"
321
+ assert s["kind"] == "video"
322
+
323
+ assert encoder_implementation in s["encoderImplementation"]
324
+ print(s["encoderImplementation"])
325
+
326
+ if s["qualityLimitationReason"] != "none":
327
+ pytest.skip(f"qualityLimitationReason: {s['qualityLimitationReason']}")
328
+
329
+ assert s["keyFramesEncoded"] > 0
330
+ assert s["bytesSent"] > 500
331
+ assert s["packetsSent"] > 5
332
+
333
+ assert s["targetBitrate"] >= expect_target_bitrate(
334
+ video_codec_type, s["frameWidth"], s["frameHeight"]
335
+ )
336
+
337
+ assert "scalabilityMode" in s
338
+ assert s["scalabilityMode"] == "L1T1"
339
+
340
+ print(
341
+ s["rid"],
342
+ video_codec_type,
343
+ s["encoderImplementation"],
344
+ s["scalabilityMode"],
345
+ video_bit_rate * 1000,
346
+ s["targetBitrate"],
347
+ s["frameWidth"],
348
+ s["frameHeight"],
349
+ s["bytesSent"],
350
+ s["packetsSent"],
351
+ )
352
+
353
+
354
+ @pytest.mark.skipif(sys.platform == "darwin", reason="Apple では SW コーデックは動作させない")
355
+ @pytest.mark.parametrize(
356
+ (
357
+ "video_codec_type",
358
+ "encoder_implementation",
359
+ "video_bit_rate",
360
+ "video_width",
361
+ "video_height",
362
+ ),
363
+ [
364
+ # どうやら scaleResolutionDownTo を指定すると規定されたテーブルのビットレートでは足りない模様
365
+ ("VP8", "libvpx", 1200 * 3, 960, 540),
366
+ ("VP9", "libvpx", 879 * 3, 960, 540),
367
+ ("AV1", "libaom", 879 * 3, 960, 540),
368
+ ],
369
+ )
370
+ def test_authz_simulcast_scale_resolution_down_to(
371
+ setup,
372
+ video_codec_type,
373
+ encoder_implementation,
374
+ video_bit_rate,
375
+ video_width,
376
+ video_height,
377
+ ):
378
+ signaling_urls = setup.get("signaling_urls")
379
+ channel_id_prefix = setup.get("channel_id_prefix")
380
+ secret = setup.get("secret")
381
+
382
+ channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}"
383
+
384
+ simulcast_encodings = [
385
+ {
386
+ "rid": "r0",
387
+ "active": True,
388
+ "scaleResolutionDownTo": {"maxWidth": 640, "maxHeight": 360},
389
+ "scalabilityMode": "L1T1",
390
+ },
391
+ {
392
+ "rid": "r1",
393
+ "active": True,
394
+ "scaleResolutionDownTo": {"maxWidth": 640, "maxHeight": 360},
395
+ "scalabilityMode": "L1T1",
396
+ },
397
+ {
398
+ "rid": "r2",
399
+ "active": True,
400
+ "scaleResolutionDownTo": {"maxWidth": 640, "maxHeight": 360},
401
+ "scalabilityMode": "L1T1",
402
+ },
403
+ ]
404
+
405
+ access_token = jwt.encode(
406
+ {
407
+ "channel_id": channel_id,
408
+ "video": True,
409
+ "video_codec_type": video_codec_type,
410
+ "video_bit_rate": video_bit_rate,
411
+ "simulcast": True,
412
+ "simulcast_encodings": simulcast_encodings,
413
+ # 現在時刻 + 300 秒 (5分)
414
+ "exp": int(time.time()) + 300,
415
+ },
416
+ secret,
417
+ algorithm="HS256",
418
+ )
419
+
420
+ sendonly = SoraClient(
421
+ signaling_urls,
422
+ SoraRole.SENDONLY,
423
+ channel_id,
424
+ audio=False,
425
+ video=True,
426
+ metadata={"access_token": access_token},
427
+ video_width=video_width,
428
+ video_height=video_height,
429
+ )
430
+ sendonly.connect(fake_video=True)
431
+
432
+ time.sleep(10)
433
+
434
+ # "type": "offer" の SDP で Simulcast があるかどうか
435
+ assert sendonly.offer_message is not None
436
+ assert sendonly.offer_message["sdp"] is not None
437
+ assert video_codec_type in sendonly.offer_message["sdp"]
438
+ assert "a=simulcast:recv r0;r1;r2" in sendonly.offer_message["sdp"]
439
+
440
+ assert "encodings" in sendonly.offer_message
441
+ assert len(sendonly.offer_message["encodings"]) == 3
442
+
443
+ assert sendonly.offer_message["encodings"][0]["rid"] == simulcast_encodings[0]["rid"]
444
+ assert sendonly.offer_message["encodings"][1]["rid"] == simulcast_encodings[1]["rid"]
445
+ assert sendonly.offer_message["encodings"][2]["rid"] == simulcast_encodings[2]["rid"]
446
+
447
+ assert sendonly.offer_message["encodings"][0]["active"] == simulcast_encodings[0]["active"]
448
+ assert sendonly.offer_message["encodings"][1]["active"] == simulcast_encodings[1]["active"]
449
+ assert sendonly.offer_message["encodings"][2]["active"] == simulcast_encodings[2]["active"]
450
+
451
+ assert (
452
+ sendonly.offer_message["encodings"][0]["scaleResolutionDownTo"]["maxWidth"]
453
+ == simulcast_encodings[0]["scaleResolutionDownTo"]["maxWidth"]
454
+ )
455
+ assert (
456
+ sendonly.offer_message["encodings"][1]["scaleResolutionDownTo"]["maxWidth"]
457
+ == simulcast_encodings[1]["scaleResolutionDownTo"]["maxWidth"]
458
+ )
459
+ assert (
460
+ sendonly.offer_message["encodings"][2]["scaleResolutionDownTo"]["maxWidth"]
461
+ == simulcast_encodings[2]["scaleResolutionDownTo"]["maxWidth"]
462
+ )
463
+
464
+ assert (
465
+ sendonly.offer_message["encodings"][0]["scalabilityMode"]
466
+ == simulcast_encodings[0]["scalabilityMode"]
467
+ )
468
+
469
+ assert (
470
+ sendonly.offer_message["encodings"][1]["scalabilityMode"]
471
+ == simulcast_encodings[1]["scalabilityMode"]
472
+ )
473
+
474
+ assert (
475
+ sendonly.offer_message["encodings"][2]["scalabilityMode"]
476
+ == simulcast_encodings[2]["scalabilityMode"]
477
+ )
478
+
479
+ # "type": "answer" の SDP で Simulcast があるかどうか
480
+ assert sendonly.answer_message is not None
481
+ assert "sdp" in sendonly.answer_message
482
+ assert "a=simulcast:send r0;r1;r2" in sendonly.answer_message["sdp"]
483
+
484
+ sendonly_stats = sendonly.get_stats()
485
+ sendonly.disconnect()
486
+
487
+ # codec が無かったら StopIteration 例外が上がる
488
+ sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec")
489
+ assert sendonly_codec_stats["mimeType"] == f"video/{video_codec_type}"
490
+
491
+ # 複数の outbound-rtp 統計情報を取得
492
+ outbound_rtp_stats = [
493
+ s for s in sendonly_stats if s.get("type") == "outbound-rtp" and s.get("kind") == "video"
494
+ ]
495
+ # simulcast_count に関係なく統計情報はかならず 3 本出力される
496
+ # これは SDP で rid で ~r0 とかやる減るはず
497
+ assert len(outbound_rtp_stats) == 3
498
+
499
+ # rid でソート
500
+ sorted_stats = sorted(outbound_rtp_stats, key=lambda x: x.get("rid", ""))
501
+
502
+ for i, s in enumerate(sorted_stats):
503
+ assert s["rid"] == f"r{i}"
504
+ assert s["kind"] == "video"
505
+
506
+ # VP8 の場合は scaleResolutionDownTo を指定すると SimulcastEncoderAdapter が無くなる
507
+ # TODO: 念のため他の挙動も確認すること
508
+ if video_codec_type == "VP9":
509
+ assert "SimulcastEncoderAdapter" in s["encoderImplementation"]
510
+ assert encoder_implementation in s["encoderImplementation"]
511
+
512
+ if s["qualityLimitationReason"] != "none":
513
+ pytest.skip(f"qualityLimitationReason: {s['qualityLimitationReason']}")
514
+
515
+ assert s["keyFramesEncoded"] > 0
516
+ assert s["bytesSent"] > 500
517
+ assert s["packetsSent"] > 5
518
+
519
+ assert s["frameWidth"] == 640
520
+ assert s["frameHeight"] == 352
521
+
522
+ assert s["targetBitrate"] >= expect_target_bitrate(
523
+ video_codec_type, s["frameWidth"], s["frameHeight"]
524
+ )
525
+
526
+ scalability_mode = None
527
+ if "scalabilityMode" in s:
528
+ assert s["scalabilityMode"] == "L1T1"
529
+ scalability_mode = s["scalabilityMode"]
530
+
531
+ # targetBitrate が指定したビットレートの 90% 以上、100% 以下に収まることを確認
532
+ expected_bitrate = video_bit_rate * 1000
533
+ print(
534
+ s["rid"],
535
+ video_codec_type,
536
+ s["encoderImplementation"],
537
+ scalability_mode,
538
+ expected_bitrate,
539
+ s["targetBitrate"],
540
+ s["frameWidth"],
541
+ s["frameHeight"],
542
+ s["bytesSent"],
543
+ s["packetsSent"],
544
+ )