Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove legacy webrtc provider APIs #128790

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion homeassistant/components/camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
RTCIceServer,
WebRTCClientConfiguration,
async_get_supported_providers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
register_ice_server,
ws_get_client_config,
)
Expand Down
48 changes: 1 addition & 47 deletions homeassistant/components/camera/webrtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from collections.abc import Callable, Coroutine
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Protocol

Expand Down Expand Up @@ -203,49 +203,3 @@ def remove() -> None:

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(CameraWebRTCProvider):
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)


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.
"""
provider_instance = _CameraRtspToWebRTCProvider(provider)
return async_register_webrtc_provider(hass, provider_instance)
60 changes: 37 additions & 23 deletions homeassistant/components/rtsp_to_webrtc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,22 @@
from rtsp_to_webrtc.exceptions import ClientError, ResponseError
from rtsp_to_webrtc.interface import WebRTCClientInterface

from homeassistant.components import camera
from homeassistant.components.camera.webrtc import RTCIceServer, register_ice_server
from homeassistant.components.camera import Camera
from homeassistant.components.camera.webrtc import (
CameraWebRTCProvider,
RTCIceServer,
async_register_webrtc_provider,
register_ice_server,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession

_LOGGER = logging.getLogger(__name__)

_RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}

DOMAIN = "rtsp_to_webrtc"
DATA_SERVER_URL = "server_url"
DATA_UNSUB = "unsub"
Expand Down Expand Up @@ -64,34 +71,41 @@

entry.async_on_unload(register_ice_server(hass, get_server))

async def async_offer_for_stream_source(
stream_source: str,
offer_sdp: str,
stream_id: str,
) -> str:
"""Handle the signal path for a WebRTC stream.

This signal path is used to route the offer created by the client to the
proxy server that translates a stream to WebRTC. The communication for
the stream itself happens directly between the client and proxy.
"""
provider = WebRTCProvider(client)
entry.async_on_unload(async_register_webrtc_provider(hass, provider))
entry.async_on_unload(entry.add_update_listener(async_reload_entry))

return True


class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider."""

def __init__(self, client: WebRTCClientInterface) -> None:
"""Initialize the WebRTC provider."""
self._client = client

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

Check warning on line 97 in homeassistant/components/rtsp_to_webrtc/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/rtsp_to_webrtc/__init__.py#L97

Added line #L97 was not covered by tests
stream_id = camera.entity_id
try:
async with asyncio.timeout(TIMEOUT):
return await client.offer_stream_id(stream_id, offer_sdp, stream_source)
return await self._client.offer_stream_id(
stream_id, offer_sdp, stream_source
)
except TimeoutError as err:
raise HomeAssistantError("Timeout talking to RTSPtoWebRTC server") from err
except ClientError as err:
raise HomeAssistantError(str(err)) from err

entry.async_on_unload(
camera.async_register_rtsp_to_web_rtc_provider(
hass, DOMAIN, async_offer_for_stream_source
)
)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
Expand Down
156 changes: 0 additions & 156 deletions tests/components/camera/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,6 @@ async def mock_hls_stream_source_fixture() -> Generator[AsyncMock]:
yield mock_hls_stream_source


async def provide_web_rtc_answer(stream_source: str, offer: str, stream_id: str) -> str:
"""Simulate an rtsp to webrtc provider."""
assert stream_source == STREAM_SOURCE
assert offer == WEBRTC_OFFER
return WEBRTC_ANSWER


@pytest.fixture(name="mock_rtsp_to_web_rtc")
def mock_rtsp_to_web_rtc_fixture(hass: HomeAssistant) -> Generator[Mock]:
"""Fixture that registers a mock rtsp to web_rtc provider."""
mock_provider = Mock(side_effect=provide_web_rtc_answer)
unsub = camera.async_register_rtsp_to_web_rtc_provider(
hass, "mock_domain", mock_provider
)
yield mock_provider
unsub()


@pytest.mark.usefixtures("image_mock_url")
async def test_get_image_from_camera(hass: HomeAssistant) -> None:
"""Grab an image from camera entity."""
Expand Down Expand Up @@ -869,144 +851,6 @@ async def test_stream_unavailable(
assert demo_camera.state == camera.CameraState.STREAMING


@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_web_rtc_offer(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_rtsp_to_web_rtc: Mock,
) -> None:
"""Test creating a web_rtc offer from an rstp provider."""
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 9,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()

assert response.get("id") == 9
assert response.get("type") == TYPE_RESULT
assert response.get("success")
assert "result" in response
assert response["result"] == {"answer": WEBRTC_ANSWER}

assert mock_rtsp_to_web_rtc.called


@pytest.mark.usefixtures(
"mock_camera",
"mock_hls_stream_source", # Not an RTSP stream source
"mock_rtsp_to_web_rtc",
)
async def test_unsupported_rtsp_to_web_rtc_stream_type(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test rtsp-to-webrtc is not registered for non-RTSP streams."""
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 10,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()

assert response.get("id") == 10
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]


@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_web_rtc_provider_unregistered(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test creating a web_rtc offer from an rstp provider."""
mock_provider = Mock(side_effect=provide_web_rtc_answer)
unsub = camera.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(
{
"id": 11,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["id"] == 11
assert response["type"] == TYPE_RESULT
assert response["success"]
assert response["result"]["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(
{
"id": 12,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response.get("id") == 12
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]

assert not mock_provider.called


@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_rtsp_to_web_rtc_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) -> str:
"""Simulate a provider that can't accept the offer."""
return None

mock_provider = Mock(side_effect=provide_none)
unsub = camera.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(
{
"id": 11,
"type": "camera/web_rtc_offer",
"entity_id": "camera.demo_camera",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["id"] == 11
assert response.get("type") == TYPE_RESULT
assert "success" in response
assert not response["success"]

assert mock_provider.called

unsub()


@pytest.mark.usefixtures("mock_camera")
async def test_use_stream_for_stills(
hass: HomeAssistant, hass_client: ClientSessionGenerator
Expand Down
2 changes: 2 additions & 0 deletions tests/components/camera/test_webrtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ async def async_handle_web_rtc_offer(
await hass.async_block_till_done()

assert camera.frontend_stream_type is StreamType.WEB_RTC
answer = await camera.async_handle_web_rtc_offer("offer")
assert answer == "answer"

# Mark stream as unsupported
stream_supported = False
Expand Down
Loading