Skip to content

Commit 0865d3f

Browse files
authored
Add support for stream orientation in go2rtc (home-assistant#148832)
1 parent 095f73d commit 0865d3f

File tree

5 files changed

+337
-10
lines changed

5 files changed

+337
-10
lines changed

homeassistant/components/camera/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@
8181
)
8282
from .helper import get_camera_from_entity_id
8383
from .img_util import scale_jpeg_camera_image
84-
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
84+
from .prefs import (
85+
CameraPreferences,
86+
DynamicStreamSettings, # noqa: F401
87+
get_dynamic_camera_stream_settings,
88+
)
8589
from .webrtc import (
8690
DATA_ICE_SERVERS,
8791
CameraWebRTCProvider,
@@ -550,9 +554,9 @@ async def async_create_stream(self) -> Stream | None:
550554
self.hass,
551555
source,
552556
options=self.stream_options,
553-
dynamic_stream_settings=await self.hass.data[
554-
DATA_CAMERA_PREFS
555-
].get_dynamic_stream_settings(self.entity_id),
557+
dynamic_stream_settings=await get_dynamic_camera_stream_settings(
558+
self.hass, self.entity_id
559+
),
556560
stream_label=self.entity_id,
557561
)
558562
self.stream.set_update_callback(self.async_write_ha_state)
@@ -942,9 +946,7 @@ async def websocket_get_prefs(
942946
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
943947
) -> None:
944948
"""Handle request for account info."""
945-
stream_prefs = await hass.data[DATA_CAMERA_PREFS].get_dynamic_stream_settings(
946-
msg["entity_id"]
947-
)
949+
stream_prefs = await get_dynamic_camera_stream_settings(hass, msg["entity_id"])
948950
connection.send_result(msg["id"], asdict(stream_prefs))
949951

950952

homeassistant/components/camera/prefs.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from homeassistant.helpers.storage import Store
1414
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
1515

16-
from .const import DOMAIN, PREF_ORIENTATION, PREF_PRELOAD_STREAM
16+
from .const import DATA_CAMERA_PREFS, DOMAIN, PREF_ORIENTATION, PREF_PRELOAD_STREAM
1717

1818
STORAGE_KEY: Final = DOMAIN
1919
STORAGE_VERSION: Final = 1
@@ -106,3 +106,12 @@ async def get_dynamic_stream_settings(
106106
)
107107
self._dynamic_stream_settings_by_entity_id[entity_id] = settings
108108
return settings
109+
110+
111+
async def get_dynamic_camera_stream_settings(
112+
hass: HomeAssistant, entity_id: str
113+
) -> DynamicStreamSettings:
114+
"""Get dynamic stream settings for a camera entity."""
115+
if DATA_CAMERA_PREFS not in hass.data:
116+
raise HomeAssistantError("Camera integration not set up")
117+
return await hass.data[DATA_CAMERA_PREFS].get_dynamic_stream_settings(entity_id)

homeassistant/components/go2rtc/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
WebRTCSendMessage,
3232
async_register_webrtc_provider,
3333
)
34+
from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings
3435
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
36+
from homeassistant.components.stream import Orientation
3537
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
3638
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
3739
from homeassistant.core import Event, HomeAssistant, callback
@@ -57,12 +59,13 @@
5759

5860
_LOGGER = logging.getLogger(__name__)
5961

