view tests/unit/frontends/test_webrtc.py @ 4309:b56b1eae7994

component email gateway: add multicasting: XEP-0033 multicasting is now supported both for incoming and outgoing messages. XEP-0033 metadata are converted to suitable Email headers and vice versa. Email address and JID are both supported, and delivery is done by the gateway when suitable on incoming messages. rel 450
author Goffi <goffi@goffi.org>
date Thu, 26 Sep 2024 16:12:01 +0200
parents f1d0cde61af7
children
line wrap: on
line source

#!/usr/bin/env python3

# Libervia: an XMPP client
# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
from unittest.mock import AsyncMock, MagicMock

import pytest

try:
    from gi.repository import Gst
except ImportError:
    pytest.skip("Gst not available.", allow_module_level=True)

from libervia.backend.core import exceptions
from libervia.backend.tools.common import data_format
from libervia.frontends.tools import webrtc as webrtc_mod


@pytest.fixture
def host(monkeypatch):
    host = MagicMock()
    host.bridge = AsyncMock()
    host.app.expand = lambda s: s
    return host


@pytest.fixture(scope="function")
def webrtc(host):
    """Fixture for WebRTC instantiation."""
    profile = "test_profile"
    instance = webrtc_mod.WebRTC(host.bridge, profile)

    instance._set_media_types = MagicMock()
    instance.start_pipeline = MagicMock()
    instance.link_element_or_pad = MagicMock()
    instance.webrtcbin = MagicMock()
    instance.webrtcbin.emit = MagicMock()

    instance.GstSdp_SDPMessage_new_from_text = MagicMock()
    instance.GstWebRTC_WebRTCSessionDescription_new = MagicMock()
    instance.Gst_Promise_new_with_change_func = MagicMock()

    return instance


class TestWebRtc:
    def test_get_payload_types(self, webrtc):
        """The method can identify the correct payload types for video and audio."""
        fake_sdpmsg = MagicMock()
        fake_media = MagicMock()
        fake_caps = MagicMock()
        fake_structure = MagicMock()

        # This side effect will return 'fake_video_encoding' first, then
        # 'fake_audio_encoding'.
        fake_structure.__getitem__.side_effect = [
            "fake_video_encoding",
            "fake_audio_encoding",
        ]
        fake_caps.get_structure.return_value = fake_structure
        fake_media.get_format.side_effect = ["webrtc-datachannel", "10", "20"]
        fake_media.get_caps_from_media.return_value = fake_caps
        fake_sdpmsg.get_media.return_value = fake_media
        fake_sdpmsg.medias_len.return_value = 1
        fake_media.formats_len.return_value = 3

        result = webrtc.get_payload_types(
            fake_sdpmsg, "fake_video_encoding", "fake_audio_encoding"
        )
        expected_result = {"fake_video_encoding": 10, "fake_audio_encoding": 20}

        assert result == expected_result

    def test_on_accepted_call(self, webrtc):
        """The method correctly sets the remote SDP upon acceptance of an outgoing call."""
        sdp_str = "mock_sdp_string"
        profile_str = "test_profile"

        webrtc.on_accepted_call(sdp_str, profile_str)

        # remote description must be set
        assert webrtc.webrtcbin.emit.call_count == 1
        assert webrtc.webrtcbin.emit.call_args[0][0] == "set-remote-description"

    @pytest.mark.asyncio
    async def test_answer_call(self, webrtc, monkeypatch):
        """The method correctly answers an incoming call."""
        mock_setup_call = AsyncMock()

        def mock_get_payload_types(sdpmsg, video_encoding, audio_encoding):
            return {"VP8": 96, "OPUS": 97}

        monkeypatch.setattr(webrtc, "setup_call", mock_setup_call)
        monkeypatch.setattr(webrtc, "get_payload_types", mock_get_payload_types)

        sdp_str = "mock_sdp_string"
        profile_str = "mock_profile"

        await webrtc.answer_call(sdp_str, profile_str)

        mock_setup_call.assert_called_once_with("responder", audio_pt=97, video_pt=96)

        # remote description must be set
        assert webrtc.webrtcbin.emit.call_count == 1
        assert webrtc.webrtcbin.emit.call_args[0][0] == "set-remote-description"

    def test_on_remote_decodebin_stream_video(self, webrtc, monkeypatch):
        """The method correctly handles video streams from the remote decodebin."""
        mock_pipeline = MagicMock()
        monkeypatch.setattr(webrtc, "pipeline", mock_pipeline)

        mock_pad = MagicMock()
        mock_caps = MagicMock()
        mock_structure = MagicMock()

        mock_pad.has_current_caps.return_value = True
        mock_pad.get_current_caps.return_value = mock_caps
        mock_caps.__len__.return_value = 1
        mock_caps.__getitem__.return_value = mock_structure
        mock_structure.get_name.return_value = "video/x-h264"
        # We use non-standard resolution as example to trigger the workaround
        mock_structure.get_int.side_effect = lambda x: MagicMock(
            value=990 if x == "width" else 557
        )

        webrtc.on_remote_decodebin_stream(None, mock_pad)

        assert webrtc._remote_video_pad == mock_pad
        mock_pipeline.add.assert_called()
        mock_pad.link.assert_called()

    def test_on_remote_decodebin_stream_audio(self, webrtc, monkeypatch):
        """The method correctly handles audio streams from the remote decodebin."""
        mock_pipeline = MagicMock()
        monkeypatch.setattr(webrtc, "pipeline", mock_pipeline)

        mock_pad = MagicMock()
        mock_caps = MagicMock()
        mock_structure = MagicMock()

        mock_pad.has_current_caps.return_value = True
        mock_pad.get_current_caps.return_value = mock_caps
        mock_caps.__len__.return_value = 1
        mock_caps.__getitem__.return_value = mock_structure
        mock_structure.get_name.return_value = "audio/x-raw"

        webrtc.on_remote_decodebin_stream(None, mock_pad)

        mock_pipeline.add.assert_called()
        mock_pad.link.assert_called()

    @pytest.mark.skipif(Gst is None, reason="GStreamer is not available")
    @pytest.mark.asyncio
    async def test_setup_call_correct_role(self, host, webrtc, monkeypatch):
        """Roles are set in setup_call."""
        monkeypatch.setattr(Gst, "parse_launch", MagicMock())
        # we use MagicMock class and not instance on purpose, to pass the "isinstance"
        # test of "setup_call".
        monkeypatch.setattr(Gst, "Pipeline", MagicMock)
        monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[]))

        await webrtc.setup_call("initiator")
        assert webrtc.role == "initiator"

        await webrtc.setup_call("responder")
        assert webrtc.role == "responder"

        with pytest.raises(AssertionError):
            await webrtc.setup_call("invalid_role")

    @pytest.mark.asyncio
    async def test_setup_call_test_mode(self, host, webrtc, monkeypatch):
        """Test mode use fake video and audio in setup_call."""
        monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[]))
        monkeypatch.setattr(webrtc, "sources_data", webrtc_mod.SourcesTest())
        await webrtc.setup_call("initiator")
        assert "videotestsrc" in webrtc.gst_pipe_desc
        assert "audiotestsrc" in webrtc.gst_pipe_desc

    @pytest.mark.asyncio
    async def test_setup_call_normal_mode(self, host, webrtc, monkeypatch):
        """Normal mode use real video and audio in setup_call."""
        monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[]))
        monkeypatch.setattr(webrtc, "sources_data", webrtc_mod.SourcesAuto())
        await webrtc.setup_call("initiator")
        assert "v4l2src" in webrtc.gst_pipe_desc
        assert "pulsesrc" in webrtc.gst_pipe_desc

    @pytest.mark.skipif(Gst is None, reason="GStreamer is not available")
    @pytest.mark.asyncio
    async def test_setup_call_with_stun_and_turn(self, host, webrtc, monkeypatch):
        """STUN and TURN server configurations are done in setup_call."""
        mock_pipeline = MagicMock()
        mock_parse_launch = MagicMock()
        mock_parse_launch.return_value = mock_pipeline
        # As for "test_setup_call_correct_role" we user MagicMock class and not instance
        # on purpose here.
        monkeypatch.setattr(Gst, "Pipeline", MagicMock)
        monkeypatch.setattr(Gst, "parse_launch", mock_parse_launch)

        mock_pipeline.get_by_name.return_value = webrtc.webrtcbin

        mock_external_disco = [
            {"type": "stun", "transport": "udp", "host": "stun.host", "port": "3478"},
            {
                "type": "turn",
                "transport": "udp",
                "host": "turn.host",
                "port": "3478",
                "username": "user",
                "password": "pass",
            },
        ]

        monkeypatch.setattr(
            data_format, "deserialise", MagicMock(return_value=mock_external_disco)
        )

        mock_emit = AsyncMock()
        monkeypatch.setattr(webrtc.webrtcbin, "emit", mock_emit)

        mock_set_property = AsyncMock()
        monkeypatch.setattr(webrtc.webrtcbin, "set_property", mock_set_property)

        await webrtc.setup_call("initiator")

        host.bridge.external_disco_get.assert_called_once_with("", webrtc.profile)
        mock_set_property.assert_any_call("stun-server", "stun://stun.host:3478")
        mock_emit.assert_called_once_with(
            "add-turn-server", "turn://user:pass@turn.host:3478"
        )

    @pytest.mark.skipif(Gst is None, reason="GStreamer is not available")
    @pytest.mark.asyncio
    async def test_setup_call_gstreamer_pipeline_failure(self, webrtc, monkeypatch):
        """Test setup_call method handling Gstreamer pipeline failure."""
        monkeypatch.setattr(Gst, "parse_launch", lambda _: None)
        with pytest.raises(exceptions.InternalError):
            await webrtc.setup_call("initiator")