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` +