From 7eed62cf3f8f66fbb719c08544f4857d994c39af Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:58:50 +0000 Subject: [PATCH 1/4] Add sdp m line index to WebRtc Ice Candidates --- homeassistant/components/camera/webrtc.py | 4 +++- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/camera/test_webrtc.py | 17 ++++++++++++----- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 0612c96e40c8a9..16a0cb37c3ed0f 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -85,6 +85,7 @@ def as_dict(self) -> dict[str, Any]: return { "type": self._get_type(), "candidate": self.candidate.candidate, + "sdp_m_line_index": self.candidate.sdp_m_line_index, } @@ -308,6 +309,7 @@ async def ws_get_client_config( vol.Required("entity_id"): cv.entity_id, vol.Required("session_id"): str, vol.Required("candidate"): str, + vol.Required("sdp_m_line_index"): int, } ) @websocket_api.async_response @@ -329,7 +331,7 @@ async def ws_candidate( return await camera.async_on_webrtc_candidate( - msg["session_id"], RTCIceCandidate(msg["candidate"]) + msg["session_id"], RTCIceCandidate(msg["candidate"], msg["sdp_m_line_index"]) ) connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56155d53fd51f2..319a286cd6b902 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -66,7 +66,7 @@ uv==0.4.28 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 -webrtc-models==0.2.0 +webrtc-models==0.3.0 yarl==1.17.1 zeroconf==0.136.0 diff --git a/pyproject.toml b/pyproject.toml index 4a2857b5065d4b..6bd556537889a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.5", "yarl==1.17.1", - "webrtc-models==0.2.0", + "webrtc-models==0.3.0", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index a5beecec8ff9a8..75a2665bae3276 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,4 +45,4 @@ voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 yarl==1.17.1 -webrtc-models==0.2.0 +webrtc-models==0.3.0 diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index 2970a41408c981..027d78362294ac 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -529,8 +529,8 @@ async def test_websocket_webrtc_offer( ("message", "expected_frontend_message"), [ ( - WebRTCCandidate(RTCIceCandidate("candidate")), - {"type": "candidate", "candidate": "candidate"}, + WebRTCCandidate(RTCIceCandidate("candidate", 0)), + {"type": "candidate", "candidate": "candidate", "sdp_m_line_index": 0}, ), ( WebRTCError("webrtc_offer_failed", "error"), @@ -1012,13 +1012,14 @@ async def test_ws_webrtc_candidate( "entity_id": "camera.demo_camera", "session_id": session_id, "candidate": candidate, + "sdp_m_line_index": 1, } ) response = await client.receive_json() assert response["type"] == TYPE_RESULT assert response["success"] mock_on_webrtc_candidate.assert_called_once_with( - session_id, RTCIceCandidate(candidate) + session_id, RTCIceCandidate(candidate, 1) ) @@ -1034,6 +1035,7 @@ async def test_ws_webrtc_candidate_not_supported( "entity_id": "camera.demo_camera", "session_id": "session_id", "candidate": "candidate", + "sdp_m_line_index": 1, } ) response = await client.receive_json() @@ -1064,13 +1066,14 @@ async def test_ws_webrtc_candidate_webrtc_provider( "entity_id": "camera.demo_camera", "session_id": session_id, "candidate": candidate, + "sdp_m_line_index": 1, } ) response = await client.receive_json() assert response["type"] == TYPE_RESULT assert response["success"] mock_on_webrtc_candidate.assert_called_once_with( - session_id, RTCIceCandidate(candidate) + session_id, RTCIceCandidate(candidate, 1) ) @@ -1086,6 +1089,7 @@ async def test_ws_webrtc_candidate_invalid_entity( "entity_id": "camera.does_not_exist", "session_id": "session_id", "candidate": "candidate", + "sdp_m_line_index": 1, } ) response = await client.receive_json() @@ -1130,6 +1134,7 @@ async def test_ws_webrtc_candidate_invalid_stream_type( "entity_id": "camera.demo_camera", "session_id": "session_id", "candidate": "candidate", + "sdp_m_line_index": 0, } ) response = await client.receive_json() @@ -1182,7 +1187,9 @@ async def async_on_webrtc_candidate( await provider.async_handle_async_webrtc_offer( Mock(), "offer_sdp", "session_id", Mock() ) - await provider.async_on_webrtc_candidate("session_id", RTCIceCandidate("candidate")) + await provider.async_on_webrtc_candidate( + "session_id", RTCIceCandidate("candidate", 0) + ) provider.async_close_session("session_id") From 55ce4228f6ce3dc32512da6fb56be65e149a8c1a Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:16:37 +0000 Subject: [PATCH 2/4] Send RTCIceCandidate object in messages --- homeassistant/components/camera/webrtc.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 16a0cb37c3ed0f..826393c98a48d2 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -84,8 +84,7 @@ def as_dict(self) -> dict[str, Any]: """Return a dict representation of the message.""" return { "type": self._get_type(), - "candidate": self.candidate.candidate, - "sdp_m_line_index": self.candidate.sdp_m_line_index, + "candidate": self.candidate.to_dict(), } @@ -308,8 +307,7 @@ async def ws_get_client_config( vol.Required("type"): "camera/webrtc/candidate", vol.Required("entity_id"): cv.entity_id, vol.Required("session_id"): str, - vol.Required("candidate"): str, - vol.Required("sdp_m_line_index"): int, + vol.Required("candidate"): dict, } ) @websocket_api.async_response @@ -331,7 +329,7 @@ async def ws_candidate( return await camera.async_on_webrtc_candidate( - msg["session_id"], RTCIceCandidate(msg["candidate"], msg["sdp_m_line_index"]) + msg["session_id"], RTCIceCandidate.from_dict(msg["candidate"]) ) connection.send_message(websocket_api.result_message(msg["id"])) From 3b2ede82a4ffaf6f0b0172d3d319ca5b6c2fdde7 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:40:15 +0000 Subject: [PATCH 3/4] Update tests --- tests/components/camera/test_webrtc.py | 37 +++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index 027d78362294ac..ef9966b8f5beb5 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -529,8 +529,18 @@ async def test_websocket_webrtc_offer( ("message", "expected_frontend_message"), [ ( - WebRTCCandidate(RTCIceCandidate("candidate", 0)), - {"type": "candidate", "candidate": "candidate", "sdp_m_line_index": 0}, + WebRTCCandidate(RTCIceCandidate("candidate", sdp_m_line_index=0)), + { + "type": "candidate", + "candidate": {"candidate": "candidate", "sdpMLineIndex": 0}, + }, + ), + ( + WebRTCCandidate(RTCIceCandidate("candidate", sdp_mid="0")), + { + "type": "candidate", + "candidate": {"candidate": "candidate", "sdpMid": "0"}, + }, ), ( WebRTCError("webrtc_offer_failed", "error"), @@ -538,7 +548,7 @@ async def test_websocket_webrtc_offer( ), (WebRTCAnswer("answer"), {"type": "answer", "answer": "answer"}), ], - ids=["candidate", "error", "answer"], + ids=["candidate-mlineindex", "candidate-mid", "error", "answer"], ) @pytest.mark.usefixtures("mock_stream_source", "mock_camera") async def test_websocket_webrtc_offer_webrtc_provider( @@ -1011,15 +1021,14 @@ async def test_ws_webrtc_candidate( "type": "camera/webrtc/candidate", "entity_id": "camera.demo_camera", "session_id": session_id, - "candidate": candidate, - "sdp_m_line_index": 1, + "candidate": {"candidate": candidate, "sdpMLineIndex": 1}, } ) response = await client.receive_json() assert response["type"] == TYPE_RESULT assert response["success"] mock_on_webrtc_candidate.assert_called_once_with( - session_id, RTCIceCandidate(candidate, 1) + session_id, RTCIceCandidate(candidate, sdp_m_line_index=1) ) @@ -1034,8 +1043,7 @@ async def test_ws_webrtc_candidate_not_supported( "type": "camera/webrtc/candidate", "entity_id": "camera.demo_camera", "session_id": "session_id", - "candidate": "candidate", - "sdp_m_line_index": 1, + "candidate": {"candidate": "candidate", "sdpMLineIndex": 1}, } ) response = await client.receive_json() @@ -1065,15 +1073,14 @@ async def test_ws_webrtc_candidate_webrtc_provider( "type": "camera/webrtc/candidate", "entity_id": "camera.demo_camera", "session_id": session_id, - "candidate": candidate, - "sdp_m_line_index": 1, + "candidate": {"candidate": candidate, "sdpMLineIndex": 1}, } ) response = await client.receive_json() assert response["type"] == TYPE_RESULT assert response["success"] mock_on_webrtc_candidate.assert_called_once_with( - session_id, RTCIceCandidate(candidate, 1) + session_id, RTCIceCandidate(candidate, sdp_m_line_index=1) ) @@ -1088,8 +1095,7 @@ async def test_ws_webrtc_candidate_invalid_entity( "type": "camera/webrtc/candidate", "entity_id": "camera.does_not_exist", "session_id": "session_id", - "candidate": "candidate", - "sdp_m_line_index": 1, + "candidate": {"candidate": "candidate", "sdpMLineIndex": 1}, } ) response = await client.receive_json() @@ -1133,8 +1139,7 @@ async def test_ws_webrtc_candidate_invalid_stream_type( "type": "camera/webrtc/candidate", "entity_id": "camera.demo_camera", "session_id": "session_id", - "candidate": "candidate", - "sdp_m_line_index": 0, + "candidate": {"candidate": "candidate", "sdp_m_line_index": 0}, } ) response = await client.receive_json() @@ -1188,7 +1193,7 @@ async def async_on_webrtc_candidate( Mock(), "offer_sdp", "session_id", Mock() ) await provider.async_on_webrtc_candidate( - "session_id", RTCIceCandidate("candidate", 0) + "session_id", RTCIceCandidate("candidate", sdp_m_line_index=0) ) provider.async_close_session("session_id") From dd6c77db32f157f8be49416ee451b9a59ab816a5 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:58:34 +0000 Subject: [PATCH 4/4] Update go2rtc to hardcode spdMid to 0 string on receive --- homeassistant/components/go2rtc/__init__.py | 4 +++- tests/components/go2rtc/test_init.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 5be1dbc1a4841d..916288092ebd1d 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -222,7 +222,9 @@ def on_messages(message: ReceiveMessages) -> None: value: WebRTCMessage match message: case WebRTCCandidate(): - value = HAWebRTCCandidate(RTCIceCandidate(message.candidate)) + value = HAWebRTCCandidate( + RTCIceCandidate(message.candidate, sdp_mid="0") + ) case WebRTCAnswer(): value = HAWebRTCAnswer(message.sdp) case WsError(): diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 847de248aaf4f2..91191f99326343 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -392,7 +392,7 @@ async def message_callbacks( [ ( WebRTCCandidate("candidate"), - HAWebRTCCandidate(RTCIceCandidate("candidate")), + HAWebRTCCandidate(RTCIceCandidate("candidate", sdp_mid="0")), ), ( WebRTCAnswer(ANSWER_SDP), @@ -428,7 +428,9 @@ async def test_on_candidate( session_id = "session_id" # Session doesn't exist - await camera.async_on_webrtc_candidate(session_id, RTCIceCandidate("candidate")) + await camera.async_on_webrtc_candidate( + session_id, RTCIceCandidate("candidate", sdp_mid="0") + ) assert ( "homeassistant.components.go2rtc", logging.DEBUG, @@ -448,7 +450,9 @@ async def test_on_candidate( ) ws_client.reset_mock() - await camera.async_on_webrtc_candidate(session_id, RTCIceCandidate("candidate")) + await camera.async_on_webrtc_candidate( + session_id, RTCIceCandidate("candidate", sdp_mid="0") + ) ws_client.send.assert_called_once_with(WebRTCCandidate("candidate")) assert caplog.record_tuples == []