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,
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

View File

@ -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(

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_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

View File

@ -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)

View File

@ -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,

View File

@ -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),
}
),
)

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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:

View File

@ -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,

View File

@ -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],
}