Remove deprecated legacy WebRTC provider (#144547)

This commit is contained in:
Robert Resch 2025-05-09 13:50:38 +02:00 committed by GitHub
parent a93bf3c150
commit 1f84c5e1f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 7 additions and 402 deletions

View File

@ -85,7 +85,6 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
@ -93,10 +92,8 @@ from .webrtc import (
WebRTCError,
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_legacy_provider,
async_get_supported_provider,
async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@ -476,7 +473,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
@ -646,14 +642,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
return
if self._legacy_webrtc_provider and (
answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer(
self, offer_sdp
)
):
send_message(WebRTCAnswer(answer))
else:
raise HomeAssistantError("Camera does not support WebRTC")
raise HomeAssistantError("Camera does not support WebRTC")
def camera_image(
self, width: int | None = None, height: int | None = None
@ -772,9 +761,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
providers or inputs to the state attributes change.
"""
old_provider = self._webrtc_provider
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None
new_legacy_provider = None
# Skip all providers if the camera has a native WebRTC implementation
if not (
@ -785,15 +772,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async_get_supported_provider
)
if new_provider is None:
# Only add the legacy provider if the new provider is not available
new_legacy_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_legacy_provider
)
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
if old_provider != new_provider:
self._webrtc_provider = new_provider
self._legacy_webrtc_provider = new_legacy_provider
self._invalidate_camera_capabilities_cache()
if write_state:
self.async_write_ha_state()
@ -828,10 +808,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = (
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
config.get_candidates_upfront = self._supports_native_sync_webrtc
return config
@ -867,7 +844,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else:
frontend_stream_types.add(StreamType.HLS)
if self._webrtc_provider or self._legacy_webrtc_provider:
if self._webrtc_provider:
frontend_stream_types.add(StreamType.WEB_RTC)
return CameraCapabilities(frontend_stream_types)

View File

@ -46,10 +46,6 @@
}
}
}
},
"legacy_webrtc_provider": {
"title": "Detected use of legacy WebRTC provider registered by {legacy_integration}",
"description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant."
}
},
"services": {

View File

@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
from typing import TYPE_CHECKING, Any, Protocol
from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
@ -22,8 +22,7 @@ from webrtc_models import (
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.deprecation import deprecated_function
from homeassistant.helpers import config_validation as cv
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid
@ -39,9 +38,6 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
"camera_webrtc_legacy_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
@ -163,18 +159,6 @@ class CameraWebRTCProvider(ABC):
return ## This is an optional method so we need a default here.
class CameraWebRTCLegacyProvider(Protocol):
"""WebRTC provider."""
async def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
@ -204,8 +188,6 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers."""
_async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT]
await asyncio.gather(
*(camera.async_refresh_providers() for camera in component.entities)
@ -380,21 +362,6 @@ async def async_get_supported_provider(
return None
async def async_get_supported_legacy_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCLegacyProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return None
for provider in providers.values():
if await provider.async_is_supported(stream_source):
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
@ -411,94 +378,3 @@ def async_register_ice_servers(
servers.append(get_ice_server_fn)
return remove
# The following code is legacy code that was introduced with rtsp_to_webrtc and will be deprecated/removed in the future.
# Left it so custom integrations can still use it.
_RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}
# An RtspToWebRtcProvider accepts these inputs:
# stream_source: The RTSP url
# offer_sdp: The WebRTC SDP offer
# stream_id: A unique id for the stream, used to update an existing source
# The output is the SDP answer, or None if the source or offer is not eligible.
# The Callable may throw HomeAssistantError on failure.
type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]]
class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider):
def __init__(self, fn: RtspToWebRtcProviderType) -> None:
"""Initialize the RTSP to WebRTC provider."""
self._fn = fn
async def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
return any(stream_source.startswith(prefix) for prefix in _RTSP_PREFIXES)
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
if not (stream_source := await camera.stream_source()):
return None
return await self._fn(stream_source, offer_sdp, camera.entity_id)
@deprecated_function("async_register_webrtc_provider", breaks_in_ha_version="2025.6")
def async_register_rtsp_to_web_rtc_provider(
hass: HomeAssistant,
domain: str,
provider: RtspToWebRtcProviderType,
) -> Callable[[], None]:
"""Register an RTSP to WebRTC provider.
The first provider to satisfy the offer will be used.
"""
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {})
if domain in legacy_providers:
raise ValueError("Provider already registered")
provider_instance = _CameraRtspToWebRTCProvider(provider)
@callback
def remove_provider() -> None:
legacy_providers.pop(domain)
hass.async_create_task(_async_refresh_providers(hass))
legacy_providers[domain] = provider_instance
hass.async_create_task(_async_refresh_providers(hass))
return remove_provider
@callback
def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None:
"""Check if a legacy provider is registered together with the builtin provider."""
builtin_provider_domain = "go2rtc"
if (
(legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS))
and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS))
and any(provider.domain == builtin_provider_domain for provider in providers)
):
for domain in legacy_providers:
ir.async_create_issue(
hass,
DOMAIN,
f"legacy_webrtc_provider_{domain}",
is_fixable=False,
is_persistent=False,
issue_domain=domain,
learn_more_url="https://www.home-assistant.io/integrations/go2rtc/",
severity=ir.IssueSeverity.WARNING,
translation_key="legacy_webrtc_provider",
translation_placeholders={
"legacy_integration": domain,
"builtin_integration": builtin_provider_domain,
},
)

View File

