Use IntEnum for stream orientation (#81835)

* Use IntEnum for stream orientation

* Rename enum values

* Add comments

* Fix import
This commit is contained in:
uvjustin 2022-11-09 23:28:28 +08:00 committed by GitHub
parent 9de4d7cba3
commit 84725f15a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 28 deletions

View File

@ -30,6 +30,7 @@ from homeassistant.components.media_player import (
from homeassistant.components.stream import ( from homeassistant.components.stream import (
FORMAT_CONTENT_TYPE, FORMAT_CONTENT_TYPE,
OUTPUT_FORMATS, OUTPUT_FORMATS,
Orientation,
Stream, Stream,
create_stream, create_stream,
) )
@ -869,7 +870,7 @@ async def websocket_get_prefs(
vol.Required("type"): "camera/update_prefs", vol.Required("type"): "camera/update_prefs",
vol.Required("entity_id"): cv.entity_id, vol.Required("entity_id"): cv.entity_id,
vol.Optional(PREF_PRELOAD_STREAM): bool, vol.Optional(PREF_PRELOAD_STREAM): bool,
vol.Optional(PREF_ORIENTATION): vol.All(int, vol.Range(min=1, max=8)), vol.Optional(PREF_ORIENTATION): vol.Coerce(Orientation),
} }
) )
@websocket_api.async_response @websocket_api.async_response

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Final, Union, cast from typing import Final, Union, cast
from homeassistant.components.stream import Orientation
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -18,11 +19,11 @@ STORAGE_VERSION: Final = 1
class CameraEntityPreferences: class CameraEntityPreferences:
"""Handle preferences for camera entity.""" """Handle preferences for camera entity."""
def __init__(self, prefs: dict[str, bool | int]) -> None: def __init__(self, prefs: dict[str, bool | Orientation]) -> None:
"""Initialize prefs.""" """Initialize prefs."""
self._prefs = prefs self._prefs = prefs
def as_dict(self) -> dict[str, bool | int]: def as_dict(self) -> dict[str, bool | Orientation]:
"""Return dictionary version.""" """Return dictionary version."""
return self._prefs return self._prefs
@ -32,9 +33,11 @@ class CameraEntityPreferences:
return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False))
@property @property
def orientation(self) -> int: def orientation(self) -> Orientation:
"""Return the current stream orientation settings.""" """Return the current stream orientation settings."""
return self._prefs.get(PREF_ORIENTATION, 1) return cast(
Orientation, self._prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM)
)
class CameraPreferences: class CameraPreferences:
@ -45,11 +48,11 @@ class CameraPreferences:
self._hass = hass self._hass = hass
# The orientation prefs are stored in in the entity registry options # The orientation prefs are stored in in the entity registry options
# The preload_stream prefs are stored in this Store # The preload_stream prefs are stored in this Store
self._store = Store[dict[str, dict[str, Union[bool, int]]]]( 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 # Local copy of the preload_stream prefs
self._prefs: dict[str, dict[str, bool | int]] | None = None self._prefs: dict[str, dict[str, bool | Orientation]] | None = None
async def async_initialize(self) -> None: async def async_initialize(self) -> None:
"""Finish initializing the preferences.""" """Finish initializing the preferences."""
@ -63,9 +66,9 @@ class CameraPreferences:
entity_id: str, entity_id: str,
*, *,
preload_stream: bool | UndefinedType = UNDEFINED, preload_stream: bool | UndefinedType = UNDEFINED,
orientation: int | UndefinedType = UNDEFINED, orientation: Orientation | UndefinedType = UNDEFINED,
stream_options: dict[str, str] | UndefinedType = UNDEFINED, stream_options: dict[str, str] | UndefinedType = UNDEFINED,
) -> dict[str, bool | int]: ) -> dict[str, bool | Orientation]:
"""Update camera preferences. """Update camera preferences.
Returns a dict with the preferences on success. Returns a dict with the preferences on success.

View File

@ -63,6 +63,7 @@ from .core import (
STREAM_SETTINGS_NON_LL_HLS, STREAM_SETTINGS_NON_LL_HLS,
IdleTimer, IdleTimer,
KeyFrameConverter, KeyFrameConverter,
Orientation,
StreamOutput, StreamOutput,
StreamSettings, StreamSettings,
) )
@ -82,6 +83,7 @@ __all__ = [
"SOURCE_TIMEOUT", "SOURCE_TIMEOUT",
"Stream", "Stream",
"create_stream", "create_stream",
"Orientation",
] ]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -229,7 +231,7 @@ 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=1, 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
@ -292,12 +294,12 @@ class Stream:
self._diagnostics = Diagnostics() self._diagnostics = Diagnostics()
@property @property
def orientation(self) -> int: def orientation(self) -> Orientation:
"""Return the current orientation setting.""" """Return the current orientation setting."""
return self._stream_settings.orientation return self._stream_settings.orientation
@orientation.setter @orientation.setter
def orientation(self, value: int) -> None: def orientation(self, value: Orientation) -> None:
"""Set the stream orientation setting.""" """Set the stream orientation setting."""
self._stream_settings.orientation = value self._stream_settings.orientation = value

