Add a stream_id parameter to the WebRTC provider (#63625)

* Add a stream_id parameter to the WebRTC provider

Add an additional stream id parameter (effectively, the camera entity id)
to allow updating an existing stream source when the strema changes. This
is useful for stream sources that have expiring URLs, like Nest.

* Redefine the provider using a type

* Use old typing methods for type definintion to pass python3.8 pylint
This commit is contained in:
Allen Porter 2022-01-10 17:56:18 -08:00 committed by GitHub
parent 8f6e24aa1e
commit 596edc8919
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 19 additions and 12 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import collections import collections
from collections.abc import Awaitable, Callable, Iterable, Mapping from collections.abc import Iterable, Mapping
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -14,7 +14,7 @@ import inspect
import logging import logging
import os import os
from random import SystemRandom from random import SystemRandom
from typing import Final, cast, final from typing import Awaitable, Callable, Final, Optional, cast, final
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
@ -287,17 +287,23 @@ def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera:
return cast(Camera, camera) return cast(Camera, camera)
# An RtspToWebRtcProvider accepts these inputs:
# stream_source: The RTSP url
# offer_sdp: The WebRTC SDP offer
# stream_id: A unique id for the stream, used to update an existing source
# The output is the SDP answer, or None if the source or offer is not eligible.
# The Callable may throw HomeAssistantError on failure.
RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[Optional[str]]]
def async_register_rtsp_to_web_rtc_provider( def async_register_rtsp_to_web_rtc_provider(
hass: HomeAssistant, hass: HomeAssistant,
domain: str, domain: str,
provider: Callable[[str, str], Awaitable[str | None]], provider: RtspToWebRtcProviderType,
) -> Callable[[], None]: ) -> Callable[[], None]:
"""Register an RTSP to WebRTC provider. """Register an RTSP to WebRTC provider.
Integrations may register a Callable that accepts a `stream_source` and The first provider to satisfy the offer will be used.
SDP `offer` as an input, and the output is the SDP `answer`. An implementation
may return None if the source or offer is not eligible or throw HomeAssistantError
on failure. The first provider to satisfy the offer will be used.
""" """
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded") raise ValueError("Unexpected state, camera not loaded")
@ -327,9 +333,9 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None:
def _async_get_rtsp_to_web_rtc_providers( def _async_get_rtsp_to_web_rtc_providers(
hass: HomeAssistant, hass: HomeAssistant,
) -> Iterable[Callable[[str, str], Awaitable[str | None]]]: ) -> Iterable[RtspToWebRtcProviderType]:
"""Return registered RTSP to WebRTC providers.""" """Return registered RTSP to WebRTC providers."""
providers: dict[str, Callable[[str, str], Awaitable[str | None]]] = hass.data.get( providers: dict[str, RtspToWebRtcProviderType] = hass.data.get(
DATA_RTSP_TO_WEB_RTC, {} DATA_RTSP_TO_WEB_RTC, {}
) )
return providers.values() return providers.values()
@ -548,7 +554,7 @@ class Camera(Entity):
if not stream_source: if not stream_source:
return None return None
for provider in _async_get_rtsp_to_web_rtc_providers(self.hass): for provider in _async_get_rtsp_to_web_rtc_providers(self.hass):
answer_sdp = await provider(stream_source, offer_sdp) answer_sdp = await provider(stream_source, offer_sdp, self.entity_id)
if answer_sdp: if answer_sdp:
return answer_sdp return answer_sdp
raise HomeAssistantError("WebRTC offer was not accepted by any providers") raise HomeAssistantError("WebRTC offer was not accepted by any providers")

View File

@ -57,6 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_offer_for_stream_source( async def async_offer_for_stream_source(
stream_source: str, stream_source: str,
offer_sdp: str, offer_sdp: str,
stream_id: str,
) -> str: ) -> str:
"""Handle the signal path for a WebRTC stream. """Handle the signal path for a WebRTC stream.
@ -66,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
""" """
try: try:
async with async_timeout.timeout(TIMEOUT): async with async_timeout.timeout(TIMEOUT):
return await client.offer(offer_sdp, stream_source) return await client.offer_stream_id(offer_sdp, stream_source, stream_id)
except TimeoutError as err: except TimeoutError as err:
raise HomeAssistantError("Timeout talking to RTSPtoWebRTC server") from err raise HomeAssistantError("Timeout talking to RTSPtoWebRTC server") from err
except ClientError as err: except ClientError as err:

View File

@ -116,7 +116,7 @@ async def mock_hls_stream_source_fixture():
yield mock_hls_stream_source yield mock_hls_stream_source
async def provide_web_rtc_answer(stream_source: str, offer: str) -> str: async def provide_web_rtc_answer(stream_source: str, offer: str, stream_id: str) -> str:
"""Simulate an rtsp to webrtc provider.""" """Simulate an rtsp to webrtc provider."""
assert stream_source == STREAM_SOURCE assert stream_source == STREAM_SOURCE
assert offer == WEBRTC_OFFER assert offer == WEBRTC_OFFER