@ -1,6 +1,6 @@
"""Test camera WebRTC."""
from collections.abc import AsyncGenerator, Generator
from collections.abc import AsyncGenerator
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
@ -20,9 +20,7 @@ from homeassistant.components.camera import (
WebRTCError,
WebRTCMessage,
WebRTCSendMessage,
async_get_supported_legacy_provider,
async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider,
async_register_webrtc_provider,
get_camera_from_entity_id,
)
@ -31,7 +29,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
@ -427,21 +424,6 @@ async def provide_webrtc_answer(stream_source: str, offer: str, stream_id: str)
return WEBRTC_ANSWER
@pytest.fixture(name="mock_rtsp_to_webrtc")
def mock_rtsp_to_webrtc_fixture(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> Generator[Mock]:
"""Fixture that registers a mock rtsp to webrtc provider."""
mock_provider = Mock(side_effect=provide_webrtc_answer)
unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider)
assert (
"async_register_rtsp_to_web_rtc_provider is a deprecated function which will"
" be removed in HA Core 2025.6. Use async_register_webrtc_provider instead"
) in caplog.text
yield mock_provider
unsub()
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_websocket_webrtc_offer(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
@ -804,45 +786,6 @@ async def test_websocket_webrtc_offer_invalid_stream_type(
}
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_webrtc_offer(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_rtsp_to_webrtc: Mock,
) -> None:
"""Test creating a webrtc offer from an rstp provider."""
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.demo_camera",
"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": "answer",
"answer": WEBRTC_ANSWER,
}
assert mock_rtsp_to_webrtc.called
@pytest.fixture(name="mock_hls_stream_source")
async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]:
"""Fixture to create an HLS stream source."""
@ -853,117 +796,6 @@ async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]:
yield mock_hls_stream_source
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_webrtc_provider_unregistered(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test creating a webrtc offer from an rstp provider."""
mock_provider = Mock(side_effect=provide_webrtc_answer)
unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider)
client = await hass_ws_client(hass)
# Registered provider can handle the WebRTC offer
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.demo_camera",
"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": "answer",
"answer": WEBRTC_ANSWER,
}
assert mock_provider.called
mock_provider.reset_mock()
# Unregister provider, then verify the WebRTC offer cannot be handled
unsub()
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response.get("type") == TYPE_RESULT
assert not response["success"]
assert response["error"] == {
"code": "webrtc_offer_failed",
"message": "Camera does not support WebRTC, frontend_stream_types={<StreamType.HLS: 'hls'>}",
}
assert not mock_provider.called
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_webrtc_offer_not_accepted(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test a provider that can't satisfy the rtsp to webrtc offer."""
async def provide_none(
stream_source: str, offer: str, stream_id: str
) -> str | None:
"""Simulate a provider that can't accept the offer."""
return None
mock_provider = Mock(side_effect=provide_none)
unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider)
client = await hass_ws_client(hass)
# Registered provider can handle the WebRTC offer
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.demo_camera",
"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": "Camera does not support WebRTC",
}
assert mock_provider.called
unsub()
@pytest.mark.parametrize(
("frontend_candidate", "expected_candidate"),
[
@ -1224,79 +1056,3 @@ async def test_webrtc_provider_optional_interface(hass: HomeAssistant) -> None:
"session_id", RTCIceCandidateInit("candidate")
)
provider.async_close_session("session_id")
@pytest.mark.usefixtures("mock_camera")
async def test_repair_issue_legacy_provider(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue created for legacy provider."""
# Ensure no issue if no provider is registered
assert not issue_registry.async_get_issue(
"camera", "legacy_webrtc_provider_mock_domain"
)
# Register a legacy provider
legacy_provider = Mock(side_effect=provide_webrtc_answer)
unsub_legacy_provider = async_register_rtsp_to_web_rtc_provider(
hass, "mock_domain", legacy_provider
)
await hass.async_block_till_done()
# Ensure no issue if only legacy provider is registered
assert not issue_registry.async_get_issue(
"camera", "legacy_webrtc_provider_mock_domain"
)
provider = Go2RTCProvider()
unsub_go2rtc_provider = async_register_webrtc_provider(hass, provider)
await hass.async_block_till_done()
# Ensure issue when legacy and builtin provider are registered
issue = issue_registry.async_get_issue(
"camera", "legacy_webrtc_provider_mock_domain"
)
assert issue
assert issue.is_fixable is False
assert issue.is_persistent is False
assert issue.issue_domain == "mock_domain"
assert issue.learn_more_url == "https://www.home-assistant.io/integrations/go2rtc/"
assert issue.severity == ir.IssueSeverity.WARNING
assert issue.issue_id == "legacy_webrtc_provider_mock_domain"
assert issue.translation_key == "legacy_webrtc_provider"
assert issue.translation_placeholders == {
"legacy_integration": "mock_domain",
"builtin_integration": "go2rtc",
}
unsub_legacy_provider()
unsub_go2rtc_provider()
@pytest.mark.usefixtures("mock_camera", "register_test_provider", "mock_rtsp_to_webrtc")
async def test_no_repair_issue_without_new_provider(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue not created if no go2rtc provider exists."""
assert not issue_registry.async_get_issue(
"camera", "legacy_webrtc_provider_mock_domain"
)
@pytest.mark.usefixtures("mock_camera", "mock_rtsp_to_webrtc")
async def test_registering_same_legacy_provider(
hass: HomeAssistant,
) -> None:
"""Test registering the same legacy provider twice."""
legacy_provider = Mock(side_effect=provide_webrtc_answer)
with pytest.raises(ValueError, match="Provider already registered"):
async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", legacy_provider)
@pytest.mark.usefixtures("mock_hls_stream_source", "mock_camera", "mock_rtsp_to_webrtc")
async def test_get_not_supported_legacy_provider(hass: HomeAssistant) -> None:
"""Test getting a not supported legacy provider."""
camera = get_camera_from_entity_id(hass, "camera.demo_camera")
assert await async_get_supported_legacy_provider(hass, camera) is None