Skip to content

Commit

Permalink
Create repair issue for legacy webrtc provider (#129334)
Browse files Browse the repository at this point in the history
* Add repair issue

* Add tests

* Add option to not use builtin go2rtc provider

* Add test

* Add domain to new providers

* Add learn more url

* Update placeholder

* Promote the builtin provider

* Refactor provider storage

* Move check for legacy provider conflict to refresh

* Test provider registration race

* Add test for registering the same legacy provider twice

* Test test_get_not_supported_legacy_provider

* Remove blank line between bullets

* Call it built-in

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Revert "Add option to not use builtin go2rtc provider"

This reverts commit 4e31bad.

* Revert "Add test"

This reverts commit ddf85fd.

* Update issue description

* async_close_session is optional

* Clean up after rebase

* Add required domain property to provider tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
  • Loading branch information
MartinHjelmare and joostlek authored Oct 30, 2024
1 parent b4e69ba commit 405a480
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 45 deletions.
4 changes: 4 additions & 0 deletions homeassistant/components/camera/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
}
}
}
},
"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": {
Expand Down
111 changes: 75 additions & 36 deletions homeassistant/components/camera/webrtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
Expand All @@ -15,7 +16,7 @@
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
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid

Expand All @@ -31,7 +32,7 @@
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[set[CameraWebRTCLegacyProvider]] = HassKey(
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
"camera_webrtc_legacy_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
Expand Down Expand Up @@ -113,13 +114,20 @@ def to_frontend_dict(self) -> dict[str, Any]:
return data


class CameraWebRTCProvider(Protocol):
class CameraWebRTCProvider(ABC):
"""WebRTC provider."""

@property
@abstractmethod
def domain(self) -> str:
"""Return the integration domain of the provider."""

@callback
@abstractmethod
def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""

@abstractmethod
async def async_handle_async_webrtc_offer(
self,
camera: Camera,
Expand All @@ -129,6 +137,7 @@ async def async_handle_async_webrtc_offer(
) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback."""

@abstractmethod
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
"""Handle the WebRTC candidate."""

Expand All @@ -150,10 +159,10 @@ async def async_handle_web_rtc_offer(
"""Handle the WebRTC offer and return an answer."""


def _async_register_webrtc_provider[_T](
@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
key: HassKey[set[_T]],
provider: _T,
provider: CameraWebRTCProvider,
) -> Callable[[], None]:
"""Register a WebRTC provider.
Expand All @@ -162,7 +171,7 @@ def _async_register_webrtc_provider[_T](
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")

providers = hass.data.setdefault(key, set())
providers = hass.data.setdefault(DATA_WEBRTC_PROVIDERS, set())

@callback
def remove_provider() -> None:
Expand All @@ -177,20 +186,9 @@ def remove_provider() -> None:
return remove_provider


@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
provider: CameraWebRTCProvider,
) -> Callable[[], None]:
"""Register a WebRTC provider.
The first provider to satisfy the offer will be used.
"""
return _async_register_webrtc_provider(hass, DATA_WEBRTC_PROVIDERS, 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(
Expand Down Expand Up @@ -334,11 +332,11 @@ def async_register_ws(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_candidate)


async def _async_get_supported_provider[
_T: CameraWebRTCLegacyProvider | CameraWebRTCProvider
](hass: HomeAssistant, camera: Camera, key: HassKey[set[_T]]) -> _T | None:
async def async_get_supported_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(key)
providers = hass.data.get(DATA_WEBRTC_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return None

Expand All @@ -349,20 +347,19 @@ async def _async_get_supported_provider[
return None


async def async_get_supported_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCProvider | None:
"""Return the first supported provider for the camera."""
return await _async_get_supported_provider(hass, camera, DATA_WEBRTC_PROVIDERS)
async def async_get_supported_legacy_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCLegacyProvider | None:
"""Return the first supported provider for the camera."""
return await _async_get_supported_provider(
hass, camera, DATA_WEBRTC_LEGACY_PROVIDERS
)
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
Expand Down Expand Up @@ -425,7 +422,49 @@ def async_register_rtsp_to_web_rtc_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)
return _async_register_webrtc_provider(
hass, DATA_WEBRTC_LEGACY_PROVIDERS, provider_instance
)

@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,
},
)
5 changes: 5 additions & 0 deletions homeassistant/components/go2rtc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ def __init__(self, hass: HomeAssistant, url: str) -> None:
self._rest_client = Go2RtcRestClient(self._session, url)
self._sessions: dict[str, Go2RtcWsClient] = {}

@property
def domain(self) -> str:
"""Return the integration domain of the provider."""
return DOMAIN

@callback
def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
Expand Down
5 changes: 5 additions & 0 deletions tests/components/camera/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,11 @@ async def test(expected_types: set[StreamType]) -> None:
class SomeTestProvider(CameraWebRTCProvider):
"""Test provider."""

@property
def domain(self) -> str:
"""Return domain."""
return "test"

@callback
def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""
Expand Down
Loading

0 comments on commit 405a480

Please sign in to comment.