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:
parent
8d7744a74f
commit
1fa6329c2e
2 changed files with 104 additions and 1 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue