Refactor camera stream settings (#81663)
This commit is contained in:
parent
1fe85c9b17
commit
ee910bd0e4
14 changed files with 226 additions and 152 deletions
|
@ -5,7 +5,7 @@ import asyncio
|
||||||
import collections
|
import collections
|
||||||
from collections.abc import Awaitable, Callable, Iterable
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
@ -74,7 +74,7 @@ from .const import ( # noqa: F401
|
||||||
StreamType,
|
StreamType,
|
||||||
)
|
)
|
||||||
from .img_util import scale_jpeg_camera_image
|
from .img_util import scale_jpeg_camera_image
|
||||||
from .prefs import CameraPreferences
|
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -346,7 +346,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
prefs = CameraPreferences(hass)
|
prefs = CameraPreferences(hass)
|
||||||
await prefs.async_initialize()
|
|
||||||
hass.data[DATA_CAMERA_PREFS] = prefs
|
hass.data[DATA_CAMERA_PREFS] = prefs
|
||||||
|
|
||||||
hass.http.register_view(CameraImageView(component))
|
hass.http.register_view(CameraImageView(component))
|
||||||
|
@ -361,13 +360,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
async def preload_stream(_event: Event) -> None:
|
async def preload_stream(_event: Event) -> None:
|
||||||
for camera in component.entities:
|
for camera in component.entities:
|
||||||
camera_prefs = prefs.get(camera.entity_id)
|
stream_prefs = await prefs.get_dynamic_stream_settings(camera.entity_id)
|
||||||
if not camera_prefs.preload_stream:
|
if not stream_prefs.preload_stream:
|
||||||
continue
|
continue
|
||||||
stream = await camera.async_create_stream()
|
stream = await camera.async_create_stream()
|
||||||
if not stream:
|
if not stream:
|
||||||
continue
|
continue
|
||||||
stream.keepalive = True
|
|
||||||
stream.add_provider("hls")
|
stream.add_provider("hls")
|
||||||
await stream.start()
|
await stream.start()
|
||||||
|
|
||||||
|
@ -524,6 +522,9 @@ class Camera(Entity):
|
||||||
self.hass,
|
self.hass,
|
||||||
source,
|
source,
|
||||||
options=self.stream_options,
|
options=self.stream_options,
|
||||||
|
dynamic_stream_settings=await self.hass.data[
|
||||||
|
DATA_CAMERA_PREFS
|
||||||
|
].get_dynamic_stream_settings(self.entity_id),
|
||||||
stream_label=self.entity_id,
|
stream_label=self.entity_id,
|
||||||
)
|
)
|
||||||
self.stream.set_update_callback(self.async_write_ha_state)
|
self.stream.set_update_callback(self.async_write_ha_state)
|
||||||
|
@ -861,8 +862,8 @@ async def websocket_get_prefs(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle request for account info."""
|
"""Handle request for account info."""
|
||||||
prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS]
|
prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS]
|
||||||
camera_prefs = prefs.get(msg["entity_id"])
|
stream_prefs = await prefs.get_dynamic_stream_settings(msg["entity_id"])
|
||||||
connection.send_result(msg["id"], camera_prefs.as_dict())
|
connection.send_result(msg["id"], asdict(stream_prefs))
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
|
@ -956,12 +957,6 @@ async def _async_stream_endpoint_url(
|
||||||
f"{camera.entity_id} does not support play stream service"
|
f"{camera.entity_id} does not support play stream service"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update keepalive setting which manages idle shutdown
|
|
||||||
prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS]
|
|
||||||
camera_prefs = prefs.get(camera.entity_id)
|
|
||||||
stream.keepalive = camera_prefs.preload_stream
|
|
||||||
stream.orientation = camera_prefs.orientation
|
|
||||||
|
|
||||||
stream.add_provider(fmt)
|
stream.add_provider(fmt)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
return stream.endpoint_url(fmt)
|
return stream.endpoint_url(fmt)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Preference management for camera component."""
|
"""Preference management for camera component."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
from typing import Final, Union, cast
|
from typing import Final, Union, cast
|
||||||
|
|
||||||
from homeassistant.components.stream import Orientation
|
from homeassistant.components.stream import Orientation
|
||||||
|
@ -16,28 +17,12 @@ STORAGE_KEY: Final = DOMAIN
|
||||||
STORAGE_VERSION: Final = 1
|
STORAGE_VERSION: Final = 1
|
||||||
|
|
||||||
|
|
||||||
class CameraEntityPreferences:
|
@dataclass
|
||||||
"""Handle preferences for camera entity."""
|
class DynamicStreamSettings:
|
||||||
|
"""Stream settings which are managed and updated by the camera entity."""
|
||||||
|
|
||||||
def __init__(self, prefs: dict[str, bool | Orientation]) -> None:
|
preload_stream: bool = False
|
||||||
"""Initialize prefs."""
|
orientation: Orientation = Orientation.NO_TRANSFORM
|
||||||
self._prefs = prefs
|
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, bool | Orientation]:
|
|
||||||
"""Return dictionary version."""
|
|
||||||
return self._prefs
|
|
||||||
|
|
||||||
@property
|
|
||||||
def preload_stream(self) -> bool:
|
|
||||||
"""Return if stream is loaded on hass start."""
|
|
||||||
return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def orientation(self) -> Orientation:
|
|
||||||
"""Return the current stream orientation settings."""
|
|
||||||
return cast(
|
|
||||||
Orientation, self._prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CameraPreferences:
|
class CameraPreferences:
|
||||||
|
@ -51,15 +36,9 @@ class CameraPreferences:
|
||||||
self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]](
|
self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]](
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY
|
hass, STORAGE_VERSION, STORAGE_KEY
|
||||||
)
|
)
|
||||||
# Local copy of the preload_stream prefs
|
self._dynamic_stream_settings_by_entity_id: dict[
|
||||||
self._prefs: dict[str, dict[str, bool | Orientation]] | None = None
|
str, DynamicStreamSettings
|
||||||
|
] = {}
|
||||||
async def async_initialize(self) -> None:
|
|
||||||
"""Finish initializing the preferences."""
|
|
||||||
if (prefs := await self._store.async_load()) is None:
|
|
||||||
prefs = {}
|
|
||||||
|
|
||||||
self._prefs = prefs
|
|
||||||
|
|
||||||
async def async_update(
|
async def async_update(
|
||||||
self,
|
self,
|
||||||
|
@ -67,20 +46,25 @@ class CameraPreferences:
|
||||||
*,
|
*,
|
||||||
preload_stream: bool | UndefinedType = UNDEFINED,
|
preload_stream: bool | UndefinedType = UNDEFINED,
|
||||||
orientation: Orientation | UndefinedType = UNDEFINED,
|
orientation: Orientation | UndefinedType = UNDEFINED,
|
||||||
stream_options: dict[str, str] | UndefinedType = UNDEFINED,
|
|
||||||
) -> dict[str, bool | Orientation]:
|
) -> dict[str, bool | Orientation]:
|
||||||
"""Update camera preferences.
|
"""Update camera preferences.
|
||||||
|
|
||||||
|
Also update the DynamicStreamSettings if they exist.
|
||||||
|
preload_stream is stored in a Store
|
||||||
|
orientation is stored in the Entity Registry
|
||||||
|
|
||||||
Returns a dict with the preferences on success.
|
Returns a dict with the preferences on success.
|
||||||
Raises HomeAssistantError on failure.
|
Raises HomeAssistantError on failure.
|
||||||
"""
|
"""
|
||||||
|
dynamic_stream_settings = self._dynamic_stream_settings_by_entity_id.get(
|
||||||
|
entity_id
|
||||||
|
)
|
||||||
if preload_stream is not UNDEFINED:
|
if preload_stream is not UNDEFINED:
|
||||||
# Prefs already initialized.
|
if dynamic_stream_settings:
|
||||||
assert self._prefs is not None
|
dynamic_stream_settings.preload_stream = preload_stream
|
||||||
if not self._prefs.get(entity_id):
|
preload_prefs = await self._store.async_load() or {}
|
||||||
self._prefs[entity_id] = {}
|
preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
|
||||||
self._prefs[entity_id][PREF_PRELOAD_STREAM] = preload_stream
|
await self._store.async_save(preload_prefs)
|
||||||
await self._store.async_save(self._prefs)
|
|
||||||
|
|
||||||
if orientation is not UNDEFINED:
|
if orientation is not UNDEFINED:
|
||||||
if (registry := er.async_get(self._hass)).async_get(entity_id):
|
if (registry := er.async_get(self._hass)).async_get(entity_id):
|
||||||
|
@ -91,12 +75,26 @@ class CameraPreferences:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Orientation is only supported on entities set up through config flows"
|
"Orientation is only supported on entities set up through config flows"
|
||||||
)
|
)
|
||||||
return self.get(entity_id).as_dict()
|
if dynamic_stream_settings:
|
||||||
|
dynamic_stream_settings.orientation = orientation
|
||||||
|
return asdict(await self.get_dynamic_stream_settings(entity_id))
|
||||||
|
|
||||||
def get(self, entity_id: str) -> CameraEntityPreferences:
|
async def get_dynamic_stream_settings(
|
||||||
"""Get preferences for an entity."""
|
self, entity_id: str
|
||||||
# Prefs are already initialized.
|
) -> DynamicStreamSettings:
|
||||||
assert self._prefs is not None
|
"""Get the DynamicStreamSettings for the entity."""
|
||||||
|
if settings := self._dynamic_stream_settings_by_entity_id.get(entity_id):
|
||||||
|
return settings
|
||||||
|
# Get preload stream setting from prefs
|
||||||
|
# Get orientation setting from entity registry
|
||||||
reg_entry = er.async_get(self._hass).async_get(entity_id)
|
reg_entry = er.async_get(self._hass).async_get(entity_id)
|
||||||
er_prefs = reg_entry.options.get(DOMAIN, {}) if reg_entry else {}
|
er_prefs = reg_entry.options.get(DOMAIN, {}) if reg_entry else {}
|
||||||
return CameraEntityPreferences(self._prefs.get(entity_id, {}) | er_prefs)
|
preload_prefs = await self._store.async_load() or {}
|
||||||
|
settings = DynamicStreamSettings(
|
||||||
|
preload_stream=cast(
|
||||||
|
bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False)
|
||||||
|
),
|
||||||
|
orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM),
|
||||||
|
)
|
||||||
|
self._dynamic_stream_settings_by_entity_id[entity_id] = settings
|
||||||
|
return settings
|
||||||
|
|
|
@ -16,7 +16,11 @@ from httpx import HTTPStatusError, RequestError, TimeoutException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
from homeassistant.components.camera import CAMERA_IMAGE_TIMEOUT, _async_get_image
|
from homeassistant.components.camera import (
|
||||||
|
CAMERA_IMAGE_TIMEOUT,
|
||||||
|
DynamicStreamSettings,
|
||||||
|
_async_get_image,
|
||||||
|
)
|
||||||
from homeassistant.components.http.view import HomeAssistantView
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
from homeassistant.components.stream import (
|
from homeassistant.components.stream import (
|
||||||
CONF_RTSP_TRANSPORT,
|
CONF_RTSP_TRANSPORT,
|
||||||
|
@ -246,7 +250,13 @@ async def async_test_stream(
|
||||||
url = url.with_user(username).with_password(password)
|
url = url.with_user(username).with_password(password)
|
||||||
stream_source = str(url)
|
stream_source = str(url)
|
||||||
try:
|
try:
|
||||||
stream = create_stream(hass, stream_source, stream_options, "test_stream")
|
stream = create_stream(
|
||||||
|
hass,
|
||||||
|
stream_source,
|
||||||
|
stream_options,
|
||||||
|
DynamicStreamSettings(),
|
||||||
|
"test_stream",
|
||||||
|
)
|
||||||
hls_provider = stream.add_provider(HLS_PROVIDER)
|
hls_provider = stream.add_provider(HLS_PROVIDER)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
|
if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
|
||||||
|
|
|
@ -137,7 +137,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
|
|
||||||
if self.stream and self.stream.keepalive:
|
if self.stream and self.stream.dynamic_stream_settings.preload_stream:
|
||||||
return await self.stream.async_get_image(width, height)
|
return await self.stream.async_get_image(width, height)
|
||||||
|
|
||||||
if self.device.capabilities.snapshot:
|
if self.device.capabilities.snapshot:
|
||||||
|
|
|
@ -25,7 +25,7 @@ import secrets
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, Final, cast
|
from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -70,6 +70,9 @@ from .core import (
|
||||||
from .diagnostics import Diagnostics
|
from .diagnostics import Diagnostics
|
||||||
from .hls import HlsStreamOutput, async_setup_hls
|
from .hls import HlsStreamOutput, async_setup_hls
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.components.camera import DynamicStreamSettings
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ATTR_SETTINGS",
|
"ATTR_SETTINGS",
|
||||||
"CONF_EXTRA_PART_WAIT_TIME",
|
"CONF_EXTRA_PART_WAIT_TIME",
|
||||||
|
@ -105,6 +108,7 @@ def create_stream(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
stream_source: str,
|
stream_source: str,
|
||||||
options: Mapping[str, str | bool | float],
|
options: Mapping[str, str | bool | float],
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
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.
|
||||||
|
@ -156,6 +160,7 @@ def create_stream(
|
||||||
stream_source,
|
stream_source,
|
||||||
pyav_options=pyav_options,
|
pyav_options=pyav_options,
|
||||||
stream_settings=stream_settings,
|
stream_settings=stream_settings,
|
||||||
|
dynamic_stream_settings=dynamic_stream_settings,
|
||||||
stream_label=stream_label,
|
stream_label=stream_label,
|
||||||
)
|
)
|
||||||
hass.data[DOMAIN][ATTR_STREAMS].append(stream)
|
hass.data[DOMAIN][ATTR_STREAMS].append(stream)
|
||||||
|
@ -231,7 +236,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
part_target_duration=conf[CONF_PART_DURATION],
|
part_target_duration=conf[CONF_PART_DURATION],
|
||||||
hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3),
|
hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3),
|
||||||
hls_part_timeout=2 * conf[CONF_PART_DURATION],
|
hls_part_timeout=2 * conf[CONF_PART_DURATION],
|
||||||
orientation=Orientation.NO_TRANSFORM,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS
|
hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS
|
||||||
|
@ -246,7 +250,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
async def shutdown(event: Event) -> None:
|
async def shutdown(event: Event) -> None:
|
||||||
"""Stop all stream workers."""
|
"""Stop all stream workers."""
|
||||||
for stream in hass.data[DOMAIN][ATTR_STREAMS]:
|
for stream in hass.data[DOMAIN][ATTR_STREAMS]:
|
||||||
stream.keepalive = False
|
stream.dynamic_stream_settings.preload_stream = False
|
||||||
if awaitables := [
|
if awaitables := [
|
||||||
asyncio.create_task(stream.stop())
|
asyncio.create_task(stream.stop())
|
||||||
for stream in hass.data[DOMAIN][ATTR_STREAMS]
|
for stream in hass.data[DOMAIN][ATTR_STREAMS]
|
||||||
|
@ -268,6 +272,7 @@ class Stream:
|
||||||
source: str,
|
source: str,
|
||||||
pyav_options: dict[str, str],
|
pyav_options: dict[str, str],
|
||||||
stream_settings: StreamSettings,
|
stream_settings: StreamSettings,
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
stream_label: str | None = None,
|
stream_label: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a stream."""
|
"""Initialize a stream."""
|
||||||
|
@ -276,14 +281,16 @@ class Stream:
|
||||||
self.pyav_options = pyav_options
|
self.pyav_options = pyav_options
|
||||||
self._stream_settings = stream_settings
|
self._stream_settings = stream_settings
|
||||||
self._stream_label = stream_label
|
self._stream_label = stream_label
|
||||||
self.keepalive = False
|
self.dynamic_stream_settings = dynamic_stream_settings
|
||||||
self.access_token: str | None = None
|
self.access_token: str | None = None
|
||||||
self._start_stop_lock = asyncio.Lock()
|
self._start_stop_lock = asyncio.Lock()
|
||||||
self._thread: threading.Thread | None = None
|
self._thread: threading.Thread | None = None
|
||||||
self._thread_quit = threading.Event()
|
self._thread_quit = threading.Event()
|
||||||
self._outputs: dict[str, StreamOutput] = {}
|
self._outputs: dict[str, StreamOutput] = {}
|
||||||
self._fast_restart_once = False
|
self._fast_restart_once = False
|
||||||
self._keyframe_converter = KeyFrameConverter(hass, stream_settings)
|
self._keyframe_converter = KeyFrameConverter(
|
||||||
|
hass, stream_settings, dynamic_stream_settings
|
||||||
|
)
|
||||||
self._available: bool = True
|
self._available: bool = True
|
||||||
self._update_callback: Callable[[], None] | None = None
|
self._update_callback: Callable[[], None] | None = None
|
||||||
self._logger = (
|
self._logger = (
|
||||||
|
@ -293,16 +300,6 @@ class Stream:
|
||||||
)
|
)
|
||||||
self._diagnostics = Diagnostics()
|
self._diagnostics = Diagnostics()
|
||||||
|
|
||||||
@property
|
|
||||||
def orientation(self) -> Orientation:
|
|
||||||
"""Return the current orientation setting."""
|
|
||||||
return self._stream_settings.orientation
|
|
||||||
|
|
||||||
@orientation.setter
|
|
||||||
def orientation(self, value: Orientation) -> None:
|
|
||||||
"""Set the stream orientation setting."""
|
|
||||||
self._stream_settings.orientation = value
|
|
||||||
|
|
||||||
def endpoint_url(self, fmt: str) -> str:
|
def endpoint_url(self, fmt: str) -> str:
|
||||||
"""Start the stream and returns a url for the output format."""
|
"""Start the stream and returns a url for the output format."""
|
||||||
if fmt not in self._outputs:
|
if fmt not in self._outputs:
|
||||||
|
@ -326,7 +323,8 @@ class Stream:
|
||||||
|
|
||||||
async def idle_callback() -> None:
|
async def idle_callback() -> None:
|
||||||
if (
|
if (
|
||||||
not self.keepalive or fmt == RECORDER_PROVIDER
|
not self.dynamic_stream_settings.preload_stream
|
||||||
|
or fmt == RECORDER_PROVIDER
|
||||||
) and fmt in self._outputs:
|
) and fmt in self._outputs:
|
||||||
await self.remove_provider(self._outputs[fmt])
|
await self.remove_provider(self._outputs[fmt])
|
||||||
self.check_idle()
|
self.check_idle()
|
||||||
|
@ -335,6 +333,7 @@ class Stream:
|
||||||
self.hass,
|
self.hass,
|
||||||
IdleTimer(self.hass, timeout, idle_callback),
|
IdleTimer(self.hass, timeout, idle_callback),
|
||||||
self._stream_settings,
|
self._stream_settings,
|
||||||
|
self.dynamic_stream_settings,
|
||||||
)
|
)
|
||||||
self._outputs[fmt] = provider
|
self._outputs[fmt] = provider
|
||||||
|
|
||||||
|
@ -413,8 +412,12 @@ class Stream:
|
||||||
while not self._thread_quit.wait(timeout=wait_timeout):
|
while not self._thread_quit.wait(timeout=wait_timeout):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
self.hass.add_job(self._async_update_state, True)
|
self.hass.add_job(self._async_update_state, True)
|
||||||
self._diagnostics.set_value("keepalive", self.keepalive)
|
self._diagnostics.set_value(
|
||||||
self._diagnostics.set_value("orientation", self.orientation)
|
"keepalive", self.dynamic_stream_settings.preload_stream
|
||||||
|
)
|
||||||
|
self._diagnostics.set_value(
|
||||||
|
"orientation", self.dynamic_stream_settings.orientation
|
||||||
|
)
|
||||||
self._diagnostics.increment("start_worker")
|
self._diagnostics.increment("start_worker")
|
||||||
try:
|
try:
|
||||||
stream_worker(
|
stream_worker(
|
||||||
|
@ -473,7 +476,7 @@ class Stream:
|
||||||
self._outputs = {}
|
self._outputs = {}
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
|
|
||||||
if not self.keepalive:
|
if not self.dynamic_stream_settings.preload_stream:
|
||||||
await self._stop()
|
await self._stop()
|
||||||
|
|
||||||
async def _stop(self) -> None:
|
async def _stop(self) -> None:
|
||||||
|
|
|
@ -29,6 +29,8 @@ from .const import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from av import CodecContext, Packet
|
from av import CodecContext, Packet
|
||||||
|
|
||||||
|
from homeassistant.components.camera import DynamicStreamSettings
|
||||||
|
|
||||||
from . import Stream
|
from . import Stream
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -58,7 +60,6 @@ class StreamSettings:
|
||||||
part_target_duration: float = attr.ib()
|
part_target_duration: float = attr.ib()
|
||||||
hls_advance_part_limit: int = attr.ib()
|
hls_advance_part_limit: int = attr.ib()
|
||||||
hls_part_timeout: float = attr.ib()
|
hls_part_timeout: float = attr.ib()
|
||||||
orientation: Orientation = attr.ib()
|
|
||||||
|
|
||||||
|
|
||||||
STREAM_SETTINGS_NON_LL_HLS = StreamSettings(
|
STREAM_SETTINGS_NON_LL_HLS = StreamSettings(
|
||||||
|
@ -67,7 +68,6 @@ STREAM_SETTINGS_NON_LL_HLS = StreamSettings(
|
||||||
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
hls_advance_part_limit=3,
|
hls_advance_part_limit=3,
|
||||||
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
orientation=Orientation.NO_TRANSFORM,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,12 +273,14 @@ class StreamOutput:
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
idle_timer: IdleTimer,
|
idle_timer: IdleTimer,
|
||||||
stream_settings: StreamSettings,
|
stream_settings: StreamSettings,
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
deque_maxlen: int | None = None,
|
deque_maxlen: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a stream output."""
|
"""Initialize a stream output."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.idle_timer = idle_timer
|
self.idle_timer = idle_timer
|
||||||
self.stream_settings = stream_settings
|
self.stream_settings = stream_settings
|
||||||
|
self.dynamic_stream_settings = dynamic_stream_settings
|
||||||
self._event = asyncio.Event()
|
self._event = asyncio.Event()
|
||||||
self._part_event = asyncio.Event()
|
self._part_event = asyncio.Event()
|
||||||
self._segments: deque[Segment] = deque(maxlen=deque_maxlen)
|
self._segments: deque[Segment] = deque(maxlen=deque_maxlen)
|
||||||
|
@ -427,7 +429,12 @@ class KeyFrameConverter:
|
||||||
If unsuccessful, get_image will return the previous image
|
If unsuccessful, get_image will return the previous image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, stream_settings: StreamSettings) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
stream_settings: StreamSettings,
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
|
|
||||||
# Keep import here so that we can import stream integration without installing reqs
|
# Keep import here so that we can import stream integration without installing reqs
|
||||||
|
@ -441,6 +448,7 @@ class KeyFrameConverter:
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._codec_context: CodecContext | None = None
|
self._codec_context: CodecContext | None = None
|
||||||
self._stream_settings = stream_settings
|
self._stream_settings = stream_settings
|
||||||
|
self._dynamic_stream_settings = dynamic_stream_settings
|
||||||
|
|
||||||
def create_codec_context(self, codec_context: CodecContext) -> None:
|
def create_codec_context(self, codec_context: CodecContext) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -498,12 +506,13 @@ class KeyFrameConverter:
|
||||||
if frames:
|
if frames:
|
||||||
frame = frames[0]
|
frame = frames[0]
|
||||||
if width and height:
|
if width and height:
|
||||||
if self._stream_settings.orientation >= 5:
|
if self._dynamic_stream_settings.orientation >= 5:
|
||||||
frame = frame.reformat(width=height, height=width)
|
frame = frame.reformat(width=height, height=width)
|
||||||
else:
|
else:
|
||||||
frame = frame.reformat(width=width, height=height)
|
frame = frame.reformat(width=width, height=height)
|
||||||
bgr_array = self.transform_image(
|
bgr_array = self.transform_image(
|
||||||
frame.to_ndarray(format="bgr24"), self._stream_settings.orientation
|
frame.to_ndarray(format="bgr24"),
|
||||||
|
self._dynamic_stream_settings.orientation,
|
||||||
)
|
)
|
||||||
self._image = bytes(self._turbojpeg.encode(bgr_array))
|
self._image = bytes(self._turbojpeg.encode(bgr_array))
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ from .core import (
|
||||||
from .fmp4utils import get_codec_string, transform_init
|
from .fmp4utils import get_codec_string, transform_init
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.components.camera import DynamicStreamSettings
|
||||||
|
|
||||||
from . import Stream
|
from . import Stream
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,9 +52,16 @@ class HlsStreamOutput(StreamOutput):
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
idle_timer: IdleTimer,
|
idle_timer: IdleTimer,
|
||||||
stream_settings: StreamSettings,
|
stream_settings: StreamSettings,
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize HLS output."""
|
"""Initialize HLS output."""
|
||||||
super().__init__(hass, idle_timer, stream_settings, deque_maxlen=MAX_SEGMENTS)
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
idle_timer,
|
||||||
|
stream_settings,
|
||||||
|
dynamic_stream_settings,
|
||||||
|
deque_maxlen=MAX_SEGMENTS,
|
||||||
|
)
|
||||||
self._target_duration = stream_settings.min_segment_duration
|
self._target_duration = stream_settings.min_segment_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -339,7 +348,7 @@ class HlsInitView(StreamView):
|
||||||
if not (segments := track.get_segments()) or not (body := segments[0].init):
|
if not (segments := track.get_segments()) or not (body := segments[0].init):
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
return web.Response(
|
return web.Response(
|
||||||
body=transform_init(body, stream.orientation),
|
body=transform_init(body, stream.dynamic_stream_settings.orientation),
|
||||||
headers={"Content-Type": "video/mp4"},
|
headers={"Content-Type": "video/mp4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ from .fmp4utils import read_init, transform_init
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import deque
|
import deque
|
||||||
|
|
||||||
|
from homeassistant.components.camera import DynamicStreamSettings
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,9 +40,10 @@ class RecorderOutput(StreamOutput):
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
idle_timer: IdleTimer,
|
idle_timer: IdleTimer,
|
||||||
stream_settings: StreamSettings,
|
stream_settings: StreamSettings,
|
||||||
|
dynamic_stream_settings: DynamicStreamSettings,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize recorder output."""
|
"""Initialize recorder output."""
|
||||||
super().__init__(hass, idle_timer, stream_settings)
|
super().__init__(hass, idle_timer, stream_settings, dynamic_stream_settings)
|
||||||
self.video_path: str
|
self.video_path: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -154,7 +157,7 @@ class RecorderOutput(StreamOutput):
|
||||||
video_path, mode="wb"
|
video_path, mode="wb"
|
||||||
) as out_file:
|
) as out_file:
|
||||||
init = transform_init(
|
init = transform_init(
|
||||||
read_init(in_file), self.stream_settings.orientation
|
read_init(in_file), self.dynamic_stream_settings.orientation
|
||||||
)
|
)
|
||||||
out_file.write(init)
|
out_file.write(init)
|
||||||
in_file.seek(len(init))
|
in_file.seek(len(init))
|
||||||
|
|
|
@ -12,7 +12,6 @@ from homeassistant.components.camera.const import (
|
||||||
PREF_ORIENTATION,
|
PREF_ORIENTATION,
|
||||||
PREF_PRELOAD_STREAM,
|
PREF_PRELOAD_STREAM,
|
||||||
)
|
)
|
||||||
from homeassistant.components.camera.prefs import CameraEntityPreferences
|
|
||||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.config import async_process_ha_core_config
|
from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -302,8 +301,9 @@ async def test_websocket_update_preload_prefs(hass, hass_ws_client, mock_camera)
|
||||||
)
|
)
|
||||||
msg = await client.receive_json()
|
msg = await client.receive_json()
|
||||||
|
|
||||||
# There should be no preferences
|
# The default prefs should be returned. Preload stream should be False
|
||||||
assert not msg["result"]
|
assert msg["success"]
|
||||||
|
assert msg["result"][PREF_PRELOAD_STREAM] is False
|
||||||
|
|
||||||
# Update the preference
|
# Update the preference
|
||||||
await client.send_json(
|
await client.send_json(
|
||||||
|
@ -421,12 +421,12 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream):
|
||||||
|
|
||||||
async def test_no_preload_stream(hass, mock_stream):
|
async def test_no_preload_stream(hass, mock_stream):
|
||||||
"""Test camera preload preference."""
|
"""Test camera preload preference."""
|
||||||
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False})
|
demo_settings = camera.DynamicStreamSettings()
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.camera.Stream.endpoint_url",
|
"homeassistant.components.camera.Stream.endpoint_url",
|
||||||
) as mock_request_stream, patch(
|
) as mock_request_stream, patch(
|
||||||
"homeassistant.components.camera.prefs.CameraPreferences.get",
|
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||||
return_value=demo_prefs,
|
return_value=demo_settings,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||||
new_callable=PropertyMock,
|
new_callable=PropertyMock,
|
||||||
|
@ -440,12 +440,12 @@ async def test_no_preload_stream(hass, mock_stream):
|
||||||
|
|
||||||
async def test_preload_stream(hass, mock_stream):
|
async def test_preload_stream(hass, mock_stream):
|
||||||
"""Test camera preload preference."""
|
"""Test camera preload preference."""
|
||||||
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True})
|
demo_settings = camera.DynamicStreamSettings(preload_stream=True)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.camera.create_stream"
|
"homeassistant.components.camera.create_stream"
|
||||||
) as mock_create_stream, patch(
|
) as mock_create_stream, patch(
|
||||||
"homeassistant.components.camera.prefs.CameraPreferences.get",
|
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||||
return_value=demo_prefs,
|
return_value=demo_settings,
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||||
return_value="http://example.com",
|
return_value="http://example.com",
|
||||||
|
|
|
@ -8,7 +8,8 @@ import io
|
||||||
import av
|
import av
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from homeassistant.components.stream.core import Segment
|
from homeassistant.components.camera import DynamicStreamSettings
|
||||||
|
from homeassistant.components.stream.core import Orientation, Segment
|
||||||
from homeassistant.components.stream.fmp4utils import (
|
from homeassistant.components.stream.fmp4utils import (
|
||||||
TRANSFORM_MATRIX_TOP,
|
TRANSFORM_MATRIX_TOP,
|
||||||
XYW_ROW,
|
XYW_ROW,
|
||||||
|
@ -16,8 +17,8 @@ from homeassistant.components.stream.fmp4utils import (
|
||||||
)
|
)
|
||||||
|
|
||||||
FAKE_TIME = datetime.utcnow()
|
FAKE_TIME = datetime.utcnow()
|
||||||
# Segment with defaults filled in for use in tests
|
|
||||||
|
|
||||||
|
# Segment with defaults filled in for use in tests
|
||||||
DefaultSegment = partial(
|
DefaultSegment = partial(
|
||||||
Segment,
|
Segment,
|
||||||
init=None,
|
init=None,
|
||||||
|
@ -157,7 +158,7 @@ def remux_with_audio(source, container_format, audio_codec):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int):
|
def assert_mp4_has_transform_matrix(mp4: bytes, orientation: Orientation):
|
||||||
"""Assert that the mp4 (or init) has the proper transformation matrix."""
|
"""Assert that the mp4 (or init) has the proper transformation matrix."""
|
||||||
# Find moov
|
# Find moov
|
||||||
moov_location = next(find_box(mp4, b"moov"))
|
moov_location = next(find_box(mp4, b"moov"))
|
||||||
|
@ -170,3 +171,8 @@ def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int):
|
||||||
mp4[tkhd_location + tkhd_length - 44 : tkhd_location + tkhd_length - 8]
|
mp4[tkhd_location + tkhd_length - 44 : tkhd_location + tkhd_length - 8]
|
||||||
== TRANSFORM_MATRIX_TOP[orientation] + XYW_ROW
|
== TRANSFORM_MATRIX_TOP[orientation] + XYW_ROW
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dynamic_stream_settings():
|
||||||
|
"""Create new dynamic stream settings."""
|
||||||
|
return DynamicStreamSettings()
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.components.stream.const import (
|
||||||
MAX_SEGMENTS,
|
MAX_SEGMENTS,
|
||||||
NUM_PLAYLIST_SEGMENTS,
|
NUM_PLAYLIST_SEGMENTS,
|
||||||
)
|
)
|
||||||
from homeassistant.components.stream.core import Part
|
from homeassistant.components.stream.core import Orientation, Part
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ from .common import (
|
||||||
FAKE_TIME,
|
FAKE_TIME,
|
||||||
DefaultSegment as Segment,
|
DefaultSegment as Segment,
|
||||||
assert_mp4_has_transform_matrix,
|
assert_mp4_has_transform_matrix,
|
||||||
|
dynamic_stream_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed
|
||||||
|
@ -145,7 +146,7 @@ async def test_hls_stream(
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -185,7 +186,7 @@ async def test_hls_stream(
|
||||||
assert stream.get_diagnostics() == {
|
assert stream.get_diagnostics() == {
|
||||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
"keepalive": False,
|
"keepalive": False,
|
||||||
"orientation": 1,
|
"orientation": Orientation.NO_TRANSFORM,
|
||||||
"start_worker": 1,
|
"start_worker": 1,
|
||||||
"video_codec": "h264",
|
"video_codec": "h264",
|
||||||
"worker_error": 1,
|
"worker_error": 1,
|
||||||
|
@ -199,7 +200,7 @@ async def test_stream_timeout(
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
available_states = []
|
available_states = []
|
||||||
|
|
||||||
|
@ -252,7 +253,7 @@ async def test_stream_timeout_after_stop(
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -272,7 +273,7 @@ async def test_stream_retries(hass, setup_component, should_retry):
|
||||||
"""Test hls stream is retried on failure."""
|
"""Test hls stream is retried on failure."""
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
source = "test_stream_keepalive_source"
|
source = "test_stream_keepalive_source"
|
||||||
stream = create_stream(hass, source, {})
|
stream = create_stream(hass, source, {}, dynamic_stream_settings())
|
||||||
track = stream.add_provider(HLS_PROVIDER)
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
track.num_segments = 2
|
track.num_segments = 2
|
||||||
|
|
||||||
|
@ -320,7 +321,7 @@ async def test_stream_retries(hass, setup_component, should_retry):
|
||||||
|
|
||||||
async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream):
|
async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream):
|
||||||
"""Test rendering the hls playlist with no output segments."""
|
"""Test rendering the hls playlist with no output segments."""
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls_client = await hls_stream(stream)
|
hls_client = await hls_stream(stream)
|
||||||
|
@ -332,7 +333,7 @@ async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream):
|
||||||
|
|
||||||
async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worker_sync):
|
async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worker_sync):
|
||||||
"""Test rendering the hls playlist with 1 and 2 output segments."""
|
"""Test rendering the hls playlist with 1 and 2 output segments."""
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -363,7 +364,7 @@ async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worke
|
||||||
|
|
||||||
async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker_sync):
|
async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker_sync):
|
||||||
"""Test rendering the hls playlist with more segments than the segment deque can hold."""
|
"""Test rendering the hls playlist with more segments than the segment deque can hold."""
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -415,7 +416,7 @@ async def test_hls_playlist_view_discontinuity(
|
||||||
):
|
):
|
||||||
"""Test a discontinuity across segments in the stream with 3 segments."""
|
"""Test a discontinuity across segments in the stream with 3 segments."""
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -452,7 +453,7 @@ async def test_hls_max_segments_discontinuity(
|
||||||
hass, setup_component, hls_stream, stream_worker_sync
|
hass, setup_component, hls_stream, stream_worker_sync
|
||||||
):
|
):
|
||||||
"""Test a discontinuity with more segments than the segment deque can hold."""
|
"""Test a discontinuity with more segments than the segment deque can hold."""
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -495,7 +496,7 @@ async def test_remove_incomplete_segment_on_exit(
|
||||||
hass, setup_component, stream_worker_sync
|
hass, setup_component, stream_worker_sync
|
||||||
):
|
):
|
||||||
"""Test that the incomplete segment gets removed when the worker thread quits."""
|
"""Test that the incomplete segment gets removed when the worker thread quits."""
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
await stream.start()
|
await stream.start()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -536,7 +537,7 @@ async def test_hls_stream_rotate(
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -549,14 +550,14 @@ async def test_hls_stream_rotate(
|
||||||
assert master_playlist_response.status == HTTPStatus.OK
|
assert master_playlist_response.status == HTTPStatus.OK
|
||||||
|
|
||||||
# Fetch rotated init
|
# Fetch rotated init
|
||||||
stream.orientation = 6
|
stream.dynamic_stream_settings.orientation = Orientation.ROTATE_LEFT
|
||||||
init_response = await hls_client.get("/init.mp4")
|
init_response = await hls_client.get("/init.mp4")
|
||||||
assert init_response.status == HTTPStatus.OK
|
assert init_response.status == HTTPStatus.OK
|
||||||
init = await init_response.read()
|
init = await init_response.read()
|
||||||
|
|
||||||
stream_worker_sync.resume()
|
stream_worker_sync.resume()
|
||||||
|
|
||||||
assert_mp4_has_transform_matrix(init, stream.orientation)
|
assert_mp4_has_transform_matrix(init, stream.dynamic_stream_settings.orientation)
|
||||||
|
|
||||||
# Stop stream, if it hasn't quit already
|
# Stop stream, if it hasn't quit already
|
||||||
await stream.stop()
|
await stream.stop()
|
||||||
|
|
|
@ -22,7 +22,12 @@ from homeassistant.components.stream.const import (
|
||||||
from homeassistant.components.stream.core import Part
|
from homeassistant.components.stream.core import Part
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .common import FAKE_TIME, DefaultSegment as Segment, generate_h264_video
|
from .common import (
|
||||||
|
FAKE_TIME,
|
||||||
|
DefaultSegment as Segment,
|
||||||
|
dynamic_stream_settings,
|
||||||
|
generate_h264_video,
|
||||||
|
)
|
||||||
from .test_hls import STREAM_SOURCE, HlsClient, make_playlist
|
from .test_hls import STREAM_SOURCE, HlsClient, make_playlist
|
||||||
|
|
||||||
SEGMENT_DURATION = 6
|
SEGMENT_DURATION = 6
|
||||||
|
@ -135,7 +140,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync):
|
||||||
num_playlist_segments = 3
|
num_playlist_segments = 3
|
||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
source = generate_h264_video(duration=num_playlist_segments * SEGMENT_DURATION + 2)
|
source = generate_h264_video(duration=num_playlist_segments * SEGMENT_DURATION + 2)
|
||||||
stream = create_stream(hass, source, {})
|
stream = create_stream(hass, source, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -259,7 +264,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -328,7 +333,7 @@ async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -393,7 +398,7 @@ async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream, stream_worker_sync
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -462,7 +467,7 @@ async def test_ll_hls_playlist_rollover_part(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -541,7 +546,7 @@ async def test_ll_hls_playlist_msn_part(hass, hls_stream, stream_worker_sync, hl
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -607,7 +612,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
|
|
||||||
hls = stream.add_provider(HLS_PROVIDER)
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.stream.const import (
|
||||||
OUTPUT_IDLE_TIMEOUT,
|
OUTPUT_IDLE_TIMEOUT,
|
||||||
RECORDER_PROVIDER,
|
RECORDER_PROVIDER,
|
||||||
)
|
)
|
||||||
from homeassistant.components.stream.core import Part
|
from homeassistant.components.stream.core import Orientation, Part
|
||||||
from homeassistant.components.stream.fmp4utils import find_box
|
from homeassistant.components.stream.fmp4utils import find_box
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
@ -23,6 +23,7 @@ import homeassistant.util.dt as dt_util
|
||||||
from .common import (
|
from .common import (
|
||||||
DefaultSegment as Segment,
|
DefaultSegment as Segment,
|
||||||
assert_mp4_has_transform_matrix,
|
assert_mp4_has_transform_matrix,
|
||||||
|
dynamic_stream_settings,
|
||||||
generate_h264_video,
|
generate_h264_video,
|
||||||
remux_with_audio,
|
remux_with_audio,
|
||||||
)
|
)
|
||||||
|
@ -56,7 +57,7 @@ async def test_record_stream(hass, filename, h264_video):
|
||||||
worker_finished.set()
|
worker_finished.set()
|
||||||
|
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
|
@ -79,7 +80,7 @@ async def test_record_stream(hass, filename, h264_video):
|
||||||
async def test_record_lookback(hass, filename, h264_video):
|
async def test_record_lookback(hass, filename, h264_video):
|
||||||
"""Exercise record with lookback."""
|
"""Exercise record with lookback."""
|
||||||
|
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Start an HLS feed to enable lookback
|
# Start an HLS feed to enable lookback
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
@ -96,7 +97,7 @@ async def test_record_lookback(hass, filename, h264_video):
|
||||||
async def test_record_path_not_allowed(hass, h264_video):
|
async def test_record_path_not_allowed(hass, h264_video):
|
||||||
"""Test where the output path is not allowed by home assistant configuration."""
|
"""Test where the output path is not allowed by home assistant configuration."""
|
||||||
|
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
with patch.object(
|
with patch.object(
|
||||||
hass.config, "is_allowed_path", return_value=False
|
hass.config, "is_allowed_path", return_value=False
|
||||||
), pytest.raises(HomeAssistantError):
|
), pytest.raises(HomeAssistantError):
|
||||||
|
@ -146,7 +147,7 @@ async def test_recorder_discontinuity(hass, filename, h264_video):
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True), patch(
|
with patch.object(hass.config, "is_allowed_path", return_value=True), patch(
|
||||||
"homeassistant.components.stream.Stream", wraps=MockStream
|
"homeassistant.components.stream.Stream", wraps=MockStream
|
||||||
), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"):
|
), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"):
|
||||||
stream = create_stream(hass, "blank", {})
|
stream = create_stream(hass, "blank", {}, dynamic_stream_settings())
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
await provider_ready.wait()
|
await provider_ready.wait()
|
||||||
|
|
||||||
|
@ -166,7 +167,7 @@ async def test_recorder_discontinuity(hass, filename, h264_video):
|
||||||
async def test_recorder_no_segments(hass, filename):
|
async def test_recorder_no_segments(hass, filename):
|
||||||
"""Test recorder behavior with a stream failure which causes no segments."""
|
"""Test recorder behavior with a stream failure which causes no segments."""
|
||||||
|
|
||||||
stream = create_stream(hass, BytesIO(), {})
|
stream = create_stream(hass, BytesIO(), {}, dynamic_stream_settings())
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
|
@ -219,7 +220,7 @@ async def test_record_stream_audio(
|
||||||
worker_finished.set()
|
worker_finished.set()
|
||||||
|
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
||||||
stream = create_stream(hass, source, {})
|
stream = create_stream(hass, source, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
|
@ -252,7 +253,9 @@ async def test_record_stream_audio(
|
||||||
|
|
||||||
async def test_recorder_log(hass, filename, caplog):
|
async def test_recorder_log(hass, filename, caplog):
|
||||||
"""Test starting a stream to record logs the url without username and password."""
|
"""Test starting a stream to record logs the url without username and password."""
|
||||||
stream = create_stream(hass, "https://abcd:efgh@foo.bar", {})
|
stream = create_stream(
|
||||||
|
hass, "https://abcd:efgh@foo.bar", {}, dynamic_stream_settings()
|
||||||
|
)
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
await stream.async_record(filename)
|
await stream.async_record(filename)
|
||||||
assert "https://abcd:efgh@foo.bar" not in caplog.text
|
assert "https://abcd:efgh@foo.bar" not in caplog.text
|
||||||
|
@ -273,8 +276,8 @@ async def test_record_stream_rotate(hass, filename, h264_video):
|
||||||
worker_finished.set()
|
worker_finished.set()
|
||||||
|
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
stream.orientation = 8
|
stream.dynamic_stream_settings.orientation = Orientation.ROTATE_RIGHT
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
|
@ -293,4 +296,6 @@ async def test_record_stream_rotate(hass, filename, h264_video):
|
||||||
# Assert
|
# Assert
|
||||||
assert os.path.exists(filename)
|
assert os.path.exists(filename)
|
||||||
with open(filename, "rb") as rotated_mp4:
|
with open(filename, "rb") as rotated_mp4:
|
||||||
assert_mp4_has_transform_matrix(rotated_mp4.read(), stream.orientation)
|
assert_mp4_has_transform_matrix(
|
||||||
|
rotated_mp4.read(), stream.dynamic_stream_settings.orientation
|
||||||
|
)
|
||||||
|
|
|
@ -39,7 +39,7 @@ from homeassistant.components.stream.const import (
|
||||||
SEGMENT_DURATION_ADJUSTER,
|
SEGMENT_DURATION_ADJUSTER,
|
||||||
TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
)
|
)
|
||||||
from homeassistant.components.stream.core import StreamSettings
|
from homeassistant.components.stream.core import Orientation, StreamSettings
|
||||||
from homeassistant.components.stream.worker import (
|
from homeassistant.components.stream.worker import (
|
||||||
StreamEndedError,
|
StreamEndedError,
|
||||||
StreamState,
|
StreamState,
|
||||||
|
@ -48,7 +48,7 @@ from homeassistant.components.stream.worker import (
|
||||||
)
|
)
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .common import generate_h264_video, generate_h265_video
|
from .common import dynamic_stream_settings, generate_h264_video, generate_h265_video
|
||||||
from .test_ll_hls import TEST_PART_DURATION
|
from .test_ll_hls import TEST_PART_DURATION
|
||||||
|
|
||||||
from tests.components.camera.common import EMPTY_8_6_JPEG, mock_turbo_jpeg
|
from tests.components.camera.common import EMPTY_8_6_JPEG, mock_turbo_jpeg
|
||||||
|
@ -90,7 +90,6 @@ def mock_stream_settings(hass):
|
||||||
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
hls_advance_part_limit=3,
|
hls_advance_part_limit=3,
|
||||||
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
orientation=1,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +286,7 @@ def run_worker(hass, stream, stream_source, stream_settings=None):
|
||||||
{},
|
{},
|
||||||
stream_settings or hass.data[DOMAIN][ATTR_SETTINGS],
|
stream_settings or hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
stream_state,
|
stream_state,
|
||||||
KeyFrameConverter(hass, 1),
|
KeyFrameConverter(hass, stream_settings, dynamic_stream_settings()),
|
||||||
threading.Event(),
|
threading.Event(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -295,7 +294,11 @@ def run_worker(hass, stream, stream_source, stream_settings=None):
|
||||||
async def async_decode_stream(hass, packets, py_av=None, stream_settings=None):
|
async def async_decode_stream(hass, packets, py_av=None, stream_settings=None):
|
||||||
"""Start a stream worker that decodes incoming stream packets into output segments."""
|
"""Start a stream worker that decodes incoming stream packets into output segments."""
|
||||||
stream = Stream(
|
stream = Stream(
|
||||||
hass, STREAM_SOURCE, {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS]
|
hass,
|
||||||
|
STREAM_SOURCE,
|
||||||
|
{},
|
||||||
|
stream_settings or hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
|
dynamic_stream_settings(),
|
||||||
)
|
)
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -322,7 +325,13 @@ async def async_decode_stream(hass, packets, py_av=None, stream_settings=None):
|
||||||
|
|
||||||
async def test_stream_open_fails(hass):
|
async def test_stream_open_fails(hass):
|
||||||
"""Test failure on stream open."""
|
"""Test failure on stream open."""
|
||||||
stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS])
|
stream = Stream(
|
||||||
|
hass,
|
||||||
|
STREAM_SOURCE,
|
||||||
|
{},
|
||||||
|
hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
|
dynamic_stream_settings(),
|
||||||
|
)
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
with patch("av.open") as av_open, pytest.raises(StreamWorkerError):
|
with patch("av.open") as av_open, pytest.raises(StreamWorkerError):
|
||||||
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
||||||
|
@ -636,7 +645,13 @@ async def test_stream_stopped_while_decoding(hass):
|
||||||
worker_open = threading.Event()
|
worker_open = threading.Event()
|
||||||
worker_wake = threading.Event()
|
worker_wake = threading.Event()
|
||||||
|
|
||||||
stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS])
|
stream = Stream(
|
||||||
|
hass,
|
||||||
|
STREAM_SOURCE,
|
||||||
|
{},
|
||||||
|
hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
|
dynamic_stream_settings(),
|
||||||
|
)
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
py_av = MockPyAv()
|
py_av = MockPyAv()
|
||||||
|
@ -666,7 +681,13 @@ async def test_update_stream_source(hass):
|
||||||
worker_open = threading.Event()
|
worker_open = threading.Event()
|
||||||
worker_wake = threading.Event()
|
worker_wake = threading.Event()
|
||||||
|
|
||||||
stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS])
|
stream = Stream(
|
||||||
|
hass,
|
||||||
|
STREAM_SOURCE,
|
||||||
|
{},
|
||||||
|
hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
|
dynamic_stream_settings(),
|
||||||
|
)
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
# Note that retries are disabled by default in tests, however the stream is "restarted" when
|
# Note that retries are disabled by default in tests, however the stream is "restarted" when
|
||||||
# the stream source is updated.
|
# the stream source is updated.
|
||||||
|
@ -709,7 +730,11 @@ async def test_update_stream_source(hass):
|
||||||
async def test_worker_log(hass, caplog):
|
async def test_worker_log(hass, caplog):
|
||||||
"""Test that the worker logs the url without username and password."""
|
"""Test that the worker logs the url without username and password."""
|
||||||
stream = Stream(
|
stream = Stream(
|
||||||
hass, "https://abcd:efgh@foo.bar", {}, hass.data[DOMAIN][ATTR_SETTINGS]
|
hass,
|
||||||
|
"https://abcd:efgh@foo.bar",
|
||||||
|
{},
|
||||||
|
hass.data[DOMAIN][ATTR_SETTINGS],
|
||||||
|
dynamic_stream_settings(),
|
||||||
)
|
)
|
||||||
stream.add_provider(HLS_PROVIDER)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
|
@ -764,7 +789,9 @@ async def test_durations(hass, worker_finished_stream):
|
||||||
worker_finished, mock_stream = worker_finished_stream
|
worker_finished, mock_stream = worker_finished_stream
|
||||||
|
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
||||||
stream = create_stream(hass, source, {}, stream_label="camera")
|
stream = create_stream(
|
||||||
|
hass, source, {}, dynamic_stream_settings(), stream_label="camera"
|
||||||
|
)
|
||||||
|
|
||||||
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
|
@ -839,7 +866,9 @@ async def test_has_keyframe(hass, h264_video, worker_finished_stream):
|
||||||
worker_finished, mock_stream = worker_finished_stream
|
worker_finished, mock_stream = worker_finished_stream
|
||||||
|
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
||||||
stream = create_stream(hass, h264_video, {}, stream_label="camera")
|
stream = create_stream(
|
||||||
|
hass, h264_video, {}, dynamic_stream_settings(), stream_label="camera"
|
||||||
|
)
|
||||||
|
|
||||||
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
|
@ -880,7 +909,9 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream):
|
||||||
|
|
||||||
worker_finished, mock_stream = worker_finished_stream
|
worker_finished, mock_stream = worker_finished_stream
|
||||||
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
with patch("homeassistant.components.stream.Stream", wraps=mock_stream):
|
||||||
stream = create_stream(hass, source, {}, stream_label="camera")
|
stream = create_stream(
|
||||||
|
hass, source, {}, dynamic_stream_settings(), stream_label="camera"
|
||||||
|
)
|
||||||
|
|
||||||
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
|
@ -900,7 +931,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream):
|
||||||
assert stream.get_diagnostics() == {
|
assert stream.get_diagnostics() == {
|
||||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
"keepalive": False,
|
"keepalive": False,
|
||||||
"orientation": 1,
|
"orientation": Orientation.NO_TRANSFORM,
|
||||||
"start_worker": 1,
|
"start_worker": 1,
|
||||||
"video_codec": "hevc",
|
"video_codec": "hevc",
|
||||||
"worker_error": 1,
|
"worker_error": 1,
|
||||||
|
@ -916,7 +947,7 @@ async def test_get_image(hass, h264_video, filename):
|
||||||
"homeassistant.components.camera.img_util.TurboJPEGSingleton"
|
"homeassistant.components.camera.img_util.TurboJPEGSingleton"
|
||||||
) as mock_turbo_jpeg_singleton:
|
) as mock_turbo_jpeg_singleton:
|
||||||
mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
|
mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
|
@ -937,7 +968,6 @@ async def test_worker_disable_ll_hls(hass):
|
||||||
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
hls_advance_part_limit=3,
|
hls_advance_part_limit=3,
|
||||||
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||||
orientation=1,
|
|
||||||
)
|
)
|
||||||
py_av = MockPyAv()
|
py_av = MockPyAv()
|
||||||
py_av.container.format.name = "hls"
|
py_av.container.format.name = "hls"
|
||||||
|
@ -959,9 +989,9 @@ async def test_get_image_rotated(hass, h264_video, filename):
|
||||||
"homeassistant.components.camera.img_util.TurboJPEGSingleton"
|
"homeassistant.components.camera.img_util.TurboJPEGSingleton"
|
||||||
) as mock_turbo_jpeg_singleton:
|
) as mock_turbo_jpeg_singleton:
|
||||||
mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
|
mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
|
||||||
for orientation in (1, 8):
|
for orientation in (Orientation.NO_TRANSFORM, Orientation.ROTATE_RIGHT):
|
||||||
stream = create_stream(hass, h264_video, {})
|
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||||
stream._stream_settings.orientation = orientation
|
stream.dynamic_stream_settings.orientation = orientation
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue