From 065e8e845eb252bd533ecb6fafbbdfb7a275bba9 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:40:36 +0000 Subject: [PATCH] Add RTCIceCandidateInit and allow setting sdpMid and sdpMLineIndex (#2) Co-authored-by: Robert Resch --- .gitignore | 3 + tests/__snapshots__/test_init.ambr | 20 ++++++- .../RTCIceCandidateInit_candidate.json | 4 ++ tests/fixtures/RTCIceCandidateInit_end.json | 3 + .../fixtures/RTCIceCandidateInit_invalid.json | 4 ++ tests/test_init.py | 60 +++++++++++++++++-- webrtc_models/__init__.py | 40 +++++++++++++ 7 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/RTCIceCandidateInit_candidate.json create mode 100644 tests/fixtures/RTCIceCandidateInit_end.json create mode 100644 tests/fixtures/RTCIceCandidateInit_invalid.json diff --git a/.gitignore b/.gitignore index 82f9275..3a73ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# vscode +.vscode diff --git a/tests/__snapshots__/test_init.ambr b/tests/__snapshots__/test_init.ambr index 5c3ba07..11f8f33 100644 --- a/tests/__snapshots__/test_init.ambr +++ b/tests/__snapshots__/test_init.ambr @@ -35,14 +35,20 @@ ]), }) # --- -# name: test_decoding_and_encoding[RTCIceCandidate-RTCIceCandidate_candidate.json][dataclass] +# name: test_decoding_and_encoding[RTCIceCandidateInit-RTCIceCandidateInit_candidate.json][dataclass] dict({ 'candidate': '3932168448 1 udp 1694498815 1.2.3.4 10676 typ srflx raddr 0.0.0.0 rport 37566', + 'sdp_m_line_index': 0, + 'sdp_mid': None, + 'user_fragment': None, }) # --- -# name: test_decoding_and_encoding[RTCIceCandidate-RTCIceCandidate_end.json][dataclass] +# name: test_decoding_and_encoding[RTCIceCandidateInit-RTCIceCandidateInit_end.json][dataclass] dict({ 'candidate': '', + 'sdp_m_line_index': None, + 'sdp_mid': None, + 'user_fragment': None, }) # --- # name: test_decoding_and_encoding[RTCIceServer-RTCIceServer_only_urls_list.json][dataclass] @@ -79,3 +85,13 @@ 'username': 'username', }) # --- +# name: test_decoding_and_encoding_deprecated[RTCIceCandidate-RTCIceCandidate_candidate.json][dataclass] + dict({ + 'candidate': '3932168448 1 udp 1694498815 1.2.3.4 10676 typ srflx raddr 0.0.0.0 rport 37566', + }) +# --- +# name: test_decoding_and_encoding_deprecated[RTCIceCandidate-RTCIceCandidate_end.json][dataclass] + dict({ + 'candidate': '', + }) +# --- diff --git a/tests/fixtures/RTCIceCandidateInit_candidate.json b/tests/fixtures/RTCIceCandidateInit_candidate.json new file mode 100644 index 0000000..d9110ed --- /dev/null +++ b/tests/fixtures/RTCIceCandidateInit_candidate.json @@ -0,0 +1,4 @@ +{ + "candidate": "3932168448 1 udp 1694498815 1.2.3.4 10676 typ srflx raddr 0.0.0.0 rport 37566", + "sdpMLineIndex": 0 +} diff --git a/tests/fixtures/RTCIceCandidateInit_end.json b/tests/fixtures/RTCIceCandidateInit_end.json new file mode 100644 index 0000000..2ba24b3 --- /dev/null +++ b/tests/fixtures/RTCIceCandidateInit_end.json @@ -0,0 +1,3 @@ +{ + "candidate": "" +} diff --git a/tests/fixtures/RTCIceCandidateInit_invalid.json b/tests/fixtures/RTCIceCandidateInit_invalid.json new file mode 100644 index 0000000..f28c412 --- /dev/null +++ b/tests/fixtures/RTCIceCandidateInit_invalid.json @@ -0,0 +1,4 @@ +{ + "candidate": "3932168448 1 udp 1694498815 1.2.3.4 10676 typ srflx raddr 0.0.0.0 rport 37566", + "sdpMLineIndex": -1 +} diff --git a/tests/test_init.py b/tests/test_init.py index 816e882..7a60d1d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -8,7 +8,12 @@ import pytest from tests import load_fixture -from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceServer +from webrtc_models import ( + RTCConfiguration, + RTCIceCandidate, + RTCIceCandidateInit, + RTCIceServer, +) if TYPE_CHECKING: from mashumaro.mixins.orjson import DataClassORJSONMixin @@ -27,9 +32,9 @@ (RTCConfiguration, "RTCConfiguration_empty.json"), (RTCConfiguration, "RTCConfiguration_one_iceServer.json"), (RTCConfiguration, "RTCConfiguration_multiple_iceServers.json"), - # RTCIceCandidate - (RTCIceCandidate, "RTCIceCandidate_end.json"), - (RTCIceCandidate, "RTCIceCandidate_candidate.json"), + # RTCIceCandidateInit + (RTCIceCandidateInit, "RTCIceCandidateInit_end.json"), + (RTCIceCandidateInit, "RTCIceCandidateInit_candidate.json"), ], ) def test_decoding_and_encoding( @@ -51,3 +56,50 @@ def test_decoding_and_encoding( # Verify dict assert instance_dict == file_content_dict assert instance == clazz.from_dict(instance_dict) + + +@pytest.mark.parametrize( + ("clazz", "filename"), + [ + # RTCIceCandidate + (RTCIceCandidate, "RTCIceCandidate_end.json"), + (RTCIceCandidate, "RTCIceCandidate_candidate.json"), + ], +) +def test_decoding_and_encoding_deprecated( + snapshot: SnapshotAssertion, + clazz: type[DataClassORJSONMixin], + filename: str, +) -> None: + """Test decoding/encoding.""" + file_content = load_fixture(filename) + with pytest.deprecated_call(): + instance = clazz.from_json(file_content) + assert instance == snapshot(name="dataclass") + + file_content_dict = orjson.loads(file_content) + instance_dict = instance.to_dict() + + # Verify json + assert instance.to_json() == orjson.dumps(file_content_dict).decode() + + # Verify dict + assert instance_dict == file_content_dict + with pytest.deprecated_call(): + assert instance == clazz.from_dict(instance_dict) + + +def test_no_mid_and_mlineindex() -> None: + """Test spd_mid and sdp_multilineindex raises TypeError.""" + file_content = load_fixture("RTCIceCandidate_candidate.json") + cand = RTCIceCandidateInit.from_json(file_content) + assert cand.sdp_m_line_index == 0 + assert cand.sdp_mid is None + + +def test_invalid_mlineindex() -> None: + """Test spd_mid and sdp_multilineindex raises TypeError.""" + file_content = load_fixture("RTCIceCandidateInit_invalid.json") + msg = "sdpMLineIndex must be greater than or equal to 0" + with pytest.raises(ValueError, match=msg): + RTCIceCandidateInit.from_json(file_content) diff --git a/webrtc_models/__init__.py b/webrtc_models/__init__.py index ab0c214..2c87197 100644 --- a/webrtc_models/__init__.py +++ b/webrtc_models/__init__.py @@ -1,6 +1,7 @@ """WebRTC models.""" from dataclasses import dataclass, field +from warnings import warn from mashumaro import field_options from mashumaro.config import BaseConfig @@ -56,3 +57,42 @@ class RTCIceCandidate(_RTCBaseModel): """ candidate: str + + def __post_init__(self) -> None: + """Initialize class.""" + msg = "Using RTCIceCandidate is deprecated. Use RTCIceCandidateInit instead" + warn(msg, DeprecationWarning, stacklevel=2) + + +@dataclass(frozen=True) +class RTCIceCandidateInit(RTCIceCandidate): + """RTC Ice Candidate Init. + + If neither sdp_mid nor sdp_m_line_index are provided and candidate is not an empty + string, sdp_m_line_index is set to 0. + See https://www.w3.org/TR/webrtc/#dom-rtcicecandidateinit + """ + + candidate: str + sdp_mid: str | None = field( + metadata=field_options(alias="sdpMid"), default=None, kw_only=True + ) + sdp_m_line_index: int | None = field( + metadata=field_options(alias="sdpMLineIndex"), default=None, kw_only=True + ) + user_fragment: str | None = field( + metadata=field_options(alias="userFragment"), default=None, kw_only=True + ) + + def __post_init__(self) -> None: + """Initialize class.""" + if not self.candidate: + # An empty string represents an end-of-candidates indication + # or a peer reflexive remote candidate + return + + if self.sdp_mid is None and self.sdp_m_line_index is None: + object.__setattr__(self, "sdp_m_line_index", 0) + elif (sdp := self.sdp_m_line_index) is not None and sdp < 0: + msg = "sdpMLineIndex must be greater than or equal to 0" + raise ValueError(msg)