From 28cd9b6408bd1a21a58b620aa25798b99e2dda03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Feb 2022 00:55:01 -0800 Subject: [PATCH] Show when media is being loaded (#11750) --- src/data/media-player.ts | 3 +- .../media-browser/browser-media-player.ts | 65 ++--- .../media-browser/ha-bar-media-player.ts | 268 +++++++++++------- .../media-browser/ha-panel-media-browser.ts | 27 +- 4 files changed, 216 insertions(+), 147 deletions(-) diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 9f7856183c..7a8a944957 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -33,7 +33,8 @@ import type { HomeAssistant } from "../types"; import { UNAVAILABLE_STATES } from "./entity"; interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { - media_content_type?: any; + media_content_id?: string; + media_content_type?: string; media_artist?: string; media_playlist?: string; media_series_title?: string; diff --git a/src/panels/media-browser/browser-media-player.ts b/src/panels/media-browser/browser-media-player.ts index ac2ed08137..40b05fdae4 100644 --- a/src/panels/media-browser/browser-media-player.ts +++ b/src/panels/media-browser/browser-media-player.ts @@ -5,61 +5,60 @@ import { SUPPORT_PAUSE, SUPPORT_PLAY, } from "../../data/media-player"; -import { resolveMediaSource } from "../../data/media_source"; +import { ResolvedMediaSource } from "../../data/media_source"; import { HomeAssistant } from "../../types"; export class BrowserMediaPlayer { - private player?: HTMLAudioElement; + private player: HTMLAudioElement; - private stopped = false; + // We pretend we're playing while still buffering. + public buffering = true; + + private _removed = false; constructor( public hass: HomeAssistant, public item: MediaPlayerItem, + public resolved: ResolvedMediaSource, private onChange: () => void - ) {} - - public async initialize() { - const resolvedUrl: any = await resolveMediaSource( - this.hass, - this.item.media_content_id - ); - - const player = new Audio(resolvedUrl.url); + ) { + const player = new Audio(this.resolved.url); player.addEventListener("play", this._handleChange); - player.addEventListener("playing", this._handleChange); + player.addEventListener("playing", () => { + this.buffering = false; + this._handleChange(); + }); player.addEventListener("pause", this._handleChange); player.addEventListener("ended", this._handleChange); player.addEventListener("canplaythrough", () => { - if (this.stopped) { + if (this._removed) { return; } - this.player = player; - player.play(); + if (this.buffering) { + player.play(); + } this.onChange(); }); + this.player = player; } private _handleChange = () => { - if (!this.stopped) { + if (!this._removed) { this.onChange(); } }; public pause() { - if (this.player) { - this.player.pause(); - } + this.buffering = false; + this.player.pause(); } public play() { - if (this.player) { - this.player.play(); - } + this.player.play(); } - public stop() { - this.stopped = true; + public remove() { + this._removed = true; // @ts-ignore this.onChange = undefined; if (this.player) { @@ -68,9 +67,7 @@ export class BrowserMediaPlayer { } public get isPlaying(): boolean { - return ( - this.player !== undefined && !this.player.paused && !this.player.ended - ); + return this.buffering || (!this.player.paused && !this.player.ended); } static idleStateObj(): MediaPlayerEntity { @@ -88,19 +85,19 @@ export class BrowserMediaPlayer { toStateObj(): MediaPlayerEntity { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement const base = BrowserMediaPlayer.idleStateObj(); - if (!this.player) { - return base; - } base.state = this.isPlaying ? "playing" : "paused"; base.attributes = { media_title: this.item.title, - media_duration: this.player.duration, - media_position: this.player.currentTime, - media_position_updated_at: base.last_updated, entity_picture: this.item.thumbnail, // eslint-disable-next-line no-bitwise supported_features: SUPPORT_PLAY | SUPPORT_PAUSE, }; + + if (this.player.duration) { + base.attributes.media_duration = this.player.duration; + base.attributes.media_position = this.player.currentTime; + base.attributes.media_position_updated_at = base.last_updated; + } return base; } } diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 3f76564e1e..4bc0a6a767 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -20,6 +20,7 @@ import { } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { until } from "lit/directives/until"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; @@ -27,6 +28,7 @@ import { computeStateName } from "../../common/entity/compute_state_name"; import { domainIcon } from "../../common/entity/domain_icon"; import { supportsFeature } from "../../common/entity/supports-feature"; import "../../components/ha-button-menu"; +import "../../components/ha-circular-progress"; import "../../components/ha-icon-button"; import { UNAVAILABLE_STATES } from "../../data/entity"; import { @@ -43,6 +45,7 @@ import { SUPPORT_PLAY, SUPPORT_STOP, } from "../../data/media-player"; +import { ResolvedMediaSource } from "../../data/media_source"; import type { HomeAssistant } from "../../types"; import "../lovelace/components/hui-marquee"; import { BrowserMediaPlayer } from "./browser-media-player"; @@ -54,7 +57,7 @@ declare global { } @customElement("ha-bar-media-player") -class BarMediaPlayer extends LitElement { +export class BarMediaPlayer extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public entityId!: string; @@ -68,6 +71,8 @@ class BarMediaPlayer extends LitElement { @state() private _marqueeActive = false; + @state() private _newMediaExpected = false; + @state() private _browserPlayer?: BrowserMediaPlayer; private _progressInterval?: number; @@ -98,32 +103,54 @@ class BarMediaPlayer extends LitElement { clearInterval(this._progressInterval); this._progressInterval = undefined; } - - if (this._browserPlayer) { - this._browserPlayer.stop(); - this._browserPlayer = undefined; - } + this._tearDownBrowserPlayer(); } - public async playItem(item: MediaPlayerItem) { + public showResolvingNewMediaPicked() { + this._tearDownBrowserPlayer(); + this._newMediaExpected = true; + } + + public hideResolvingNewMediaPicked() { + this._newMediaExpected = false; + } + + public playItem(item: MediaPlayerItem, resolved: ResolvedMediaSource) { if (this.entityId !== BROWSER_PLAYER) { throw Error("Only browser supported"); } - if (this._browserPlayer) { - this._browserPlayer.stop(); - } - this._browserPlayer = new BrowserMediaPlayer(this.hass, item, () => - this.requestUpdate("_browserPlayer") + this._tearDownBrowserPlayer(); + this._browserPlayer = new BrowserMediaPlayer( + this.hass, + item, + resolved, + () => this.requestUpdate("_browserPlayer") ); - await this._browserPlayer.initialize(); + this._newMediaExpected = false; } protected render(): TemplateResult { + if (this._newMediaExpected) { + return html` +
+ ${until( + // Only show spinner after 500ms + new Promise((resolve) => setTimeout(resolve, 500)).then( + () => html`` + ) + )} +
+ `; + } + const isBrowser = this.entityId === BROWSER_PLAYER; const stateObj = this._stateObj; - const controls = !stateObj - ? undefined - : !this.narrow + + if (!stateObj) { + return this._renderChoosePlayer(stateObj); + } + + const controls = !this.narrow ? computeMediaControls(stateObj) : (stateObj.state === "playing" && (supportsFeature(stateObj, SUPPORT_PAUSE) || @@ -152,16 +179,14 @@ class BarMediaPlayer extends LitElement { }, ] : [{}]; - const mediaDescription = stateObj ? computeMediaDescription(stateObj) : ""; - const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration); + const mediaDescription = computeMediaDescription(stateObj); + const mediaDuration = formatMediaTime(stateObj.attributes.media_duration); const mediaTitleClean = cleanupMediaTitle( - stateObj?.attributes.media_title || "" + stateObj.attributes.media_title || "" ); - - const mediaArt = stateObj - ? stateObj.attributes.entity_picture_local || - stateObj.attributes.entity_picture - : undefined; + const mediaArt = + stateObj.attributes.entity_picture_local || + stateObj.attributes.entity_picture; return html`
-
- ${controls === undefined - ? "" - : controls.map( - (control) => html` - - - ` - )} -
- ${stateObj?.attributes.media_duration === Infinity - ? html`` - : this.narrow - ? html`` + ${this._browserPlayer?.buffering + ? html` ` : html` -
-
- -
${mediaDuration}
+
+ ${controls === undefined + ? "" + : controls.map( + (control) => html` + + + ` + )}
+ ${stateObj.attributes.media_duration === Infinity + ? html`` + : this.narrow + ? html`` + : html` +
+
+ +
${mediaDuration}
+
+ `} `}
-
- - ${this.narrow - ? html` - - ` - : html` - + + ${ + this.narrow + ? html` + + ` + : html` + + + + + ` + } + + ${this.hass.localize("ui.components.media-browser.web-browser")} + + ${this._mediaPlayerEntities.map( + (source) => html` + - - - - `} - - ${this.hass.localize("ui.components.media-browser.web-browser")} - - ${this._mediaPlayerEntities.map( - (source) => html` - - ${computeStateName(source)} - - ` - )} - + ${computeStateName(source)} + + ` + )} + +
+ `; } public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); + if (changedProps.has("entityId")) { + this._tearDownBrowserPlayer(); + } + if (!changedProps.has("hass") || this.entityId === BROWSER_PLAYER) { + return; + } + // Reset new media expected if media player state changes + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if ( - changedProps.has("entityId") && - this.entityId !== BROWSER_PLAYER && - this._browserPlayer + !oldHass || + oldHass.states[this.entityId] !== this.hass.states[this.entityId] ) { - this._browserPlayer?.stop(); - this._browserPlayer = undefined; + this._newMediaExpected = false; } } @@ -329,6 +382,13 @@ class BarMediaPlayer extends LitElement { return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined; } + private _tearDownBrowserPlayer() { + if (this._browserPlayer) { + this._browserPlayer.remove(); + this._browserPlayer = undefined; + } + } + private _openMoreInfo() { if (this._browserPlayer) { return; diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index 9f3fa2ca1d..9bf6df9990 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -37,6 +37,7 @@ import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant, Route } from "../../types"; import "./ha-bar-media-player"; +import type { BarMediaPlayer } from "./ha-bar-media-player"; import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { @@ -79,6 +80,8 @@ class PanelMediaBrowser extends LitElement { @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse; + @query("ha-bar-media-player") private _player!: BarMediaPlayer; + protected render(): TemplateResult { return html` @@ -235,15 +238,23 @@ class PanelMediaBrowser extends LitElement { ev: HASSDomEvent ): Promise { const item = ev.detail.item; + if (this._entityId !== BROWSER_PLAYER) { - 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, - }); + this._player.showResolvingNewMediaPicked(); + try { + await 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, + }); + } catch (err) { + this._player.hideResolvingNewMediaPicked(); + } return; } + // We won't cancel current media being played if we're going to + // open a camera. if (isCameraMediaSource(item.media_content_id)) { fireEvent(this, "hass-more-info", { entityId: getEntityIdFromCameraMediaSource(item.media_content_id), @@ -251,15 +262,15 @@ class PanelMediaBrowser extends LitElement { return; } + this._player.showResolvingNewMediaPicked(); + const resolvedUrl = await resolveMediaSource( this.hass, item.media_content_id ); if (resolvedUrl.mime_type.startsWith("audio/")) { - await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem( - item - ); + this._player.playItem(item, resolvedUrl); return; }