async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Return a streamable URL and associated mime type for a UniFi Protect event. Accepted identifier format are * {nvr_id}:event:{event_id} - MP4 video clip for specific event * {nvr_id}:eventthumb:{event_id} - Thumbnail JPEG for specific event """ parts = item.identifier.split(":") if len(parts) != 3 or parts[1] not in ("event", "eventthumb"): return _bad_identifier_media(item.identifier) thumbnail_only = parts[1] == "eventthumb" try: data = self.data_sources[parts[0]] except (KeyError, IndexError) as err: return _bad_identifier_media(item.identifier, err) event = data.api.bootstrap.events.get(parts[2]) if event is None: try: event = await data.api.get_event(parts[2]) except NvrError as err: return _bad_identifier_media(item.identifier, err) else: # cache the event for later data.api.bootstrap.events[event.id] = event nvr = data.api.bootstrap.nvr if thumbnail_only: return PlayMedia(async_generate_thumbnail_url(event.id, nvr.id), "image/jpeg") return PlayMedia(async_generate_event_video_url(event), "video/mp4")
async def test_async_resolve_media_success(hass: HomeAssistant) -> None: """Test successful resolve media.""" client = create_mock_motioneye_client() config = await setup_mock_motioneye_config_entry(hass, client=client) device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, ) # Test successful resolve for a movie. client.get_movie_url = Mock(return_value="http://movie-url") media = await media_source.async_resolve_media( hass, (f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4"), None, ) assert media == PlayMedia(url="http://movie-url", mime_type="video/mp4") assert client.get_movie_url.call_args == call(TEST_CAMERA_ID, "/foo.mp4") # Test successful resolve for an image. client.get_image_url = Mock(return_value="http://image-url") media = await media_source.async_resolve_media( hass, (f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/foo.jpg"), None, ) assert media == PlayMedia(url="http://image-url", mime_type="image/jpeg") assert client.get_image_url.call_args == call(TEST_CAMERA_ID, "/foo.jpg")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" media = await async_parse_uri(item.identifier) for mass_instance in self.hass.data[DOMAIN].values(): if mass_instance.server_id != media["mass_server_id"]: continue if media["media_type"] in ["track", "radio"]: url = f"{mass_instance.base_url}/stream_media/" url += f'{media["media_type"]}/{media["provider"]}/{media["item_id"]}' return PlayMedia(url, "audio/flac") else: return PlayMedia(item.identifier, "application/musicassistant") raise BrowseError("Invalid Music Assistance instance")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" parsed = URL(item.identifier) if "message" not in parsed.query: raise Unresolvable("No message specified.") options = dict(parsed.query) kwargs: dict[str, Any] = { "engine": parsed.name, "message": options.pop("message"), "language": options.pop("language", None), "options": options, } if "cache" in options: kwargs["cache"] = options.pop("cache") == "true" manager: SpeechManager = self.hass.data[DOMAIN] try: url = await manager.async_get_url_path(**kwargs) except HomeAssistantError as err: raise Unresolvable(str(err)) from err mime_type = mimetypes.guess_type(url)[0] or "audio/mpeg" if manager.base_url and manager.base_url != get_url(self.hass): url = f"{manager.base_url}{url}" return PlayMedia(url, mime_type)
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" parsed = URL(item.identifier) if "message" not in parsed.query: raise Unresolvable("No message specified.") options = dict(parsed.query) kwargs = { "engine": parsed.name, "message": options.pop("message"), "language": options.pop("language", None), "options": options, } manager: SpeechManager = self.hass.data[DOMAIN] try: url = await manager.async_get_url_path(**kwargs ) # type: ignore[arg-type] except HomeAssistantError as err: raise Unresolvable(str(err)) from err mime_type = mimetypes.guess_type(url)[0] or "audio/mpeg" return PlayMedia(url, mime_type)
class RadioMediaSource(MediaSource): """Provide Radio stations as media sources.""" name = "Radio Browser" def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize CameraMediaSource.""" super().__init__(DOMAIN) self.hass = hass self.entry = entry @property def radios(self) -> RadioBrowser | None: """Return the radio browser.""" return self.hass.data.get(DOMAIN) async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve selected Radio station to a streaming URL.""" radios = self.radios if radios is None: raise Unresolvable("Radio Browser not initialized") station = await radios.station(uuid=item.identifier) if not station: raise Unresolvable("Radio station is no longer available") if not (mime_type := self._async_get_station_mime_type(station)): raise Unresolvable( "Could not determine stream type of radio station") # Register "click" with Radio Browser await radios.station_click(uuid=station.uuid) return PlayMedia(station.url, mime_type)
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve a media item to a playable item.""" _, camera_id, event_id = async_parse_identifier(item) data: dict = self.hass.data[self.domain] entry: dict = data.get(camera_id) if camera_id else None base: ReolinkBase = entry.get(BASE) if entry else None if not base: raise BrowseError("Camera does not exist.") file = unquote_plus(event_id) if not file: raise BrowseError("Event does not exist.") _LOGGER.debug("file = %s", file) url = await base.api.get_vod_source(file) _LOGGER.debug("Load VOD %s", url) stream = create_stream(self.hass, url) stream.add_provider("hls", timeout=3600) url: str = stream.endpoint_url("hls") # the media browser seems to have a problem with the master_playlist # ( it does not load the referenced playlist ) so we will just # force the reference playlist instead, this seems to work # though technically wrong url = url.replace("master_", "") _LOGGER.debug("Proxy %s", url) return PlayMedia(url, MIME_TYPE)
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" config_id, device_id, kind, path = self._parse_identifier( item.identifier) if not config_id or not device_id or not kind or not path: raise Unresolvable( f"Incomplete media identifier specified: {item.identifier}") config = self._get_config_or_raise(config_id) device = self._get_device_or_raise(device_id) camera_id = self._get_camera_id_or_raise(config, device) self._verify_kind_or_raise(kind) path = self._get_path_or_raise(path) client = self.hass.data[DOMAIN][config.entry_id][CONF_CLIENT] try: if kind == "movies": url = client.get_movie_url(camera_id, path) else: url = client.get_image_url(camera_id, path) except MotionEyeClientPathError as exc: raise Unresolvable from exc return PlayMedia(url, MIME_TYPE_MAP[kind])
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Return a streamable URL and associated mime type.""" media_item = await self.hass.async_add_executor_job( self.api.get_item, item.identifier) stream_url = self._get_stream_url(media_item) mime_type = _media_mime_type(media_item) return PlayMedia(stream_url, mime_type)
async def test_async_resolve_media(frigate_client: AsyncMock, hass: HomeAssistant) -> None: """Test successful resolve media.""" await setup_mock_frigate_config_entry(hass, client=frigate_client) # Test resolving a clip. media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_FRIGATE_INSTANCE_ID}/event/clips/camera/CLIP-FOO", ) assert media == PlayMedia( url= f"/api/frigate/{TEST_FRIGATE_INSTANCE_ID}/vod/event/CLIP-FOO/index.m3u8", mime_type="application/x-mpegURL", ) # Test resolving a recording. media = await media_source.async_resolve_media( hass, (f"{const.URI_SCHEME}{DOMAIN}/{TEST_FRIGATE_INSTANCE_ID}" "/recordings/2021-05/30/15/front_door/46.08.mp4"), ) assert media == PlayMedia( url= (f"/api/frigate/{TEST_FRIGATE_INSTANCE_ID}/vod/2021-05/30/15/front_door/index.m3u8" ), mime_type="application/x-mpegURL", ) # Test resolving a snapshot. media = await media_source.async_resolve_media( hass, (f"{const.URI_SCHEME}{DOMAIN}/{TEST_FRIGATE_INSTANCE_ID}" "/event/snapshots/camera/event_id"), ) assert media == PlayMedia( url=f"/api/frigate/{TEST_FRIGATE_INSTANCE_ID}/snapshot/event_id", mime_type="image/jpg", ) with pytest.raises(Unresolvable): media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/UNKNOWN")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve a media item to a playable item.""" autolog("<<<") if not item or not item.identifier: return None media_content_type, media_content_id = self.parse_mediasource_identifier( item.identifier) t = await self.jelly_cm.get_stream_url(media_content_id, media_content_type) return PlayMedia(t[0], t[1])
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 (stream_type := camera.frontend_stream_type) is None: return PlayMedia(f"/api/camera_proxy_stream/{camera.entity_id}", camera.content_type)
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" identifier = Identifier.from_str( item.identifier, default_frigate_instance_id=self._get_default_frigate_instance_id(), ) if identifier: server_path = identifier.get_integration_proxy_path() return PlayMedia( f"/api/frigate/{identifier.frigate_instance_id}/{server_path}", identifier.mime_type, ) raise Unresolvable("Unknown identifier: %s" % item.identifier)
async def async_resolve_media( self, item: MediaSourceItem, ) -> PlayMedia: """Resolve media to a url.""" entry_id, path, mime_type = item.identifier.split("~~", 2) entry = self.hass.config_entries.async_get_entry(entry_id) if entry is None: raise ValueError("Invalid entry") path_split = path.split("/", 1) return PlayMedia( f"{_build_base_url(entry)}&base={path_split[0]}&path={path_split[1]}", mime_type, )
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" mass = self.get_mass() if mass is None: raise Unresolvable("MusicAssistant is not initialized") if item.target_media_player is None: # TODO: How to intercept a play request for the 'webbrowser' player # or at least hide our source for the webbrowser player ? raise Unresolvable("Playback not supported on the device.") # get/create mass player instance attached to this entity id player = await async_register_player_control(self.hass, mass, item.target_media_player) if not player: return PlayMedia(item.identifier, MEDIA_TYPE_MUSIC) # send the mass library uri to the player(queue) await player.active_queue.play_media(item.identifier, passive=True) # tell the actual player to play the stream url content_type = player.active_queue.settings.stream_type.value return PlayMedia(player.active_queue.stream.url, f"audio/{content_type}")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve a media item to a playable item.""" _, camera_id, event_id = async_parse_identifier(item) cache: typings.MediaSourceCacheEntry = self.cache[camera_id] event = cache["playback_events"][event_id] base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE] url = await base.api.get_vod_source(event["file"]) _LOGGER.debug("Load VOD %s", url) stream = create_stream(self.hass, url) stream.add_provider("hls", timeout=600) url: str = stream.endpoint_url("hls") # the media browser seems to have a problem with the master_playlist # ( it does not load the referenced playlist ) so we will just # force the reference playlist instead, this seems to work # though technically wrong url = url.replace("master_", "") _LOGGER.debug("Proxy %s", url) return PlayMedia(url, MIME_TYPE)
class NestMediaSource(MediaSource): """Provide Nest Media Sources for Nest Cameras. The media source generates a directory tree of devices and media associated with events for each device (e.g. motion, person, etc). Each node in the tree has a unique MediaId. The lifecycle for event media is handled outside of NestMediaSource, and instead it just asks the device for all events it knows about. """ name: str = MEDIA_SOURCE_TITLE def __init__(self, hass: HomeAssistant) -> None: """Initialize NestMediaSource.""" super().__init__(DOMAIN) self.hass = hass async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media identifier to a url.""" media_id: MediaId | None = parse_media_id(item.identifier) if not media_id: raise Unresolvable("No identifier specified for MediaSourceItem") if not media_id.event_token: raise Unresolvable( "Identifier missing an event_token: %s" % item.identifier ) devices = await self.devices() if not (device := devices.get(media_id.device_id)): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) # Infer content type from the device, since it only supports one # snapshot type (either jpg or mp4 clip) content_type = EventImageType.IMAGE.content_type if CameraClipPreviewTrait.NAME in device.traits: content_type = EventImageType.CLIP_PREVIEW.content_type return PlayMedia( EVENT_MEDIA_API_URL_FORMAT.format( device_id=media_id.device_id, event_token=media_id.event_token ), content_type, )
class NestMediaSource(MediaSource): """Provide Nest Media Sources for Nest Cameras. The media source generates a directory tree of devices and media associated with events for each device (e.g. motion, person, etc). Each node in the tree has a unique MediaId. The lifecycle for event media is handled outside of NestMediaSource, and instead it just asks the device for all events it knows about. """ name: str = MEDIA_SOURCE_TITLE def __init__(self, hass: HomeAssistant) -> None: """Initialize NestMediaSource.""" super().__init__(DOMAIN) self.hass = hass async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media identifier to a url.""" media_id: MediaId | None = parse_media_id(item.identifier) if not media_id: raise Unresolvable("No identifier specified for MediaSourceItem") if not media_id.event_id: raise Unresolvable("Identifier missing an event_id: %s" % item.identifier) devices = await self.devices() if not (device := devices.get(media_id.device_id)): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) events = await _get_events(device) if media_id.event_id not in events: raise Unresolvable( "Unable to find event with identifier: %s" % item.identifier ) event = events[media_id.event_id] return PlayMedia( EVENT_MEDIA_API_URL_FORMAT.format( device_id=media_id.device_id, event_id=media_id.event_id ), event.event_image_type.content_type, )
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" config_id, device_id, kind, path = self._parse_identifier(item.identifier) if not config_id or not device_id or not kind or not path: raise Unresolvable( f"Incomplete media identifier specified: {item.identifier}" ) config = self._get_config_or_raise(config_id) device = self._get_device_or_raise(device_id) self._verify_kind_or_raise(kind) url = get_media_url( self.hass.data[DOMAIN][config.entry_id][CONF_CLIENT], self._get_camera_id_or_raise(config, device), self._get_path_or_raise(path), kind == "images", ) if not url: raise Unresolvable(f"Could not resolve media item: {item.identifier}") return PlayMedia(url, MIME_TYPE_MAP[kind])
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 (stream_type := camera.frontend_stream_type) is None: return PlayMedia(f"/api/camera_proxy_stream/{camera.entity_id}", camera.content_type) if stream_type != STREAM_TYPE_HLS: raise Unresolvable( "Camera does not support MJPEG or HLS streaming.") if "stream" not in self.hass.config.components: raise Unresolvable("Stream integration not loaded") 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_resolve_media(self, item: MediaSourceItem) -> str: """Resolve media to a url.""" iteminfo = self.async_parse_identifier(item) mime_type, _ = mimetypes.guess_type(str(iteminfo.path)) return PlayMedia(f"/media/{item.identifier}", mime_type)
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" _, category, url = async_parse_identifier(item) kind = category.split("#", 1)[1] return PlayMedia(url, MIME_TYPE_MAP[kind])
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" _, camera_id, event_id = async_parse_identifier(item) url = self.events[camera_id][event_id]["media_url"] return PlayMedia(url, MIME_TYPE)
# The device resolves to the most recent event if available if not (last_event_id := await _async_get_recent_event_id( media_id, device)): raise Unresolvable( "Unable to resolve recent event for device: %s" % item.identifier) media_id = last_event_id # Infer content type from the device, since it only supports one # snapshot type (either jpg or mp4 clip) content_type = EventImageType.IMAGE.content_type if CameraClipPreviewTrait.NAME in device.traits: content_type = EventImageType.CLIP_PREVIEW.content_type return PlayMedia( EVENT_MEDIA_API_URL_FORMAT.format( device_id=media_id.device_id, event_token=media_id.event_token), content_type, ) async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media for the specified level of the directory tree. The top level is the root that contains devices. Inside each device are media for events for that device. """ media_id: MediaId | None = parse_media_id(item.identifier) _LOGGER.debug("Browsing media for identifier=%s, media_id=%s", item.identifier, media_id) devices = await self.devices() if media_id is None:
async def test_async_browse_media(hass): """Test browse media.""" assert await async_setup_component(hass, DOMAIN, {}) # Prepare cached Netatmo event date hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_EVENTS] = { "12:34:56:78:90:ab": { 1599152672: { "id": "12345", "type": "person", "time": 1599152672, "camera_id": "12:34:56:78:90:ab", "snapshot": { "url": "https://netatmocameraimage", }, "video_id": "98765", "video_status": "available", "message": "<b>Paulus</b> seen", "media_url": "http:///files/high/index.m3u8", }, 1599152673: { "id": "12346", "type": "person", "time": 1599152673, "camera_id": "12:34:56:78:90:ab", "snapshot": { "url": "https://netatmocameraimage", }, "message": "<b>Tobias</b> seen", }, 1599152674: { "id": "12347", "type": "outdoor", "time": 1599152674, "camera_id": "12:34:56:78:90:ac", "snapshot": { "url": "https://netatmocameraimage", }, "video_id": "98766", "video_status": "available", "event_list": [ { "type": "vehicle", "time": 1599152674, "id": "12347-0", "offset": 0, "message": "Vehicle detected", "snapshot": { "url": "https://netatmocameraimage", }, }, { "type": "human", "time": 1599152674, "id": "12347-1", "offset": 8, "message": "Person detected", "snapshot": { "url": "https://netatmocameraimage", }, }, ], "media_url": "http:///files/high/index.m3u8", }, } } hass.data[DOMAIN][DATA_CAMERAS] = { "12:34:56:78:90:ab": "MyCamera", "12:34:56:78:90:ac": "MyOutdoorCamera", } assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() # Test camera not exists with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/98:76:54:32:10:ff") assert str(excinfo.value) == "Camera does not exist." # Test browse event with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/12345") assert str(excinfo.value) == "Event does not exist." # Test invalid base with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/invalid/base") assert str(excinfo.value) == "Unknown source directory." # Test successful listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/") # Test successful events listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab") # Test successful event listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672") assert media # Test successful event resolve media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672") assert media == PlayMedia(url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL")
async def test_async_browse_media(hass): """Test browse media.""" assert await async_setup_component(hass, DOMAIN, {}) # Prepare cached Netatmo event date hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_EVENTS] = ast.literal_eval( load_fixture("netatmo/events.txt")) hass.data[DOMAIN][DATA_CAMERAS] = { "12:34:56:78:90:ab": "MyCamera", "12:34:56:78:90:ac": "MyOutdoorCamera", } assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() # Test camera not exists with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/98:76:54:32:10:ff") assert str(excinfo.value) == "Camera does not exist." # Test browse event with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/12345") assert str(excinfo.value) == "Event does not exist." # Test invalid base with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/invalid/base") assert str(excinfo.value) == "Unknown source directory." # Test invalid base with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") assert str(excinfo.value) == "Not a media source item" # Test successful listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events") # Test successful listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/") # Test successful events listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab") # Test successful event listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672") assert media # Test successful event resolve media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672") assert media == PlayMedia(url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL")
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" return PlayMedia(f"/api/frigate/{item.identifier}", MIME_TYPE)