mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Webrtc use RTCIceCandidateInit messages with frontend (#129879)
* Add sdp m line index to WebRtc Ice Candidates * Send RTCIceCandidate object in messages * Update tests * Update go2rtc to hardcode spdMid to 0 string on receive * Update for latest webrtc-model changes * Add error check for mushamuro error * Remove sdp_line_index from expected fail tests * Validate and parse message dict * Catch mashumaro error and raise vol.Invalid * Revert conftest change * Use custom validator instead --------- Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
d55eb896d2
commit
7d1a7b0870
@ -10,6 +10,7 @@ from functools import cache, partial, wraps
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from mashumaro import MissingField
|
||||
import voluptuous as vol
|
||||
from webrtc_models import (
|
||||
RTCConfiguration,
|
||||
@ -89,7 +90,7 @@ class WebRTCCandidate(WebRTCMessage):
|
||||
"""Return a dict representation of the message."""
|
||||
return {
|
||||
"type": self._get_type(),
|
||||
"candidate": self.candidate.candidate,
|
||||
"candidate": self.candidate.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
@ -328,12 +329,20 @@ async def ws_get_client_config(
|
||||
)
|
||||
|
||||
|
||||
def _parse_webrtc_candidate_init(value: Any) -> RTCIceCandidateInit:
|
||||
"""Validate and parse a WebRTCCandidateInit dict."""
|
||||
try:
|
||||
return RTCIceCandidateInit.from_dict(value)
|
||||
except (MissingField, ValueError) as ex:
|
||||
raise vol.Invalid(str(ex)) from ex
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "camera/webrtc/candidate",
|
||||
vol.Required("entity_id"): cv.entity_id,
|
||||
vol.Required("session_id"): str,
|
||||
vol.Required("candidate"): str,
|
||||
vol.Required("candidate"): _parse_webrtc_candidate_init,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@ -342,9 +351,7 @@ async def ws_candidate(
|
||||
connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
|
||||
) -> None:
|
||||
"""Handle WebRTC candidate websocket command."""
|
||||
await camera.async_on_webrtc_candidate(
|
||||
msg["session_id"], RTCIceCandidateInit(msg["candidate"])
|
||||
)
|
||||
await camera.async_on_webrtc_candidate(msg["session_id"], msg["candidate"])
|
||||
connection.send_message(websocket_api.result_message(msg["id"]))
|
||||
|
||||
|
||||
|
@ -495,7 +495,7 @@ async def test_websocket_webrtc_offer_webrtc_provider_deprecated(
|
||||
hass_ws_client,
|
||||
register_test_provider,
|
||||
WebRTCCandidate(RTCIceCandidate("candidate")),
|
||||
{"type": "candidate", "candidate": "candidate"},
|
||||
{"type": "candidate", "candidate": {"candidate": "candidate"}},
|
||||
)
|
||||
|
||||
|
||||
@ -504,7 +504,10 @@ async def test_websocket_webrtc_offer_webrtc_provider_deprecated(
|
||||
[
|
||||
(
|
||||
WebRTCCandidate(RTCIceCandidateInit("candidate")),
|
||||
{"type": "candidate", "candidate": "candidate"},
|
||||
{
|
||||
"type": "candidate",
|
||||
"candidate": {"candidate": "candidate", "sdpMLineIndex": 0},
|
||||
},
|
||||
),
|
||||
(
|
||||
WebRTCError("webrtc_offer_failed", "error"),
|
||||
@ -955,14 +958,34 @@ async def test_rtsp_to_webrtc_offer_not_accepted(
|
||||
unsub()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("frontend_candidate", "expected_candidate"),
|
||||
[
|
||||
(
|
||||
{"candidate": "candidate", "sdpMLineIndex": 0},
|
||||
RTCIceCandidateInit("candidate"),
|
||||
),
|
||||
(
|
||||
{"candidate": "candidate", "sdpMLineIndex": 1},
|
||||
RTCIceCandidateInit("candidate", sdp_m_line_index=1),
|
||||
),
|
||||
(
|
||||
{"candidate": "candidate", "sdpMid": "1"},
|
||||
RTCIceCandidateInit("candidate", sdp_mid="1"),
|
||||
),
|
||||
],
|
||||
ids=["candidate", "candidate-mline-index", "candidate-mid"],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_ws_webrtc_candidate(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
frontend_candidate: dict[str, Any],
|
||||
expected_candidate: RTCIceCandidateInit,
|
||||
) -> None:
|
||||
"""Test ws webrtc candidate command."""
|
||||
client = await hass_ws_client(hass)
|
||||
session_id = "session_id"
|
||||
candidate = "candidate"
|
||||
with patch.object(
|
||||
get_camera_from_entity_id(hass, "camera.async"), "async_on_webrtc_candidate"
|
||||
) as mock_on_webrtc_candidate:
|
||||
@ -971,15 +994,64 @@ async def test_ws_webrtc_candidate(
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.async",
|
||||
"session_id": session_id,
|
||||
"candidate": candidate,
|
||||
"candidate": frontend_candidate,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert response["success"]
|
||||
mock_on_webrtc_candidate.assert_called_once_with(
|
||||
session_id, RTCIceCandidateInit(candidate)
|
||||
mock_on_webrtc_candidate.assert_called_once_with(session_id, expected_candidate)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected_error_msg"),
|
||||
[
|
||||
(
|
||||
{"sdpMLineIndex": 0},
|
||||
(
|
||||
'Field "candidate" of type str is missing in RTCIceCandidateInit instance'
|
||||
" for dictionary value @ data['candidate']. Got {'sdpMLineIndex': 0}"
|
||||
),
|
||||
),
|
||||
(
|
||||
{"candidate": "candidate", "sdpMLineIndex": -1},
|
||||
(
|
||||
"sdpMLineIndex must be greater than or equal to 0 for dictionary value @ "
|
||||
"data['candidate']. Got {'candidate': 'candidate', 'sdpMLineIndex': -1}"
|
||||
),
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"candidate missing",
|
||||
"spd_mline_index smaller than 0",
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_ws_webrtc_candidate_invalid_candidate_message(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
message: dict,
|
||||
expected_error_msg: str,
|
||||
) -> None:
|
||||
"""Test ws WebRTC candidate command for a camera with a different stream_type."""
|
||||
client = await hass_ws_client(hass)
|
||||
with patch("homeassistant.components.camera.Camera.async_on_webrtc_candidate"):
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.async",
|
||||
"session_id": "session_id",
|
||||
"candidate": message,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["type"] == TYPE_RESULT
|
||||
assert not response["success"]
|
||||
assert response["error"] == {
|
||||
"code": "invalid_format",
|
||||
"message": expected_error_msg,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
@ -993,7 +1065,7 @@ async def test_ws_webrtc_candidate_not_supported(
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.sync",
|
||||
"session_id": "session_id",
|
||||
"candidate": "candidate",
|
||||
"candidate": {"candidate": "candidate"},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
@ -1023,14 +1095,14 @@ async def test_ws_webrtc_candidate_webrtc_provider(
|
||||
"type": "camera/webrtc/candidate",
|
||||
"entity_id": "camera.demo_camera",
|
||||
"session_id": session_id,
|
||||
"candidate": candidate,
|
||||
"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, RTCIceCandidateInit(candidate)
|
||||
session_id, RTCIceCandidateInit(candidate, sdp_m_line_index=1)
|
||||
)
|
||||
|
||||
|
||||
@ -1045,7 +1117,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",
|
||||
"candidate": {"candidate": "candidate"},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
@ -1089,7 +1161,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",
|
||||
"candidate": {"candidate": "candidate"},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
Loading…
x
Reference in New Issue
Block a user