Refactor camera stream settings (#81663)

This commit is contained in:
uvjustin 2022-11-13 01:22:59 +08:00 committed by GitHub
parent 1fe85c9b17
commit ee910bd0e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 226 additions and 152 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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