Remove deprecated camera async_handle_web_rtc_offer function (#144561)

This commit is contained in:
Robert Resch 2025-05-12 14:47:49 +02:00 committed by GitHub
parent 4dde314338
commit 7b23f21712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 27 additions and 342 deletions

View File

@ -55,7 +55,6 @@ from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum, DeprecatedConstantEnum,
all_with_deprecated_constants, all_with_deprecated_constants,
check_if_deprecated_constant, check_if_deprecated_constant,
deprecated_function,
dir_with_deprecated_constants, dir_with_deprecated_constants,
) )
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import ( from .webrtc import (
DATA_ICE_SERVERS, DATA_ICE_SERVERS,
CameraWebRTCProvider, CameraWebRTCProvider,
WebRTCAnswer, WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401 WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration, WebRTCClientConfiguration,
WebRTCError, WebRTCError, # noqa: F401
WebRTCMessage, # noqa: F401 WebRTCMessage, # noqa: F401
WebRTCSendMessage, WebRTCSendMessage,
async_get_supported_provider, async_get_supported_provider,
@ -473,9 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token() self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None self._webrtc_provider: CameraWebRTCProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
self._supports_native_async_webrtc = ( self._supports_native_async_webrtc = (
type(self).async_handle_async_webrtc_offer type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer != Camera.async_handle_async_webrtc_offer
@ -579,15 +575,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
""" """
return None return None
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return an answer.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.WEB_RTC.
Integrations can override with a native WebRTC implementation.
"""
async def async_handle_async_webrtc_offer( async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None: ) -> None:
@ -600,42 +587,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation. Integrations can override with a native WebRTC implementation.
""" """
if self._supports_native_sync_webrtc:
try:
answer = await deprecated_function(
"async_handle_async_webrtc_offer",
breaks_in_ha_version="2025.6",
)(self.async_handle_web_rtc_offer)(offer_sdp)
except ValueError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
except TimeoutError:
# This catch was already here and should stay through the deprecation
_LOGGER.error("Timeout handling WebRTC offer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"Timeout handling WebRTC offer",
)
)
else:
if answer:
send_message(WebRTCAnswer(answer))
else:
_LOGGER.error("Error handling WebRTC offer: No answer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"No answer on WebRTC offer",
)
)
return
if self._webrtc_provider: if self._webrtc_provider:
await self._webrtc_provider.async_handle_async_webrtc_offer( await self._webrtc_provider.async_handle_async_webrtc_offer(
self, offer_sdp, session_id, send_message self, offer_sdp, session_id, send_message
@ -764,9 +715,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
new_provider = None new_provider = None
# Skip all providers if the camera has a native WebRTC implementation # Skip all providers if the camera has a native WebRTC implementation
if not ( if not self._supports_native_async_webrtc:
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
):
# Camera doesn't have a native WebRTC implementation # Camera doesn't have a native WebRTC implementation
new_provider = await self._async_get_supported_webrtc_provider( new_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_provider async_get_supported_provider
@ -798,17 +747,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers.""" """Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration() config = self._async_get_webrtc_client_configuration()
if not self._supports_native_sync_webrtc: ice_servers = [
# Until 2024.11, the frontend was not resolving any ice servers server
# The async approach was added 2024.11 and new integrations need to use it for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
ice_servers = [ for server in servers()
server ]
for servers in self.hass.data.get(DATA_ICE_SERVERS, []) config.configuration.ice_servers.extend(ice_servers)
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = self._supports_native_sync_webrtc
return config return config
@ -838,7 +782,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the camera capabilities.""" """Return the camera capabilities."""
frontend_stream_types = set() frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat: if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc: if self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation # The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC) frontend_stream_types.add(StreamType.WEB_RTC)
else: else:

View File

@ -111,13 +111,11 @@ class WebRTCClientConfiguration:
configuration: RTCConfiguration = field(default_factory=RTCConfiguration) configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
data_channel: str | None = None data_channel: str | None = None
get_candidates_upfront: bool = False
def to_frontend_dict(self) -> dict[str, Any]: def to_frontend_dict(self) -> dict[str, Any]:
"""Return a dict that can be used by the frontend.""" """Return a dict that can be used by the frontend."""
data: dict[str, Any] = { data: dict[str, Any] = {
"configuration": self.configuration.to_dict(), "configuration": self.configuration.to_dict(),
"getCandidatesUpfront": self.get_candidates_upfront,
} }
if self.data_channel is not None: if self.data_channel is not None:
data["dataChannel"] = self.data_channel data["dataChannel"] = self.data_channel

View File

@ -165,13 +165,15 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
async def stream_source(self) -> str | None: async def stream_source(self) -> str | None:
return STREAM_SOURCE return STREAM_SOURCE
class SyncCamera(BaseCamera): class AsyncNoCandidateCamera(BaseCamera):
"""Mock Camera with native sync WebRTC support.""" """Mock Camera with native async WebRTC support but not implemented candidate support."""
_attr_name = "Sync" _attr_name = "Async No Candidate"
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: async def async_handle_async_webrtc_offer(
return WEBRTC_ANSWER self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
send_message(WebRTCAnswer(WEBRTC_ANSWER))
class AsyncCamera(BaseCamera): class AsyncCamera(BaseCamera):
"""Mock Camera with native async WebRTC support.""" """Mock Camera with native async WebRTC support."""
@ -221,7 +223,10 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
), ),
) )
setup_test_component_platform( setup_test_component_platform(
hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True hass,
camera.DOMAIN,
[AsyncNoCandidateCamera(), AsyncCamera()],
from_config_entry=True,
) )
mock_platform(hass, f"{domain}.config_flow", Mock()) mock_platform(hass, f"{domain}.config_flow", Mock())

View File

@ -968,24 +968,19 @@ async def test_camera_capabilities_webrtc(
"""Test WebRTC camera capabilities.""" """Test WebRTC camera capabilities."""
await _test_capabilities( await _test_capabilities(
hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC} hass, hass_ws_client, "camera.async", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
) )
@pytest.mark.parametrize(
("entity_id", "expect_native_async_webrtc"),
[("camera.sync", False), ("camera.async", True)],
)
@pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider") @pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider")
async def test_webrtc_provider_not_added_for_native_webrtc( async def test_webrtc_provider_not_added_for_native_webrtc(
hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool hass: HomeAssistant,
) -> None: ) -> None:
"""Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support.""" """Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support."""
camera_obj = get_camera_from_entity_id(hass, entity_id) camera_obj = get_camera_from_entity_id(hass, "camera.async")
assert camera_obj assert camera_obj
assert camera_obj._webrtc_provider is None assert camera_obj._webrtc_provider is None
assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc assert camera_obj._supports_native_async_webrtc is True
assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc
@pytest.mark.usefixtures("mock_camera", "mock_stream_source") @pytest.mark.usefixtures("mock_camera", "mock_stream_source")
@ -1016,14 +1011,12 @@ async def test_camera_capabilities_changing_non_native_support(
@pytest.mark.usefixtures("mock_test_webrtc_cameras") @pytest.mark.usefixtures("mock_test_webrtc_cameras")
@pytest.mark.parametrize(("entity_id"), ["camera.sync", "camera.async"])
async def test_camera_capabilities_changing_native_support( async def test_camera_capabilities_changing_native_support(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
entity_id: str,
) -> None: ) -> None:
"""Test WebRTC camera capabilities.""" """Test WebRTC camera capabilities."""
cam = get_camera_from_entity_id(hass, entity_id) cam = get_camera_from_entity_id(hass, "camera.async")
assert cam.supported_features == camera.CameraEntityFeature.STREAM assert cam.supported_features == camera.CameraEntityFeature.STREAM
await _test_capabilities( await _test_capabilities(

View File

@ -1,7 +1,6 @@
"""Test camera WebRTC.""" """Test camera WebRTC."""
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
import logging
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
@ -10,9 +9,7 @@ from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer
from homeassistant.components.camera import ( from homeassistant.components.camera import (
DATA_ICE_SERVERS, DATA_ICE_SERVERS,
DOMAIN as CAMERA_DOMAIN,
Camera, Camera,
CameraEntityFeature,
CameraWebRTCProvider, CameraWebRTCProvider,
StreamType, StreamType,
WebRTCAnswer, WebRTCAnswer,
@ -25,22 +22,12 @@ from homeassistant.components.camera import (
get_camera_from_entity_id, get_camera_from_entity_id,
) )
from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
from tests.common import (
MockConfigEntry,
MockModule,
mock_config_flow,
mock_integration,
mock_platform,
setup_test_component_platform,
)
from tests.typing import WebSocketGenerator from tests.typing import WebSocketGenerator
WEBRTC_OFFER = "v=0\r\n" WEBRTC_OFFER = "v=0\r\n"
@ -57,84 +44,6 @@ class Go2RTCProvider(SomeTestProvider):
return "go2rtc" return "go2rtc"
class MockCamera(Camera):
"""Mock Camera Entity."""
_attr_name = "Test"
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
def __init__(self) -> None:
"""Initialize the mock entity."""
super().__init__()
self._sync_answer: str | None | Exception = WEBRTC_ANSWER
def set_sync_answer(self, value: str | None | Exception) -> None:
"""Set sync offer answer."""
self._sync_answer = value
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return the answer."""
if isinstance(self._sync_answer, Exception):
raise self._sync_answer
return self._sync_answer
async def stream_source(self) -> str | None:
"""Return the source of the stream.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.HLS.
"""
return "rtsp://stream"
@pytest.fixture
async def init_test_integration(
hass: HomeAssistant,
) -> MockCamera:
"""Initialize components."""
entry = MockConfigEntry(domain=TEST_INTEGRATION_DOMAIN)
entry.add_to_hass(hass)
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [CAMERA_DOMAIN]
)
return True
async def async_unload_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Unload test config entry."""
await hass.config_entries.async_forward_entry_unload(
config_entry, CAMERA_DOMAIN
)
return True
mock_integration(
hass,
MockModule(
TEST_INTEGRATION_DOMAIN,
async_setup_entry=async_setup_entry_init,
async_unload_entry=async_unload_entry_init,
),
)
test_camera = MockCamera()
setup_test_component_platform(
hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True
)
mock_platform(hass, f"{TEST_INTEGRATION_DOMAIN}.config_flow", Mock())
with mock_config_flow(TEST_INTEGRATION_DOMAIN, ConfigFlow):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return test_camera
@pytest.mark.usefixtures("mock_camera", "mock_stream_source") @pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_async_register_webrtc_provider( async def test_async_register_webrtc_provider(
hass: HomeAssistant, hass: HomeAssistant,
@ -302,7 +211,6 @@ async def test_ws_get_client_config(
}, },
], ],
}, },
"getCandidatesUpfront": False,
} }
@callback @callback
@ -341,30 +249,6 @@ async def test_ws_get_client_config(
}, },
], ],
}, },
"getCandidatesUpfront": False,
}
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_ws_get_client_config_sync_offer(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test get WebRTC client config, when camera is supporting sync offer."""
await async_setup_component(hass, "camera", {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"}
)
msg = await client.receive_json()
# Assert WebSocket response
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"] == {
"configuration": {},
"getCandidatesUpfront": True,
} }
@ -391,7 +275,6 @@ async def test_ws_get_client_config_custom_config(
assert msg["success"] assert msg["success"]
assert msg["result"] == { assert msg["result"] == {
"configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]}, "configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]},
"getCandidatesUpfront": False,
} }
@ -625,144 +508,6 @@ async def test_websocket_webrtc_offer_missing_offer(
assert response["error"]["code"] == "invalid_format" assert response["error"]["code"] == "invalid_format"
@pytest.mark.parametrize(
("error", "expected_message"),
[
(ValueError("value error"), "value error"),
(HomeAssistantError("offer failed"), "offer failed"),
(TimeoutError(), "Timeout handling WebRTC offer"),
],
)
async def test_websocket_webrtc_offer_failure(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
init_test_integration: MockCamera,
error: Exception,
expected_message: str,
) -> None:
"""Test WebRTC stream that fails handling the offer."""
client = await hass_ws_client(hass)
init_test_integration.set_sync_answer(error)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.test",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Error
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {
"type": "error",
"code": "webrtc_offer_failed",
"message": expected_message,
}
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_websocket_webrtc_offer_sync(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sync WebRTC stream offer."""
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.sync",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert (
"tests.components.camera.conftest",
logging.WARNING,
(
"async_handle_web_rtc_offer was called from camera, this is a deprecated "
"function which will be removed in HA Core 2025.6. Use "
"async_handle_async_webrtc_offer instead"
),
) in caplog.record_tuples
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Answer
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {"type": "answer", "answer": WEBRTC_ANSWER}
async def test_websocket_webrtc_offer_sync_no_answer(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
init_test_integration: MockCamera,
) -> None:
"""Test sync WebRTC stream offer with no answer."""
client = await hass_ws_client(hass)
init_test_integration.set_sync_answer(None)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.test",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Answer
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {
"type": "error",
"code": "webrtc_offer_failed",
"message": "No answer on WebRTC offer",
}
assert (
"homeassistant.components.camera",
logging.ERROR,
"Error handling WebRTC offer: No answer",
) in caplog.record_tuples
@pytest.mark.usefixtures("mock_camera") @pytest.mark.usefixtures("mock_camera")
async def test_websocket_webrtc_offer_invalid_stream_type( async def test_websocket_webrtc_offer_invalid_stream_type(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator hass: HomeAssistant, hass_ws_client: WebSocketGenerator
@ -901,7 +646,7 @@ async def test_ws_webrtc_candidate_not_supported(
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "camera/webrtc/candidate", "type": "camera/webrtc/candidate",
"entity_id": "camera.sync", "entity_id": "camera.async_no_candidate",
"session_id": "session_id", "session_id": "session_id",
"candidate": {"candidate": "candidate"}, "candidate": {"candidate": "candidate"},
} }