Mercurial > libervia-backend
comparison tests/unit/frontends/test_webrtc.py @ 4140:13dd5660c28f
tests (unit/frontends): tests for webrtc implementation:
rel 426
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 01 Nov 2023 14:04:25 +0100 |
parents | |
children | dfccc90cacc6 |
comparison
equal
deleted
inserted
replaced
4139:6745c6bd4c7a | 4140:13dd5660c28f |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia: an XMPP client | |
4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 from unittest.mock import AsyncMock, MagicMock | |
19 | |
20 from gi.repository import Gst | |
21 import pytest | |
22 | |
23 from libervia.backend.core import exceptions | |
24 from libervia.backend.tools.common import data_format | |
25 from libervia.frontends.tools import webrtc as webrtc_mod | |
26 | |
27 | |
28 | |
29 @pytest.fixture | |
30 def host(monkeypatch): | |
31 host = MagicMock() | |
32 host.bridge = AsyncMock() | |
33 host.app.expand = lambda s: s | |
34 return host | |
35 | |
36 @pytest.fixture(scope="function") | |
37 def webrtc(host): | |
38 """Fixture for WebRTC instantiation.""" | |
39 profile = "test_profile" | |
40 instance = webrtc_mod.WebRTC(host.bridge, profile) | |
41 | |
42 instance._set_media_types = MagicMock() | |
43 instance.start_pipeline = MagicMock() | |
44 instance.link_element_or_pad = MagicMock() | |
45 instance.webrtcbin = MagicMock() | |
46 instance.webrtcbin.emit = MagicMock() | |
47 | |
48 instance.GstSdp_SDPMessage_new_from_text = MagicMock() | |
49 instance.GstWebRTC_WebRTCSessionDescription_new = MagicMock() | |
50 instance.Gst_Promise_new_with_change_func = MagicMock() | |
51 | |
52 return instance | |
53 | |
54 | |
55 class TestWebRtc: | |
56 def test_get_payload_types(self, webrtc): | |
57 """The method can identify the correct payload types for video and audio.""" | |
58 fake_sdpmsg = MagicMock() | |
59 fake_media = MagicMock() | |
60 fake_caps = MagicMock() | |
61 fake_structure = MagicMock() | |
62 | |
63 # This side effect will return 'fake_video_encoding' first, then | |
64 # 'fake_audio_encoding'. | |
65 fake_structure.__getitem__.side_effect = [ | |
66 "fake_video_encoding", | |
67 "fake_audio_encoding", | |
68 ] | |
69 fake_caps.get_structure.return_value = fake_structure | |
70 fake_media.get_format.side_effect = ["webrtc-datachannel", "10", "20"] | |
71 fake_media.get_caps_from_media.return_value = fake_caps | |
72 fake_sdpmsg.get_media.return_value = fake_media | |
73 fake_sdpmsg.medias_len.return_value = 1 | |
74 fake_media.formats_len.return_value = 3 | |
75 | |
76 result = webrtc.get_payload_types( | |
77 fake_sdpmsg, "fake_video_encoding", "fake_audio_encoding" | |
78 ) | |
79 expected_result = {"fake_video_encoding": 10, "fake_audio_encoding": 20} | |
80 | |
81 assert result == expected_result | |
82 | |
83 def test_on_accepted_call(self, webrtc): | |
84 """The method correctly sets the remote SDP upon acceptance of an outgoing call.""" | |
85 sdp_str = "mock_sdp_string" | |
86 profile_str = "test_profile" | |
87 | |
88 webrtc.on_accepted_call(sdp_str, profile_str) | |
89 | |
90 # remote description must be set | |
91 assert webrtc.webrtcbin.emit.call_count == 1 | |
92 assert webrtc.webrtcbin.emit.call_args[0][0] == "set-remote-description" | |
93 | |
94 @pytest.mark.asyncio | |
95 async def test_answer_call(self, webrtc, monkeypatch): | |
96 """The method correctly answers an incoming call.""" | |
97 mock_setup_call = AsyncMock() | |
98 | |
99 def mock_get_payload_types(sdpmsg, video_encoding, audio_encoding): | |
100 return {"VP8": 96, "OPUS": 97} | |
101 | |
102 monkeypatch.setattr(webrtc, "setup_call", mock_setup_call) | |
103 monkeypatch.setattr(webrtc, "get_payload_types", mock_get_payload_types) | |
104 | |
105 sdp_str = "mock_sdp_string" | |
106 profile_str = "mock_profile" | |
107 | |
108 await webrtc.answer_call(sdp_str, profile_str) | |
109 | |
110 mock_setup_call.assert_called_once_with("responder", audio_pt=97, video_pt=96) | |
111 | |
112 # remote description must be set | |
113 assert webrtc.webrtcbin.emit.call_count == 1 | |
114 assert webrtc.webrtcbin.emit.call_args[0][0] == "set-remote-description" | |
115 | |
116 def test_on_remote_decodebin_stream_video(self, webrtc, monkeypatch): | |
117 """The method correctly handles video streams from the remote decodebin.""" | |
118 mock_pipeline = MagicMock() | |
119 monkeypatch.setattr(webrtc, "pipeline", mock_pipeline) | |
120 | |
121 mock_pad = MagicMock() | |
122 mock_caps = MagicMock() | |
123 mock_structure = MagicMock() | |
124 | |
125 mock_pad.has_current_caps.return_value = True | |
126 mock_pad.get_current_caps.return_value = mock_caps | |
127 mock_caps.__len__.return_value = 1 | |
128 mock_caps.__getitem__.return_value = mock_structure | |
129 mock_structure.get_name.return_value = "video/x-h264" | |
130 # We use non-standard resolution as example to trigger the workaround | |
131 mock_structure.get_int.side_effect = lambda x: MagicMock( | |
132 value=990 if x == "width" else 557 | |
133 ) | |
134 | |
135 webrtc.on_remote_decodebin_stream(None, mock_pad) | |
136 | |
137 assert webrtc._remote_video_pad == mock_pad | |
138 mock_pipeline.add.assert_called() | |
139 mock_pad.link.assert_called() | |
140 | |
141 def test_on_remote_decodebin_stream_audio(self, webrtc, monkeypatch): | |
142 """The method correctly handles audio streams from the remote decodebin.""" | |
143 mock_pipeline = MagicMock() | |
144 monkeypatch.setattr(webrtc, "pipeline", mock_pipeline) | |
145 | |
146 mock_pad = MagicMock() | |
147 mock_caps = MagicMock() | |
148 mock_structure = MagicMock() | |
149 | |
150 mock_pad.has_current_caps.return_value = True | |
151 mock_pad.get_current_caps.return_value = mock_caps | |
152 mock_caps.__len__.return_value = 1 | |
153 mock_caps.__getitem__.return_value = mock_structure | |
154 mock_structure.get_name.return_value = "audio/x-raw" | |
155 | |
156 webrtc.on_remote_decodebin_stream(None, mock_pad) | |
157 | |
158 mock_pipeline.add.assert_called() | |
159 mock_pad.link.assert_called() | |
160 | |
161 @pytest.mark.asyncio | |
162 async def test_setup_call_correct_role(self, host, webrtc, monkeypatch): | |
163 """Roles are set in setup_call.""" | |
164 monkeypatch.setattr(Gst, "parse_launch", MagicMock()) | |
165 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
166 | |
167 await webrtc.setup_call("initiator") | |
168 assert webrtc.role == "initiator" | |
169 | |
170 await webrtc.setup_call("responder") | |
171 assert webrtc.role == "responder" | |
172 | |
173 with pytest.raises(AssertionError): | |
174 await webrtc.setup_call("invalid_role") | |
175 | |
176 @pytest.mark.asyncio | |
177 async def test_setup_call_test_mode(self, host, webrtc, monkeypatch): | |
178 """Test mode use fake video and audio in setup_call.""" | |
179 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
180 monkeypatch.setattr(webrtc, "sources", webrtc_mod.SINKS_TEST) | |
181 await webrtc.setup_call("initiator") | |
182 assert "videotestsrc" in webrtc.gst_pipe_desc | |
183 assert "audiotestsrc" in webrtc.gst_pipe_desc | |
184 | |
185 @pytest.mark.asyncio | |
186 async def test_setup_call_normal_mode(self, host, webrtc, monkeypatch): | |
187 """Normal mode use real video and audio in setup_call.""" | |
188 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
189 monkeypatch.setattr(webrtc, "sources", webrtc_mod.SOURCES_AUTO) | |
190 await webrtc.setup_call("initiator") | |
191 assert "v4l2src" in webrtc.gst_pipe_desc | |
192 assert "pulsesrc" in webrtc.gst_pipe_desc | |
193 | |
194 @pytest.mark.asyncio | |
195 async def test_setup_call_with_stun_and_turn(self, host, webrtc, monkeypatch): | |
196 """STUN and TURN server configurations are done in setup_call.""" | |
197 mock_pipeline = MagicMock() | |
198 mock_parse_launch = MagicMock() | |
199 mock_parse_launch.return_value = mock_pipeline | |
200 monkeypatch.setattr(Gst, "parse_launch", mock_parse_launch) | |
201 | |
202 mock_pipeline.get_by_name.return_value = webrtc.webrtcbin | |
203 | |
204 mock_external_disco = [ | |
205 {"type": "stun", "transport": "udp", "host": "stun.host", "port": "3478"}, | |
206 { | |
207 "type": "turn", | |
208 "transport": "udp", | |
209 "host": "turn.host", | |
210 "port": "3478", | |
211 "username": "user", | |
212 "password": "pass", | |
213 }, | |
214 ] | |
215 | |
216 monkeypatch.setattr( | |
217 data_format, "deserialise", MagicMock(return_value=mock_external_disco) | |
218 ) | |
219 | |
220 mock_emit = AsyncMock() | |
221 monkeypatch.setattr(webrtc.webrtcbin, "emit", mock_emit) | |
222 | |
223 mock_set_property = AsyncMock() | |
224 monkeypatch.setattr(webrtc.webrtcbin, "set_property", mock_set_property) | |
225 | |
226 await webrtc.setup_call("initiator") | |
227 | |
228 host.bridge.external_disco_get.assert_called_once_with("", webrtc.profile) | |
229 mock_set_property.assert_any_call("stun-server", "stun://stun.host:3478") | |
230 mock_emit.assert_called_once_with( | |
231 "add-turn-server", "turn://user:pass@turn.host:3478" | |
232 ) | |
233 | |
234 @pytest.mark.asyncio | |
235 async def test_setup_call_gstreamer_pipeline_failure(self, webrtc, monkeypatch): | |
236 """Test setup_call method handling Gstreamer pipeline failure.""" | |
237 monkeypatch.setattr(Gst, "parse_launch", lambda _: None) | |
238 with pytest.raises(exceptions.InternalError): | |
239 await webrtc.setup_call("initiator") |