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
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
|
@ -74,7 +74,7 @@ from .const import ( # noqa: F401
|
|||
StreamType,
|
||||
)
|
||||
from .img_util import scale_jpeg_camera_image
|
||||
from .prefs import CameraPreferences
|
||||
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -346,7 +346,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
)
|
||||
|
||||
prefs = CameraPreferences(hass)
|
||||
await prefs.async_initialize()
|
||||
hass.data[DATA_CAMERA_PREFS] = prefs
|
||||
|
||||
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:
|
||||
for camera in component.entities:
|
||||
camera_prefs = prefs.get(camera.entity_id)
|
||||
if not camera_prefs.preload_stream:
|
||||
stream_prefs = await prefs.get_dynamic_stream_settings(camera.entity_id)
|
||||
if not stream_prefs.preload_stream:
|
||||
continue
|
||||
stream = await camera.async_create_stream()
|
||||
if not stream:
|
||||
continue
|
||||
stream.keepalive = True
|
||||
stream.add_provider("hls")
|
||||
await stream.start()
|
||||
|
||||
|
@ -524,6 +522,9 @@ class Camera(Entity):
|
|||
self.hass,
|
||||
source,
|
||||
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,
|
||||
)
|
||||
self.stream.set_update_callback(self.async_write_ha_state)
|
||||
|
@ -861,8 +862,8 @@ async def websocket_get_prefs(
|
|||
) -> None:
|
||||
"""Handle request for account info."""
|
||||
prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS]
|
||||
camera_prefs = prefs.get(msg["entity_id"])
|
||||
connection.send_result(msg["id"], camera_prefs.as_dict())
|
||||
stream_prefs = await prefs.get_dynamic_stream_settings(msg["entity_id"])
|
||||
connection.send_result(msg["id"], asdict(stream_prefs))
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
@ -956,12 +957,6 @@ async def _async_stream_endpoint_url(
|
|||
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)
|
||||
await stream.start()
|
||||
return stream.endpoint_url(fmt)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Preference management for camera component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Final, Union, cast
|
||||
|
||||
from homeassistant.components.stream import Orientation
|
||||
|
@ -16,28 +17,12 @@ STORAGE_KEY: Final = DOMAIN
|
|||
STORAGE_VERSION: Final = 1
|
||||
|
||||
|
||||
class CameraEntityPreferences:
|
||||
"""Handle preferences for camera entity."""
|
||||
@dataclass
|
||||
class DynamicStreamSettings:
|
||||
"""Stream settings which are managed and updated by the camera entity."""
|
||||
|
||||
def __init__(self, prefs: dict[str, bool | Orientation]) -> None:
|
||||
"""Initialize prefs."""
|
||||
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)
|
||||
)
|
||||
preload_stream: bool = False
|
||||
orientation: Orientation = Orientation.NO_TRANSFORM
|
||||
|
||||
|
||||
class CameraPreferences:
|
||||
|
@ -51,15 +36,9 @@ class CameraPreferences:
|
|||
self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]](
|
||||
hass, STORAGE_VERSION, STORAGE_KEY
|
||||
)
|
||||
# Local copy of the preload_stream prefs
|
||||
self._prefs: dict[str, dict[str, bool | Orientation]] | None = None
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
"""Finish initializing the preferences."""
|
||||
if (prefs := await self._store.async_load()) is None:
|
||||
prefs = {}
|
||||
|
||||
self._prefs = prefs
|
||||
self._dynamic_stream_settings_by_entity_id: dict[
|
||||
str, DynamicStreamSettings
|
||||
] = {}
|
||||
|
||||
async def async_update(
|
||||
self,
|
||||
|
@ -67,20 +46,25 @@ class CameraPreferences:
|
|||
*,
|
||||
preload_stream: bool | UndefinedType = UNDEFINED,
|
||||
orientation: Orientation | UndefinedType = UNDEFINED,
|
||||
stream_options: dict[str, str] | UndefinedType = UNDEFINED,
|
||||
) -> dict[str, bool | Orientation]:
|
||||
"""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.
|
||||
Raises HomeAssistantError on failure.
|
||||
"""
|
||||
dynamic_stream_settings = self._dynamic_stream_settings_by_entity_id.get(
|
||||
entity_id
|
||||
)
|
||||
if preload_stream is not UNDEFINED:
|
||||
# Prefs already initialized.
|
||||
assert self._prefs is not None
|
||||
if not self._prefs.get(entity_id):
|
||||
self._prefs[entity_id] = {}
|
||||
self._prefs[entity_id][PREF_PRELOAD_STREAM] = preload_stream
|
||||
await self._store.async_save(self._prefs)
|
||||
if dynamic_stream_settings:
|
||||
dynamic_stream_settings.preload_stream = preload_stream
|
||||
preload_prefs = await self._store.async_load() or {}
|
||||
preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
|
||||
await self._store.async_save(preload_prefs)
|
||||
|
||||
if orientation is not UNDEFINED:
|
||||
if (registry := er.async_get(self._hass)).async_get(entity_id):
|
||||
|
@ -91,12 +75,26 @@ class CameraPreferences:
|
|||
raise HomeAssistantError(
|
||||
"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:
|
||||
"""Get preferences for an entity."""
|
||||
# Prefs are already initialized.
|
||||
assert self._prefs is not None
|
||||
async def get_dynamic_stream_settings(
|
||||
self, entity_id: str
|
||||
) -> DynamicStreamSettings:
|
||||
"""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)
|
||||
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 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.stream import (
|
||||
CONF_RTSP_TRANSPORT,
|
||||
|
@ -246,7 +250,13 @@ async def async_test_stream(
|
|||
url = url.with_user(username).with_password(password)
|
||||
stream_source = str(url)
|
||||
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)
|
||||
await stream.start()
|
||||
if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
|
||||
|
|
|
@ -137,7 +137,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
|
|||
) -> bytes | None:
|
||||
"""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)
|
||||
|
||||
if self.device.capabilities.snapshot:
|
||||
|
|
|
@ -25,7 +25,7 @@ import secrets
|
|||
import threading
|
||||
import time
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -70,6 +70,9 @@ from .core import (
|
|||
from .diagnostics import Diagnostics
|
||||
from .hls import HlsStreamOutput, async_setup_hls
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.camera import DynamicStreamSettings
|
||||
|
||||
__all__ = [
|
||||
"ATTR_SETTINGS",
|
||||
"CONF_EXTRA_PART_WAIT_TIME",
|
||||
|
@ -105,6 +108,7 @@ def create_stream(
|
|||
hass: HomeAssistant,
|
||||
stream_source: str,
|
||||
options: Mapping[str, str | bool | float],
|
||||
dynamic_stream_settings: DynamicStreamSettings,
|
||||
stream_label: str | None = None,
|
||||
) -> Stream:
|
||||
"""Create a stream with the specified identfier based on the source url.
|
||||
|
@ -156,6 +160,7 @@ def create_stream(
|
|||
stream_source,
|
||||
pyav_options=pyav_options,
|
||||
stream_settings=stream_settings,
|
||||
dynamic_stream_settings=dynamic_stream_settings,
|
||||
stream_label=stream_label,
|
||||
)
|
||||
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],
|
||||
hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3),
|
||||
hls_part_timeout=2 * conf[CONF_PART_DURATION],
|
||||
orientation=Orientation.NO_TRANSFORM,
|
||||
)
|
||||
else:
|
||||
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:
|
||||
"""Stop all stream workers."""
|
||||
for stream in hass.data[DOMAIN][ATTR_STREAMS]:
|
||||
stream.keepalive = False
|
||||
stream.dynamic_stream_settings.preload_stream = False
|
||||
if awaitables := [
|
||||
asyncio.create_task(stream.stop())
|
||||
for stream in hass.data[DOMAIN][ATTR_STREAMS]
|
||||
|
@ -268,6 +272,7 @@ class Stream:
|
|||
source: str,
|
||||
pyav_options: dict[str, str],
|
||||
stream_settings: StreamSettings,
|
||||
dynamic_stream_settings: DynamicStreamSettings,
|
||||
stream_label: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a stream."""
|
||||
|
@ -276,14 +281,16 @@ class Stream:
|
|||
self.pyav_options = pyav_options
|
||||
self._stream_settings = stream_settings
|
||||
self._stream_label = stream_label
|
||||
self.keepalive = False
|
||||
self.dynamic_stream_settings = dynamic_stream_settings
|
||||
self.access_token: str | None = None
|
||||
self._start_stop_lock = asyncio.Lock()
|
||||
self._thread: threading.Thread | None = None
|
||||
self._thread_quit = threading.Event()
|
||||
self._outputs: dict[str, StreamOutput] = {}
|
||||
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._update_callback: Callable[[], None] | None = None
|
||||
self._logger = (
|
||||
|
@ -293,16 +300,6 @@ class Stream:
|
|||
)
|
||||
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:
|
||||
"""Start the stream and returns a url for the output format."""
|
||||
if fmt not in self._outputs:
|
||||
|
@ -326,7 +323,8 @@ class Stream:
|
|||
|
||||
async def idle_callback() -> None:
|
||||
if (
|
||||
not self.keepalive or fmt == RECORDER_PROVIDER
|
||||
not self.dynamic_stream_settings.preload_stream
|
||||
or fmt == RECORDER_PROVIDER
|
||||
) and fmt in self._outputs:
|
||||
await self.remove_provider(self._outputs[fmt])
|
||||
self.check_idle()
|
||||
|
@ -335,6 +333,7 @@ class Stream:
|
|||
self.hass,
|
||||
IdleTimer(self.hass, timeout, idle_callback),
|
||||
self._stream_settings,
|
||||
self.dynamic_stream_settings,
|
||||
)
|
||||
self._outputs[fmt] = provider
|
||||
|
||||
|
@ -413,8 +412,12 @@ class Stream:
|
|||
while not self._thread_quit.wait(timeout=wait_timeout):
|
||||
start_time = time.time()
|
||||
self.hass.add_job(self._async_update_state, True)
|
||||
self._diagnostics.set_value("keepalive", self.keepalive)
|
||||
self._diagnostics.set_value("orientation", self.orientation)
|
||||
self._diagnostics.set_value(
|
||||
"keepalive", self.dynamic_stream_settings.preload_stream
|
||||
)
|
||||
self._diagnostics.set_value(
|
||||
"orientation", self.dynamic_stream_settings.orientation
|
||||
)
|
||||
self._diagnostics.increment("start_worker")
|
||||
try:
|
||||
stream_worker(
|
||||
|
@ -473,7 +476,7 @@ class Stream:
|
|||
self._outputs = {}
|
||||
self.access_token = None
|
||||
|
||||
if not self.keepalive:
|
||||
if not self.dynamic_stream_settings.preload_stream:
|
||||
await self._stop()
|
||||
|
||||
async def _stop(self) -> None:
|
||||
|
|
|
@ -29,6 +29,8 @@ from .const import (
|
|||
if TYPE_CHECKING:
|
||||
from av import CodecContext, Packet
|
||||
|
||||
from homeassistant.components.camera import DynamicStreamSettings
|
||||
|
||||
from . import Stream
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -58,7 +60,6 @@ class StreamSettings:
|
|||
part_target_duration: float = attr.ib()
|
||||
hls_advance_part_limit: int = attr.ib()
|
||||
hls_part_timeout: float = attr.ib()
|
||||
orientation: Orientation = attr.ib()
|
||||
|
||||
|
||||
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,
|
||||
hls_advance_part_limit=3,
|
||||
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||
orientation=Orientation.NO_TRANSFORM,
|
||||
)
|
||||
|
||||
|
||||
|
@ -273,12 +273,14 @@ class StreamOutput:
|
|||
hass: HomeAssistant,
|
||||
idle_timer: IdleTimer,
|
||||
stream_settings: StreamSettings,
|
||||
dynamic_stream_settings: DynamicStreamSettings,
|
||||
deque_maxlen: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize a stream output."""
|
||||
self._hass = hass
|
||||
self.idle_timer = idle_timer
|
||||
self.stream_settings = stream_settings
|
||||
self.dynamic_stream_settings = dynamic_stream_settings
|
||||
self._event = asyncio.Event()
|
||||
self._part_event = asyncio.Event()
|
||||
self._segments: deque[Segment] = deque(maxlen=deque_maxlen)
|
||||
|
@ -427,7 +429,12 @@ class KeyFrameConverter:
|
|||
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."""
|
||||
|
||||
# Keep import here so that we can import stream integration without installing reqs
|
||||
|
@ -441,6 +448,7 @@ class KeyFrameConverter:
|
|||
self._lock = asyncio.Lock()
|
||||
self._codec_context: CodecContext | None = None
|
||||
self._stream_settings = stream_settings
|
||||
self._dynamic_stream_settings = dynamic_stream_settings
|
||||
|
||||
def create_codec_context(self, codec_context: CodecContext) -> None:
|
||||
"""
|
||||
|
@ -498,12 +506,13 @@ class KeyFrameConverter:
|
|||
if frames:
|
||||
frame = frames[0]
|
||||
if width and height:
|
||||
if self._stream_settings.orientation >= 5:
|
||||
if self._dynamic_stream_settings.orientation >= 5:
|
||||
frame = frame.reformat(width=height, height=width)
|
||||
else:
|
||||
frame = frame.reformat(width=width, height=height)
|
||||
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))
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ from .core import (
|
|||
from .fmp4utils import get_codec_string, transform_init
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.camera import DynamicStreamSettings
|
||||
|
||||
from . import Stream
|
||||
|
||||
|
||||
|
@ -50,9 +52,16 @@ class HlsStreamOutput(StreamOutput):
|
|||
hass: HomeAssistant,
|
||||
idle_timer: IdleTimer,
|
||||
stream_settings: StreamSettings,
|
||||
dynamic_stream_settings: DynamicStreamSettings,
|
||||
) -> None:
|
||||
"""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
|
||||
|
||||
@property
|
||||
|
@ -339,7 +348,7 @@ class HlsInitView(StreamView):
|
|||
if not (segments := track.get_segments()) or not (body := segments[0].init):
|
||||
return web.HTTPNotFound()
|
||||
return web.Response(
|
||||
body=transform_init(body, stream.orientation),
|
||||
body=transform_init(body, stream.dynamic_stream_settings.orientation),
|
||||
headers={"Content-Type": "video/mp4"},
|
||||
)
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ from .fmp4utils import read_init, transform_init
|
|||
if TYPE_CHECKING:
|
||||
import deque
|
||||
|
||||
from homeassistant.components.camera import DynamicStreamSettings
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -38,9 +40,10 @@ class RecorderOutput(StreamOutput):
|
|||
hass: HomeAssistant,
|
||||
idle_timer: IdleTimer,
|
||||
stream_settings: StreamSettings,
|
||||
dynamic_stream_settings: DynamicStreamSettings,
|
||||
) -> None:
|
||||
"""Initialize recorder output."""
|
||||
super().__init__(hass, idle_timer, stream_settings)
|
||||
super().__init__(hass, idle_timer, stream_settings, dynamic_stream_settings)
|
||||
self.video_path: str
|
||||
|
||||
@property
|
||||
|
@ -154,7 +157,7 @@ class RecorderOutput(StreamOutput):
|
|||
video_path, mode="wb"
|
||||
) as out_file:
|
||||
init = transform_init(
|
||||
read_init(in_file), self.stream_settings.orientation
|
||||
read_init(in_file), self.dynamic_stream_settings.orientation
|
||||
)
|
||||
out_file.write(init)
|
||||
in_file.seek(len(init))
|
||||
|
|
|
@ -12,7 +12,6 @@ from homeassistant.components.camera.const import (
|
|||
PREF_ORIENTATION,
|
||||
PREF_PRELOAD_STREAM,
|
||||
)
|
||||
from homeassistant.components.camera.prefs import CameraEntityPreferences
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
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()
|
||||
|
||||
# There should be no preferences
|
||||
assert not msg["result"]
|
||||
# The default prefs should be returned. Preload stream should be False
|
||||
assert msg["success"]
|
||||
assert msg["result"][PREF_PRELOAD_STREAM] is False
|
||||
|
||||
# Update the preference
|
||||
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):
|
||||
"""Test camera preload preference."""
|
||||
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False})
|
||||
demo_settings = camera.DynamicStreamSettings()
|
||||
with patch(
|
||||
"homeassistant.components.camera.Stream.endpoint_url",
|
||||
) as mock_request_stream, patch(
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get",
|
||||
return_value=demo_prefs,
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||
return_value=demo_settings,
|
||||
), patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
new_callable=PropertyMock,
|
||||
|
@ -440,12 +440,12 @@ async def test_no_preload_stream(hass, mock_stream):
|
|||
|
||||
async def test_preload_stream(hass, mock_stream):
|
||||
"""Test camera preload preference."""
|
||||
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True})
|
||||
demo_settings = camera.DynamicStreamSettings(preload_stream=True)
|
||||
with patch(
|
||||
"homeassistant.components.camera.create_stream"
|
||||
) as mock_create_stream, patch(
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get",
|
||||
return_value=demo_prefs,
|
||||
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
|
||||
return_value=demo_settings,
|
||||
), patch(
|
||||
"homeassistant.components.demo.camera.DemoCamera.stream_source",
|
||||
return_value="http://example.com",
|
||||
|
|
|
@ -8,7 +8,8 @@ import io
|
|||
import av
|
||||
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 (
|
||||
TRANSFORM_MATRIX_TOP,
|
||||
XYW_ROW,
|
||||
|
@ -16,8 +17,8 @@ from homeassistant.components.stream.fmp4utils import (
|
|||
)
|
||||
|
||||
FAKE_TIME = datetime.utcnow()
|
||||
# Segment with defaults filled in for use in tests
|
||||
|
||||
# Segment with defaults filled in for use in tests
|
||||
DefaultSegment = partial(
|
||||
Segment,
|
||||
init=None,
|
||||
|
@ -157,7 +158,7 @@ def remux_with_audio(source, container_format, audio_codec):
|
|||
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."""
|
||||
# Find 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]
|
||||
== 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,
|
||||
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
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -24,6 +24,7 @@ from .common import (
|
|||
FAKE_TIME,
|
||||
DefaultSegment as Segment,
|
||||
assert_mp4_has_transform_matrix,
|
||||
dynamic_stream_settings,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
@ -145,7 +146,7 @@ async def test_hls_stream(
|
|||
stream_worker_sync.pause()
|
||||
|
||||
# Setup demo HLS track
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
|
||||
# Request stream
|
||||
stream.add_provider(HLS_PROVIDER)
|
||||
|
@ -185,7 +186,7 @@ async def test_hls_stream(
|
|||
assert stream.get_diagnostics() == {
|
||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"keepalive": False,
|
||||
"orientation": 1,
|
||||
"orientation": Orientation.NO_TRANSFORM,
|
||||
"start_worker": 1,
|
||||
"video_codec": "h264",
|
||||
"worker_error": 1,
|
||||
|
@ -199,7 +200,7 @@ async def test_stream_timeout(
|
|||
stream_worker_sync.pause()
|
||||
|
||||
# Setup demo HLS track
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
|
||||
available_states = []
|
||||
|
||||
|
@ -252,7 +253,7 @@ async def test_stream_timeout_after_stop(
|
|||
stream_worker_sync.pause()
|
||||
|
||||
# Setup demo HLS track
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
|
||||
# Request stream
|
||||
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."""
|
||||
# Setup demo HLS track
|
||||
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.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):
|
||||
"""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)
|
||||
|
||||
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):
|
||||
"""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()
|
||||
hls = stream.add_provider(HLS_PROVIDER)
|
||||
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):
|
||||
"""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()
|
||||
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."""
|
||||
|
||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
||||
stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings())
|
||||
stream_worker_sync.pause()
|
||||
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
|
||||
):
|
||||
"""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()
|
||||
hls = stream.add_provider(HLS_PROVIDER)
|
||||
|
||||
|
@ -495,7 +496,7 @@ async def test_remove_incomplete_segment_on_exit(
|
|||
hass, setup_component, stream_worker_sync
|
||||
):
|
||||
"""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()
|
||||
await stream.start()
|
||||
hls = stream.add_provider(HLS_PROVIDER)
|
||||
|
@ -536,7 +537,7 @@ async def test_hls_stream_rotate(
|
|||
stream_worker_sync.pause()
|
||||
|
||||
# Setup demo HLS track
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
|
||||
# Request stream
|
||||
stream.add_provider(HLS_PROVIDER)
|
||||
|
@ -549,14 +550,14 @@ async def test_hls_stream_rotate(
|
|||
assert master_playlist_response.status == HTTPStatus.OK
|
||||
|
||||
# Fetch rotated init
|
||||
stream.orientation = 6
|
||||
stream.dynamic_stream_settings.orientation = Orientation.ROTATE_LEFT
|
||||
init_response = await hls_client.get("/init.mp4")
|
||||
assert init_response.status == HTTPStatus.OK
|
||||
init = await init_response.read()
|
||||
|
||||
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
|
||||
await stream.stop()
|
||||
|
|
|
@ -22,7 +22,12 @@ from homeassistant.components.stream.const import (
|
|||
from homeassistant.components.stream.core import Part
|
||||
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
|
||||
|
||||
SEGMENT_DURATION = 6
|
||||
|
@ -135,7 +140,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync):
|
|||
num_playlist_segments = 3
|
||||
# Setup demo HLS track
|
||||
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
|
||||
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()
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
hls = stream.add_provider(HLS_PROVIDER)
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.stream.const import (
|
|||
OUTPUT_IDLE_TIMEOUT,
|
||||
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.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
@ -23,6 +23,7 @@ import homeassistant.util.dt as dt_util
|
|||
from .common import (
|
||||
DefaultSegment as Segment,
|
||||
assert_mp4_has_transform_matrix,
|
||||
dynamic_stream_settings,
|
||||
generate_h264_video,
|
||||
remux_with_audio,
|
||||
)
|
||||
|
@ -56,7 +57,7 @@ async def test_record_stream(hass, filename, h264_video):
|
|||
worker_finished.set()
|
||||
|
||||
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):
|
||||
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):
|
||||
"""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
|
||||
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):
|
||||
"""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(
|
||||
hass.config, "is_allowed_path", return_value=False
|
||||
), 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(
|
||||
"homeassistant.components.stream.Stream", wraps=MockStream
|
||||
), 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))
|
||||
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):
|
||||
"""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
|
||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||
|
@ -219,7 +220,7 @@ async def test_record_stream_audio(
|
|||
worker_finished.set()
|
||||
|
||||
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):
|
||||
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):
|
||||
"""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):
|
||||
await stream.async_record(filename)
|
||||
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()
|
||||
|
||||
with patch("homeassistant.components.stream.Stream", wraps=MockStream):
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream.orientation = 8
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
stream.dynamic_stream_settings.orientation = Orientation.ROTATE_RIGHT
|
||||
|
||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||
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 os.path.exists(filename)
|
||||
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,
|
||||
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 (
|
||||
StreamEndedError,
|
||||
StreamState,
|
||||
|
@ -48,7 +48,7 @@ from homeassistant.components.stream.worker import (
|
|||
)
|
||||
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 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,
|
||||
hls_advance_part_limit=3,
|
||||
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_state,
|
||||
KeyFrameConverter(hass, 1),
|
||||
KeyFrameConverter(hass, stream_settings, dynamic_stream_settings()),
|
||||
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):
|
||||
"""Start a stream worker that decodes incoming stream packets into output segments."""
|
||||
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)
|
||||
|
||||
|
@ -322,7 +325,13 @@ async def async_decode_stream(hass, packets, py_av=None, stream_settings=None):
|
|||
|
||||
async def test_stream_open_fails(hass):
|
||||
"""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)
|
||||
with patch("av.open") as av_open, pytest.raises(StreamWorkerError):
|
||||
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_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)
|
||||
|
||||
py_av = MockPyAv()
|
||||
|
@ -666,7 +681,13 @@ async def test_update_stream_source(hass):
|
|||
worker_open = 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)
|
||||
# Note that retries are disabled by default in tests, however the stream is "restarted" when
|
||||
# the stream source is updated.
|
||||
|
@ -709,7 +730,11 @@ async def test_update_stream_source(hass):
|
|||
async def test_worker_log(hass, caplog):
|
||||
"""Test that the worker logs the url without username and password."""
|
||||
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)
|
||||
|
||||
|
@ -764,7 +789,9 @@ async def test_durations(hass, worker_finished_stream):
|
|||
worker_finished, mock_stream = worker_finished_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)
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
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)
|
||||
await stream.start()
|
||||
|
@ -900,7 +931,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream):
|
|||
assert stream.get_diagnostics() == {
|
||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"keepalive": False,
|
||||
"orientation": 1,
|
||||
"orientation": Orientation.NO_TRANSFORM,
|
||||
"start_worker": 1,
|
||||
"video_codec": "hevc",
|
||||
"worker_error": 1,
|
||||
|
@ -916,7 +947,7 @@ async def test_get_image(hass, h264_video, filename):
|
|||
"homeassistant.components.camera.img_util.TurboJPEGSingleton"
|
||||
) as mock_turbo_jpeg_singleton:
|
||||
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):
|
||||
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,
|
||||
hls_advance_part_limit=3,
|
||||
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||
orientation=1,
|
||||
)
|
||||
py_av = MockPyAv()
|
||||
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"
|
||||
) as mock_turbo_jpeg_singleton:
|
||||
mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
|
||||
for orientation in (1, 8):
|
||||
stream = create_stream(hass, h264_video, {})
|
||||
stream._stream_settings.orientation = orientation
|
||||
for orientation in (Orientation.NO_TRANSFORM, Orientation.ROTATE_RIGHT):
|
||||
stream = create_stream(hass, h264_video, {}, dynamic_stream_settings())
|
||||
stream.dynamic_stream_settings.orientation = orientation
|
||||
|
||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||
make_recording = hass.async_create_task(stream.async_record(filename))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue