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

Add back option to use Frigate-native WebRTC support #784

Merged
merged 13 commits into from
Nov 27, 2024
2 changes: 0 additions & 2 deletions custom_components/frigate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
ATTR_WS_EVENT_PROXY,
ATTRIBUTE_LABELS,
CONF_CAMERA_STATIC_IMAGE_HEIGHT,
CONF_ENABLE_WEBRTC,
CONF_RTMP_URL_TEMPLATE,
DOMAIN,
FRIGATE_RELEASES_URL,
Expand Down Expand Up @@ -268,7 +267,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Remove old options.
OLD_OPTIONS = [
CONF_CAMERA_STATIC_IMAGE_HEIGHT,
CONF_ENABLE_WEBRTC,
CONF_RTMP_URL_TEMPLATE,
]
if any(option in entry.options for option in OLD_OPTIONS):
Expand Down
78 changes: 71 additions & 7 deletions custom_components/frigate/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
from yarl import URL

from custom_components.frigate.api import FrigateApiClient
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.camera import (
Camera,
CameraEntityFeature,
StreamType,
WebRTCAnswer,
WebRTCSendMessage,
)
from homeassistant.components.mqtt import async_publish
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL
Expand Down Expand Up @@ -44,6 +50,7 @@
ATTR_PTZ_ACTION,
ATTR_PTZ_ARGUMENT,
ATTR_START_TIME,
CONF_ENABLE_WEBRTC,
CONF_RTSP_URL_TEMPLATE,
DEVICE_CLASS_CAMERA,
DOMAIN,
Expand All @@ -67,9 +74,13 @@ async def async_setup_entry(
client_id = get_frigate_instance_id_for_config_entry(hass, entry)
coordinator = hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATOR]

frigate_webrtc = entry.options.get(CONF_ENABLE_WEBRTC, False)
camera_type = FrigateCameraWebRTC if frigate_webrtc else FrigateCamera
birdseye_type = BirdseyeCameraWebRTC if frigate_webrtc else BirdseyeCamera

