Decouple stream options from PyAV options (#71247)

Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
uvjustin 2022-05-15 14:31:18 +08:00 committed by GitHub
parent e0bf1fba8d
commit 617b0d04dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 83 additions and 64 deletions

View File

@ -12,6 +12,11 @@ from homeassistant.components.camera import (
Camera, Camera,
CameraEntityFeature, 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.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
@ -34,14 +39,10 @@ from .const import (
CONF_CONTENT_TYPE, CONF_CONTENT_TYPE,
CONF_FRAMERATE, CONF_FRAMERATE,
CONF_LIMIT_REFETCH_TO_URL_CHANGE, CONF_LIMIT_REFETCH_TO_URL_CHANGE,
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DEFAULT_NAME, DEFAULT_NAME,
FFMPEG_OPTION_MAP,
GET_IMAGE_TIMEOUT, GET_IMAGE_TIMEOUT,
RTSP_TRANSPORTS,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,7 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
cv.small_float, cv.positive_int cv.small_float, cv.positive_int
), ),
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, 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.content_type = device_info[CONF_CONTENT_TYPE]
self.verify_ssl = device_info[CONF_VERIFY_SSL] self.verify_ssl = device_info[CONF_VERIFY_SSL]
if device_info.get(CONF_RTSP_TRANSPORT): if device_info.get(CONF_RTSP_TRANSPORT):
self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT]
CONF_RTSP_TRANSPORT
]
self._auth = generate_auth(device_info) self._auth = generate_auth(device_info)
if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
self.stream_options[ self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]
] = "1"
self._last_url = None self._last_url = None
self._last_image = None self._last_image = None

View File