View File

@ -5,6 +5,7 @@ import asyncio
from collections import deque from collections import deque
from collections.abc import Callable, Coroutine, Iterable from collections.abc import Callable, Coroutine, Iterable
import datetime import datetime
from enum import IntEnum
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -35,6 +36,19 @@ _LOGGER = logging.getLogger(__name__)
PROVIDERS: Registry[str, type[StreamOutput]] = Registry() PROVIDERS: Registry[str, type[StreamOutput]] = Registry()
class Orientation(IntEnum):
"""Orientations for stream transforms. These are based on EXIF orientation tags."""
NO_TRANSFORM = 1
MIRROR = 2
ROTATE_180 = 3
FLIP = 4
ROTATE_LEFT_AND_FLIP = 5
ROTATE_LEFT = 6
ROTATE_RIGHT_AND_FLIP = 7
ROTATE_RIGHT = 8
@attr.s(slots=True) @attr.s(slots=True)
class StreamSettings: class StreamSettings:
"""Stream settings.""" """Stream settings."""
@ -44,7 +58,7 @@ 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: int = attr.ib() orientation: Orientation = attr.ib()
STREAM_SETTINGS_NON_LL_HLS = StreamSettings( STREAM_SETTINGS_NON_LL_HLS = StreamSettings(
@ -53,7 +67,7 @@ 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=1, orientation=Orientation.NO_TRANSFORM,
) )

View File

@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .core import Orientation
if TYPE_CHECKING: if TYPE_CHECKING:
from io import BufferedIOBase from io import BufferedIOBase
@ -179,22 +181,24 @@ ROTATE_LEFT_FLIP = (ZERO32 + NEGONE32 + ZERO32) + (NEGONE32 + ZERO32 + ZERO32)
ROTATE_RIGHT_FLIP = (ZERO32 + ONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32) ROTATE_RIGHT_FLIP = (ZERO32 + ONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32)
TRANSFORM_MATRIX_TOP = ( TRANSFORM_MATRIX_TOP = (
# The first two entries are just to align the indices with the EXIF orientation tags # The index into this tuple corresponds to the EXIF orientation tag
b"", # Only index values of 2 through 8 are used
b"", # The first two entries are just to keep everything aligned
MIRROR, b"", # 0
ROTATE_180, b"", # 1
FLIP, MIRROR, # 2
ROTATE_LEFT_FLIP, ROTATE_180, # 3
ROTATE_LEFT, FLIP, # 4
ROTATE_RIGHT_FLIP, ROTATE_LEFT_FLIP, # 5
ROTATE_RIGHT, ROTATE_LEFT, # 6
ROTATE_RIGHT_FLIP, # 7
ROTATE_RIGHT, # 8
) )
def transform_init(init: bytes, orientation: int) -> bytes: def transform_init(init: bytes, orientation: Orientation) -> bytes:
"""Change the transformation matrix in the header.""" """Change the transformation matrix in the header."""
if orientation == 1: if orientation == Orientation.NO_TRANSFORM:
return init return init
# Find moov # Find moov
moov_location = next(find_box(init, b"moov")) moov_location = next(find_box(init, b"moov"))

View File

@ -367,7 +367,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam
assert response["success"] assert response["success"]
er_camera_prefs = registry.async_get("camera.demo_uniquecamera").options[DOMAIN] er_camera_prefs = registry.async_get("camera.demo_uniquecamera").options[DOMAIN]
assert er_camera_prefs[PREF_ORIENTATION] == 3 assert er_camera_prefs[PREF_ORIENTATION] == camera.Orientation.ROTATE_180
assert response["result"][PREF_ORIENTATION] == er_camera_prefs[PREF_ORIENTATION] assert response["result"][PREF_ORIENTATION] == er_camera_prefs[PREF_ORIENTATION]
# Check that the preference was saved # Check that the preference was saved
await client.send_json( await client.send_json(
@ -375,7 +375,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam
) )
msg = await client.receive_json() msg = await client.receive_json()
# orientation entry for this camera should have been added # orientation entry for this camera should have been added
assert msg["result"]["orientation"] == 3 assert msg["result"]["orientation"] == camera.Orientation.ROTATE_180
async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): async def test_play_stream_service_no_source(hass, mock_camera, mock_stream):