From 617b0d04dcab521ce3b68b4f4956c1f341f6ea60 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 15 May 2022 14:31:18 +0800 Subject: [PATCH] Decouple stream options from PyAV options (#71247) Co-authored-by: Allen Porter --- homeassistant/components/generic/camera.py | 19 ++++---- .../components/generic/config_flow.py | 17 ++++---- homeassistant/components/generic/const.py | 12 ------ homeassistant/components/onvif/__init__.py | 11 ++--- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/onvif/config_flow.py | 15 ++----- homeassistant/components/onvif/const.py | 3 -- homeassistant/components/stream/__init__.py | 43 ++++++++++++++++--- homeassistant/components/stream/const.py | 11 +++++ homeassistant/components/stream/worker.py | 4 ++ tests/components/generic/test_config_flow.py | 6 ++- tests/components/onvif/test_config_flow.py | 4 +- 12 files changed, 83 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 197890efad3..4886e3a0693 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -12,6 +12,11 @@ from homeassistant.components.camera import ( Camera, CameraEntityFeature, ) +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_AUTHENTICATION, @@ -34,14 +39,10 @@ from .const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, - FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, - RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -63,7 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( cv.small_float, cv.positive_int ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS.keys()), + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS), } ) @@ -157,14 +158,10 @@ class GenericCamera(Camera): self.content_type = device_info[CONF_CONTENT_TYPE] self.verify_ssl = device_info[CONF_VERIFY_SSL] if device_info.get(CONF_RTSP_TRANSPORT): - self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ - CONF_RTSP_TRANSPORT - ] + self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT] self._auth = generate_auth(device_info) if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - self.stream_options[ - FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] - ] = "1" + self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 0a49393d9cc..bf9499c07df 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -16,7 +16,12 @@ from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl -from homeassistant.components.stream.const import SOURCE_TIMEOUT +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, + SOURCE_TIMEOUT, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_AUTHENTICATION, @@ -38,15 +43,11 @@ from .const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, DOMAIN, - FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, - RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -200,16 +201,16 @@ async def async_test_stream(hass, info) -> dict[str, str]: # For RTSP streams, prefer TCP. This code is duplicated from # homeassistant.components.stream.__init__.py:create_stream() # It may be possible & better to call create_stream() directly. - stream_options: dict[str, str] = {} + stream_options: dict[str, bool | str] = {} if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": stream_options = { "rtsp_flags": "prefer_tcp", "stimeout": "5000000", } if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): - stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - stream_options[FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]] = "1" + stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True _LOGGER.debug("Attempting to open stream %s", stream_source) container = await hass.async_add_executor_job( partial( diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 8ae5f16c4c4..eb0d81d493c 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -7,18 +7,6 @@ CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" -CONF_RTSP_TRANSPORT = "rtsp_transport" -CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" -FFMPEG_OPTION_MAP = { - CONF_RTSP_TRANSPORT: "rtsp_transport", - CONF_USE_WALLCLOCK_AS_TIMESTAMPS: "use_wallclock_as_timestamps", -} -RTSP_TRANSPORTS = { - "tcp": "TCP", - "udp": "UDP", - "udp_multicast": "UDP Multicast", - "http": "HTTP", -} GET_IMAGE_TIMEOUT = 10 DEFAULT_USERNAME = None diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 8956f0ae2f9..d966549a36f 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -2,6 +2,7 @@ from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, @@ -12,13 +13,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import ( - CONF_RTSP_TRANSPORT, - CONF_SNAPSHOT_AUTH, - DEFAULT_ARGUMENTS, - DOMAIN, - RTSP_TRANS_PROTOCOLS, -) +from .const import CONF_SNAPSHOT_AUTH, DEFAULT_ARGUMENTS, DOMAIN from .device import ONVIFDevice @@ -99,7 +94,7 @@ async def async_populate_options(hass, entry): """Populate default options for device.""" options = { CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, - CONF_RTSP_TRANSPORT: RTSP_TRANS_PROTOCOLS[0], + CONF_RTSP_TRANSPORT: next(iter(RTSP_TRANSPORTS)), } hass.config_entries.async_update_entry(entry, options=options) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 3475df241b0..6b61a37eb16 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -9,6 +9,7 @@ from yarl import URL from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager +from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -27,7 +28,6 @@ from .const import ( ATTR_SPEED, ATTR_TILT, ATTR_ZOOM, - CONF_RTSP_TRANSPORT, CONF_SNAPSHOT_AUTH, CONTINUOUS_MOVE, DIR_DOWN, diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index c4579702675..894f2fee3df 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -13,6 +13,7 @@ from zeep.exceptions import Fault from homeassistant import config_entries from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,15 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback -from .const import ( - CONF_DEVICE_ID, - CONF_RTSP_TRANSPORT, - DEFAULT_ARGUMENTS, - DEFAULT_PORT, - DOMAIN, - LOGGER, - RTSP_TRANS_PROTOCOLS, -) +from .const import CONF_DEVICE_ID, DEFAULT_ARGUMENTS, DEFAULT_PORT, DOMAIN, LOGGER from .device import get_device CONF_MANUAL_INPUT = "Manually configure ONVIF device" @@ -294,9 +287,9 @@ class OnvifOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_RTSP_TRANSPORT, default=self.config_entry.options.get( - CONF_RTSP_TRANSPORT, RTSP_TRANS_PROTOCOLS[0] + CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ), - ): vol.In(RTSP_TRANS_PROTOCOLS), + ): vol.In(RTSP_TRANSPORTS), } ), ) diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index 3a2e802a5a0..410088f28df 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -9,11 +9,8 @@ DEFAULT_PORT = 80 DEFAULT_ARGUMENTS = "-pred 1" CONF_DEVICE_ID = "deviceid" -CONF_RTSP_TRANSPORT = "rtsp_transport" CONF_SNAPSHOT_AUTH = "snapshot_auth" -RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] - ATTR_PAN = "pan" ATTR_TILT = "tilt" ATTR_ZOOM = "zoom" diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index abaf367486d..fbdfd97f9b2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,7 +23,7 @@ import secrets import threading import time from types import MappingProxyType -from typing import Any, cast +from typing import Any, Final, cast import voluptuous as vol @@ -39,12 +39,15 @@ from .const import ( ATTR_STREAMS, CONF_LL_HLS, CONF_PART_DURATION, + CONF_RTSP_TRANSPORT, CONF_SEGMENT_DURATION, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, HLS_PROVIDER, MAX_SEGMENTS, OUTPUT_IDLE_TIMEOUT, RECORDER_PROVIDER, + RTSP_TRANSPORTS, SEGMENT_DURATION_ADJUSTER, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, @@ -72,28 +75,32 @@ def redact_credentials(data: str) -> str: def create_stream( hass: HomeAssistant, stream_source: str, - options: dict[str, str], + options: dict[str, Any], stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and - options are passed into pyav / ffmpeg as options. + options (see STREAM_OPTIONS_SCHEMA) are converted and passed into pyav / ffmpeg. The stream_label is a string used as an additional message in logging. """ if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") + # Convert extra stream options into PyAV options + pyav_options = convert_stream_options(options) # For RTSP streams, prefer TCP if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": - options = { + pyav_options = { "rtsp_flags": "prefer_tcp", "stimeout": "5000000", - **options, + **pyav_options, } - stream = Stream(hass, stream_source, options=options, stream_label=stream_label) + stream = Stream( + hass, stream_source, options=pyav_options, stream_label=stream_label + ) hass.data[DOMAIN][ATTR_STREAMS].append(stream) return stream @@ -464,3 +471,27 @@ class Stream: def _should_retry() -> bool: """Return true if worker failures should be retried, for disabling during tests.""" return True + + +STREAM_OPTIONS_SCHEMA: Final = vol.Schema( + { + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS), + vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): bool, + } +) + + +def convert_stream_options(stream_options: dict[str, Any]) -> dict[str, str]: + """Convert options from stream options into PyAV options.""" + pyav_options: dict[str, str] = {} + try: + STREAM_OPTIONS_SCHEMA(stream_options) + except vol.Invalid as exc: + raise HomeAssistantError("Invalid stream options") from exc + + if rtsp_transport := stream_options.get(CONF_RTSP_TRANSPORT): + pyav_options["rtsp_transport"] = rtsp_transport + if stream_options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + pyav_options["use_wallclock_as_timestamps"] = "1" + + return pyav_options diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 50ae43df0d0..f8c9ba85d59 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -42,3 +42,14 @@ STREAM_RESTART_RESET_TIME = 300 # Reset wait_timeout after this many seconds CONF_LL_HLS = "ll_hls" CONF_PART_DURATION = "part_duration" CONF_SEGMENT_DURATION = "segment_duration" + +CONF_PREFER_TCP = "prefer_tcp" +CONF_RTSP_TRANSPORT = "rtsp_transport" +# The first dict entry below may be used as the default when populating options +RTSP_TRANSPORTS = { + "tcp": "TCP", + "udp": "UDP", + "udp_multicast": "UDP Multicast", + "http": "HTTP", +} +CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index bde5ab0fb05..f8d12c1cb44 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -452,6 +452,10 @@ def stream_worker( ) -> None: """Handle consuming streams.""" + if av.library_versions["libavformat"][0] >= 59 and "stimeout" in options: + # the stimeout option was renamed to timeout as of ffmpeg 5.0 + options["timeout"] = options["stimeout"] + del options["stimeout"] try: container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) except av.AVError as err: diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index dd53cb8548e..82cf41c6e91 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -15,12 +15,14 @@ from homeassistant.components.generic.const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, ) +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 0760ead2ba1..1e4dabfe7cf 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -323,12 +323,12 @@ async def test_option_flow(hass): result["flow_id"], user_input={ config_flow.CONF_EXTRA_ARGUMENTS: "", - config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", - config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], }