62+
_FFMPEG = "ffmpeg"
6063
_SUPPORTED_STREAMS = frozenset(
6164
(
6265
"bubble",
6366
"dvrip",
6467
"expr",
65-
"ffmpeg",
68+
_FFMPEG,
6669
"gopro",
6770
"homekit",
6871
"http",
@@ -315,6 +318,32 @@ async def _update_stream_source(self, camera: Camera) -> None:
315318
await self.teardown()
316319
raise HomeAssistantError("Stream source is not supported by go2rtc")
317320

321+
camera_prefs = await get_dynamic_camera_stream_settings(
322+
self._hass, camera.entity_id
323+
)
324+
if camera_prefs.orientation is not Orientation.NO_TRANSFORM:
325+
# Camera orientation manually set by user
326+
if not stream_source.startswith(_FFMPEG):
327+
stream_source = _FFMPEG + ":" + stream_source
328+
stream_source += "#video=h264#audio=copy"
329+
match camera_prefs.orientation:
330+
case Orientation.MIRROR:
331+
stream_source += "#raw=-vf hflip"
332+
case Orientation.ROTATE_180:
333+
stream_source += "#rotate=180"
334+
case Orientation.FLIP:
335+
stream_source += "#raw=-vf vflip"
336+
case Orientation.ROTATE_LEFT_AND_FLIP:
337+
# Cannot use any filter when using raw one
338+
stream_source += "#raw=-vf transpose=2,vflip"
339+
case Orientation.ROTATE_LEFT:
340+
stream_source += "#rotate=-90"
341+
case Orientation.ROTATE_RIGHT_AND_FLIP:
342+
# Cannot use any filter when using raw one
343+
stream_source += "#raw=-vf transpose=1,vflip"
344+
case Orientation.ROTATE_RIGHT:
345+
stream_source += "#rotate=90"
346+
318347
streams = await self._rest_client.streams.list()
319348

320349
if (stream := streams.get(camera.entity_id)) is None or not any(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Test camera helper functions."""
2+
3+
import pytest
4+
5+
from homeassistant.components.camera.const import DATA_CAMERA_PREFS
6+
from homeassistant.components.camera.prefs import (
7+
CameraPreferences,
8+
DynamicStreamSettings,
9+
get_dynamic_camera_stream_settings,
10+
)
11+
from homeassistant.components.stream import Orientation
12+
from homeassistant.core import HomeAssistant
13+
from homeassistant.exceptions import HomeAssistantError
14+
15+
16+
async def test_get_dynamic_camera_stream_settings_missing_prefs(
17+
hass: HomeAssistant,
18+
) -> None:
19+
"""Test get_dynamic_camera_stream_settings when camera prefs are not set up."""
20+
with pytest.raises(HomeAssistantError, match="Camera integration not set up"):
21+
await get_dynamic_camera_stream_settings(hass, "camera.test")
22+
23+
24+
async def test_get_dynamic_camera_stream_settings_success(hass: HomeAssistant) -> None:
25+
"""Test successful retrieval of dynamic camera stream settings."""
26+
# Set up camera preferences
27+
prefs = CameraPreferences(hass)
28+
await prefs.async_load()
29+
hass.data[DATA_CAMERA_PREFS] = prefs
30+
31+
# Test with default settings
32+
settings = await get_dynamic_camera_stream_settings(hass, "camera.test")
33+
assert settings.orientation == Orientation.NO_TRANSFORM
34+
assert settings.preload_stream is False
35+
36+
37+
async def test_get_dynamic_camera_stream_settings_with_custom_orientation(
38+
hass: HomeAssistant,
39+
) -> None:
40+
"""Test get_dynamic_camera_stream_settings with custom orientation set."""
41+
# Set up camera preferences
42+
prefs = CameraPreferences(hass)
43+
await prefs.async_load()
44+
hass.data[DATA_CAMERA_PREFS] = prefs
45+
46+
# Set custom orientation - this requires entity registry
47+
# For this test, we'll directly manipulate the internal state
48+
# since entity registry setup is complex for a unit test
49+
test_settings = DynamicStreamSettings(
50+
orientation=Orientation.ROTATE_LEFT, preload_stream=False
51+
)
52+
prefs._dynamic_stream_settings_by_entity_id["camera.test"] = test_settings
53+
54+
settings = await get_dynamic_camera_stream_settings(hass, "camera.test")
55+
assert settings.orientation == Orientation.ROTATE_LEFT
56+
assert settings.preload_stream is False
57+
58+
59+
async def test_get_dynamic_camera_stream_settings_with_preload_stream(
60+
hass: HomeAssistant,
61+
) -> None:
62+
"""Test get_dynamic_camera_stream_settings with preload stream enabled."""
63+
# Set up camera preferences
64+
prefs = CameraPreferences(hass)
65+
await prefs.async_load()
66+
hass.data[DATA_CAMERA_PREFS] = prefs
67+
68+
# Set preload stream by directly setting the dynamic stream settings
69+
test_settings = DynamicStreamSettings(
70+
orientation=Orientation.NO_TRANSFORM, preload_stream=True
71+
)
72+
prefs._dynamic_stream_settings_by_entity_id["camera.test"] = test_settings
73+
74+
settings = await get_dynamic_camera_stream_settings(hass, "camera.test")
75+
assert settings.orientation == Orientation.NO_TRANSFORM
76+
assert settings.preload_stream is True

0 commit comments

Comments
 (0)