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
49 changes: 45 additions & 4 deletions custom_components/frigate/camera.py
Original file line number Diff line number Diff line change
@@ -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
@@ -189,6 +195,8 @@ def __init__(
)

if self._attr_is_streaming:
self._attr_frontend_stream_type = StreamType.WEB_RTC

streaming_template = config_entry.options.get(
CONF_RTSP_URL_TEMPLATE, ""
).strip()
@@ -299,6 +307,21 @@ async def async_enable_motion_detection(self) -> None:
False,
)

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

async def async_disable_motion_detection(self) -> None:
"""Disable motion detection for this camera."""
await async_publish(
@@ -348,6 +371,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]
@@ -358,6 +382,8 @@ def __init__(
self._attr_is_streaming = True
self._attr_is_recording = False

self._attr_frontend_stream_type = StreamType.WEB_RTC

streaming_template = config_entry.options.get(
CONF_RTSP_URL_TEMPLATE, ""
).strip()
@@ -368,10 +394,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:
@@ -409,7 +435,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 {})
)

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

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
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ aiohttp
aiohttp_cors
attr
janus
homeassistant==2024.10.4
homeassistant==2024.11.3
paho-mqtt
python-dateutil
yarl
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -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
123 changes: 122 additions & 1 deletion tests/test_camera.py
Original file line number Diff line number Diff line change
@@ -34,9 +34,11 @@
DOMAIN as CAMERA_DOMAIN,
SERVICE_DISABLE_MOTION,
SERVICE_ENABLE_MOTION,
StreamType,
async_get_image,
async_get_stream_source,
)
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -62,6 +64,7 @@
async def test_frigate_camera_setup(
hass: HomeAssistant,
aioclient_mock: Any,
hass_ws_client: Any,
) -> None:
"""Set up a camera."""

@@ -71,6 +74,7 @@ async def test_frigate_camera_setup(
assert entity_state
assert entity_state.state == "streaming"
assert entity_state.attributes["supported_features"] == 2
assert entity_state.attributes["frontend_stream_type"] == StreamType.WEB_RTC

source = await async_get_stream_source(hass, TEST_CAMERA_FRONT_DOOR_ENTITY_ID)
assert source
@@ -85,8 +89,63 @@ async def test_frigate_camera_setup(
assert image
assert image.content == b"data-277"

client = await hass_ws_client(hass)

async def test_frigate_camera_setup_birdseye(hass: HomeAssistant) -> None:
aioclient_mock.post(
"http://example.com/api/go2rtc/webrtc?src=front_door",
json={"type": "answer", "sdp": "return_sdp"},
)

await client.send_json(
{
"id": 5,
"type": "camera/webrtc/offer",
"entity_id": TEST_CAMERA_FRONT_DOOR_ENTITY_ID,
"offer": "send_sdp",
}
)

response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == TYPE_RESULT
assert response["success"]

# Session id
response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == "event"
assert response["event"]["type"] == "session"

# Answer
response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == "event"
assert response["event"] == {
"type": "answer",
"answer": "return_sdp",
}

await client.send_json(
{
"id": 6,
"type": "camera/webrtc/candidate",
"entity_id": TEST_CAMERA_FRONT_DOOR_ENTITY_ID,
"session_id": "session_id",
"candidate": "candidate",
}
)

response = await client.receive_json()
assert response["id"] == 6
assert response["type"] == TYPE_RESULT
assert response["success"]


async def test_frigate_camera_setup_birdseye(
hass: HomeAssistant,
aioclient_mock: Any,
hass_ws_client: Any,
) -> None:
"""Set up birdseye camera."""

config: dict[str, Any] = copy.deepcopy(TEST_CONFIG)
@@ -98,11 +157,73 @@ async def test_frigate_camera_setup_birdseye(hass: HomeAssistant) -> None:
entity_state = hass.states.get(TEST_CAMERA_BIRDSEYE_ENTITY_ID)
assert entity_state
assert entity_state.state == "streaming"
assert entity_state.attributes["supported_features"] == 2
assert entity_state.attributes["frontend_stream_type"] == StreamType.WEB_RTC

source = await async_get_stream_source(hass, TEST_CAMERA_BIRDSEYE_ENTITY_ID)
assert source
assert source == "rtsp://example.com:8554/birdseye"

aioclient_mock.get(
"http://example.com/api/birdseye/latest.jpg?h=299",
content=b"data-299",
)

image = await async_get_image(hass, TEST_CAMERA_BIRDSEYE_ENTITY_ID, height=299)
assert image
assert image.content == b"data-299"

client = await hass_ws_client(hass)

aioclient_mock.post(
"http://example.com/api/go2rtc/webrtc?src=birdseye",
json={"type": "answer", "sdp": "return_sdp"},
)

await client.send_json(
{
"id": 5,
"type": "camera/webrtc/offer",
"entity_id": TEST_CAMERA_BIRDSEYE_ENTITY_ID,
"offer": "send_sdp",
}
)

response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == TYPE_RESULT
assert response["success"]

# Session id
response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == "event"
assert response["event"]["type"] == "session"

# Answer
response = await client.receive_json()
assert response["id"] == 5
assert response["type"] == "event"
assert response["event"] == {
"type": "answer",
"answer": "return_sdp",
}

await client.send_json(
{
"id": 6,
"type": "camera/webrtc/candidate",
"entity_id": TEST_CAMERA_BIRDSEYE_ENTITY_ID,
"session_id": "session_id",
"candidate": "candidate",
}
)

response = await client.receive_json()
assert response["id"] == 6
assert response["type"] == TYPE_RESULT
assert response["success"]


async def test_frigate_extra_attributes(hass: HomeAssistant) -> None:
"""Test that frigate extra attributes are correct."""