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 .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import ( from .webrtc import (
DATA_ICE_SERVERS, DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider, CameraWebRTCProvider,
WebRTCAnswer, WebRTCAnswer,
WebRTCCandidate, # noqa: F401 WebRTCCandidate, # noqa: F401
@ -93,10 +92,8 @@ from .webrtc import (
WebRTCError, WebRTCError,
WebRTCMessage, # noqa: F401 WebRTCMessage, # noqa: F401
WebRTCSendMessage, WebRTCSendMessage,
async_get_supported_legacy_provider,
async_get_supported_provider, async_get_supported_provider,
async_register_ice_servers, async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
async_register_webrtc_provider, # noqa: F401 async_register_webrtc_provider, # noqa: F401
async_register_ws, async_register_ws,
) )
@ -476,7 +473,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._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._supports_native_sync_webrtc = ( self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
) )
@ -646,13 +642,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
) )
return 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( def camera_image(
@ -772,9 +761,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
providers or inputs to the state attributes change. providers or inputs to the state attributes change.
""" """
old_provider = self._webrtc_provider old_provider = self._webrtc_provider
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None new_provider = None
new_legacy_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 (
@ -785,15 +772,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async_get_supported_provider async_get_supported_provider
) )
if new_provider is None: if old_provider != new_provider:
# 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:
self._webrtc_provider = new_provider self._webrtc_provider = new_provider
self._legacy_webrtc_provider = new_legacy_provider
self._invalidate_camera_capabilities_cache() self._invalidate_camera_capabilities_cache()
if write_state: if write_state:
self.async_write_ha_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.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = ( config.get_candidates_upfront = self._supports_native_sync_webrtc
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
return config return config
@ -867,7 +844,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else: else:
frontend_stream_types.add(StreamType.HLS) 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) frontend_stream_types.add(StreamType.WEB_RTC)
return CameraCapabilities(frontend_stream_types) 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": { "services": {

View File

@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps from functools import cache, partial, wraps
import logging import logging
from typing import TYPE_CHECKING, Any, Protocol from typing import TYPE_CHECKING, Any
from mashumaro import MissingField from mashumaro import MissingField
import voluptuous as vol import voluptuous as vol
@ -22,8 +22,7 @@ from webrtc_models import (
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.deprecation import deprecated_function
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid from homeassistant.util.ulid import ulid
@ -39,9 +38,6 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey( DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers" "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( DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers" "camera_webrtc_ice_servers"
) )
@ -163,18 +159,6 @@ class CameraWebRTCProvider(ABC):
return ## This is an optional method so we need a default here. 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 @callback
def async_register_webrtc_provider( def async_register_webrtc_provider(
hass: HomeAssistant, hass: HomeAssistant,
@ -204,8 +188,6 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None: async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers.""" """Check all cameras for any state changes for registered providers."""
_async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT] component = hass.data[DATA_COMPONENT]
await asyncio.gather( await asyncio.gather(
*(camera.async_refresh_providers() for camera in component.entities) *(camera.async_refresh_providers() for camera in component.entities)
@ -380,21 +362,6 @@ async def async_get_supported_provider(
return None 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 @callback
def async_register_ice_servers( def async_register_ice_servers(
hass: HomeAssistant, hass: HomeAssistant,
@ -411,94 +378,3 @@ def async_register_ice_servers(
servers.append(get_ice_server_fn) servers.append(get_ice_server_fn)
return remove 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.""" """Test camera WebRTC."""
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator
import logging import logging
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
@ -20,9 +20,7 @@ from homeassistant.components.camera import (
WebRTCError, WebRTCError,
WebRTCMessage, WebRTCMessage,
WebRTCSendMessage, WebRTCSendMessage,
async_get_supported_legacy_provider,
async_register_ice_servers, async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider,
async_register_webrtc_provider, async_register_webrtc_provider,
get_camera_from_entity_id, 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 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.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
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
@ -427,21 +424,6 @@ async def provide_webrtc_answer(stream_source: str, offer: str, stream_id: str)
return WEBRTC_ANSWER 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") @pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_websocket_webrtc_offer( async def test_websocket_webrtc_offer(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator 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") @pytest.fixture(name="mock_hls_stream_source")
async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]: async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]:
"""Fixture to create an HLS stream source.""" """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 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( @pytest.mark.parametrize(
("frontend_candidate", "expected_candidate"), ("frontend_candidate", "expected_candidate"),
[ [
@ -1224,79 +1056,3 @@ async def test_webrtc_provider_optional_interface(hass: HomeAssistant) -> None:
"session_id", RTCIceCandidateInit("candidate") "session_id", RTCIceCandidateInit("candidate")
) )
provider.async_close_session("session_id") 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