async_add_entities(
[
FrigateCamera(
camera_type(
entry,
cam_name,
frigate_client,
Expand All @@ -81,7 +92,7 @@ async def async_setup_entry(
for cam_name, camera_config in frigate_config["cameras"].items()
]
+ (
[BirdseyeCamera(entry, frigate_client)]
[birdseye_type(entry, frigate_client)]
if frigate_config.get("birdseye", {}).get("restream", False)
else []
)
Expand Down Expand Up @@ -336,7 +347,7 @@ async def ptz(self, action: str, argument: str) -> None:


class BirdseyeCamera(FrigateEntity, Camera):
"""Representation of the Frigate birdseye camera."""
"""A Frigate birdseye camera."""

# sets the entity name to same as device name ex: camera.front_doorbell
_attr_name = None
Expand All @@ -348,6 +359,7 @@ def __init__(
) -> None:
"""Initialize the birdseye camera."""
self._client = frigate_client
self._cam_name = "birdseye"
FrigateEntity.__init__(self, config_entry)
Camera.__init__(self)
self._url = config_entry.data[CONF_URL]
Expand All @@ -368,10 +380,10 @@ def __init__(
# template instead. This means templates cannot access HomeAssistant
# state, but rather only the camera config.
self._stream_source = Template(streaming_template).render(
{"name": "birdseye"}
{"name": self._cam_name}
)
else:
self._stream_source = f"rtsp://{URL(self._url).host}:8554/birdseye"
self._stream_source = f"rtsp://{URL(self._url).host}:8554/{self._cam_name}"

@property
def unique_id(self) -> str:
Expand Down Expand Up @@ -409,7 +421,7 @@ async def async_camera_image(

image_url = str(
URL(self._url)
/ "api/birdseye/latest.jpg"
/ f"api/{self._cam_name}/latest.jpg"
% ({"h": height} if height is not None and height > 0 else {})
)

Expand All @@ -420,3 +432,55 @@ async def async_camera_image(
async def stream_source(self) -> str | None:
"""Return the source of the stream."""
return self._stream_source


class FrigateCameraWebRTC(FrigateCamera):
"""A Frigate camera with WebRTC support."""

# TODO: this property can be removed after this fix is released:
# https://github.com/home-assistant/core/pull/130932/files#diff-75655c0eec1c3e736cad1bdb5627100a4595ece9accc391b5c85343bb998594fR598-R603
@property
def frontend_stream_type(self) -> StreamType | None:
"""Return the type of stream supported by this camera."""
return StreamType.WEB_RTC

async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
"""Handle the WebRTC offer and return an answer."""
websession = async_get_clientsession(self.hass)
url = f"{self._url}/api/go2rtc/webrtc?src={self._cam_name}"
payload = {"type": "offer", "sdp": offer_sdp}
async with websession.post(url, json=payload) as resp:
answer = await resp.json()
send_message(WebRTCAnswer(answer["sdp"]))

async def async_on_webrtc_candidate(self, session_id: str, candidate: Any) -> None:
"""Ignore WebRTC candidates for Frigate cameras."""
return


class BirdseyeCameraWebRTC(BirdseyeCamera):
"""A Frigate birdseye camera with WebRTC support."""

# TODO: this property can be removed after this fix is released:
# https://github.com/home-assistant/core/pull/130932/files#diff-75655c0eec1c3e736cad1bdb5627100a4595ece9accc391b5c85343bb998594fR598-R603
@property
def frontend_stream_type(self) -> StreamType | None:
"""Return the type of stream supported by this camera."""
return StreamType.WEB_RTC

async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
"""Handle the WebRTC offer and return an answer."""
websession = async_get_clientsession(self.hass)
url = f"{self._url}/api/go2rtc/webrtc?src={self._cam_name}"
payload = {"type": "offer", "sdp": offer_sdp}
async with websession.post(url, json=payload) as resp:
answer = await resp.json()
send_message(WebRTCAnswer(answer["sdp"]))

async def async_on_webrtc_candidate(self, session_id: str, candidate: Any) -> None:
"""Ignore WebRTC candidates for Frigate cameras."""
return
9 changes: 9 additions & 0 deletions custom_components/frigate/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from .api import FrigateApiClient, FrigateApiClientError
from .const import (
CONF_ENABLE_WEBRTC,
CONF_MEDIA_BROWSER_ENABLE,
CONF_NOTIFICATION_PROXY_ENABLE,
CONF_NOTIFICATION_PROXY_EXPIRE_AFTER_SECONDS,
Expand Down Expand Up @@ -122,6 +123,14 @@ async def async_step_init(
return self.async_abort(reason="only_advanced_options")

schema: dict[Any, Any] = {
# Whether to enable Frigate-native WebRTC for camera streaming
vol.Optional(
CONF_ENABLE_WEBRTC,
default=self._config_entry.options.get(
CONF_ENABLE_WEBRTC,
False,
),
): bool,
# The input URL is not validated as being a URL to allow for the
# possibility the template input won't be a valid URL until after
# it's rendered.
Expand Down
3 changes: 2 additions & 1 deletion custom_components/frigate/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"step": {
"init": {
"data": {
"enable_webrtc": "Use Frigate-native WebRTC support",
"rtsp_url_template": "RTSP URL template (see documentation)",
"media_browser_enable": "Enable the media browser",
"notification_proxy_enable": "Enable the unauthenticated notification event proxy",
Expand All @@ -31,4 +32,4 @@
"only_advanced_options": "Advanced mode is disabled and there are only advanced options"
}
}
}
}
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ aiohttp
aiohttp_cors
attr
janus
homeassistant==2024.10.4
homeassistant==2024.11.3
paho-mqtt
python-dateutil
yarl
Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ flake8
mypy
pre-commit
pytest
pytest-homeassistant-custom-component==0.13.175
pytest-homeassistant-custom-component==0.13.184
pylint-pytest
pylint
pytest-aiohttp
Expand Down
Loading
Loading