Add rtsptowebrtc integration (#59660)

* Add initial version of the webrtc integration

Add the webrtc integration. This integration proxies the
signal 'offer' from the client to a RTSPtoWebRTCP server that
returns an 'answer'.

The RTSPtoWebRTC server is a go binary based on pion, and this is
what is currently used by the WebRTC custom_component:
https://github.com/AlexxIT/WebRTC
https://github.com/deepch/RTSPtoWebRTC

* Readability improvements for webrtc

* Reach 100% test coverage

* Use rtsp-to-webrtc client library package

* Rename webrtc to rtstptowebrtc

This is to reflect naming as one type of approach to webrtc since other webrtc integrations would look very different.

* Remove internal quality scale

* Bump rtsptowebrtc to support heartbeats

* Shorten server url variable and remove const.py

* Add config flow validation for RTSPtoWebRTC server

* Add RTSPtoWebRTC server health checks

* Accept translation suggestion

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update rtsptowebrtc to use new camera registry API

Update rtsptowebrtc to use new API added in #62962

* Remove unused variable

* Fix lint and typing errors for python 3.8

* Rename to rtsp_to_webrtc to follow standards

* Use async_on_unload for unsubscribing camera webrtc provider

* Remove unnecessary translations in config flow

* Remove unnecessary configuration setup

* Cleanup test setup and typing

* Patch integration setup to avoid starting the whole integration

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Allen Porter 2022-01-01 12:36:31 -08:00 committed by GitHub
parent 8af545a4e3
commit c7b991f56b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 542 additions and 0 deletions

View file

@ -0,0 +1,83 @@
"""RTSPtoWebRTC integration with an external RTSPToWebRTC Server.
WebRTC uses a direct communication from the client (e.g. a web browser) to a
camera device. Home Assistant acts as the signal path for initial set up,
passing through the client offer and returning a camera answer, then the client
and camera communicate directly.
However, not all cameras natively support WebRTC. This integration is a shim
for camera devices that support RTSP streams only, relying on an external
server RTSPToWebRTC that is a proxy. Home Assistant does not participate in
the offer/answer SDP protocol, other than as a signal path pass through.
Other integrations may use this integration with these steps:
- Check if this integration is loaded
- Call is_suported_stream_source for compatibility
- Call async_offer_for_stream_source to get back an answer for a client offer
"""
from __future__ import annotations
import logging
import async_timeout
from rtsp_to_webrtc.client import Client
from rtsp_to_webrtc.exceptions import ClientError, ResponseError
from homeassistant.components import camera
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__)
DOMAIN = "rtsp_to_webrtc"
DATA_SERVER_URL = "server_url"
DATA_UNSUB = "unsub"
TIMEOUT = 10
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up RTSPtoWebRTC from a config entry."""
hass.data.setdefault(DOMAIN, {})
client = Client(async_get_clientsession(hass), entry.data[DATA_SERVER_URL])
try:
async with async_timeout.timeout(TIMEOUT):
await client.heartbeat()
except ResponseError as err:
raise ConfigEntryNotReady from err
except (TimeoutError, ClientError) as err:
raise ConfigEntryNotReady from err
async def async_offer_for_stream_source(
stream_source: str,
offer_sdp: 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.
"""
try:
async with async_timeout.timeout(TIMEOUT):
return await client.offer(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
)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return True

View file

@ -0,0 +1,65 @@
"""Config flow for RTSPtoWebRTC."""
from __future__ import annotations
import logging
from typing import Any
from urllib.parse import urlparse
import rtsp_to_webrtc
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import DATA_SERVER_URL, DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({vol.Required(DATA_SERVER_URL): str})
class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""RTSPtoWebRTC config flow."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Configure the RTSPtoWebRTC server url."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
url = user_input[DATA_SERVER_URL]
result = urlparse(url)
if not all([result.scheme, result.netloc]):
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors={DATA_SERVER_URL: "invalid_url"},
)
errors = {}
client = rtsp_to_webrtc.client.Client(async_get_clientsession(self.hass), url)
try:
await client.heartbeat()
except rtsp_to_webrtc.exceptions.ResponseError as err:
_LOGGER.error("RTSPtoWebRTC server failure: %s", str(err))
errors["base"] = "server_failure"
except rtsp_to_webrtc.exceptions.ClientError as err:
_LOGGER.error("RTSPtoWebRTC communication failure: %s", str(err))
errors["base"] = "server_unreachable"
if errors:
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
)
await self.async_set_unique_id(DOMAIN)
return self.async_create_entry(
title=url,
data={DATA_SERVER_URL: url},
)

View file

@ -0,0 +1,12 @@
{
"domain": "rtsp_to_webrtc",
"name": "RTSPtoWebRTC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc",
"requirements": ["rtsp-to-webrtc==0.2.7"],
"dependencies": ["camera"],
"codeowners": [
"@allenporter"
],
"iot_class": "local_push"
}

View file

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"title": "Configure RTSPtoWebRTC",
"description": "The RTSPtoWebRTC integration requires a server to translate RTSP streams into WebRTC. Enter the URL to the RTSPtoWebRTC server.",
"data": {
"server_url": "RTSPtoWebRTC server URL e.g. https://example.com"
}
}
},
"error": {
"invalid_url": "Must be a valid RTSPtoWebRTC server URL e.g. https://example.com",
"server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.",
"server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information."
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}

View file

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"no_devices_found": "No devices found on the network",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"invalid_url": "Must be a valid RTSPtoWebRTC server URL e.g. https://example.com",
"server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.",
"server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information."
},
"step": {
"confirm": {
"description": "Do you want to start set up?"
},
"user": {
"data": {
"server_url": "RTSPtoWebRTC server URL e.g. https://example.com"
},
"description": "The RTSPtoWebRTC integration requires a server to translate RTSP streams into WebRTC. Enter the URL to the RTSPtoWebRTC server.\n",
"title": "Configure RTSPtoWebRTC"
}
}
}
}