mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Decouple stream options from PyAV options (#71247)
Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
parent
e0bf1fba8d
commit
617b0d04dc
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user