From e879ab0eef05c4d04bd969782a15b2d62c36f81e Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 18 Feb 2024 20:12:08 +0200 Subject: [PATCH] Show WebRTC cameras that also support HLS in the media browser (#108796) * Show WebRTC cameras in the media browser * Only show webrtc cameras with source in the browser * Address code review * Refactor BrowseMediaSource creation * Refactor * Address code review --- .../components/camera/media_source.py | 57 +++++++++++-------- tests/components/camera/test_media_source.py | 36 ++++++++---- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index e681ddbbd7e..3c9a386f958 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -1,6 +1,8 @@ """Expose cameras as media sources.""" from __future__ import annotations +import asyncio + from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( @@ -23,6 +25,19 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource: return CameraMediaSource(hass) +def _media_source_for_camera(camera: Camera, content_type: str) -> BrowseMediaSource: + return BrowseMediaSource( + domain=DOMAIN, + identifier=camera.entity_id, + media_class=MediaClass.VIDEO, + media_content_type=content_type, + title=camera.name, + thumbnail=f"/api/camera_proxy/{camera.entity_id}", + can_play=True, + can_expand=False, + ) + + class CameraMediaSource(MediaSource): """Provide camera feeds as media sources.""" @@ -71,36 +86,28 @@ class CameraMediaSource(MediaSource): can_stream_hls = "stream" in self.hass.config.components - # Root. List cameras. - component: EntityComponent[Camera] = self.hass.data[DOMAIN] - children = [] - not_shown = 0 - for camera in component.entities: + async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None: stream_type = camera.frontend_stream_type - if stream_type is None: - content_type = camera.content_type + return _media_source_for_camera(camera, camera.content_type) + if not can_stream_hls: + return None - elif can_stream_hls and stream_type == StreamType.HLS: - content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + if stream_type != StreamType.HLS and not (await camera.stream_source()): + return None - else: - not_shown += 1 - continue - - children.append( - BrowseMediaSource( - domain=DOMAIN, - identifier=camera.entity_id, - media_class=MediaClass.VIDEO, - media_content_type=content_type, - title=camera.name, - thumbnail=f"/api/camera_proxy/{camera.entity_id}", - can_play=True, - can_expand=False, - ) - ) + return _media_source_for_camera(camera, content_type) + component: EntityComponent[Camera] = self.hass.data[DOMAIN] + results = await asyncio.gather( + *(_filter_browsable_camera(camera) for camera in component.entities), + return_exceptions=True, + ) + children = [ + result for result in results if isinstance(result, BrowseMediaSource) + ] + not_shown = len(results) - len(children) return BrowseMediaSource( domain=DOMAIN, identifier=None, diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 7aa41b98efa..f965bdadb09 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -17,7 +17,7 @@ async def setup_media_source(hass): async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: - """Test browsing camera media source.""" + """Test browsing HLS camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None assert item.title == "Camera" @@ -34,7 +34,7 @@ async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: - """Test browsing camera media source.""" + """Test browsing MJPEG camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None assert item.title == "Camera" @@ -43,15 +43,29 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: assert item.children[0].media_content_type == "image/jpg" -async def test_browsing_filter_web_rtc( - hass: HomeAssistant, mock_camera_web_rtc -) -> None: - """Test browsing camera media source hides non-HLS cameras.""" - item = await media_source.async_browse_media(hass, "media-source://camera") - assert item is not None - assert item.title == "Camera" - assert len(item.children) == 0 - assert item.not_shown == 3 +async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None: + """Test browsing WebRTC camera media source.""" + # 3 cameras: + # one only supports WebRTC (no stream source) + # one raises when getting the source + # One has a stream source, and should be the only browsable one + with patch( + "homeassistant.components.camera.Camera.stream_source", + side_effect=["test", None, Exception], + ): + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 0 + assert item.not_shown == 3 + + # Adding stream enables HLS camera + hass.config.components.add("stream") + + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 2 + assert len(item.children) == 1 + assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None: