diff --git a/.github/workflows/examples-e2e-test.yml b/.github/workflows/examples-e2e-test.yml index 85286bb0..da58b13e 100644 --- a/.github/workflows/examples-e2e-test.yml +++ b/.github/workflows/examples-e2e-test.yml @@ -38,8 +38,10 @@ jobs: - run: rye pin ${{ matrix.python_version }} - run: rye sync - run: rye run pytest tests/test_openh264.py -s - - run: rye run pytest tests/test_messaging_sendonly.py -s + - run: rye run pytest tests/test_messaging.py -s - run: rye run pytest tests/test_sendonly_recvonly.py -s + - run: rye run pytest tests/test_vad.py -s + - run: rye run pytest tests/test_switched.py -s e2e_macos_test: strategy: @@ -69,8 +71,10 @@ jobs: - run: rye sync - run: rye run pytest tests/test_macos.py -s - run: rye run pytest tests/test_openh264.py -s - - run: rye run pytest tests/test_messaging_sendonly.py -s + - run: rye run pytest tests/test_messaging.py -s - run: rye run pytest tests/test_sendonly_recvonly.py -s + - run: rye run pytest tests/test_vad.py -s + - run: rye run pytest tests/test_switched.py -s e2e_windows_test: strategy: @@ -99,8 +103,10 @@ jobs: - uses: eifinger/setup-rye@v4 - run: rye pin ${{ matrix.python_version }} - run: rye sync - - run: rye run pytest tests/test_messaging_sendonly.py -s + - run: rye run pytest tests/test_messaging.py -s - run: rye run pytest tests/test_sendonly_recvonly.py -s + - run: rye run pytest tests/test_vad.py -s + - run: rye run pytest tests/test_switched.py -s slack_notify_succeeded: needs: [e2e_ubuntu_test, e2e_macos_test, e2e_windows_test] diff --git a/examples/pyproject.toml b/examples/pyproject.toml index 4dbf8823..248e0445 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -18,6 +18,7 @@ requires-python = ">= 3.10" [project.scripts] media_sendonly = "media.sendonly:sendonly" media_recvonly = "media.recvonly:recvonly" +media_vad = "media.vad:vad" messaging_sendrecv = "messaging.sendrecv:sendrecv" messaging_sendonly = "messaging.sendonly:sendonly" messaging_recvonly = "messaging.recvonly:recvonly" diff --git a/examples/src/media/__init__.py b/examples/src/media/__init__.py index 178623f8..2bd3fd89 100644 --- a/examples/src/media/__init__.py +++ b/examples/src/media/__init__.py @@ -37,6 +37,7 @@ def __init__( video: Optional[bool] = None, video_codec_type: Optional[str] = None, video_bit_rate: Optional[int] = None, + data_channel_signaling: Optional[bool] = None, openh264_path: Optional[str] = None, use_hwa: bool = False, audio_channels: int = 1, @@ -58,8 +59,11 @@ def __init__( :param audio_sample_rate: 音声サンプリングレート(デフォルト: 16000) :param video_capture: カメラからのビデオキャプチャ """ - self.audio_channels: int = audio_channels - self.audio_sample_rate: int = audio_sample_rate + self._signaling_urls: list[str] = signaling_urls + self._channel_id: str = channel_id + + self._audio_channels: int = audio_channels + self._audio_sample_rate: int = audio_sample_rate self._sora: Sora = Sora(openh264=openh264_path, use_hardware_encoder=use_hwa) @@ -67,7 +71,7 @@ def __init__( self._fake_video_thread: Optional[threading.Thread] = None self._audio_source = self._sora.create_audio_source( - self.audio_channels, self.audio_sample_rate + self._audio_channels, self._audio_sample_rate ) self._video_source = self._sora.create_video_source() @@ -75,21 +79,24 @@ def __init__( signaling_urls=signaling_urls, role="sendonly", channel_id=channel_id, + metadata=metadata, audio=audio, video=video, video_codec_type=video_codec_type, video_bit_rate=video_bit_rate, - metadata=metadata, + data_channel_signaling=data_channel_signaling, audio_source=self._audio_source, video_source=self._video_source, ) self._connection_id: Optional[str] = None self._connected: Event = Event() + self._switched: bool = False self._closed: Event = Event() self._default_connection_timeout_s: float = 10.0 self._connection.on_set_offer = self._on_set_offer + self._connection.on_switched = self._on_switched self._connection.on_notify = self._on_notify self._connection.on_disconnect = self._on_disconnect @@ -107,12 +114,10 @@ def connect(self, fake_audio=False, fake_video=False) -> None: if fake_audio: self._fake_audio_thread = threading.Thread(target=self._fake_audio_loop, daemon=True) self._fake_audio_thread.start() - print("Fake audio thread started.") if fake_video: self._fake_video_thread = threading.Thread(target=self._fake_video_loop, daemon=True) self._fake_video_thread.start() - print("Fake video thread started.") assert self._connected.wait( self._default_connection_timeout_s @@ -124,8 +129,15 @@ def disconnect(self) -> None: def get_stats(self): raw_stats = self._connection.get_stats() - stats = json.loads(raw_stats) - return stats + return json.loads(raw_stats) + + @property + def connected(self) -> bool: + return self._connected.is_set() + + @property + def switched(self) -> bool: + return self._switched def _fake_audio_loop(self): while not self._closed.is_set(): @@ -137,6 +149,22 @@ def _fake_video_loop(self): time.sleep(1.0 / 30) self._video_source.on_captured(numpy.zeros((480, 640, 3), dtype=numpy.uint8)) + def _on_set_offer(self, raw_message: str) -> None: + """ + オファー設定イベントを処理します。 + + :param raw_message: オファーを含む生のメッセージ + """ + message: dict[str, Any] = json.loads(raw_message) + if message["type"] == "offer": + self._connection_id = message["connection_id"] + + def _on_switched(self, raw_message: str) -> None: + message = json.loads(raw_message) + if message["type"] == "switched": + print(f"Switched to DataChannel Signaling: connection_id={self._connection_id}") + self._switched = True + def _on_notify(self, raw_message: str) -> None: """ Sora からの通知イベントを処理します。 @@ -152,16 +180,6 @@ def _on_notify(self, raw_message: str) -> None: print(f"Connected Sora: connection_id={self._connection_id}") self._connected.set() - def _on_set_offer(self, raw_message: str) -> None: - """ - オファー設定イベントを処理します。 - - :param raw_message: オファーを含む生のメッセージ - """ - message: dict[str, Any] = json.loads(raw_message) - if message["type"] == "offer": - self._connection_id = message["connection_id"] - def _on_disconnect(self, error_code: SoraSignalingErrorCode, message: str) -> None: """ 切断イベントを処理します。 @@ -179,7 +197,7 @@ def _on_disconnect(self, error_code: SoraSignalingErrorCode, message: str) -> No if self._fake_video_thread is not None: self._fake_video_thread.join(timeout=10) - def _callback( + def _sounddevice_input_stream_callback( self, indata: ndarray, frames: int, time: Any, status: sounddevice.CallbackFlags ) -> None: """ @@ -197,10 +215,10 @@ def run(self) -> None: ビデオフレームの送信と音声の送信を行うメインループ。 """ with sounddevice.InputStream( - samplerate=self.audio_sample_rate, - channels=self.audio_channels, + samplerate=self._audio_sample_rate, + channels=self._audio_channels, dtype="int16", - callback=self._callback, + callback=self._sounddevice_input_stream_callback, ): self.connect() try: @@ -224,6 +242,7 @@ def __init__( signaling_urls: list[str], channel_id: str, metadata: Optional[dict[str, Any]] = None, + data_channel_signaling: Optional[bool] = None, openh264_path: Optional[str] = None, use_hwa: Optional[bool] = False, output_frequency: int = 16000, @@ -242,6 +261,9 @@ def __init__( :param output_frequency: 音声出力周波数(Hz)、デフォルトは 16000 :param output_channels: 音声出力チャンネル数、デフォルトは 1 """ + self._signaling_urls: list[str] = signaling_urls + self._channel_id: str = channel_id + self._output_frequency: int = output_frequency self._output_channels: int = output_channels @@ -251,10 +273,12 @@ def __init__( role="recvonly", channel_id=channel_id, metadata=metadata, + data_channel_signaling=data_channel_signaling, ) self._connection_id: Optional[str] = None self._connected: Event = Event() + self._switched: bool = False self._closed: Event = Event() self._default_connection_timeout_s: float = 10.0 @@ -264,6 +288,7 @@ def __init__( self._q_out: queue.Queue = queue.Queue() self._connection.on_set_offer = self._on_set_offer + self._connection.on_switched = self._on_switched self._connection.on_notify = self._on_notify self._connection.on_disconnect = self._on_disconnect self._connection.on_track = self._on_track @@ -286,8 +311,21 @@ def disconnect(self) -> None: def get_stats(self): raw_stats = self._connection.get_stats() - stats = json.loads(raw_stats) - return stats + return json.loads(raw_stats) + + @property + def connected(self) -> bool: + return self._connected.is_set() + + @property + def switched(self) -> bool: + """データチャネルシグナリングへの切り替えが完了しているかどうかを示すブール値。""" + return self._switched + + @property + def closed(self): + """接続が閉じられているかどうかを示すブール値。""" + return self._closed.is_set() def _on_set_offer(self, raw_message: str) -> None: """ @@ -299,6 +337,12 @@ def _on_set_offer(self, raw_message: str) -> None: if message["type"] == "offer": self._connection_id = message["connection_id"] + def _on_switched(self, raw_message: str) -> None: + message = json.loads(raw_message) + if message["type"] == "switched": + print(f"Switched to DataChannel Signaling: connection_id={self._connection_id}") + self._switched = True + def _on_notify(self, raw_message: str) -> None: """ Sora からの通知イベントを処理します。 diff --git a/examples/src/media/vad.py b/examples/src/media/vad.py new file mode 100644 index 00000000..ab9183e4 --- /dev/null +++ b/examples/src/media/vad.py @@ -0,0 +1,145 @@ +import json +import os +from threading import Event +from typing import Any, Optional + +from dotenv import load_dotenv +from sora_sdk import ( + Sora, + SoraAudioFrame, + SoraAudioStreamSink, + SoraMediaTrack, + SoraVAD, +) + + +class VAD: + def __init__( + self, signaling_urls: list[str], channel_id: str, metadata: Optional[dict[str, Any]] + ): + self._signaling_urls: list[str] = signaling_urls + self._channel_id: str = channel_id + + self._vad = SoraVAD() + + self._connection_id: str + + # 接続した + self._connected: Event = Event() + # 終了 + self._closed = Event() + + self._audio_output_frequency: int = 24000 + self._audio_output_channels: int = 1 + + self._sora = Sora() + + self._connection = self._sora.create_connection( + signaling_urls=signaling_urls, + role="recvonly", + channel_id=channel_id, + metadata=metadata, + audio=True, + video=False, + ) + + self._connection.on_set_offer = self._on_set_offer + self._connection.on_notify = self._on_notify + self._connection.on_disconnect = self._on_disconnect + + self._connection.on_track = self._on_track + + def connect(self): + self._connection.connect() + + # _connected が set されるまで 30 秒待つ + assert self._connected.wait(30) + + return self + + def disconnect(self): + self._connection.disconnect() + + def get_stats(self): + raw_stats = self._connection.get_stats() + stats = json.loads(raw_stats) + return stats + + def _on_set_offer(self, raw_offer): + offer = json.loads(raw_offer) + if offer["type"] == "offer": + self._connection_id = offer["connection_id"] + print(f"Received 'Offer': connection_id={self._connection_id}") + + def _on_notify(self, raw_message): + message = json.loads(raw_message) + if ( + message["type"] == "notify" + and message["event_type"] == "connection.created" + and message["connection_id"] == self._connection_id + ): + print(f"Connected Sora: connection_id={self._connection_id}") + self._connected.set() + + def _on_disconnect(self, error_code, message): + print(f"Disconnected Sora: error_code='{error_code}' message='{message}'") + self._closed = True + self._connected.clear() + + def _on_frame(self, frame: SoraAudioFrame): + # frame が音声である確率を求める + voice_probability = self._vad.analyze(frame) + if voice_probability > 0.95: # 0.95 は libwebrtc の判定値 + print(f"Voice! voice_probability={voice_probability}") + else: + pass + + def _on_track(self, track: SoraMediaTrack): + if track.kind == "audio": + # SoraAudioStreamSink + self._audio_stream_sink = SoraAudioStreamSink( + track, self._audio_output_frequency, self._audio_output_channels + ) + self._audio_stream_sink.on_frame = self._on_frame + + def run(self) -> None: + """ビデオフレームの受信と表示、および音声の再生を行うメインループ。""" + self.connect() + try: + while self._connected.is_set(): + pass + except KeyboardInterrupt: + pass + finally: + self.disconnect() + + +def vad() -> None: + """ + 環境変数を使用して Sendonly インスタンスを設定し実行します。 + + :raises ValueError: 必要な環境変数が設定されていない場合 + """ + load_dotenv() + + if not (raw_signaling_urls := os.getenv("SORA_SIGNALING_URLS")): + raise ValueError("環境変数 SORA_SIGNALING_URLS が設定されていません") + signaling_urls = raw_signaling_urls.split(",") + + if not (channel_id := os.getenv("SORA_CHANNEL_ID")): + raise ValueError("環境変数 SORA_CHANNEL_ID が設定されていません") + + metadata = None + if raw_metadata := os.getenv("SORA_METADATA"): + metadata = json.loads(raw_metadata) + + vad = VAD( + signaling_urls, + channel_id, + metadata=metadata, + ) + vad.run() + + +if __name__ == "__main__": + vad() diff --git a/examples/src/messaging/__init__.py b/examples/src/messaging/__init__.py index 33a5d5c5..88320451 100644 --- a/examples/src/messaging/__init__.py +++ b/examples/src/messaging/__init__.py @@ -15,7 +15,7 @@ def __init__( signaling_urls: list[str], channel_id: str, data_channels: list[dict[str, Any]], - metadata: Optional[dict[str, Any]], + metadata: Optional[dict[str, Any]] = None, ): """ Messaging インスタンスを初期化します。 @@ -44,7 +44,8 @@ def __init__( self._connection_id: Optional[str] = None self._connected = Event() - self._closed = False + self._switched: bool = False + self._closed = Event() self._default_connection_timeout_s: float = 10.0 self._label = data_channels[0]["label"] @@ -54,6 +55,7 @@ def __init__( self.sender_id = random.randint(1, 10000) self._connection.on_set_offer = self._on_set_offer + self._connection.on_switched = self._on_switched self._connection.on_notify = self._on_notify self._connection.on_data_channel = self._on_data_channel self._connection.on_message = self._on_message @@ -62,7 +64,7 @@ def __init__( @property def closed(self): """接続が閉じられているかどうかを示すブール値。""" - return self._closed + return self._closed.is_set() def connect(self): """ @@ -80,6 +82,19 @@ def disconnect(self): """Sora から切断します。""" self._connection.disconnect() + def get_stats(self): + raw_stats = self._connection.get_stats() + stats = json.loads(raw_stats) + return stats + + @property + def connected(self) -> bool: + return self._connected.is_set() + + @property + def switched(self) -> bool: + return self._switched + def send(self, data: bytes): """ データチャネルを通じてメッセージを送信します。 @@ -87,7 +102,7 @@ def send(self, data: bytes): :param data: 送信するバイトデータ """ # on_data_channel() が呼ばれるまではデータチャネルの準備ができていないので待機 - while not self._is_data_channel_ready and not self._closed: + while not self._is_data_channel_ready and not self._closed.is_set(): time.sleep(0.01) self._connection.send_data_channel(self._label, data) @@ -103,6 +118,16 @@ def _on_set_offer(self, raw_message: str): # "type": "offer" に入ってくる自分の connection_id を保存する self._connection_id = message["connection_id"] + def _on_switched(self, raw_message: str): + """ + スイッチイベントを処理します。 + + :param raw_message: 生のスイッチメッセージ + """ + message: dict[str, Any] = json.loads(raw_message) + if message["type"] == "switched": + self._switched = True + def _on_notify(self, raw_message: str): """ Sora からの通知イベントを処理します。 @@ -129,7 +154,7 @@ def _on_disconnect(self, error_code: SoraSignalingErrorCode, message: str): """ print(f"Disconnected Sora: error_code='{error_code}' message='{message}'") self._connected.clear() - self._closed = True + self._closed.set() def _on_message(self, label: str, data: bytes): """ diff --git a/examples/tests/test_macos.py b/examples/tests/test_macos.py index 11ebe49f..405788c2 100644 --- a/examples/tests/test_macos.py +++ b/examples/tests/test_macos.py @@ -33,6 +33,8 @@ def test_macos_h264_sendonly(setup): sendonly_stats = sendonly.get_stats() + sendonly.disconnect() + # codec が無かったら StopIteration 例外が上がる codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") # H.264 が採用されているかどうか確認する @@ -44,8 +46,6 @@ def test_macos_h264_sendonly(setup): assert outbound_rtp_stats["bytesSent"] > 0 assert outbound_rtp_stats["packetsSent"] > 0 - sendonly.disconnect() - def test_macos_h265_sendonly(setup): signaling_urls = setup.get("signaling_urls") @@ -69,6 +69,8 @@ def test_macos_h265_sendonly(setup): sendonly_stats = sendonly.get_stats() + sendonly.disconnect() + # codec が無かったら StopIteration 例外が上がる codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") # H.264 が採用されているかどうか確認する @@ -80,8 +82,6 @@ def test_macos_h265_sendonly(setup): assert outbound_rtp_stats["bytesSent"] > 0 assert outbound_rtp_stats["packetsSent"] > 0 - sendonly.disconnect() - @pytest.mark.skip(reason="ローカルでは成功する") def test_macos_h264_sendonly_recvonly(setup): @@ -115,6 +115,9 @@ def test_macos_h264_sendonly_recvonly(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") # H.264 が採用されているかどうか確認する @@ -137,9 +140,6 @@ def test_macos_h264_sendonly_recvonly(setup): assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - sendonly.disconnect() - recvonly.disconnect() - @pytest.mark.skip(reason="ローカルでは成功する") def test_macos_h265_sendonly_recvonly(setup): @@ -173,6 +173,9 @@ def test_macos_h265_sendonly_recvonly(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") assert sendonly_codec_stats["mimeType"] == "video/H265" @@ -192,6 +195,3 @@ def test_macos_h265_sendonly_recvonly(setup): assert inbound_rtp_stats["decoderImplementation"] == "VideoToolbox" assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - - sendonly.disconnect() - recvonly.disconnect() diff --git a/examples/tests/test_messaging.py b/examples/tests/test_messaging.py new file mode 100644 index 00000000..264dd1d0 --- /dev/null +++ b/examples/tests/test_messaging.py @@ -0,0 +1,71 @@ +import sys +import time +import uuid + +from messaging import Messaging + + +def test_messaging(setup): + signaling_urls = setup.get("signaling_urls") + channel_id_prefix = setup.get("channel_id_prefix") + messaging_label = "#test" + + channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}" + + metadata = setup.get("metadata") + + messaging_sendonly = Messaging( + signaling_urls, + channel_id, + [{"label": messaging_label, "direction": "sendonly"}], + metadata=metadata, + ) + + messaging_recvonly = Messaging( + signaling_urls, + channel_id, + [{"label": messaging_label, "direction": "recvonly"}], + metadata=metadata, + ) + + # Sora に接続する + messaging_sendonly.connect() + messaging_recvonly.connect() + + time.sleep(3) + + assert messaging_sendonly.switched + assert messaging_recvonly.switched + + message1 = "spam".encode("utf-8") + message2 = "ham".encode("utf-8") + + messaging_sendonly.send(message1) + messaging_sendonly.send(message2) + + time.sleep(3) + + messaging_sendonly_stats = messaging_sendonly.get_stats() + messaging_recvonly_stats = messaging_recvonly.get_stats() + + messaging_sendonly.disconnect() + messaging_recvonly.disconnect() + + sendonly_data_channel_stats = next( + s + for s in messaging_sendonly_stats + if s.get("type") == "data-channel" and s.get("label") == messaging_label + ) + print(sendonly_data_channel_stats) + assert sendonly_data_channel_stats["state"] == "open" + assert sendonly_data_channel_stats["messagesSent"] == 2 + assert sendonly_data_channel_stats["bytesSent"] == (len(message1) + len(message2)) + + recvonly_data_channel_stats = next( + s + for s in messaging_recvonly_stats + if s.get("type") == "data-channel" and s.get("label") == messaging_label + ) + assert recvonly_data_channel_stats["state"] == "open" + assert recvonly_data_channel_stats["messagesReceived"] == 2 + assert recvonly_data_channel_stats["bytesReceived"] == (len(message1) + len(message2)) diff --git a/examples/tests/test_messaging_sendonly.py b/examples/tests/test_messaging_sendonly.py deleted file mode 100644 index b55b531d..00000000 --- a/examples/tests/test_messaging_sendonly.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -import time -import uuid - -from messaging import Messaging - - -def test_messaging_sendonly(setup): - signaling_urls = setup.get("signaling_urls") - channel_id_prefix = setup.get("channel_id_prefix") - messaging_label = "#test" - - channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}" - - metadata = setup.get("metadata") - - data_channels = [{"label": messaging_label, "direction": "sendonly"}] - messaging_sendonly = Messaging(signaling_urls, channel_id, data_channels, metadata) - - # Sora に接続する - messaging_sendonly.connect() - - messaging_sendonly.send("spam".encode("utf-8")) - messaging_sendonly.send("エッグ".encode("utf-8")) - - time.sleep(3) - - messaging_sendonly.disconnect() diff --git a/examples/tests/test_openh264.py b/examples/tests/test_openh264.py index d5549e68..f53c05a0 100644 --- a/examples/tests/test_openh264.py +++ b/examples/tests/test_openh264.py @@ -40,7 +40,8 @@ def test_openh264_sendonly_recvonly(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() - print(sendonly_stats) + sendonly.disconnect() + recvonly.disconnect() # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") @@ -61,6 +62,3 @@ def test_openh264_sendonly_recvonly(setup): assert outbound_rtp_stats["encoderImplementation"] == "OpenH264" assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - - sendonly.disconnect() - recvonly.disconnect() diff --git a/examples/tests/test_sendonly_recvonly.py b/examples/tests/test_sendonly_recvonly.py index b7faa841..80a5eeeb 100644 --- a/examples/tests/test_sendonly_recvonly.py +++ b/examples/tests/test_sendonly_recvonly.py @@ -33,6 +33,9 @@ def test_sendonly_recvonly_opus(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") assert sendonly_codec_stats["mimeType"] == "audio/opus" @@ -53,9 +56,6 @@ def test_sendonly_recvonly_opus(setup): assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - sendonly.disconnect() - recvonly.disconnect() - def test_sendonly_recvonly_vp8(setup): signaling_urls = setup.get("signaling_urls") @@ -86,6 +86,9 @@ def test_sendonly_recvonly_vp8(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") assert sendonly_codec_stats["mimeType"] == "video/VP8" @@ -106,9 +109,6 @@ def test_sendonly_recvonly_vp8(setup): assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - sendonly.disconnect() - recvonly.disconnect() - def test_sendonly_recvonly_vp9(setup): signaling_urls = setup.get("signaling_urls") @@ -139,6 +139,9 @@ def test_sendonly_recvonly_vp9(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") assert sendonly_codec_stats["mimeType"] == "video/VP9" @@ -159,9 +162,6 @@ def test_sendonly_recvonly_vp9(setup): assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - sendonly.disconnect() - recvonly.disconnect() - def test_sendonly_recvonly_av1(setup): signaling_urls = setup.get("signaling_urls") @@ -192,6 +192,9 @@ def test_sendonly_recvonly_av1(setup): sendonly_stats = sendonly.get_stats() recvonly_stats = recvonly.get_stats() + sendonly.disconnect() + recvonly.disconnect() + # codec が無かったら StopIteration 例外が上がる sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") assert sendonly_codec_stats["mimeType"] == "video/AV1" @@ -211,6 +214,3 @@ def test_sendonly_recvonly_av1(setup): assert inbound_rtp_stats["decoderImplementation"] == "dav1d" assert inbound_rtp_stats["bytesReceived"] > 0 assert inbound_rtp_stats["packetsReceived"] > 0 - - sendonly.disconnect() - recvonly.disconnect() diff --git a/examples/tests/test_switched.py b/examples/tests/test_switched.py new file mode 100644 index 00000000..df103164 --- /dev/null +++ b/examples/tests/test_switched.py @@ -0,0 +1,22 @@ +import sys +import time +import uuid + +from media import Sendonly + + +def test_switched(setup): + signaling_urls = setup.get("signaling_urls") + channel_id_prefix = setup.get("channel_id_prefix") + metadata = setup.get("metadata") + + channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}" + + sendonly = Sendonly(signaling_urls, channel_id, metadata=metadata, data_channel_signaling=True) + sendonly.connect() + + time.sleep(3) + + assert sendonly.switched + + sendonly.disconnect() diff --git a/examples/tests/test_vad.py b/examples/tests/test_vad.py new file mode 100644 index 00000000..d78712ce --- /dev/null +++ b/examples/tests/test_vad.py @@ -0,0 +1,48 @@ +import sys +import time +import uuid + +from media import Sendonly +from media.vad import VAD + + +def test_vad(setup): + signaling_urls = setup.get("signaling_urls") + channel_id_prefix = setup.get("channel_id_prefix") + metadata = setup.get("metadata") + + channel_id = f"{channel_id_prefix}_{__name__}_{sys._getframe().f_code.co_name}_{uuid.uuid4()}" + + sendonly = Sendonly(signaling_urls, channel_id, metadata=metadata) + sendonly.connect(fake_audio=True) + + vad = VAD(signaling_urls, channel_id, metadata=metadata) + vad.connect() + + time.sleep(5) + + sendonly_stats = sendonly.get_stats() + vad_stats = vad.get_stats() + + sendonly.disconnect() + vad.disconnect() + + # codec が無かったら StopIteration 例外が上がる + sendonly_codec_stats = next(s for s in sendonly_stats if s.get("type") == "codec") + assert sendonly_codec_stats["mimeType"] == "audio/opus" + + # outbound-rtp が無かったら StopIteration 例外が上がる + outbound_rtp_stats = next(s for s in sendonly_stats if s.get("type") == "outbound-rtp") + # audio には encoderImplementation が無い + assert outbound_rtp_stats["bytesSent"] > 0 + assert outbound_rtp_stats["packetsSent"] > 0 + + # codec が無かったら StopIteration 例外が上がる + vad_codec_stats = next(s for s in vad_stats if s.get("type") == "codec") + assert vad_codec_stats["mimeType"] == "audio/opus" + + # outbound-rtp が無かったら StopIteration 例外が上がる + inbound_rtp_stats = next(s for s in vad_stats if s.get("type") == "inbound-rtp") + # audio には decoderImplementation が無い + assert inbound_rtp_stats["bytesReceived"] > 0 + assert inbound_rtp_stats["packetsReceived"] > 0