sora-sdk 2024.4.0.dev15__tar.gz → 2024.4.0.dev18__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.
Files changed (30) hide show
  1. {sora_sdk-2024.4.0.dev15/src/sora_sdk.egg-info → sora_sdk-2024.4.0.dev18}/PKG-INFO +3 -5
  2. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/README.md +2 -4
  3. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/pyproject.toml +1 -1
  4. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk/sora_sdk_ext.cpython-310-darwin.so +0 -0
  5. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk/sora_sdk_ext.pyi +111 -1
  6. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18/src/sora_sdk.egg-info}/PKG-INFO +3 -5
  7. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk.egg-info/SOURCES.txt +1 -0
  8. sora_sdk-2024.4.0.dev18/tests/test_encoded_transform.py +438 -0
  9. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/LICENSE +0 -0
  10. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/MANIFEST.in +0 -0
  11. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/NOTICE.md +0 -0
  12. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/buildbase.py +0 -0
  13. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/pypath.py +0 -0
  14. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/run.py +0 -0
  15. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/setup.cfg +0 -0
  16. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/setup.py +0 -0
  17. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk/__init__.py +0 -0
  18. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk/py.typed +0 -0
  19. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk.egg-info/dependency_links.txt +0 -0
  20. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/src/sora_sdk.egg-info/top_level.txt +0 -0
  21. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_ca_cert.py +0 -0
  22. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_macos.py +0 -0
  23. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_messaging.py +0 -0
  24. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_openh264.py +0 -0
  25. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_re_offer_re_answer_sdp.py +0 -0
  26. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_sendonly_recvonly.py +0 -0
  27. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_signaling_message.py +0 -0
  28. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_simulcast.py +0 -0
  29. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_sora_disconnect.py +0 -0
  30. {sora_sdk-2024.4.0.dev15 → sora_sdk-2024.4.0.dev18}/tests/test_vad.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sora_sdk
