Add camera media source (#65977)
This commit is contained in:
parent
b216f6f448
commit
716a1e2a64
10 changed files with 432 additions and 109 deletions
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import base64
|
||||
import collections
|
||||
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
@ -26,7 +26,6 @@ from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
|||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_EXTRA,
|
||||
DOMAIN as DOMAIN_MP,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
)
|
||||
|
@ -49,7 +48,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
|
|||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription, entity_sources
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
@ -970,56 +969,22 @@ async def async_handle_play_stream_service(
|
|||
camera: Camera, service_call: ServiceCall
|
||||
) -> None:
|
||||
"""Handle play stream services calls."""
|
||||
hass = camera.hass
|
||||
fmt = service_call.data[ATTR_FORMAT]
|
||||
url = await _async_stream_endpoint_url(camera.hass, camera, fmt)
|
||||
url = f"{get_url(hass)}{url}"
|
||||
|
||||
hass = camera.hass
|
||||
data: Mapping[str, str] = {
|
||||
ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}",
|
||||
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt],
|
||||
}
|
||||
|
||||
# It is required to send a different payload for cast media players
|
||||
entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
|
||||
sources = entity_sources(hass)
|
||||
cast_entity_ids = [
|
||||
entity
|
||||
for entity in entity_ids
|
||||
# All entities should be in sources. This extra guard is to
|
||||
# avoid people writing to the state machine and breaking it.
|
||||
if entity in sources and sources[entity]["domain"] == "cast"
|
||||
]
|
||||
other_entity_ids = list(set(entity_ids) - set(cast_entity_ids))
|
||||
|
||||
if cast_entity_ids:
|
||||
await hass.services.async_call(
|
||||
DOMAIN_MP,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: cast_entity_ids,
|
||||
**data,
|
||||
ATTR_MEDIA_EXTRA: {
|
||||
"stream_type": "LIVE",
|
||||
"media_info": {
|
||||
"hlsVideoSegmentFormat": "fmp4",
|
||||
},
|
||||
},
|
||||
},
|
||||
blocking=True,
|
||||
context=service_call.context,
|
||||
)
|
||||
|
||||
if other_entity_ids:
|
||||
await hass.services.async_call(
|
||||
DOMAIN_MP,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: other_entity_ids,
|
||||
**data,
|
||||
},
|
||||
blocking=True,
|
||||
context=service_call.context,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
DOMAIN_MP,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: service_call.data[ATTR_MEDIA_PLAYER],
|
||||
ATTR_MEDIA_CONTENT_ID: url,
|
||||
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt],
|
||||
},
|
||||
blocking=True,
|
||||
context=service_call.context,
|
||||
)
|
||||
|
||||
|
||||
async def _async_stream_endpoint_url(
|
||||
|
|
103
homeassistant/components/camera/media_source.py
Normal file
103
homeassistant/components/camera/media_source.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
"""Expose cameras as media sources."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, cast
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_APP,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
)
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.components.media_source.error import Unresolvable
|
||||
from homeassistant.components.media_source.models import (
|
||||
BrowseMediaSource,
|
||||
MediaSource,
|
||||
MediaSourceItem,
|
||||
PlayMedia,
|
||||
)
|
||||
from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import Camera, _async_stream_endpoint_url
|
||||
from .const import DOMAIN, STREAM_TYPE_HLS
|
||||
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource:
|
||||
"""Set up camera media source."""
|
||||
return CameraMediaSource(hass)
|
||||
|
||||
|
||||
class CameraMediaSource(MediaSource):
|
||||
"""Provide camera feeds as media sources."""
|
||||
|
||||
name: str = "Camera"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize CameraMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
component: EntityComponent = self.hass.data[DOMAIN]
|
||||
camera = cast(Optional[Camera], component.get_entity(item.identifier))
|
||||
|
||||
if not camera:
|
||||
raise Unresolvable(f"Could not resolve media item: {item.identifier}")
|
||||
|
||||
if camera.frontend_stream_type != STREAM_TYPE_HLS:
|
||||
raise Unresolvable("Camera does not support HLS streaming.")
|
||||
|
||||
try:
|
||||
url = await _async_stream_endpoint_url(self.hass, camera, HLS_PROVIDER)
|
||||
except HomeAssistantError as err:
|
||||
raise Unresolvable(str(err)) from err
|
||||
|
||||
return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER])
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
if item.identifier:
|
||||
raise BrowseError("Unknown item")
|
||||
|
||||
if "stream" not in self.hass.config.components:
|
||||
raise BrowseError("Stream integration is not loaded")
|
||||
|
||||
# Root. List cameras.
|
||||
component: EntityComponent = self.hass.data[DOMAIN]
|
||||
children = []
|
||||
for camera in component.entities:
|
||||
camera = cast(Camera, camera)
|
||||
|
||||
if camera.frontend_stream_type != STREAM_TYPE_HLS:
|
||||
continue
|
||||
|
||||
children.append(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=camera.entity_id,
|
||||
media_class=MEDIA_CLASS_VIDEO,
|
||||
media_content_type=FORMAT_CONTENT_TYPE[HLS_PROVIDER],
|
||||
title=camera.name,
|
||||
thumbnail=f"/api/camera_proxy/{camera.entity_id}",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=None,
|
||||
media_class=MEDIA_CLASS_APP,
|
||||
media_content_type="",
|
||||
title="Camera",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MEDIA_CLASS_VIDEO,
|
||||
children=children,
|
||||
)
|
|
@ -18,6 +18,7 @@ from pychromecast.socket_client import (
|
|||
CONNECTION_STATUS_DISCONNECTED,
|
||||
)
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from homeassistant.components import media_source, zeroconf
|
||||
from homeassistant.components.http.auth import async_sign_path
|
||||
|
@ -59,7 +60,7 @@ from homeassistant.helpers import config_validation as cv
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url, is_hass_url
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.logging import async_create_catching_coro
|
||||
|
||||
|
@ -535,19 +536,6 @@ class CastDevice(MediaPlayerEntity):
|
|||
media_type = sourced_media.mime_type
|
||||
media_id = sourced_media.url
|
||||
|
||||
# If media ID is a relative URL, we serve it from HA.
|
||||
# Create a signed path.
|
||||
if media_id[0] == "/":
|
||||
media_id = async_sign_path(
|
||||
self.hass,
|
||||
quote(media_id),
|
||||
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
|
||||
)
|
||||
|
||||
# prepend external URL
|
||||
hass_url = get_url(self.hass, prefer_external=True)
|
||||
media_id = f"{hass_url}{media_id}"
|
||||
|
||||
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||
metadata = extra.get("metadata")
|
||||
|
||||
|
@ -593,6 +581,33 @@ class CastDevice(MediaPlayerEntity):
|
|||
if result:
|
||||
return
|
||||
|
||||
# If media ID is a relative URL, we serve it from HA.
|
||||
# Create a signed path.
|
||||
if media_id[0] == "/" or is_hass_url(self.hass, media_id):
|
||||
parsed = yarl.URL(media_id)
|
||||
# Configure play command for when playing a HLS stream
|
||||
if parsed.path.startswith("/api/hls/"):
|
||||
extra = {
|
||||
**extra,
|
||||
"stream_type": "LIVE",
|
||||
"media_info": {
|
||||
"hlsVideoSegmentFormat": "fmp4",
|
||||
},
|
||||
}
|
||||
|
||||
if parsed.query:
|
||||
_LOGGER.debug("Not signing path for content with query param")
|
||||
else:
|
||||
media_id = async_sign_path(
|
||||
self.hass,
|
||||
quote(media_id),
|
||||
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
|
||||
)
|
||||
|
||||
if media_id[0] == "/":
|
||||
# prepend URL
|
||||
media_id = f"{get_url(self.hass)}{media_id}"
|
||||
|
||||
# Default to play with the default media receiver
|
||||
app_data = {"media_id": media_id, "media_type": media_type, **extra}
|
||||
await self.hass.async_add_executor_job(
|
||||
|
|
|
@ -31,6 +31,48 @@ def is_internal_request(hass: HomeAssistant) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def is_hass_url(hass: HomeAssistant, url: str) -> bool:
|
||||
"""Return if the URL points at this Home Assistant instance."""
|
||||
parsed = yarl.URL(normalize_url(url))
|
||||
|
||||
def host_ip() -> str | None:
|
||||
if hass.config.api is None or is_loopback(ip_address(hass.config.api.local_ip)):
|
||||
return None
|
||||
|
||||
return str(
|
||||
yarl.URL.build(
|
||||
scheme="http", host=hass.config.api.local_ip, port=hass.config.api.port
|
||||
)
|
||||
)
|
||||
|
||||
def cloud_url() -> str | None:
|
||||
try:
|
||||
return _get_cloud_url(hass)
|
||||
except NoURLAvailableError:
|
||||
return None
|
||||
|
||||
for potential_base_factory in (
|
||||
lambda: hass.config.internal_url,
|
||||
lambda: hass.config.external_url,
|
||||
cloud_url,
|
||||
host_ip,
|
||||
):
|
||||
potential_base = potential_base_factory()
|
||||
|
||||
if potential_base is None:
|
||||
continue
|
||||
|
||||
potential_parsed = yarl.URL(normalize_url(potential_base))
|
||||
|
||||
if (
|
||||
parsed.scheme == potential_parsed.scheme
|
||||
and parsed.authority == potential_parsed.authority
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@bind_hass
|
||||
def get_url(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -8,6 +8,7 @@ from unittest.mock import Mock
|
|||
from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM
|
||||
|
||||
EMPTY_8_6_JPEG = b"empty_8_6"
|
||||
WEBRTC_ANSWER = "a=sendonly"
|
||||
|
||||
|
||||
def mock_camera_prefs(hass, entity_id, prefs=None):
|
||||
|
|
53
tests/components/camera/conftest.py
Normal file
53
tests/components/camera/conftest.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
"""Test helpers for camera."""
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import WEBRTC_ANSWER
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera")
|
||||
async def mock_camera_fixture(hass):
|
||||
"""Initialize a demo camera platform."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.Path.read_bytes",
|
||||
return_value=b"Test",
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera_hls")
|
||||
async def mock_camera_hls_fixture(mock_camera):
|
||||
"""Initialize a demo camera platform with HLS."""
|
||||
with patch(
|
||||
"homeassistant.components.camera.Camera.frontend_stream_type",
|
||||
new_callable=PropertyMock(return_value=STREAM_TYPE_HLS),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera_web_rtc")
|
||||
async def mock_camera_web_rtc_fixture(hass):
|
||||
"""Initialize a demo camera platform with WebRTC."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.camera.Camera.frontend_stream_type",
|
||||
new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC),
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.async_handle_web_rtc_offer",
|
||||
return_value=WEBRTC_ANSWER,
|
||||
):
|
||||
yield
|
|
@ -8,11 +8,7 @@ from unittest.mock import Mock, PropertyMock, mock_open, patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.camera.const import (
|
||||
DOMAIN,
|
||||
PREF_PRELOAD_STREAM,
|
||||
STREAM_TYPE_WEB_RTC,
|
||||
)
|
||||
from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM
|
||||
from homeassistant.components.camera.prefs import CameraEntityPreferences
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
|
@ -24,47 +20,11 @@ from homeassistant.const import (
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg
|
||||
|
||||
from tests.components.camera import common
|
||||
from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_camera_prefs, mock_turbo_jpeg
|
||||
|
||||
STREAM_SOURCE = "rtsp://127.0.0.1/stream"
|
||||
HLS_STREAM_SOURCE = "http://127.0.0.1/example.m3u"
|
||||
WEBRTC_OFFER = "v=0\r\n"
|
||||
WEBRTC_ANSWER = "a=sendonly"
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera")
|
||||
async def mock_camera_fixture(hass):
|
||||
"""Initialize a demo camera platform."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.demo.camera.Path.read_bytes",
|
||||
return_value=b"Test",
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera_web_rtc")
|
||||
async def mock_camera_web_rtc_fixture(hass):
|
||||
"""Initialize a demo camera platform."""
|
||||
assert await async_setup_component(
|
||||
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.camera.Camera.frontend_stream_type",
|
||||
new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC),
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.async_handle_web_rtc_offer",
|
||||
return_value=WEBRTC_ANSWER,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_stream")
|
||||
|
@ -78,7 +38,7 @@ def mock_stream_fixture(hass):
|
|||
@pytest.fixture(name="setup_camera_prefs")
|
||||
def setup_camera_prefs_fixture(hass):
|
||||
"""Initialize HTTP API."""
|
||||
return common.mock_camera_prefs(hass, "camera.demo_camera")
|
||||
return mock_camera_prefs(hass, "camera.demo_camera")
|
||||
|
||||
|
||||
@pytest.fixture(name="image_mock_url")
|
||||
|
|
72
tests/components/camera/test_media_source.py
Normal file
72
tests/components/camera/test_media_source.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
"""Test camera media source."""
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.camera.const import STREAM_TYPE_WEB_RTC
|
||||
from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_media_source(hass):
|
||||
"""Set up media source."""
|
||||
assert await async_setup_component(hass, "media_source", {})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_stream(hass):
|
||||
"""Mock stream."""
|
||||
hass.config.components.add("stream")
|
||||
|
||||
|
||||
async def test_browsing(hass, mock_camera_hls):
|
||||
"""Test browsing camera media source."""
|
||||
item = await media_source.async_browse_media(hass, "media-source://camera")
|
||||
assert item is not None
|
||||
assert item.title == "Camera"
|
||||
assert len(item.children) == 2
|
||||
|
||||
|
||||
async def test_browsing_filter_non_hls(hass, mock_camera_web_rtc):
|
||||
"""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
|
||||
|
||||
|
||||
async def test_resolving(hass, mock_camera_hls):
|
||||
"""Test resolving."""
|
||||
with patch(
|
||||
"homeassistant.components.camera.media_source._async_stream_endpoint_url",
|
||||
return_value="http://example.com/stream",
|
||||
):
|
||||
item = await media_source.async_resolve_media(
|
||||
hass, "media-source://camera/camera.demo_camera"
|
||||
)
|
||||
assert item is not None
|
||||
assert item.url == "http://example.com/stream"
|
||||
assert item.mime_type == FORMAT_CONTENT_TYPE["hls"]
|
||||
|
||||
|
||||
async def test_resolving_errors(hass, mock_camera_hls):
|
||||
"""Test resolving."""
|
||||
with pytest.raises(media_source.Unresolvable):
|
||||
await media_source.async_resolve_media(
|
||||
hass, "media-source://camera/camera.non_existing"
|
||||
)
|
||||
|
||||
with pytest.raises(media_source.Unresolvable), patch(
|
||||
"homeassistant.components.camera.Camera.frontend_stream_type",
|
||||
new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC),
|
||||
):
|
||||
await media_source.async_resolve_media(
|
||||
hass, "media-source://camera/camera.demo_camera"
|
||||
)
|
||||
|
||||
with pytest.raises(media_source.Unresolvable):
|
||||
await media_source.async_resolve_media(
|
||||
hass, "media-source://camera/camera.demo_camera"
|
||||
)
|
|
@ -10,6 +10,7 @@ import attr
|
|||
import pychromecast
|
||||
from pychromecast.const import CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP
|
||||
import pytest
|
||||
import yarl
|
||||
|
||||
from homeassistant.components import media_player, tts
|
||||
from homeassistant.components.cast import media_player as cast
|
||||
|
@ -37,7 +38,7 @@ from homeassistant.const import (
|
|||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, network
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
@ -1001,7 +1002,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
|
|||
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"external_url": "http://example.com:8123"},
|
||||
{"internal_url": "http://example.com:8123"},
|
||||
)
|
||||
|
||||
info = get_fake_chromecast_info()
|
||||
|
@ -1824,3 +1825,69 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client):
|
|||
"children": [],
|
||||
}
|
||||
assert response["result"] == expected_response
|
||||
|
||||
|
||||
async def test_cast_platform_play_media_local_media(
|
||||
hass: HomeAssistant, quick_play_mock, caplog
|
||||
):
|
||||
"""Test we process data when playing local media."""
|
||||
entity_id = "media_player.speaker"
|
||||
info = get_fake_chromecast_info()
|
||||
|
||||
chromecast, _ = await async_setup_media_player_cast(hass, info)
|
||||
_, conn_status_cb, _ = get_status_callbacks(chromecast)
|
||||
|
||||
# Bring Chromecast online
|
||||
connection_status = MagicMock()
|
||||
connection_status.status = "CONNECTED"
|
||||
conn_status_cb(connection_status)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# This will play using the cast platform
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl",
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: "/api/hls/bla/master_playlist.m3u8",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert we added extra play information
|
||||
quick_play_mock.assert_called()
|
||||
app_data = quick_play_mock.call_args[0][2]
|
||||
|
||||
assert not app_data["media_id"].startswith("/")
|
||||
assert "authSig" in yarl.URL(app_data["media_id"]).query
|
||||
assert app_data["media_type"] == "application/vnd.apple.mpegurl"
|
||||
assert app_data["stream_type"] == "LIVE"
|
||||
assert app_data["media_info"] == {
|
||||
"hlsVideoSegmentFormat": "fmp4",
|
||||
}
|
||||
|
||||
quick_play_mock.reset_mock()
|
||||
|
||||
# Test not appending if we have a signature
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl",
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert we added extra play information
|
||||
quick_play_mock.assert_called()
|
||||
app_data = quick_play_mock.call_args[0][2]
|
||||
# No authSig appended
|
||||
assert (
|
||||
app_data["media_id"]
|
||||
== f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla"
|
||||
)
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.helpers.network import (
|
|||
_get_internal_url,
|
||||
_get_request_host,
|
||||
get_url,
|
||||
is_hass_url,
|
||||
is_internal_request,
|
||||
)
|
||||
|
||||
|
@ -645,3 +646,47 @@ async def test_is_internal_request(hass: HomeAssistant):
|
|||
"homeassistant.helpers.network._get_request_host", return_value="192.168.0.1"
|
||||
):
|
||||
assert is_internal_request(hass)
|
||||
|
||||
|
||||
async def test_is_hass_url(hass):
|
||||
"""Test is_hass_url."""
|
||||
assert hass.config.api is None
|
||||
assert hass.config.internal_url is None
|
||||
assert hass.config.external_url is None
|
||||
|
||||
assert is_hass_url(hass, "http://example.com") is False
|
||||
|
||||
hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123")
|
||||
assert is_hass_url(hass, "http://192.168.123.123:8123") is True
|
||||
assert is_hass_url(hass, "https://192.168.123.123:8123") is False
|
||||
assert is_hass_url(hass, "http://192.168.123.123") is False
|
||||
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"internal_url": "http://example.local:8123"},
|
||||
)
|
||||
assert is_hass_url(hass, "http://example.local:8123") is True
|
||||
assert is_hass_url(hass, "https://example.local:8123") is False
|
||||
assert is_hass_url(hass, "http://example.local") is False
|
||||
|
||||
await async_process_ha_core_config(
|
||||
hass,
|
||||
{"external_url": "https://example.com:443"},
|
||||
)
|
||||
assert is_hass_url(hass, "https://example.com:443") is True
|
||||
assert is_hass_url(hass, "https://example.com") is True
|
||||
assert is_hass_url(hass, "http://example.com:443") is False
|
||||
assert is_hass_url(hass, "http://example.com") is False
|
||||
|
||||
with patch.object(
|
||||
hass.components.cloud,
|
||||
"async_remote_ui_url",
|
||||
return_value="https://example.nabu.casa",
|
||||
):
|
||||
assert is_hass_url(hass, "https://example.nabu.casa") is False
|
||||
|
||||
hass.config.components.add("cloud")
|
||||
assert is_hass_url(hass, "https://example.nabu.casa:443") is True
|
||||
assert is_hass_url(hass, "https://example.nabu.casa") is True
|
||||
assert is_hass_url(hass, "http://example.nabu.casa:443") is False
|
||||
assert is_hass_url(hass, "http://example.nabu.casa") is False
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue