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 <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2021-10-13 03:28:52 -07:00 committed by GitHub
parent 8d7744a74f
commit 1fa6329c2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 104 additions and 1 deletions

View file

@ -12,6 +12,7 @@ from google_nest_sdm.camera_traits import (
CameraLiveStreamTrait, CameraLiveStreamTrait,
EventImageGenerator, EventImageGenerator,
RtspStream, RtspStream,
StreamingProtocol,
) )
from google_nest_sdm.device import Device from google_nest_sdm.device import Device
from google_nest_sdm.event import ImageEventBase 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 haffmpeg.tools import IMAGE_JPEG
from homeassistant.components.camera import SUPPORT_STREAM, Camera 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.components.ffmpeg import async_get_image
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -114,9 +116,21 @@ class NestCamera(Camera):
supported_features |= SUPPORT_STREAM supported_features |= SUPPORT_STREAM
return supported_features 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: async def stream_source(self) -> str | None:
"""Return the source of the stream.""" """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 return None
trait = self._device.traits[CameraLiveStreamTrait.NAME] trait = self._device.traits[CameraLiveStreamTrait.NAME]
if not self._stream: if not self._stream:
@ -252,3 +266,9 @@ class NestCamera(Camera):
self._event_id = None self._event_id = None
self._event_image_bytes = None self._event_image_bytes = None
self._event_image_cleanup_unsub = 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

View file

@ -15,6 +15,7 @@ import pytest
from homeassistant.components import camera from homeassistant.components import camera
from homeassistant.components.camera import STATE_IDLE from homeassistant.components.camera import STATE_IDLE
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component 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) image = await async_get_image(hass)
assert image.content == b"updated image bytes" 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"