3
- Version: 2024.4.0.dev15
3
+ Version: 2024.4.0.dev18
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>
@@ -226,7 +226,8 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
226
226
  - [Sora C++ SDK](https://github.com/shiguredo/sora-cpp-sdk) ベース
227
227
  - WebRTC 部分の機能は [libwebrtc](https://webrtc.googlesource.com/src/) を採用
228
228
  - Windows / macOS / Linux (Ubuntu) プラットフォームに対応
229
- - WebRTC 統計情報の取得が可能
229
+ - [WebRTC 統計情報](https://www.w3.org/TR/webrtc-stats/) の取得が可能
230
+ - [WebRTC Encoded Transform](https://www.w3.org/TR/webrtc-encoded-transform/) に対応
230
231
  - Intel / Apple / NVIDIA のハードウェアデコーダー/エンコーダーに対応
231
232
  - Intel VPL (AV1 / H.264 / H.265)
232
233
  - Apple Video Toolbox (H.264 / H.265)
@@ -264,7 +265,6 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
264
265
 
265
266
  ```bash
266
267
  uv add sora_sdk
267
- uv sync
268
268
  ```
269
269
 
270
270
  ### pip
@@ -311,8 +311,6 @@ PyPI 経由ではインストールできません。
311
311
 
312
312
  ### 優先実装が可能な機能一覧
313
313
 
314
- **詳細は Discord やメールなどでお気軽にお問い合わせください**
315
-
316
314
  - Windows 11 arm64
317
315
  - Ubuntu 24.04 arm64
318
316
  - Ubuntu 22.04 arm64
@@ -28,7 +28,8 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
28
28
  - [Sora C++ SDK](https://github.com/shiguredo/sora-cpp-sdk) ベース
29
29
  - WebRTC 部分の機能は [libwebrtc](https://webrtc.googlesource.com/src/) を採用
30
30
  - Windows / macOS / Linux (Ubuntu) プラットフォームに対応
31
- - WebRTC 統計情報の取得が可能
31
+ - [WebRTC 統計情報](https://www.w3.org/TR/webrtc-stats/) の取得が可能
32
+ - [WebRTC Encoded Transform](https://www.w3.org/TR/webrtc-encoded-transform/) に対応
32
33
  - Intel / Apple / NVIDIA のハードウェアデコーダー/エンコーダーに対応
33
34
  - Intel VPL (AV1 / H.264 / H.265)
34
35
  - Apple Video Toolbox (H.264 / H.265)
@@ -66,7 +67,6 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
66
67
 
67
68
  ```bash
68
69
  uv add sora_sdk
69
- uv sync
70
70
  ```
71
71
 
72
72
  ### pip
@@ -113,8 +113,6 @@ PyPI 経由ではインストールできません。
113
113
 
114
114
  ### 優先実装が可能な機能一覧
115
115
 
116
- **詳細は Discord やメールなどでお気軽にお問い合わせください**
117
-
118
116
  - Windows 11 arm64
119
117
  - Ubuntu 24.04 arm64
120
118
  - Ubuntu 22.04 arm64
@@ -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 = "2024.4.0.dev15"
4
+ version = "2024.4.0.dev18"
5
5
  description = "WebRTC SFU Sora Python SDK"
6
6
  readme = "README.md"
7
7
  license = { file = "LICENSE" }
@@ -8,7 +8,7 @@ from numpy.typing import ArrayLike
8
8
  class Sora:
9
9
  def __init__(self, use_hardware_encoder: bool | None = None, openh264: str | None = None) -> None: ...
10
10
 
11
- def create_connection(self, signaling_urls: list[str], role: str, channel_id: str, client_id: Optional[str] = None, bundle_id: Optional[str] = None, metadata: Optional[dict] = None, signaling_notify_metadata: Optional[dict] = None, audio_source: Optional[SoraTrackInterface] = None, video_source: Optional[SoraTrackInterface] = None, audio: Optional[bool] = None, video: Optional[bool] = None, audio_codec_type: Optional[str] = None, video_codec_type: Optional[str] = None, video_bit_rate: Optional[int] = None, audio_bit_rate: Optional[int] = None, video_vp9_params: Optional[dict] = None, video_av1_params: Optional[dict] = None, video_h264_params: Optional[dict] = None, simulcast: Optional[bool] = None, spotlight: Optional[bool] = None, spotlight_number: Optional[int] = None, simulcast_rid: Optional[str] = None, spotlight_focus_rid: Optional[str] = None, spotlight_unfocus_rid: Optional[str] = None, forwarding_filter: Optional[dict] = None, data_channels: Optional[list[dict]] = None, data_channel_signaling: Optional[bool] = None, ignore_disconnect_websocket: Optional[bool] = None, data_channel_signaling_timeout: Optional[int] = None, disconnect_wait_timeout: Optional[int] = None, websocket_close_timeout: Optional[int] = None, websocket_connection_timeout: Optional[int] = None, audio_streaming_language_code: Optional[str] = None, insecure: Optional[bool] = None, client_cert: Optional[bytes] = None, client_key: Optional[bytes] = None, ca_cert: Optional[bytes] = None, proxy_url: Optional[str] = None, proxy_username: Optional[str] = None, proxy_password: Optional[str] = None, proxy_agent: Optional[str] = None) -> SoraConnection: ...
11
+ def create_connection(self, signaling_urls: list[str], role: str, channel_id: str, client_id: Optional[str] = None, bundle_id: Optional[str] = None, metadata: Optional[dict] = None, signaling_notify_metadata: Optional[dict] = None, audio_source: Optional[SoraTrackInterface] = None, video_source: Optional[SoraTrackInterface] = None, audio_frame_transformer: Optional[SoraAudioFrameTransformer] = None, video_frame_transformer: Optional[SoraVideoFrameTransformer] = None, audio: Optional[bool] = None, video: Optional[bool] = None, audio_codec_type: Optional[str] = None, video_codec_type: Optional[str] = None, video_bit_rate: Optional[int] = None, audio_bit_rate: Optional[int] = None, video_vp9_params: Optional[dict] = None, video_av1_params: Optional[dict] = None, video_h264_params: Optional[dict] = None, simulcast: Optional[bool] = None, spotlight: Optional[bool] = None, spotlight_number: Optional[int] = None, simulcast_rid: Optional[str] = None, spotlight_focus_rid: Optional[str] = None, spotlight_unfocus_rid: Optional[str] = None, forwarding_filter: Optional[dict] = None, data_channels: Optional[list[dict]] = None, data_channel_signaling: Optional[bool] = None, ignore_disconnect_websocket: Optional[bool] = None, data_channel_signaling_timeout: Optional[int] = None, disconnect_wait_timeout: Optional[int] = None, websocket_close_timeout: Optional[int] = None, websocket_connection_timeout: Optional[int] = None, audio_streaming_language_code: Optional[str] = None, insecure: Optional[bool] = None, client_cert: Optional[bytes] = None, client_key: Optional[bytes] = None, ca_cert: Optional[bytes] = None, proxy_url: Optional[str] = None, proxy_username: Optional[str] = None, proxy_password: Optional[str] = None, proxy_agent: Optional[str] = None) -> SoraConnection: ...
12
12
 
13
13
  def create_audio_source(self, channels: int, sample_rate: int) -> SoraAudioSource: ...
14
14
 
@@ -33,6 +33,17 @@ class SoraAudioFrame:
33
33
 
34
34
  def data(self) -> Annotated[ArrayLike, dict(dtype='int16', shape=(None, None))]: ...
35
35
 
36
+ class SoraAudioFrameTransformer(SoraFrameTransformer):
37
+ def __init__(self) -> None: ...
38
+
39
+ def __del__(self) -> None: ...
40
+
41
+ @property
42
+ def on_transform(self) -> Callable[[SoraTransformableAudioFrame], None]: ...
43
+
44
+ @on_transform.setter
45
+ def on_transform(self, arg: Callable[[SoraTransformableAudioFrame], None], /) -> None: ...
46
+
36
47
  class SoraAudioSinkImpl:
37
48
  def __init__(self, track: SoraTrackInterface, output_frequency: int = -1, output_channels: int = 0) -> None: ...
38
49
 
@@ -145,6 +156,11 @@ class SoraConnection:
145
156
  @on_data_channel.setter
146
157
  def on_data_channel(self, arg: Callable[[str], None], /) -> None: ...
147
158
 
159
+ class SoraFrameTransformer:
160
+ def enqueue(self, arg: SoraTransformableFrame, /) -> None: ...
161
+
162
+ def start_short_circuiting(self) -> None: ...
163
+
148
164
  class SoraLoggingSeverity(enum.IntEnum):
149
165
  VERBOSE = 0
150
166
 
@@ -160,6 +176,8 @@ class SoraMediaTrack(SoraTrackInterface):
160
176
  @property
161
177
  def stream_id(self) -> str: ...
162
178
 
179
+ def set_frame_transformer(self, arg: SoraFrameTransformer, /) -> None: ...
180
+
163
181
  class SoraSignalingDirection(enum.IntEnum):
164
182
  SENT = 0
165
183
 
@@ -209,6 +227,87 @@ class SoraTrackState(enum.IntEnum):
209
227
 
210
228
  ENDED = 1
211
229
 
230
+ class SoraTransformableAudioFrame(SoraTransformableFrame):
231
+ @property
232
+ def contributing_sources(self) -> Annotated[ArrayLike, dict(dtype='uint32', shape=(None), writable=False)]: ...
233
+
234
+ @property
235
+ def sequence_number(self) -> int | None: ...
236
+
237
+ @property
238
+ def absolute_capture_timestamp(self) -> int | None: ...
239
+
240
+ @property
241
+ def type(self) -> SoraTransformableAudioFrameType: ...
242
+
243
+ @property
244
+ def audio_level(self) -> int | None: ...
245
+
246
+ @property
247
+ def receive_time(self) -> int | None: ...
248
+
249
+ class SoraTransformableAudioFrameType(enum.IntEnum):
250
+ EMPTY = 0
251
+
252
+ SPEECH = 1
253
+
254
+ CN = 2
255
+
256
+ class SoraTransformableFrame:
257
+ def get_data(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None), writable=False)]: ...
258
+
259
+ def set_data(self, arg: Annotated[ArrayLike, dict(dtype='uint8', shape=(None), order='C', device='cpu', writable=False)], /) -> None: ...
260
+
261
+ @property
262
+ def payload_type(self) -> int: ...
263
+
264
+ @property
265
+ def ssrc(self) -> int: ...
266
+
267
+ @property
268
+ def rtp_timestamp(self) -> int: ...
269
+
270
+ @rtp_timestamp.setter
271
+ def rtp_timestamp(self, arg: int, /) -> None: ...
272
+
273
+ @property
274
+ def direction(self) -> SoraTransformableFrameDirection: ...
275
+
276
+ @property
277
+ def mine_type(self) -> str: ...
278
+
279
+ class SoraTransformableFrameDirection(enum.IntEnum):
280
+ UNKNOWN = 0
281
+
282
+ RECEIVER = 1
283
+
284
+ SENDER = 2
285
+
286
+ class SoraTransformableVideoFrame(SoraTransformableFrame):
287
+ @property
288
+ def is_key_frame(self) -> bool: ...
289
+
290
+ @property
291
+ def frame_id(self) -> int | None: ...
292
+
293
+ @property
294
+ def frame_dependencies(self) -> Annotated[ArrayLike, dict(dtype='int64', shape=(None), writable=False)]: ...
295
+
296
+ @property
297
+ def width(self) -> int: ...
298
+
299
+ @property
300
+ def height(self) -> int: ...
301
+
302
+ @property
303
+ def spatial_index(self) -> int: ...
304
+
305
+ @property
306
+ def temporal_index(self) -> int: ...
307
+
308
+ @property
309
+ def contributing_sources(self) -> Annotated[ArrayLike, dict(dtype='uint32', shape=(None), writable=False)]: ...
310
+
212
311
  class SoraVAD:
213
312
  def __init__(self) -> None: ...
214
313
 
@@ -217,6 +316,17 @@ class SoraVAD:
217
316
  class SoraVideoFrame:
218
317
  def data(self) -> Annotated[ArrayLike, dict(dtype='uint8', shape=(None, None, 3))]: ...
219
318
 
319
+ class SoraVideoFrameTransformer(SoraFrameTransformer):
320
+ def __init__(self) -> None: ...
321
+
322
+ def __del__(self) -> None: ...
323
+
324
+ @property
325
+ def on_transform(self) -> Callable[[SoraTransformableVideoFrame], None]: ...
326
+
327
+ @on_transform.setter
328
+ def on_transform(self, arg: Callable[[SoraTransformableVideoFrame], None], /) -> None: ...
329
+
220
330
  class SoraVideoSinkImpl:
221
331
  def __init__(self, arg: SoraTrackInterface, /) -> None: ...
222
332
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sora_sdk
3
- Version: 2024.4.0.dev15
3
+ Version: 2024.4.0.dev18
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>
@@ -226,7 +226,8 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
226
226
  - [Sora C++ SDK](https://github.com/shiguredo/sora-cpp-sdk) ベース
227
227
  - WebRTC 部分の機能は [libwebrtc](https://webrtc.googlesource.com/src/) を採用
228
228
  - Windows / macOS / Linux (Ubuntu) プラットフォームに対応
229
- - WebRTC 統計情報の取得が可能
229
+ - [WebRTC 統計情報](https://www.w3.org/TR/webrtc-stats/) の取得が可能
230
+ - [WebRTC Encoded Transform](https://www.w3.org/TR/webrtc-encoded-transform/) に対応
230
231
  - Intel / Apple / NVIDIA のハードウェアデコーダー/エンコーダーに対応
231
232
  - Intel VPL (AV1 / H.264 / H.265)
232
233
  - Apple Video Toolbox (H.264 / H.265)
@@ -264,7 +265,6 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
264
265
 
265
266
  ```bash
266
267
  uv add sora_sdk
267
- uv sync
268
268
  ```
269
269
 
270
270
  ### pip
@@ -311,8 +311,6 @@ PyPI 経由ではインストールできません。
311
311
 
312
312
  ### 優先実装が可能な機能一覧
313
313
 
314
- **詳細は Discord やメールなどでお気軽にお問い合わせください**
315
-
316
314
  - Windows 11 arm64
317
315
  - Ubuntu 24.04 arm64
318
316
  - Ubuntu 22.04 arm64
@@ -16,6 +16,7 @@ src/sora_sdk.egg-info/SOURCES.txt
16
16
  src/sora_sdk.egg-info/dependency_links.txt
17
17
  src/sora_sdk.egg-info/top_level.txt
18
18
  tests/test_ca_cert.py
19
+ tests/test_encoded_transform.py
19
20
  tests/test_macos.py
20
21
  tests/test_messaging.py
21
22
  tests/test_openh264.py
@@ -0,0 +1,438 @@
1
+ import json
2
+ import sys
3
+ import threading
4
+ import time
5
+ import uuid
6
+ from threading import Event
7
+ from typing import Any, Optional
8
+
9
+ import numpy
10
+
11
+ from sora_sdk import (
12
+ Sora,
13
+ SoraAudioFrameTransformer,
14
+ SoraAudioSource,
15
+ SoraMediaTrack,
16
+ SoraTransformableAudioFrame,
17
+ SoraTransformableVideoFrame,
18
+ SoraVideoFrameTransformer,
19
+ SoraVideoSource,
20
+ )
21
+
22
+
23
+ class SendonlyEncodedTransform:
24
+ def __init__(
25
+ self,
26
+ signaling_urls: list[str],
27
+ channel_id: str,
28
+ metadata: dict[str, Any],
29
+ ):
30
+ self._signaling_urls: list[str] = signaling_urls
31
+ self._channel_id: str = channel_id
32
+
33
+ self._connection_id: str
34
+
35
+ # 接続した
36
+ self._connected: Event = Event()
37
+ # 終了
38
+ self._closed = Event()
39
+
40
+ self._audio_channels: int = 1
41
+ self._audio_sample_rate: int = 16000
42
+
43
+ self._video_width: int = 960
44
+ self._video_height: int = 540
45
+
46
+ self._sora = Sora()
47
+
48
+ self._fake_audio_thread: Optional[threading.Thread] = None
49
+ self._fake_video_thread: Optional[threading.Thread] = None
50
+
51
+ self._audio_source: Optional[SoraAudioSource] = None
52
+ self._audio_source = self._sora.create_audio_source(
53
+ self._audio_channels, self._audio_sample_rate
54
+ )
55
+
56
+ self._video_source: Optional[SoraVideoSource] = None
57
+ self._video_source = self._sora.create_video_source()
58
+
59
+ # Audio 向けの Encoded Transformer
60
+ self._audio_transformer = SoraAudioFrameTransformer()
61
+ # Audio のエンコードフレームを受け取るコールバック関数を on_transform に設定
62
+ self._audio_transformer.on_transform = self._on_audio_transform
63
+
64
+ # Video 向けの Encoded Transformer
65
+ self._video_transformer = SoraVideoFrameTransformer()
66
+ # Video のエンコードフレームを受け取るコールバック関数を on_transform に設定
67
+ self._video_transformer.on_transform = self._on_video_transform
68
+
69
+ self._connection = self._sora.create_connection(
70
+ signaling_urls=signaling_urls,
71
+ role="sendonly",
72
+ channel_id=channel_id,
73
+ metadata=metadata,
74
+ audio=True,
75
+ video=True,
76
+ audio_source=self._audio_source,
77
+ video_source=self._video_source,
78
+ audio_frame_transformer=self._audio_transformer,
79
+ video_frame_transformer=self._video_transformer,
80
+ )
81
+
82
+ self._connection.on_set_offer = self._on_set_offer
83
+ self._connection.on_notify = self._on_notify
84
+ self._connection.on_disconnect = self._on_disconnect
85
+
86
+ self._is_called_on_audio_transform = False
87
+ self._is_called_on_video_transform = False
88
+
89
+ def connect(self):
90
+ self._connection.connect()
91
+
92
+ self._fake_audio_thread = threading.Thread(target=self._fake_audio_loop, daemon=True)
93
+ self._fake_audio_thread.start()
94
+
95
+ self._fake_video_thread = threading.Thread(target=self._fake_video_loop, daemon=True)
96
+ self._fake_video_thread.start()
97
+
98
+ # _connected が set されるまで 30 秒待つ
99
+ assert self._connected.wait(30)
100
+
101
+ return self
102
+
103
+ def disconnect(self):
104
+ self._connection.disconnect()
105
+
106
+ def get_stats(self):
107
+ raw_stats = self._connection.get_stats()
108
+ stats = json.loads(raw_stats)
109
+ return stats
110
+
111
+ @property
112
+ def is_called_on_audio_transform(self):
113
+ return self._is_called_on_audio_transform
114
+
115
+ @property
116
+ def is_called_on_video_transform(self):
117
+ return self._is_called_on_video_transform
118
+
119
+ def _fake_audio_loop(self):
120
+ while not self._closed.is_set():
121
+ time.sleep(0.02)
122
+ if self._audio_source is not None:
123
+ self._audio_source.on_data(numpy.zeros((320, 1), dtype=numpy.int16))
124
+
125
+ def _fake_video_loop(self):
126
+ while not self._closed.is_set():
127
+ time.sleep(1.0 / 30)
128
+ if self._video_source is not None:
129
+ self._video_source.on_captured(
130
+ numpy.zeros((self._video_height, self._video_width, 3), dtype=numpy.uint8)
131
+ )
132
+
133
+ def _on_set_offer(self, raw_offer):
134
+ offer = json.loads(raw_offer)
135
+ if offer["type"] == "offer":
136
+ self._connection_id = offer["connection_id"]
137
+ print(f"Received 'Offer': connection_id={self._connection_id}")
138
+
139
+ def _on_notify(self, raw_message):
140
+ message = json.loads(raw_message)
141
+ if (
142
+ message["type"] == "notify"
143
+ and message["event_type"] == "connection.created"
144
+ and message["connection_id"] == self._connection_id
145
+ ):
146
+ print(f"Connected Sora: connection_id={self._connection_id}")
147
+ self._connected.set()
148
+
149
+ def _on_disconnect(self, error_code, message):
150
+ print(f"Disconnected Sora: error_code='{error_code}' message='{message}'")
151
+ self._closed.set()
152
+ self._connected.clear()
153
+
154
+ if self._fake_audio_thread is not None:
155
+ self._fake_audio_thread.join(timeout=10)
156
+
157
+ if self._fake_video_thread is not None:
158
+ self._fake_video_thread.join(timeout=10)
159
+
160
+ def _on_audio_transform(self, frame: SoraTransformableAudioFrame):
161
+ # この実装が Encoded Transform を利用する上での基本形となる
162
+
163
+ # frame からエンコードされたフレームデータを取得する
164
+ # 戻り値は numpy.ndarray になっている
165
+ new_data = frame.get_data()
166
+
167
+ # "sora" という文字列を new_data の後ろに追加
168
+ new_data = numpy.append(new_data, numpy.frombuffer(b"sora", dtype=numpy.uint8))
169
+
170
+ self._is_called_on_audio_transform = True
171
+
172
+ # ここで new_data の末尾にデータをつける new_data を暗号化するなど任意の処理を実装する
173
+
174
+ # 加工したフレームデータで frame の フレームデータを入れ替える
175
+ frame.set_data(new_data)
176
+ self._audio_transformer.enqueue(frame)
177
+
178
+ def _on_video_transform(self, frame: SoraTransformableVideoFrame):
179
+ # この実装が Encoded Transform を利用する上での基本形となる
180
+
181
+ # frame からエンコードされたフレームデータを取得する
182
+ # 戻り値は numpy.ndarray になっている
183
+ new_data = frame.get_data()
184
+
185
+ # "sora" という文字列を new_data の後ろに追加
186
+ new_data = numpy.append(new_data, numpy.frombuffer(b"sora", dtype=numpy.uint8))
187
+
188
+ self._is_called_on_video_transform = True
189
+
190
+ # ここで new_data の末尾にデータをつける new_data を暗号化するなど任意の処理を実装する
191
+
192
+ # 加工したフレームデータで frame の フレームデータを入れ替える
193
+ frame.set_data(new_data)
194
+ self._video_transformer.enqueue(frame)
195
+
196
+
197
+ class RecvonlyEncodedTransform:
198
+ def __init__(
199
+ self,
200
+ signaling_urls: list[str],
201
+ channel_id: str,
202
+ metadata: dict[str, Any],
203
+ ):
204
+ self._signaling_urls: list[str] = signaling_urls
205
+ self._channel_id: str = channel_id
206
+
207
+ self._connection_id: str
208
+
209
+ # 接続した
210
+ self._connected: Event = Event()
211
+ # 終了
212
+ self._closed = Event()
213
+
214
+ self._audio_output_frequency: int = 24000
215
+ self._audio_output_channels: int = 1
216
+
217
+ self._sora = Sora()
218
+
219
+ self._connection = self._sora.create_connection(
220
+ signaling_urls=signaling_urls,
221
+ role="recvonly",
222
+ channel_id=channel_id,
223
+ metadata=metadata,
224
+ audio=True,
225
+ video=True,
226
+ )
227
+
228
+ self._connection.on_set_offer = self._on_set_offer
229
+ self._connection.on_notify = self._on_notify
230
+ self._connection.on_disconnect = self._on_disconnect
231
+
232
+ self._connection.on_track = self._on_track
233
+
234
+ self._is_called_on_audio_transform = False
235
+ self._is_called_on_video_transform = False
236
+
237
+ def connect(self):
238
+ self._connection.connect()
239
+
240
+ # _connected が set されるまで 30 秒待つ
241
+ assert self._connected.wait(30)
242
+
243
+ return self
244
+
245
+ def disconnect(self):
246
+ self._connection.disconnect()
247
+
248
+ def get_stats(self):
249
+ raw_stats = self._connection.get_stats()
250
+ stats = json.loads(raw_stats)
251
+ return stats
252
+
253
+ @property
254
+ def is_called_on_audio_transform(self):
255
+ return self._is_called_on_audio_transform
256
+
257
+ @property
258
+ def is_called_on_video_transform(self):
259
+ return self._is_called_on_video_transform
260
+
261
+ def _on_set_offer(self, raw_offer):
262
+ offer = json.loads(raw_offer)
263
+ if offer["type"] == "offer":
264
+ self._connection_id = offer["connection_id"]
265
+ print(f"Received 'Offer': connection_id={self._connection_id}")
266
+
267
+ def _on_notify(self, raw_message):
268
+ message = json.loads(raw_message)
269
+ if (
270
+ message["type"] == "notify"
271
+ and message["event_type"] == "connection.created"
272
+ and message["connection_id"] == self._connection_id
273
+ ):
274
+ print(f"Connected Sora: connection_id={self._connection_id}")
275
+ self._connected.set()
276
+
277
+ def _on_disconnect(self, error_code, message):
278
+ print(f"Disconnected Sora: error_code='{error_code}' message='{message}'")
279
+ self._closed = True
280
+ self._connected.clear()
281
+
282
+ def _on_track(self, track: SoraMediaTrack) -> None:
283
+ if track.kind == "audio":
284
+ # Audio 向けの Encoded Transformer
285
+ self._audio_transformer = SoraAudioFrameTransformer()
286
+ # Audio のエンコードフレームを受け取るコールバック関数を on_transform に設定
287
+ self._audio_transformer.on_transform = self._on_audio_transform
288
+ # Encoded Transformer を RTPReceiver に設定する
289
+ track.set_frame_transformer(self._audio_transformer)
290
+ if track.kind == "video":
291
+ # Video 向けの Encoded Transformer
292
+ self._video_transformer = SoraVideoFrameTransformer()
293
+ # Video のエンコードフレームを受け取るコールバック関数を on_transform に設定
294
+ self._video_transformer.on_transform = self._on_video_transform
295
+ # Encoded Transformer を SoraMediaTrack に設定する
296
+ track.set_frame_transformer(self._video_transformer)
297
+
298
+ def _on_audio_transform(self, frame: SoraTransformableAudioFrame):
299
+ # この実装が Encoded Transform を利用する上での基本形となる
300
+
301
+ # frame からエンコードされたフレームデータを取得する
302
+ # 戻り値は ArrayLike になっている
303
+ new_data = frame.get_data()
304
+
305
+ # ここで new_data の末尾にデータをつける new_data を暗号化するなど任意の処理を実装する
306
+
307
+ # ArrayLike を numpy.uint8 のバイト列に変換する
308
+ new_data = numpy.asarray(new_data, dtype=numpy.uint8)
309
+
310
+ # 後ろ4バイトを取得する
311
+ removed_data = new_data[-4:]
312
+
313
+ assert b"sora" == removed_data.tobytes()
314
+
315
+ # 後ろ4バイトを取り除く
316
+ new_data = new_data[:-4]
317
+
318
+ self._is_called_on_audio_transform = True
319
+
320
+ # 加工したフレームデータで frame の フレームデータを入れ替える
321
+ frame.set_data(new_data)
322
+ self._audio_transformer.enqueue(frame)
323
+
324
+ def _on_video_transform(self, frame: SoraTransformableVideoFrame):
325
+ # この実装が Encoded Transform を利用する上での基本形となる
326
+ # frame からエンコードされたフレームデータを取得する
327
+ # 戻り値は ArrayLike になっている
328
+ new_data = frame.get_data()
329
+
330
+ # ここで new_data の末尾にデータをつける new_data を暗号化するなど任意の処理を実装する
331
+
332
+ # ArrayLike を numpy.uint8 のバイト列に変換する
333
+ new_data = numpy.asarray(new_data, dtype=numpy.uint8)
334
+
335
+ # 後ろ4バイトを取得する
336
+ removed_data = new_data[-4:]
337
+
338
+ assert b"sora" == removed_data.tobytes()
339
+
340
+ # 後ろ4バイトを取り除く
341
+ new_data = new_data[:-4]
342
+
343
+ self._is_called_on_video_transform = True
344
+
345
+ # 加工したフレームデータで frame の フレームデータを入れ替える
346
+ frame.set_data(new_data)
347
+ self._video_transformer.enqueue(frame)
348
+
349
+
350
+ def test_encoded_transform(setup):
351
+ signaling_urls = setup.get("signaling_urls")
352
+ channel_id_prefix = setup.get("channel_id_prefix")
353
+ metadata = setup.get("metadata")
354
+
355
+ channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}"
356
+
357
+ sendonly = SendonlyEncodedTransform(
358
+ signaling_urls,
359
+ channel_id,
360
+ metadata,
361
+ )
362
+ sendonly.connect()
363
+
364
+ recvonly = RecvonlyEncodedTransform(
365
+ signaling_urls,
366
+ channel_id,
367
+ metadata,
368
+ )
369
+ recvonly.connect()
370
+
371
+ time.sleep(5)
372
+
373
+ sendonly_stats = sendonly.get_stats()
374
+ recvonly_stats = recvonly.get_stats()
375
+
376
+ sendonly.disconnect()
377
+ recvonly.disconnect()
378
+
379
+ # codec が無かったら StopIteration 例外が上がる
380
+ sendonly_codec_stats = next(
381
+ s for s in sendonly_stats if s.get("type") == "codec" and s.get("mimeType") == "audio/opus"
382
+ )
383
+ assert sendonly_codec_stats["mimeType"] == "audio/opus"
384
+
385
+ sendonly_codec_stats = next(
386
+ s for s in sendonly_stats if s.get("type") == "codec" and s.get("mimeType") == "video/VP9"
387
+ )
388
+ assert sendonly_codec_stats["mimeType"] == "video/VP9"
389
+
390
+ # outbound-rtp が無かったら StopIteration 例外が上がる
391
+ outbound_rtp_stats = next(
392
+ s for s in sendonly_stats if s.get("type") == "outbound-rtp" and s.get("kind") == "audio"
393
+ )
394
+ # audio には encoderImplementation が無い
395
+ assert outbound_rtp_stats["bytesSent"] > 0
396
+ assert outbound_rtp_stats["packetsSent"] > 0
397
+
398
+ # outbound-rtp が無かったら StopIteration 例外が上がる
399
+ outbound_rtp_stats = next(
400
+ s for s in sendonly_stats if s.get("type") == "outbound-rtp" and s.get("kind") == "video"
401
+ )
402
+ # video には encoderImplementation が無い
403
+ assert outbound_rtp_stats["bytesSent"] > 0
404
+ assert outbound_rtp_stats["packetsSent"] > 0
405
+
406
+ # codec が無かったら StopIteration 例外が上がる
407
+ recvonly_codec_stats = next(
408
+ s for s in recvonly_stats if s.get("type") == "codec" and s.get("mimeType") == "audio/opus"
409
+ )
410
+ assert recvonly_codec_stats["mimeType"] == "audio/opus"
411
+
412
+ recvonly_codec_stats = next(
413
+ s for s in recvonly_stats if s.get("type") == "codec" and s.get("mimeType") == "video/VP9"
414
+ )
415
+ assert recvonly_codec_stats["mimeType"] == "video/VP9"
416
+
417
+ # inbound-rtp が無かったら StopIteration 例外が上がる
418
+ inbound_rtp_stats = next(
419
+ s for s in recvonly_stats if s.get("type") == "inbound-rtp" and s.get("kind") == "audio"
420
+ )
421
+ # audio には encoderImplementation が無い
422
+ assert inbound_rtp_stats["bytesReceived"] > 0
423
+ assert inbound_rtp_stats["packetsReceived"] > 0
424
+
425
+ # inbound-rtp が無かったら StopIteration 例外が上がる
426
+ inbound_rtp_stats = next(
427
+ s for s in recvonly_stats if s.get("type") == "inbound-rtp" and s.get("kind") == "video"
428
+ )
429
+ # video には encoderImplementation が無い
430
+ assert inbound_rtp_stats["bytesReceived"] > 0
431
+ assert inbound_rtp_stats["packetsReceived"] > 0
432
+
433
+ # on_transform が呼ばれていることを確認
434
+ assert sendonly.is_called_on_audio_transform is True
435
+ assert sendonly.is_called_on_video_transform is True
436
+
437
+ assert recvonly.is_called_on_audio_transform is True
438
+ assert recvonly.is_called_on_video_transform is True