diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts index e4eda4b3d8..a8d34c8a18 100644 --- a/src/components/ha-camera-stream.ts +++ b/src/components/ha-camera-stream.ts @@ -3,56 +3,39 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; import { fireEvent } from "../common/dom/fire_event"; import { computeStateName } from "../common/entity/compute_state_name"; import { supportsFeature } from "../common/entity/supports-feature"; -import { nextRender } from "../common/util/render-status"; -import { getExternalConfig } from "../external_app/external_config"; import { CAMERA_SUPPORT_STREAM, computeMJPEGStreamUrl, fetchStreamUrl, } from "../data/camera"; import { CameraEntity, HomeAssistant } from "../types"; - -type HLSModule = typeof import("hls.js"); +import "./ha-hls-player"; @customElement("ha-camera-stream") class HaCameraStream extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public stateObj?: CameraEntity; + @property({ attribute: false }) public stateObj?: CameraEntity; @property({ type: Boolean }) public showControls = false; - @internalProperty() private _attached = false; - // We keep track if we should force MJPEG with a string // that way it automatically resets if we change entity. - @internalProperty() private _forceMJPEG: string | undefined = undefined; + @internalProperty() private _forceMJPEG?: string; - private _hlsPolyfillInstance?: Hls; - - private _useExoPlayer = false; - - public connectedCallback() { - super.connectedCallback(); - this._attached = true; - } - - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; - } + @internalProperty() private _url?: string; protected render(): TemplateResult { - if (!this.stateObj || !this._attached) { + if (!this.stateObj || (!this._forceMJPEG && !this._url)) { return html``; } @@ -70,50 +53,22 @@ class HaCameraStream extends LitElement { /> ` : html` - + .hass=${this.hass} + .url=${this._url!} + > `} `; } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - - const stateObjChanged = changedProps.has("stateObj"); - const attachedChanged = changedProps.has("_attached"); - - const oldState = changedProps.get("stateObj") as this["stateObj"]; - const oldEntityId = oldState ? oldState.entity_id : undefined; - const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined; - - if ( - (!stateObjChanged && !attachedChanged) || - (stateObjChanged && oldEntityId === curEntityId) - ) { - return; - } - - // If we are no longer attached, destroy polyfill. - if (attachedChanged && !this._attached) { - this._destroyPolyfill(); - return; - } - - // Nothing to do if we are render MJPEG. - if (this._shouldRenderMJPEG) { - return; - } - - // Tear down existing polyfill, if available - this._destroyPolyfill(); - - if (curEntityId) { - this._startHls(); + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("stateObj")) { + this._forceMJPEG = undefined; + this._getStreamUrl(); } } @@ -125,136 +80,35 @@ class HaCameraStream extends LitElement { ); } - private get _videoEl(): HTMLVideoElement { - return this.shadowRoot!.querySelector("video")!; - } - - private async _getUseExoPlayer(): Promise { - if (!this.hass!.auth.external) { - return false; - } - const externalConfig = await getExternalConfig(this.hass!.auth.external); - return externalConfig && externalConfig.hasExoPlayer; - } - - private async _startHls(): Promise { - // eslint-disable-next-line - let hls; - const videoEl = this._videoEl; - this._useExoPlayer = await this._getUseExoPlayer(); - if (!this._useExoPlayer) { - hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) - .default as HLSModule; - let hlsSupported = hls.isSupported(); - - if (!hlsSupported) { - hlsSupported = - videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; - } - - if (!hlsSupported) { - this._forceMJPEG = this.stateObj!.entity_id; - return; - } - } - + private async _getStreamUrl(): Promise { try { const { url } = await fetchStreamUrl( this.hass!, this.stateObj!.entity_id ); - if (this._useExoPlayer) { - this._renderHLSExoPlayer(url); - } else if (hls.isSupported()) { - this._renderHLSPolyfill(videoEl, hls, url); - } else { - this._renderHLSNative(videoEl, url); - } - return; + this._url = url; } catch (err) { // Fails if we were unable to get a stream // eslint-disable-next-line console.error(err); + this._forceMJPEG = this.stateObj!.entity_id; } } - private async _renderHLSExoPlayer(url: string) { - window.addEventListener("resize", this._resizeExoPlayer); - this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); - this._videoEl.style.visibility = "hidden"; - await this.hass!.auth.external!.sendMessage({ - type: "exoplayer/play_hls", - payload: new URL(url, window.location.href).toString(), - }); - } - - private _resizeExoPlayer = () => { - const rect = this._videoEl.getBoundingClientRect(); - this.hass!.auth.external!.fireMessage({ - type: "exoplayer/resize", - payload: { - left: rect.left, - top: rect.top, - right: rect.right, - bottom: rect.bottom, - }, - }); - }; - - private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { - videoEl.src = url; - await new Promise((resolve) => - videoEl.addEventListener("loadedmetadata", resolve) - ); - videoEl.play(); - } - - private async _renderHLSPolyfill( - videoEl: HTMLVideoElement, - // eslint-disable-next-line - Hls: HLSModule, - url: string - ) { - const hls = new Hls({ - liveBackBufferLength: 60, - fragLoadingTimeOut: 30000, - manifestLoadingTimeOut: 30000, - levelLoadingTimeOut: 30000, - }); - this._hlsPolyfillInstance = hls; - hls.attachMedia(videoEl); - hls.on(Hls.Events.MEDIA_ATTACHED, () => { - hls.loadSource(url); - }); - } - private _elementResized() { fireEvent(this, "iron-resize"); } - private _destroyPolyfill() { - if (this._hlsPolyfillInstance) { - this._hlsPolyfillInstance.destroy(); - this._hlsPolyfillInstance = undefined; - } - if (this._useExoPlayer) { - window.removeEventListener("resize", this._resizeExoPlayer); - this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" }); - } - } - static get styles(): CSSResult { return css` :host, - img, - video { + img { display: block; } - img, - video { + img { width: 100%; } `; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 04dcbb1155..4459efb5b3 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -10,7 +10,7 @@ import "./ha-icon-button"; const MwcDialog = customElements.get("mwc-dialog") as Constructor; export const createCloseHeading = (hass: HomeAssistant, title: string) => html` - ${title} + ${title} + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + const attachedChanged = changedProps.has("_attached"); + const urlChanged = changedProps.has("url"); + + if (!urlChanged && !attachedChanged) { + return; + } + + // If we are no longer attached, destroy polyfill + if (attachedChanged && !this._attached) { + // Tear down existing polyfill, if available + this._destroyPolyfill(); + return; + } + + this._destroyPolyfill(); + this._startHls(); + } + + private async _getUseExoPlayer(): Promise { + if (!this.hass!.auth.external) { + return false; + } + const externalConfig = await getExternalConfig(this.hass!.auth.external); + return externalConfig && externalConfig.hasExoPlayer; + } + + private async _startHls(): Promise { + let hls: any; + const videoEl = this._videoEl; + this._useExoPlayer = await this._getUseExoPlayer(); + if (!this._useExoPlayer) { + hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) + .default as HLSModule; + let hlsSupported = hls.isSupported(); + + if (!hlsSupported) { + hlsSupported = + videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; + } + + if (!hlsSupported) { + this._videoEl.innerHTML = this.hass.localize( + "ui.components.media-browser.video_not_supported" + ); + return; + } + } + + const url = this.url; + + if (this._useExoPlayer) { + this._renderHLSExoPlayer(url); + } else if (hls.isSupported()) { + this._renderHLSPolyfill(videoEl, hls, url); + } else { + this._renderHLSNative(videoEl, url); + } + } + + private async _renderHLSExoPlayer(url: string) { + window.addEventListener("resize", this._resizeExoPlayer); + this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); + this._videoEl.style.visibility = "hidden"; + await this.hass!.auth.external!.sendMessage({ + type: "exoplayer/play_hls", + payload: new URL(url, window.location.href).toString(), + }); + } + + private _resizeExoPlayer = () => { + const rect = this._videoEl.getBoundingClientRect(); + this.hass!.auth.external!.fireMessage({ + type: "exoplayer/resize", + payload: { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + }, + }); + }; + + private async _renderHLSPolyfill( + videoEl: HTMLVideoElement, + Hls: HLSModule, + url: string + ) { + const hls = new Hls({ + liveBackBufferLength: 60, + fragLoadingTimeOut: 30000, + manifestLoadingTimeOut: 30000, + levelLoadingTimeOut: 30000, + }); + this._hlsPolyfillInstance = hls; + hls.attachMedia(videoEl); + hls.on(Hls.Events.MEDIA_ATTACHED, () => { + hls.loadSource(url); + }); + } + + private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { + videoEl.src = url; + await new Promise((resolve) => + videoEl.addEventListener("loadedmetadata", resolve) + ); + videoEl.play(); + } + + private _elementResized() { + fireEvent(this, "iron-resize"); + } + + private _destroyPolyfill() { + if (this._hlsPolyfillInstance) { + this._hlsPolyfillInstance.destroy(); + this._hlsPolyfillInstance = undefined; + } + if (this._useExoPlayer) { + window.removeEventListener("resize", this._resizeExoPlayer); + this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" }); + } + } + + static get styles(): CSSResult { + return css` + :host, + video { + display: block; + } + + video { + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-hls-player": HaHLSPlayer; + } +} diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 5f4d14649a..9f0c07436f 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -8,7 +8,7 @@ import { property, TemplateResult, } from "lit-element"; -import { HASSDomEvent } from "../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import type { MediaPickedEvent, MediaPlayerBrowseAction, @@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement { @internalProperty() private _params?: MediaPlayerBrowseDialogParams; - public async showDialog( - params: MediaPlayerBrowseDialogParams - ): Promise { + public showDialog(params: MediaPlayerBrowseDialogParams): void { this._params = params; this._entityId = this._params.entityId; this._mediaContentId = this._params.mediaContentId; this._mediaContentType = this._params.mediaContentType; this._action = this._params.action || "play"; + } - await this.updateComplete; + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", {dialog: this.localName}); } protected render(): TemplateResult { @@ -57,7 +58,7 @@ class DialogMediaPlayerBrowse extends LitElement { escapeKeyAction hideActions flexContent - @closed=${this._closeDialog} + @closed=${this.closeDialog} > `; } - private _closeDialog() { - this._params = undefined; - } - private _mediaPicked(ev: HASSDomEvent): void { this._params!.mediaPickedCallback(ev.detail); if (this._action !== "play") { - this._closeDialog(); + this.closeDialog(); } } @@ -93,14 +90,6 @@ class DialogMediaPlayerBrowse extends LitElement { --dialog-content-padding: 0; } - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - border-bottom: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); - } - @media (min-width: 800px) { ha-dialog { --mdc-dialog-max-width: 800px; diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index c485a96453..5b3d637812 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -22,7 +22,13 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeRTLDirection } from "../../common/util/compute_rtl"; import { debounce } from "../../common/util/debounce"; -import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; +import { + browseLocalMediaPlayer, + browseMediaPlayer, + BROWSER_SOURCE, + MediaPickedEvent, + MediaPlayerBrowseAction, +} from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { haStyle } from "../../resources/styles"; @@ -50,11 +56,7 @@ export class HaMediaPlayerBrowse extends LitElement { @property() public mediaContentType?: string; - @property() public action: "pick" | "play" = "play"; - - @property({ type: Boolean }) public hideBack = false; - - @property({ type: Boolean }) public hideTitle = false; + @property() public action: MediaPlayerBrowseAction = "play"; @property({ type: Boolean }) public dialog = false; @@ -88,52 +90,53 @@ export class HaMediaPlayerBrowse extends LitElement { } protected render(): TemplateResult { - if (!this._mediaPlayerItems.length) { - return html``; - } - if (this._loading) { return html``; } - const mostRecentItem = this._mediaPlayerItems[ + if (!this._mediaPlayerItems.length) { + return html``; + } + + const currentItem = this._mediaPlayerItems[ this._mediaPlayerItems.length - 1 ]; - const previousItem = + + const previousItem: MediaPlayerItem | undefined = this._mediaPlayerItems.length > 1 ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2] : undefined; const hasExpandableChildren: | MediaPlayerItem - | undefined = this._hasExpandableChildren(mostRecentItem.children); + | undefined = this._hasExpandableChildren(currentItem.children); - const showImages = mostRecentItem.children?.some( - (child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail + const showImages: boolean | undefined = currentItem.children?.some( + (child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail ); const mediaType = this.hass.localize( - `ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` + `ui.components.media-browser.content-type.${currentItem.media_content_type}` ); return html`
- ${mostRecentItem.thumbnail + ${currentItem.thumbnail ? html`
- ${this._narrow && mostRecentItem?.can_play + ${this._narrow && currentItem?.can_play ? html` - ${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail) - ? "" - : html``} - ${mostRecentItem?.can_play && - (!mostRecentItem.thumbnail || !this._narrow) + + ${currentItem.can_play && (!currentItem.thumbnail || !this._narrow) ? html` - + ` : ""}
- ${mostRecentItem.children?.length + ${currentItem.children?.length ? hasExpandableChildren ? html`
- ${mostRecentItem.children?.length - ? html` - ${mostRecentItem.children.map( - (child) => html` -
-
- html` +
+
+ + ${child.can_expand && !child.thumbnail + ? html` + + ` + : ""} + + ${child.can_play + ? html` + - ${child.can_expand && !child.thumbnail - ? html` - - ` - : ""} - - ${child.can_play - ? html` - - - - ` - : ""} -
-
${child.title}
-
- ${this.hass.localize( - `ui.components.media-browser.content-type.${child.media_content_type}` - )} -
-
- ` - )} - ` - : ""} + + + ` + : ""} +
+
${child.title}
+
+ ${this.hass.localize( + `ui.components.media-browser.content-type.${child.media_content_type}` + )} +
+
+ ` + )}
` : html` - ${mostRecentItem.children.map( + ${currentItem.children.map( (child) => html` { @@ -383,12 +373,15 @@ export class HaMediaPlayerBrowse extends LitElement { mediaContentId?: string, mediaContentType?: string ): Promise { - const itemData = await browseMediaPlayer( - this.hass, - this.entityId, - !mediaContentId ? undefined : mediaContentId, - mediaContentType - ); + const itemData = + this.entityId !== BROWSER_SOURCE + ? await browseMediaPlayer( + this.hass, + this.entityId, + mediaContentId, + mediaContentType + ) + : await browseLocalMediaPlayer(this.hass, mediaContentId); return itemData; } @@ -485,12 +478,6 @@ export class HaMediaPlayerBrowse extends LitElement { display: block; } - .breadcrumb-overflow { - display: flex; - flex-grow: 1; - justify-content: space-between; - } - .breadcrumb { display: flex; flex-direction: column; @@ -716,6 +703,10 @@ export class HaMediaPlayerBrowse extends LitElement { -webkit-line-clamp: 1; } + :host(:not([narrow])[scroll]) .header-info { + height: 75px; + } + :host([scroll]) .header-info mwc-button, .no-img .header-info mwc-button { padding-right: 4px; diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 0a3d289259..af11824211 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -20,9 +20,10 @@ export const CONTRAST_RATIO = 4.5; export type MediaPlayerBrowseAction = "pick" | "play"; +export const BROWSER_SOURCE = "browser"; + export interface MediaPickedEvent { - media_content_id: string; - media_content_type: string; + item: MediaPlayerItem; } export interface MediaPlayerThumbnail { @@ -58,6 +59,15 @@ export const browseMediaPlayer = ( media_content_type: mediaContentType, }); +export const browseLocalMediaPlayer = ( + hass: HomeAssistant, + mediaContentId?: string +): Promise => + hass.callWS({ + type: "media_source/browse_media", + media_content_id: mediaContentId, + }); + export const getCurrentProgress = (stateObj: HassEntity): number => { let progress = stateObj.attributes.media_position; diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 0e7a445c0b..aefc6bf92f 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement { entityId: this.stateObj!.entity_id, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => this._playMedia( - pickedMedia.media_content_id, - pickedMedia.media_content_type + pickedMedia.item.media_content_id, + pickedMedia.item.media_content_type ), }); } diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index 280a32eb16..ad04160caa 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -1,6 +1,13 @@ import { PolymerElement } from "@polymer/polymer"; +import { + STATE_NOT_RUNNING, + STATE_RUNNING, + STATE_STARTING, +} from "home-assistant-js-websocket"; import { customElement, property, PropertyValues } from "lit-element"; +import { deepActiveElement } from "../common/dom/deep-active-element"; import { deepEqual } from "../common/util/deep-equal"; +import { CustomPanelInfo } from "../data/panel_custom"; import { HomeAssistant, Panels } from "../types"; import { removeInitSkeleton } from "../util/init-skeleton"; import { @@ -8,13 +15,6 @@ import { RouteOptions, RouterOptions, } from "./hass-router-page"; -import { - STATE_STARTING, - STATE_NOT_RUNNING, - STATE_RUNNING, -} from "home-assistant-js-websocket"; -import { CustomPanelInfo } from "../data/panel_custom"; -import { deepActiveElement } from "../common/dom/deep-active-element"; const CACHE_URL_PATHS = ["lovelace", "developer-tools"]; const COMPONENTS = { @@ -64,6 +64,10 @@ const COMPONENTS = { import( /* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list" ), + "media-browser": () => + import( + /* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser" + ), }; const getRoutes = (panels: Panels): RouterOptions => { diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index a176fc251c..0aa6389e21 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -667,8 +667,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { entityId: this._config!.entity, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => this._playMedia( - pickedMedia.media_content_id, - pickedMedia.media_content_type + pickedMedia.item.media_content_id, + pickedMedia.item.media_content_type ), }); } diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts new file mode 100644 index 0000000000..33cd43dd0c --- /dev/null +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -0,0 +1,151 @@ +import "@material/mwc-icon-button"; +import { mdiPlayNetwork } from "@mdi/js"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { LocalStorage } from "../../common/decorators/local-storage"; +import { HASSDomEvent } from "../../common/dom/fire_event"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { supportsFeature } from "../../common/entity/supports-feature"; +import "../../components/ha-menu-button"; +import "../../components/media-player/ha-media-player-browse"; +import { + BROWSER_SOURCE, + MediaPickedEvent, + SUPPORT_BROWSE_MEDIA, +} from "../../data/media-player"; +import "../../layouts/ha-app-layout"; +import { haStyle } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; +import { showSelectMediaPlayerDialog } from "./show-select-media-source-dialog"; + +@customElement("ha-panel-media-browser") +class PanelMediaBrowser extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) + public narrow!: boolean; + + // @ts-ignore + @LocalStorage("mediaBrowseEntityId", true) + private _entityId = BROWSER_SOURCE; + + protected render(): TemplateResult { + const stateObj = this._entityId + ? this.hass.states[this._entityId] + : undefined; + + const title = + this._entityId === BROWSER_SOURCE + ? `${this.hass.localize("ui.components.media-browser.web-browser")} - ` + : stateObj?.attributes.friendly_name + ? `${stateObj?.attributes.friendly_name} - ` + : undefined; + + return html` + + + + +
+ ${title || ""}${this.hass.localize( + "ui.components.media-browser.media-player-browser" + )} +
+ + + +
+
+
+ +
+
+ `; + } + + private _showSelectMediaPlayerDialog(): void { + showSelectMediaPlayerDialog(this, { + mediaSources: this._mediaPlayerEntities, + sourceSelectedCallback: (entityId) => { + this._entityId = entityId; + }, + }); + } + + private async _mediaPicked( + ev: HASSDomEvent + ): Promise { + const item = ev.detail.item; + if (this._entityId === BROWSER_SOURCE) { + const resolvedUrl: any = await this.hass.callWS({ + type: "media_source/resolve_media", + media_content_id: item.media_content_id, + }); + + showWebBrowserPlayMediaDialog(this, { + sourceUrl: resolvedUrl.url, + sourceType: resolvedUrl.mime_type, + title: item.title, + }); + return; + } + + this.hass!.callService("media_player", "play_media", { + entity_id: this._entityId, + media_content_id: item.media_content_id, + media_content_type: item.media_content_type, + }); + } + + private get _mediaPlayerEntities() { + return Object.values(this.hass!.states).filter((entity) => { + if ( + computeStateDomain(entity) === "media_player" && + supportsFeature(entity, SUPPORT_BROWSE_MEDIA) + ) { + return true; + } + + return false; + }); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-media-player-browse { + height: calc(100vh - 84px); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-media-browser": PanelMediaBrowser; + } +} diff --git a/src/panels/media-browser/hui-dialog-select-media-player.ts b/src/panels/media-browser/hui-dialog-select-media-player.ts new file mode 100644 index 0000000000..bacce8505e --- /dev/null +++ b/src/panels/media-browser/hui-dialog-select-media-player.ts @@ -0,0 +1,93 @@ +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { createCloseHeading } from "../../components/ha-dialog"; +import { BROWSER_SOURCE } from "../../data/media-player"; +import type { HomeAssistant } from "../../types"; +import type { SelectMediaPlayerDialogParams } from "./show-select-media-source-dialog"; + +@customElement("hui-dialog-select-media-player") +export class HuiDialogSelectMediaPlayer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + private _params?: SelectMediaPlayerDialogParams; + + public showDialog(params: SelectMediaPlayerDialogParams): void { + this._params = params; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + return html` + + ${this.hass.localize( + "ui.components.media-browser.web-browser" + )} + ${this._params.mediaSources.map( + (source) => html` + ${source.attributes.friendly_name} + ` + )} + + + `; + } + + private _selectSource(ev: CustomEvent): void { + const entityId = ev.detail.item.itemName; + this._params!.sourceSelectedCallback(entityId); + this.closeDialog(); + } + + static get styles(): CSSResult { + return css` + ha-dialog { + --dialog-content-padding: 0 24px 20px; + } + paper-item { + cursor: pointer; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-select-media-player": HuiDialogSelectMediaPlayer; + } +} diff --git a/src/panels/media-browser/hui-dialog-web-browser-play-media.ts b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts new file mode 100644 index 0000000000..05fbea66d6 --- /dev/null +++ b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts @@ -0,0 +1,122 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-hls-player"; +import type { HomeAssistant } from "../../types"; +import { WebBrowserPlayMediaDialogParams } from "./show-media-player-dialog"; + +@customElement("hui-dialog-web-browser-play-media") +export class HuiDialogWebBrowserPlayMedia extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + private _params?: WebBrowserPlayMediaDialogParams; + + public showDialog(params: WebBrowserPlayMediaDialogParams): void { + this._params = params; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params || !this._params.sourceType || !this._params.sourceUrl) { + return html``; + } + + const mediaType = this._params.sourceType.split("/", 1)[0]; + + return html` + + ${mediaType === "audio" + ? html` + + ` + : mediaType === "video" + ? html` + + ` + : this._params.sourceType === "application/x-mpegURL" + ? html` + + ` + : mediaType === "image" + ? html`` + : html`${this.hass.localize( + "ui.components.media-browser.media_not_supported" + )}`} + + `; + } + + static get styles(): CSSResult { + return css` + ha-dialog { + --mdc-dialog-heading-ink-color: var(--primary-text-color); + } + + @media (min-width: 800px) { + ha-dialog { + --mdc-dialog-max-width: 800px; + --mdc-dialog-min-width: 400px; + } + } + + video, + audio, + img { + outline: none; + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-web-browser-play-media": HuiDialogWebBrowserPlayMedia; + } +} diff --git a/src/panels/media-browser/show-media-player-dialog.ts b/src/panels/media-browser/show-media-player-dialog.ts new file mode 100644 index 0000000000..161e97fd96 --- /dev/null +++ b/src/panels/media-browser/show-media-player-dialog.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface WebBrowserPlayMediaDialogParams { + sourceUrl: string; + sourceType: string; + title?: string; +} + +export const showWebBrowserPlayMediaDialog = ( + element: HTMLElement, + webBrowserPlayMediaDialogParams: WebBrowserPlayMediaDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-web-browser-play-media", + dialogImport: () => + import( + /* webpackChunkName: "hui-dialog-media-player" */ "./hui-dialog-web-browser-play-media" + ), + dialogParams: webBrowserPlayMediaDialogParams, + }); +}; diff --git a/src/panels/media-browser/show-select-media-source-dialog.ts b/src/panels/media-browser/show-select-media-source-dialog.ts new file mode 100644 index 0000000000..ec68a67a5b --- /dev/null +++ b/src/panels/media-browser/show-select-media-source-dialog.ts @@ -0,0 +1,21 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { fireEvent } from "../../common/dom/fire_event"; + +export interface SelectMediaPlayerDialogParams { + mediaSources: HassEntity[]; + sourceSelectedCallback: (entityId: string) => void; +} + +export const showSelectMediaPlayerDialog = ( + element: HTMLElement, + selectMediaPlayereDialogParams: SelectMediaPlayerDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-select-media-player", + dialogImport: () => + import( + /* webpackChunkName: "hui-dialog-select-media-player" */ "./hui-dialog-select-media-player" + ), + dialogParams: selectMediaPlayereDialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 9f0c211c75..775859bb11 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -357,8 +357,13 @@ "play-media": "Play Media", "pick-media": "Pick Media", "no_items": "No items", - "choose-source": "Choose Source", + "choose_player": "Choose Player", "media-player-browser": "Media Player Browser", + "web-browser": "Web Browser", + "media_player": "Media Player", + "audio_not_supported": "Your browser does not support the audio element.", + "video_not_supported": "Your browser does not support the video element.", + "media_not_supported": "The Browser Media Player does not support this type of media", "content-type": { "server": "Server", "library": "Library",