Show when media is being loaded (#11750)

This commit is contained in:
Paulus Schoutsen 2022-02-21 00:55:01 -08:00 committed by GitHub
parent 9b4c6eea63
commit 28cd9b6408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 216 additions and 147 deletions

View File

@ -33,7 +33,8 @@ import type { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity"; import { UNAVAILABLE_STATES } from "./entity";
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
media_content_type?: any; media_content_id?: string;
media_content_type?: string;
media_artist?: string; media_artist?: string;
media_playlist?: string; media_playlist?: string;
media_series_title?: string; media_series_title?: string;

View File

@ -5,61 +5,60 @@ import {
SUPPORT_PAUSE, SUPPORT_PAUSE,
SUPPORT_PLAY, SUPPORT_PLAY,
} from "../../data/media-player"; } from "../../data/media-player";
import { resolveMediaSource } from "../../data/media_source"; import { ResolvedMediaSource } from "../../data/media_source";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export class BrowserMediaPlayer { 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( constructor(
public hass: HomeAssistant, public hass: HomeAssistant,
public item: MediaPlayerItem, public item: MediaPlayerItem,
public resolved: ResolvedMediaSource,
private onChange: () => void private onChange: () => void
) {} ) {
const player = new Audio(this.resolved.url);
public async initialize() {
const resolvedUrl: any = await resolveMediaSource(
this.hass,
this.item.media_content_id
);
const player = new Audio(resolvedUrl.url);
player.addEventListener("play", this._handleChange); 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("pause", this._handleChange);
player.addEventListener("ended", this._handleChange); player.addEventListener("ended", this._handleChange);
player.addEventListener("canplaythrough", () => { player.addEventListener("canplaythrough", () => {
if (this.stopped) { if (this._removed) {
return; return;
} }
this.player = player; if (this.buffering) {
player.play(); player.play();
}
this.onChange(); this.onChange();
}); });
this.player = player;
} }
private _handleChange = () => { private _handleChange = () => {
if (!this.stopped) { if (!this._removed) {
this.onChange(); this.onChange();
} }
}; };
public pause() { public pause() {
if (this.player) { this.buffering = false;
this.player.pause(); this.player.pause();
} }
}
public play() { public play() {
if (this.player) {
this.player.play(); this.player.play();
} }
}
public stop() { public remove() {
this.stopped = true; this._removed = true;
// @ts-ignore // @ts-ignore
this.onChange = undefined; this.onChange = undefined;
if (this.player) { if (this.player) {
@ -68,9 +67,7 @@ export class BrowserMediaPlayer {
} }
public get isPlaying(): boolean { public get isPlaying(): boolean {
return ( return this.buffering || (!this.player.paused && !this.player.ended);
this.player !== undefined && !this.player.paused && !this.player.ended
);
} }
static idleStateObj(): MediaPlayerEntity { static idleStateObj(): MediaPlayerEntity {
@ -88,19 +85,19 @@ export class BrowserMediaPlayer {
toStateObj(): MediaPlayerEntity { toStateObj(): MediaPlayerEntity {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
const base = BrowserMediaPlayer.idleStateObj(); const base = BrowserMediaPlayer.idleStateObj();
if (!this.player) {
return base;
}
base.state = this.isPlaying ? "playing" : "paused"; base.state = this.isPlaying ? "playing" : "paused";
base.attributes = { base.attributes = {
media_title: this.item.title, 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, entity_picture: this.item.thumbnail,
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
supported_features: SUPPORT_PLAY | SUPPORT_PAUSE, 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; return base;
} }
} }

View File

@ -20,6 +20,7 @@ import {
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_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 { domainIcon } from "../../common/entity/domain_icon";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import "../../components/ha-button-menu"; import "../../components/ha-button-menu";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import { UNAVAILABLE_STATES } from "../../data/entity"; import { UNAVAILABLE_STATES } from "../../data/entity";
import { import {
@ -43,6 +45,7 @@ import {
SUPPORT_PLAY, SUPPORT_PLAY,
SUPPORT_STOP, SUPPORT_STOP,
} from "../../data/media-player"; } from "../../data/media-player";
import { ResolvedMediaSource } from "../../data/media_source";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee"; import "../lovelace/components/hui-marquee";
import { BrowserMediaPlayer } from "./browser-media-player"; import { BrowserMediaPlayer } from "./browser-media-player";
@ -54,7 +57,7 @@ declare global {
} }
@customElement("ha-bar-media-player") @customElement("ha-bar-media-player")
class BarMediaPlayer extends LitElement { export class BarMediaPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string; @property({ attribute: false }) public entityId!: string;
@ -68,6 +71,8 @@ class BarMediaPlayer extends LitElement {
@state() private _marqueeActive = false; @state() private _marqueeActive = false;
@state() private _newMediaExpected = false;
@state() private _browserPlayer?: BrowserMediaPlayer; @state() private _browserPlayer?: BrowserMediaPlayer;
private _progressInterval?: number; private _progressInterval?: number;
@ -98,32 +103,54 @@ class BarMediaPlayer extends LitElement {
clearInterval(this._progressInterval); clearInterval(this._progressInterval);
this._progressInterval = undefined; this._progressInterval = undefined;
} }
this._tearDownBrowserPlayer();
if (this._browserPlayer) {
this._browserPlayer.stop();
this._browserPlayer = undefined;
}
} }
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) { if (this.entityId !== BROWSER_PLAYER) {
throw Error("Only browser supported"); throw Error("Only browser supported");
} }
if (this._browserPlayer) { this._tearDownBrowserPlayer();
this._browserPlayer.stop(); this._browserPlayer = new BrowserMediaPlayer(
} this.hass,
this._browserPlayer = new BrowserMediaPlayer(this.hass, item, () => item,
this.requestUpdate("_browserPlayer") resolved,
() => this.requestUpdate("_browserPlayer")
); );
await this._browserPlayer.initialize(); this._newMediaExpected = false;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._newMediaExpected) {
return html`
<div class="controls-progress">
${until(
// Only show spinner after 500ms
new Promise((resolve) => setTimeout(resolve, 500)).then(
() => html`<ha-circular-progress active></ha-circular-progress>`
)
)}
</div>
`;
}
const isBrowser = this.entityId === BROWSER_PLAYER; const isBrowser = this.entityId === BROWSER_PLAYER;
const stateObj = this._stateObj; const stateObj = this._stateObj;
const controls = !stateObj
? undefined if (!stateObj) {
: !this.narrow return this._renderChoosePlayer(stateObj);
}
const controls = !this.narrow
? computeMediaControls(stateObj) ? computeMediaControls(stateObj)
: (stateObj.state === "playing" && : (stateObj.state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) || (supportsFeature(stateObj, SUPPORT_PAUSE) ||
@ -152,16 +179,14 @@ class BarMediaPlayer extends LitElement {
}, },
] ]
: [{}]; : [{}];
const mediaDescription = stateObj ? computeMediaDescription(stateObj) : ""; const mediaDescription = computeMediaDescription(stateObj);
const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration); const mediaDuration = formatMediaTime(stateObj.attributes.media_duration);
const mediaTitleClean = cleanupMediaTitle( const mediaTitleClean = cleanupMediaTitle(
stateObj?.attributes.media_title || "" stateObj.attributes.media_title || ""
); );
const mediaArt =
const mediaArt = stateObj stateObj.attributes.entity_picture_local ||
? stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
stateObj.attributes.entity_picture
: undefined;
return html` return html`
<div <div
@ -177,7 +202,10 @@ class BarMediaPlayer extends LitElement {
<hui-marquee <hui-marquee
.text=${mediaTitleClean || .text=${mediaTitleClean ||
mediaDescription || mediaDescription ||
this.hass.localize(`ui.card.media_player.nothing_playing`)} cleanupMediaTitle(stateObj.attributes.media_content_id) ||
(stateObj.state !== "playing" && stateObj.state !== "on"
? this.hass.localize(`ui.card.media_player.nothing_playing`)
: "")}
.active=${this._marqueeActive} .active=${this._marqueeActive}
@mouseover=${this._marqueeMouseOver} @mouseover=${this._marqueeMouseOver}
@mouseleave=${this._marqueeMouseLeave} @mouseleave=${this._marqueeMouseLeave}
@ -188,6 +216,9 @@ class BarMediaPlayer extends LitElement {
</div> </div>
</div> </div>
<div class="controls-progress"> <div class="controls-progress">
${this._browserPlayer?.buffering
? html` <ha-circular-progress active></ha-circular-progress> `
: html`
<div class="controls"> <div class="controls">
${controls === undefined ${controls === undefined
? "" ? ""
@ -205,7 +236,7 @@ class BarMediaPlayer extends LitElement {
` `
)} )}
</div> </div>
${stateObj?.attributes.media_duration === Infinity ${stateObj.attributes.media_duration === Infinity
? html`` ? html``
: this.narrow : this.narrow
? html`<mwc-linear-progress></mwc-linear-progress>` ? html`<mwc-linear-progress></mwc-linear-progress>`
@ -216,10 +247,19 @@ class BarMediaPlayer extends LitElement {
<div>${mediaDuration}</div> <div>${mediaDuration}</div>
</div> </div>
`} `}
`}
</div> </div>
${this._renderChoosePlayer(stateObj)}
`;
}
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
const isBrowser = this.entityId === BROWSER_PLAYER;
return html`
<div class="choose-player ${isBrowser ? "browser" : ""}"> <div class="choose-player ${isBrowser ? "browser" : ""}">
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
${this.narrow ${
this.narrow
? html` ? html`
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
@ -233,7 +273,11 @@ class BarMediaPlayer extends LitElement {
slot="trigger" slot="trigger"
.label=${this.narrow .label=${this.narrow
? "" ? ""
: `${stateObj ? computeStateName(stateObj) : this.entityId} : `${
stateObj
? computeStateName(stateObj)
: this.entityId
}
`} `}
> >
<ha-svg-icon <ha-svg-icon
@ -247,7 +291,8 @@ class BarMediaPlayer extends LitElement {
.path=${mdiChevronDown} .path=${mdiChevronDown}
></ha-svg-icon> ></ha-svg-icon>
</mwc-button> </mwc-button>
`} `
}
<mwc-list-item <mwc-list-item
.player=${BROWSER_PLAYER} .player=${BROWSER_PLAYER}
?selected=${isBrowser} ?selected=${isBrowser}
@ -269,18 +314,26 @@ class BarMediaPlayer extends LitElement {
)} )}
</ha-button-menu> </ha-button-menu>
</div> </div>
</div>
`; `;
} }
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); 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 ( if (
changedProps.has("entityId") && !oldHass ||
this.entityId !== BROWSER_PLAYER && oldHass.states[this.entityId] !== this.hass.states[this.entityId]
this._browserPlayer
) { ) {
this._browserPlayer?.stop(); this._newMediaExpected = false;
this._browserPlayer = undefined;
} }
} }
@ -329,6 +382,13 @@ class BarMediaPlayer extends LitElement {
return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined; return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined;
} }
private _tearDownBrowserPlayer() {
if (this._browserPlayer) {
this._browserPlayer.remove();
this._browserPlayer = undefined;
}
}
private _openMoreInfo() { private _openMoreInfo() {
if (this._browserPlayer) { if (this._browserPlayer) {
return; return;

View File

@ -37,6 +37,7 @@ import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types"; import type { HomeAssistant, Route } from "../../types";
import "./ha-bar-media-player"; import "./ha-bar-media-player";
import type { BarMediaPlayer } from "./ha-bar-media-player";
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { import {
@ -79,6 +80,8 @@ class PanelMediaBrowser extends LitElement {
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse; @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
@query("ha-bar-media-player") private _player!: BarMediaPlayer;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-app-layout> <ha-app-layout>
@ -235,15 +238,23 @@ class PanelMediaBrowser extends LitElement {
ev: HASSDomEvent<MediaPickedEvent> ev: HASSDomEvent<MediaPickedEvent>
): Promise<void> { ): Promise<void> {
const item = ev.detail.item; const item = ev.detail.item;
if (this._entityId !== BROWSER_PLAYER) { if (this._entityId !== BROWSER_PLAYER) {
this.hass!.callService("media_player", "play_media", { this._player.showResolvingNewMediaPicked();
try {
await this.hass!.callService("media_player", "play_media", {
entity_id: this._entityId, entity_id: this._entityId,
media_content_id: item.media_content_id, media_content_id: item.media_content_id,
media_content_type: item.media_content_type, media_content_type: item.media_content_type,
}); });
} catch (err) {
this._player.hideResolvingNewMediaPicked();
}
return; return;
} }
// We won't cancel current media being played if we're going to
// open a camera.
if (isCameraMediaSource(item.media_content_id)) { if (isCameraMediaSource(item.media_content_id)) {
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId: getEntityIdFromCameraMediaSource(item.media_content_id), entityId: getEntityIdFromCameraMediaSource(item.media_content_id),
@ -251,15 +262,15 @@ class PanelMediaBrowser extends LitElement {
return; return;
} }
this._player.showResolvingNewMediaPicked();
const resolvedUrl = await resolveMediaSource( const resolvedUrl = await resolveMediaSource(
this.hass, this.hass,
item.media_content_id item.media_content_id
); );
if (resolvedUrl.mime_type.startsWith("audio/")) { if (resolvedUrl.mime_type.startsWith("audio/")) {
await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem( this._player.playItem(item, resolvedUrl);
item
);
return; return;
} }