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
pull/110953/head
On Freund 2024-02-18 20:12:08 +02:00 committed by GitHub
parent 8fa347fb4c
commit e879ab0eef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 57 additions and 36 deletions

View File

@ -1,6 +1,8 @@
"""Expose cameras as media sources.""" """Expose cameras as media sources."""
from __future__ import annotations from __future__ import annotations
import asyncio
from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.error import Unresolvable
from homeassistant.components.media_source.models import ( from homeassistant.components.media_source.models import (
@ -23,6 +25,19 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource:
return CameraMediaSource(hass) 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): class CameraMediaSource(MediaSource):
"""Provide camera feeds as media sources.""" """Provide camera feeds as media sources."""
@ -71,36 +86,28 @@ class CameraMediaSource(MediaSource):
can_stream_hls = "stream" in self.hass.config.components can_stream_hls = "stream" in self.hass.config.components
# Root. List cameras. async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None:
component: EntityComponent[Camera] = self.hass.data[DOMAIN]
children = []
not_shown = 0
for camera in component.entities:
stream_type = camera.frontend_stream_type stream_type = camera.frontend_stream_type
if stream_type is None: 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: return _media_source_for_camera(camera, content_type)
not_shown += 1
continue
children.append( component: EntityComponent[Camera] = self.hass.data[DOMAIN]
BrowseMediaSource( results = await asyncio.gather(
domain=DOMAIN, *(_filter_browsable_camera(camera) for camera in component.entities),
identifier=camera.entity_id, return_exceptions=True,
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,
) )
) children = [
result for result in results if isinstance(result, BrowseMediaSource)
]
not_shown = len(results) - len(children)
return BrowseMediaSource( return BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=None, identifier=None,

View File

@ -17,7 +17,7 @@ async def setup_media_source(hass):
async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: 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") item = await media_source.async_browse_media(hass, "media-source://camera")
assert item is not None assert item is not None
assert item.title == "Camera" 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: 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") item = await media_source.async_browse_media(hass, "media-source://camera")
assert item is not None assert item is not None
assert item.title == "Camera" assert item.title == "Camera"
@ -43,16 +43,30 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None:
assert item.children[0].media_content_type == "image/jpg" assert item.children[0].media_content_type == "image/jpg"
async def test_browsing_filter_web_rtc( async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None:
hass: HomeAssistant, mock_camera_web_rtc """Test browsing WebRTC camera media source."""
) -> None: # 3 cameras:
"""Test browsing camera media source hides non-HLS 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") item = await media_source.async_browse_media(hass, "media-source://camera")
assert item is not None assert item is not None
assert item.title == "Camera" assert item.title == "Camera"
assert len(item.children) == 0 assert len(item.children) == 0
assert item.not_shown == 3 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: async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None:
"""Test resolving.""" """Test resolving."""