From 1fa6329c2e2c4d3f0a3a0907210aaa75521d2b06 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 13 Oct 2021 03:28:52 -0700 Subject: [PATCH] Add Nest WebRTC and support Nest Battery Camera and Nest Battery Doorbell (#57299) * Add WebSocket API for intiting a WebRTC stream See https://github.com/home-assistant/architecture/discussions/640 * Add nest support for initiating webrtc streams Add an implementation of async_handle_web_rtc_offer in nest, with test coverage. Issue #55302 * Rename offer variable to match overriden variable name * Remove unnecessary checks covered by websocket function * Update homeassistant/components/camera/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nest/camera_sdm.py | 22 +++++- tests/components/nest/camera_sdm_test.py | 83 +++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 0a917e8cbdc..9c6ac7070e4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -12,6 +12,7 @@ from google_nest_sdm.camera_traits import ( CameraLiveStreamTrait, EventImageGenerator, RtspStream, + StreamingProtocol, ) from google_nest_sdm.device import Device from google_nest_sdm.event import ImageEventBase @@ -19,6 +20,7 @@ from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -114,9 +116,21 @@ class NestCamera(Camera): supported_features |= SUPPORT_STREAM return supported_features + @property + def stream_type(self) -> str | None: + """Return the type of stream supported by this camera.""" + if CameraLiveStreamTrait.NAME not in self._device.traits: + return None + trait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.WEB_RTC in trait.supported_protocols: + return STREAM_TYPE_WEB_RTC + return STREAM_TYPE_HLS + async def stream_source(self) -> str | None: """Return the source of the stream.""" - if CameraLiveStreamTrait.NAME not in self._device.traits: + if not self.supported_features & SUPPORT_STREAM: + return None + if self.stream_type != STREAM_TYPE_HLS: return None trait = self._device.traits[CameraLiveStreamTrait.NAME] if not self._stream: @@ -252,3 +266,9 @@ class NestCamera(Camera): self._event_id = None self._event_image_bytes = None self._event_image_cleanup_unsub = None + + async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str: + """Return the source of the stream.""" + trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] + stream = await trait.generate_web_rtc_stream(offer_sdp) + return stream.answer_sdp diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 3c0c0fdb4db..df36ae762df 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -15,6 +15,7 @@ import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -603,3 +604,85 @@ async def test_multiple_event_images(hass, auth): image = await async_get_image(hass) assert image.content == b"updated image bytes" + + +async def test_camera_web_rtc(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + expiration = utcnow() + datetime.timedelta(seconds=100) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "answerSdp": "v=0\r\ns=-\r\n", + "mediaSessionId": "yP2grqz0Y1V_wgiX9KEbMWHoLd...", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ) + ] + device_traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC"], + }, + } + await async_setup_camera(hass, device_traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" + + # Nest WebRTC cameras do not support a still image + with pytest.raises(HomeAssistantError): + await async_get_image(hass) + + +async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "web_rtc_offer_failed"