@ -16,7 +16,12 @@ from httpx import HTTPStatusError, RequestError, TimeoutException
import voluptuous as vol import voluptuous as vol
import yarl 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.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
@ -38,15 +43,11 @@ from .const import (
CONF_CONTENT_TYPE, CONF_CONTENT_TYPE,
CONF_FRAMERATE, CONF_FRAMERATE,
CONF_LIMIT_REFETCH_TO_URL_CHANGE, CONF_LIMIT_REFETCH_TO_URL_CHANGE,
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
FFMPEG_OPTION_MAP,
GET_IMAGE_TIMEOUT, GET_IMAGE_TIMEOUT,
RTSP_TRANSPORTS,
) )
_LOGGER = logging.getLogger(__name__) _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 # For RTSP streams, prefer TCP. This code is duplicated from
# homeassistant.components.stream.__init__.py:create_stream() # homeassistant.components.stream.__init__.py:create_stream()
# It may be possible & better to call create_stream() directly. # 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://": if isinstance(stream_source, str) and stream_source[:7] == "rtsp://":
stream_options = { stream_options = {
"rtsp_flags": "prefer_tcp", "rtsp_flags": "prefer_tcp",
"stimeout": "5000000", "stimeout": "5000000",
} }
if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): 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): 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) _LOGGER.debug("Attempting to open stream %s", stream_source)
container = await hass.async_add_executor_job( container = await hass.async_add_executor_job(
partial( partial(

View File

@ -7,18 +7,6 @@ CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change"
CONF_STILL_IMAGE_URL = "still_image_url" CONF_STILL_IMAGE_URL = "still_image_url"
CONF_STREAM_SOURCE = "stream_source" CONF_STREAM_SOURCE = "stream_source"
CONF_FRAMERATE = "framerate" 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 GET_IMAGE_TIMEOUT = 10
DEFAULT_USERNAME = None DEFAULT_USERNAME = None

View File

@ -2,6 +2,7 @@
from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
@ -12,13 +13,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from .const import ( from .const import CONF_SNAPSHOT_AUTH, DEFAULT_ARGUMENTS, DOMAIN
CONF_RTSP_TRANSPORT,
CONF_SNAPSHOT_AUTH,
DEFAULT_ARGUMENTS,
DOMAIN,
RTSP_TRANS_PROTOCOLS,
)
from .device import ONVIFDevice from .device import ONVIFDevice
@ -99,7 +94,7 @@ async def async_populate_options(hass, entry):
"""Populate default options for device.""" """Populate default options for device."""
options = { options = {
CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, 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) hass.config_entries.async_update_entry(entry, options=options)

View File

@ -9,6 +9,7 @@ from yarl import URL
from homeassistant.components import ffmpeg from homeassistant.components import ffmpeg
from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager 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.config_entries import ConfigEntry
from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.const import HTTP_BASIC_AUTHENTICATION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -27,7 +28,6 @@ from .const import (
ATTR_SPEED, ATTR_SPEED,
ATTR_TILT, ATTR_TILT,
ATTR_ZOOM, ATTR_ZOOM,
CONF_RTSP_TRANSPORT,
CONF_SNAPSHOT_AUTH, CONF_SNAPSHOT_AUTH,
CONTINUOUS_MOVE, CONTINUOUS_MOVE,
DIR_DOWN, DIR_DOWN,

View File

@ -13,6 +13,7 @@ from zeep.exceptions import Fault
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
@ -22,15 +23,7 @@ from homeassistant.const import (
) )
from homeassistant.core import callback from homeassistant.core import callback
from .const import ( from .const import CONF_DEVICE_ID, DEFAULT_ARGUMENTS, DEFAULT_PORT, DOMAIN, LOGGER
CONF_DEVICE_ID,
CONF_RTSP_TRANSPORT,
DEFAULT_ARGUMENTS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
RTSP_TRANS_PROTOCOLS,
)
from .device import get_device from .device import get_device
CONF_MANUAL_INPUT = "Manually configure ONVIF device" CONF_MANUAL_INPUT = "Manually configure ONVIF device"
@ -294,9 +287,9 @@ class OnvifOptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional( vol.Optional(
CONF_RTSP_TRANSPORT, CONF_RTSP_TRANSPORT,
default=self.config_entry.options.get( 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),
} }
), ),
) )

View File

@ -9,11 +9,8 @@ DEFAULT_PORT = 80
DEFAULT_ARGUMENTS = "-pred 1" DEFAULT_ARGUMENTS = "-pred 1"
CONF_DEVICE_ID = "deviceid" CONF_DEVICE_ID = "deviceid"
CONF_RTSP_TRANSPORT = "rtsp_transport"
CONF_SNAPSHOT_AUTH = "snapshot_auth" CONF_SNAPSHOT_AUTH = "snapshot_auth"
RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"]
ATTR_PAN = "pan" ATTR_PAN = "pan"
ATTR_TILT = "tilt" ATTR_TILT = "tilt"
ATTR_ZOOM = "zoom" ATTR_ZOOM = "zoom"

View File

@ -23,7 +23,7 @@ import secrets
import threading import threading
import time import time
from types import MappingProxyType from types import MappingProxyType
from typing import Any, cast from typing import Any, Final, cast
import voluptuous as vol import voluptuous as vol
@ -39,12 +39,15 @@ from .const import (
ATTR_STREAMS, ATTR_STREAMS,
CONF_LL_HLS, CONF_LL_HLS,
CONF_PART_DURATION, CONF_PART_DURATION,
CONF_RTSP_TRANSPORT,
CONF_SEGMENT_DURATION, CONF_SEGMENT_DURATION,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DOMAIN, DOMAIN,
HLS_PROVIDER, HLS_PROVIDER,
MAX_SEGMENTS, MAX_SEGMENTS,
OUTPUT_IDLE_TIMEOUT, OUTPUT_IDLE_TIMEOUT,
RECORDER_PROVIDER, RECORDER_PROVIDER,
RTSP_TRANSPORTS,
SEGMENT_DURATION_ADJUSTER, SEGMENT_DURATION_ADJUSTER,
STREAM_RESTART_INCREMENT, STREAM_RESTART_INCREMENT,
STREAM_RESTART_RESET_TIME, STREAM_RESTART_RESET_TIME,
@ -72,28 +75,32 @@ def redact_credentials(data: str) -> str:
def create_stream( def create_stream(
hass: HomeAssistant, hass: HomeAssistant,
stream_source: str, stream_source: str,
options: dict[str, str], options: dict[str, Any],
stream_label: str | None = None, stream_label: str | None = None,
) -> Stream: ) -> Stream:
"""Create a stream with the specified identfier based on the source url. """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 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. The stream_label is a string used as an additional message in logging.
""" """
if DOMAIN not in hass.config.components: if DOMAIN not in hass.config.components:
raise HomeAssistantError("Stream integration is not set up.") 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 # For RTSP streams, prefer TCP
if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": if isinstance(stream_source, str) and stream_source[:7] == "rtsp://":
options = { pyav_options = {
"rtsp_flags": "prefer_tcp", "rtsp_flags": "prefer_tcp",
"stimeout": "5000000", "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) hass.data[DOMAIN][ATTR_STREAMS].append(stream)
return stream return stream
@ -464,3 +471,27 @@ class Stream:
def _should_retry() -> bool: def _should_retry() -> bool:
"""Return true if worker failures should be retried, for disabling during tests.""" """Return true if worker failures should be retried, for disabling during tests."""
return True 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

View File

@ -42,3 +42,14 @@ STREAM_RESTART_RESET_TIME = 300 # Reset wait_timeout after this many seconds
CONF_LL_HLS = "ll_hls" CONF_LL_HLS = "ll_hls"
CONF_PART_DURATION = "part_duration" CONF_PART_DURATION = "part_duration"
CONF_SEGMENT_DURATION = "segment_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"

View File

@ -452,6 +452,10 @@ def stream_worker(
) -> None: ) -> None:
"""Handle consuming streams.""" """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: try:
container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) container = av.open(source, options=options, timeout=SOURCE_TIMEOUT)
except av.AVError as err: except av.AVError as err:

View File

@ -15,12 +15,14 @@ from homeassistant.components.generic.const import (
CONF_CONTENT_TYPE, CONF_CONTENT_TYPE,
CONF_FRAMERATE, CONF_FRAMERATE,
CONF_LIMIT_REFETCH_TO_URL_CHANGE, CONF_LIMIT_REFETCH_TO_URL_CHANGE,
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DOMAIN, DOMAIN,
) )
from homeassistant.components.stream.const import (
CONF_RTSP_TRANSPORT,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
CONF_NAME, CONF_NAME,

View File

@ -323,12 +323,12 @@ async def test_option_flow(hass):
result["flow_id"], result["flow_id"],
user_input={ user_input={
config_flow.CONF_EXTRA_ARGUMENTS: "", 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["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
config_flow.CONF_EXTRA_ARGUMENTS: "", 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],
} }