Merge pull request #794 from dermotduffy/loading-status-during-lazy-load

Refactor media load events to add unload support
This commit is contained in:
Dermot Duffy
2022-08-07 19:09:54 -07:00
committed by GitHub
7 changed files with 176 additions and 111 deletions
+16 -16
View File
@@ -60,7 +60,7 @@ import {
FrigateCardCustomAction, FrigateCardCustomAction,
FrigateCardView, FrigateCardView,
FRIGATE_CARD_VIEWS_USER_SPECIFIED, FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MediaShowInfo, MediaLoadedInfo,
MEDIA_TYPE_IMAGE, MEDIA_TYPE_IMAGE,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO,
MESSAGE_TYPE_PRIORITIES, MESSAGE_TYPE_PRIORITIES,
@@ -96,7 +96,7 @@ import {
} from './utils/ha/entity-registry.js'; } from './utils/ha/entity-registry.js';
import { ResolvedMediaCache } from './utils/ha/resolved-media.js'; import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
import { supportsFeature } from './utils/ha/update.js'; import { supportsFeature } from './utils/ha/update.js';
import { isValidMediaShowInfo } from './utils/media-info.js'; import { isValidMediaLoadedInfo } from './utils/media-info.js';
import { View } from './view.js'; import { View } from './view.js';
import pkg from '../package.json'; import pkg from '../package.json';
import { ViewContext } from 'view'; import { ViewContext } from 'view';
@@ -186,8 +186,8 @@ export class FrigateCard extends LitElement {
protected _updateTimerID: number | null = null; protected _updateTimerID: number | null = null;
// Information about loaded media items. // Information about loaded media items.
protected _currentMediaShowInfo: MediaShowInfo | null = null; protected _currentMediaLoadedInfo: MediaLoadedInfo | null = null;
protected _lastValidMediaShowInfo: MediaShowInfo | null = null; protected _lastValidMediaLoadedInfo: MediaLoadedInfo | null = null;
// Array of dynamic menu buttons to be added to menu. // Array of dynamic menu buttons to be added to menu.
protected _dynamicMenuButtons: MenuButton[] = []; protected _dynamicMenuButtons: MenuButton[] = [];
@@ -279,7 +279,7 @@ export class FrigateCard extends LitElement {
fullscreen: screenfull.isEnabled && screenfull.isFullscreen, fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
camera: this._view?.camera, camera: this._view?.camera,
state: this._hass?.states, state: this._hass?.states,
mediaLoaded: !!this._currentMediaShowInfo, mediaLoaded: !!this._currentMediaLoadedInfo,
}; };
// Update the components that need the new condition state. Passed directly // Update the components that need the new condition state. Passed directly
@@ -934,7 +934,7 @@ export class FrigateCard extends LitElement {
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void { protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
const changeView = (view: View): void => { const changeView = (view: View): void => {
if (View.isMediaChange(this._view, view)) { if (View.isMediaChange(this._view, view)) {
this._currentMediaShowInfo = null; this._currentMediaLoadedInfo = null;
} }
this._view = view; this._view = view;
this._generateConditionState(); this._generateConditionState();
@@ -1629,17 +1629,17 @@ export class FrigateCard extends LitElement {
/** /**
* Handle a new piece of media being shown. * Handle a new piece of media being shown.
* @param e Event with MediaShowInfo details for the media. * @param ev Event with MediaLoadedInfo details for the media.
*/ */
protected _mediaShowHandler(e: CustomEvent<MediaShowInfo>): void { protected _mediaLoadedHandler(ev: CustomEvent<MediaLoadedInfo>): void {
const mediaShowInfo = e.detail; const mediaLoadedInfo = ev.detail;
// In Safari, with WebRTC, 0x0 is occasionally returned during loading, // In Safari, with WebRTC, 0x0 is occasionally returned during loading,
// so treat anything less than a safety cutoff as bogus. // so treat anything less than a safety cutoff as bogus.
if (!isValidMediaShowInfo(mediaShowInfo)) { if (!isValidMediaLoadedInfo(mediaLoadedInfo)) {
return; return;
} }
this._lastValidMediaShowInfo = this._currentMediaShowInfo = mediaShowInfo; this._lastValidMediaLoadedInfo = this._currentMediaLoadedInfo = mediaLoadedInfo;
// An update may be required to draw elements. // An update may be required to draw elements.
this._generateConditionState(); this._generateConditionState();
@@ -1719,8 +1719,8 @@ export class FrigateCard extends LitElement {
} }
const aspectRatioMode = this._getConfig().dimensions.aspect_ratio_mode; const aspectRatioMode = this._getConfig().dimensions.aspect_ratio_mode;
if (aspectRatioMode == 'dynamic' && this._lastValidMediaShowInfo) { if (aspectRatioMode == 'dynamic' && this._lastValidMediaLoadedInfo) {
return `${this._lastValidMediaShowInfo.width} / ${this._lastValidMediaShowInfo.height}`; return `${this._lastValidMediaLoadedInfo.width} / ${this._lastValidMediaLoadedInfo.height}`;
} }
const defaultAspectRatio = this._getConfig().dimensions.aspect_ratio; const defaultAspectRatio = this._getConfig().dimensions.aspect_ratio;
@@ -1796,7 +1796,7 @@ export class FrigateCard extends LitElement {
@frigate-card:message=${this._messageHandler.bind(this)} @frigate-card:message=${this._messageHandler.bind(this)}
@frigate-card:view:change=${this._changeViewHandler.bind(this)} @frigate-card:view:change=${this._changeViewHandler.bind(this)}
@frigate-card:view:change-context=${this._addViewContextHandler.bind(this)} @frigate-card:view:change-context=${this._addViewContextHandler.bind(this)}
@frigate-card:media-show=${this._mediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._mediaLoadedHandler.bind(this)}
@frigate-card:render=${() => this.requestUpdate()} @frigate-card:render=${() => this.requestUpdate()}
> >
${renderMenuAbove ? this._renderMenu() : ''} ${renderMenuAbove ? this._renderMenu() : ''}
@@ -1940,8 +1940,8 @@ export class FrigateCard extends LitElement {
* @returns The Lovelace card size in units of 50px. * @returns The Lovelace card size in units of 50px.
*/ */
public getCardSize(): number { public getCardSize(): number {
if (this._lastValidMediaShowInfo) { if (this._lastValidMediaLoadedInfo) {
return this._lastValidMediaShowInfo.height / 50; return this._lastValidMediaLoadedInfo.height / 50;
} }
return 6; return 6;
} }
+23 -15
View File
@@ -35,7 +35,7 @@ import {
LiveConfig, LiveConfig,
LiveOverrides, LiveOverrides,
LiveProvider, LiveProvider,
MediaShowInfo, MediaLoadedInfo,
Message, Message,
TransitionEffect, TransitionEffect,
WebRTCCardConfig, WebRTCCardConfig,
@@ -46,15 +46,17 @@ import { getCameraIcon, getCameraTitle } from '../utils/camera.js';
import { homeAssistantSignPath } from '../utils/ha'; import { homeAssistantSignPath } from '../utils/ha';
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js'; import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
import { import {
dispatchExistingMediaShowInfoAsEvent, dispatchExistingMediaLoadedInfoAsEvent,
dispatchMediaShowEvent, dispatchMediaShowEvent,
dispatchMediaUnloadedEvent,
} from '../utils/media-info.js'; } from '../utils/media-info.js';
import { dispatchViewContextChangeEvent, View } from '../view.js'; import { dispatchViewContextChangeEvent, View } from '../view.js';
import { AutoMediaPlugin } from './embla-plugins/automedia.js'; import { AutoMediaPlugin } from './embla-plugins/automedia.js';
import { Lazyload } from './embla-plugins/lazyload.js'; import { Lazyload } from './embla-plugins/lazyload.js';
import { import {
FrigateCardMediaCarousel, FrigateCardMediaCarousel,
wrapMediaShowEventForCarousel, wrapMediaLoadedEventForCarousel,
wrapMediaUnloadedEventForCarousel,
} from './media-carousel.js'; } from './media-carousel.js';
import { dispatchErrorMessageEvent } from './message.js'; import { dispatchErrorMessageEvent } from './message.js';
import './next-prev-control.js'; import './next-prev-control.js';
@@ -101,9 +103,9 @@ export class FrigateCardLive extends LitElement {
// foreground and background (in preload mode). // foreground and background (in preload mode).
protected _intersectionObserver: IntersectionObserver; protected _intersectionObserver: IntersectionObserver;
// MediaShowInfo object and message from the underlying live object. In the // MediaLoadedInfo object and message from the underlying live object. In the
// case of pre-loading these may be propagated upwards later. // case of pre-loading these may be propagated upwards later.
protected _savedMediaShowInfo: MediaShowInfo | null = null; protected _savedMediaLoadedInfo: MediaLoadedInfo | null = null;
protected _messageReceivedPostRender = false; protected _messageReceivedPostRender = false;
protected _renderKey = 0; protected _renderKey = 0;
@@ -124,11 +126,11 @@ export class FrigateCardLive extends LitElement {
if ( if (
!this._inBackground && !this._inBackground &&
!this._messageReceivedPostRender && !this._messageReceivedPostRender &&
this._savedMediaShowInfo this._savedMediaLoadedInfo
) { ) {
// If this isn't being rendered in the background, the last render did not // If this isn't being rendered in the background, the last render did not
// generate a message and there's a saved MediaInfo, dispatch it upwards. // generate a message and there's a saved MediaInfo, dispatch it upwards.
dispatchExistingMediaShowInfoAsEvent(this, this._savedMediaShowInfo); dispatchExistingMediaLoadedInfoAsEvent(this, this._savedMediaLoadedInfo);
} }
// Trigger a re-render which may be necessary if the prior render resulted // Trigger a re-render which may be necessary if the prior render resulted
@@ -219,8 +221,8 @@ export class FrigateCardLive extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
} }
}} }}
@frigate-card:media-show=${(ev: CustomEvent<MediaShowInfo>) => { @frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
this._savedMediaShowInfo = ev.detail; this._savedMediaLoadedInfo = ev.detail;
if (this._inBackground) { if (this._inBackground) {
ev.stopPropagation(); ev.stopPropagation();
} }
@@ -509,8 +511,11 @@ export class FrigateCardLiveCarousel extends LitElement {
.label=${getCameraTitle(this.hass, cameraConfig)} .label=${getCameraTitle(this.hass, cameraConfig)}
.liveConfig=${config} .liveConfig=${config}
.hass=${this.hass} .hass=${this.hass}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => { @frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
wrapMediaShowEventForCarousel(slideIndex, e); wrapMediaLoadedEventForCarousel(slideIndex, ev);
}}
@frigate-card:media:unloaded=${(ev: CustomEvent<void>) => {
wrapMediaUnloadedEventForCarousel(slideIndex, ev);
}} }}
> >
</frigate-card-live-provider> </frigate-card-live-provider>
@@ -739,9 +744,12 @@ export class FrigateCardLiveProvider extends LitElement {
/** /**
* Called before each update. * Called before each update.
*/ */
protected willUpdate(): void { protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('disabled')) {
if (this.disabled) { if (this.disabled) {
this._isVideoMediaLoaded = false; this._isVideoMediaLoaded = false;
dispatchMediaUnloadedEvent(this);
}
} }
} }
@@ -782,7 +790,7 @@ export class FrigateCardLiveProvider extends LitElement {
class=${classMap(providerClasses)} class=${classMap(providerClasses)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
> >
</frigate-card-live-ha>` </frigate-card-live-ha>`
: provider === 'webrtc-card' : provider === 'webrtc-card'
@@ -792,7 +800,7 @@ export class FrigateCardLiveProvider extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.webRTCConfig=${this.liveConfig.webrtc_card} .webRTCConfig=${this.liveConfig.webrtc_card}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
> >
</frigate-card-live-webrtc-card>` </frigate-card-live-webrtc-card>`
: html` <frigate-card-live-jsmpeg : html` <frigate-card-live-jsmpeg
@@ -801,7 +809,7 @@ export class FrigateCardLiveProvider extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.jsmpegConfig=${this.liveConfig.jsmpeg} .jsmpegConfig=${this.liveConfig.jsmpeg}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
> >
</frigate-card-live-jsmpeg>`} </frigate-card-live-jsmpeg>`}
`; `;
+99 -49
View File
@@ -5,16 +5,16 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js';
import mediaCarouselStyle from '../scss/media-carousel.scss'; import mediaCarouselStyle from '../scss/media-carousel.scss';
import type { import type {
MediaShowInfo, MediaLoadedInfo,
NextPreviousControlConfig, NextPreviousControlConfig,
TitleControlConfig, TitleControlConfig,
TransitionEffect, TransitionEffect,
} from '../types.js'; } from '../types.js';
import { dispatchFrigateCardEvent } from '../utils/basic'; import { dispatchFrigateCardEvent } from '../utils/basic';
import { import {
createMediaShowInfo, createMediaLoadedInfo,
dispatchExistingMediaShowInfoAsEvent, dispatchExistingMediaLoadedInfoAsEvent,
isValidMediaShowInfo, isValidMediaLoadedInfo,
} from '../utils/media-info.js'; } from '../utils/media-info.js';
import { CarouselSelect, EmblaCarouselPlugins, FrigateCardCarousel } from './carousel'; import { CarouselSelect, EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
import { AutoMediaType } from './embla-plugins/automedia.js'; import { AutoMediaType } from './embla-plugins/automedia.js';
@@ -27,58 +27,93 @@ const getEmptyImageSrc = (width: number, height: number) =>
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`; `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
export const IMG_EMPTY = getEmptyImageSrc(16, 9); export const IMG_EMPTY = getEmptyImageSrc(16, 9);
export interface CarouselMediaShowInfo { export interface CarouselMediaLoadedInfo {
slide: number;
mediaLoadedInfo: MediaLoadedInfo;
}
export interface CarouselMediaUnloadedInfo {
slide: number; slide: number;
mediaShowInfo: MediaShowInfo;
} }
/** /**
* Dispatch a carousel media show event. * Dispatch a carousel media loaded event.
* @param target The target to send it from. * @param target The target to send it from.
* @param carouselMediaShowInfo The CarouselMediaShowInfo. * @param carouselMediaLoadedInfo The CarouselMediaLoadedInfo.
*/ */
const dispatchFrigateCardCarouselMediaShow = ( const dispatchFrigateCardCarouselMediaLoaded = (
target: EventTarget, target: EventTarget,
carouselMediaShowInfo: CarouselMediaShowInfo, carouselMediaLoadedInfo: CarouselMediaLoadedInfo,
): void => { ): void => {
dispatchFrigateCardEvent<CarouselMediaShowInfo>( dispatchFrigateCardEvent<CarouselMediaLoadedInfo>(
target, target,
'carousel:media-show', 'carousel:media:loaded',
carouselMediaShowInfo, carouselMediaLoadedInfo,
); );
}; };
/** /**
* Turn a MediaShowEvent into a CarouselMediaShowInfo. * Dispatch a carousel media UNloaded event.
* @param target The target to send it from.
* @param carouselMediaUnloadedInfo The CarouselMediaUnloadedInfo.
*/
const dispatchFrigateCardCarouselMediaUnloaded = (
target: EventTarget,
carouselMediaUnloadedInfo: CarouselMediaUnloadedInfo,
): void => {
dispatchFrigateCardEvent<CarouselMediaUnloadedInfo>(
target,
'carousel:media:unloaded',
carouselMediaUnloadedInfo,
);
};
/**
* Turn a MediaLoadedInfo into a CarouselMediaLoadedInfo.
* @param slide The slide number. * @param slide The slide number.
* @param event The MediaShowEvent. * @param event The MediaShowEvent.
*/ */
export const wrapMediaShowEventForCarousel = ( export const wrapMediaLoadedEventForCarousel = (
slide: number, slide: number,
event: CustomEvent<MediaShowInfo>, event: CustomEvent<MediaLoadedInfo>,
) => { ) => {
event.stopPropagation(); event.stopPropagation();
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], { dispatchFrigateCardCarouselMediaLoaded(event.composedPath()[0], {
slide: slide, slide: slide,
mediaShowInfo: event.detail, mediaLoadedInfo: event.detail,
}); });
}; };
/** /**
* Turn a (stock) media load event into a CarouselMediaShowInfo. * Turn a (raw, e.g. img) media load event into a CarouselMediaLoadedInfo.
* @param slide The slide number. * @param slide The slide number.
* @param event The MediaShowEvent. * @param event The MediaShowEvent.
*/ */
export const wrapMediaLoadEventForCarousel = (slide: number, event: Event) => { export const wrapRawMediaLoadedEventForCarousel = (slide: number, event: Event) => {
const mediaShowInfo = createMediaShowInfo(event); const mediaLoadedInfo = createMediaLoadedInfo(event);
if (mediaShowInfo) { if (mediaLoadedInfo) {
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], { dispatchFrigateCardCarouselMediaLoaded(event.composedPath()[0], {
slide: slide, slide: slide,
mediaShowInfo: mediaShowInfo, mediaLoadedInfo: mediaLoadedInfo,
}); });
} }
}; };
/**
* Turn a MediaUnloadedInfo into a CarouselMediaUnloadedInfo.
* @param slide The slide number.
* @param event The MediaUnloadedEvent.
*/
export const wrapMediaUnloadedEventForCarousel = (
slide: number,
event: CustomEvent<void>,
) => {
event.stopPropagation();
dispatchFrigateCardCarouselMediaUnloaded(event.composedPath()[0], {
slide: slide,
});
};
@customElement('frigate-card-media-carousel') @customElement('frigate-card-media-carousel')
export class FrigateCardMediaCarousel extends LitElement { export class FrigateCardMediaCarousel extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
@@ -99,8 +134,8 @@ export class FrigateCardMediaCarousel extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public titlePopupConfig?: TitleControlConfig; public titlePopupConfig?: TitleControlConfig;
// A "map" from slide number to MediaShowInfo object. // A "map" from slide number to MediaLoadedInfo object.
protected _mediaShowInfo: Record<number, MediaShowInfo> = {}; protected _mediaLoadedInfo: Record<number, MediaLoadedInfo> = {};
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef(); protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
protected _previousControlRef: Ref<FrigateCardNextPreviousControl> = createRef(); protected _previousControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
protected _titleControlRef: Ref<FrigateCardTitleControl> = createRef(); protected _titleControlRef: Ref<FrigateCardTitleControl> = createRef();
@@ -233,13 +268,13 @@ export class FrigateCardMediaCarousel extends LitElement {
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.addEventListener('frigate-card:media-show', this._boundAutoPlayHandler); this.addEventListener('frigate-card:media:loaded', this._boundAutoPlayHandler);
this.addEventListener('frigate-card:media-show', this._boundAutoUnmuteHandler); this.addEventListener('frigate-card:media:loaded', this._boundAutoUnmuteHandler);
this.addEventListener( this.addEventListener(
'frigate-card:media-show', 'frigate-card:media:loaded',
this._boundAdaptContainerHeightToSlide, this._boundAdaptContainerHeightToSlide,
); );
this.addEventListener('frigate-card:media-show', this._boundTitleHandler); this.addEventListener('frigate-card:media:loaded', this._boundTitleHandler);
this._resizeObserver.observe(this); this._resizeObserver.observe(this);
this._intersectionObserver.observe(this); this._intersectionObserver.observe(this);
} }
@@ -248,13 +283,13 @@ export class FrigateCardMediaCarousel extends LitElement {
* Component disconnected callback. * Component disconnected callback.
*/ */
disconnectedCallback(): void { disconnectedCallback(): void {
this.removeEventListener('frigate-card:media-show', this._boundAutoPlayHandler); this.removeEventListener('frigate-card:media:loaded', this._boundAutoPlayHandler);
this.removeEventListener('frigate-card:media-show', this._boundAutoUnmuteHandler); this.removeEventListener('frigate-card:media:loaded', this._boundAutoUnmuteHandler);
this.removeEventListener( this.removeEventListener(
'frigate-card:media-show', 'frigate-card:media:loaded',
this._boundAdaptContainerHeightToSlide, this._boundAdaptContainerHeightToSlide,
); );
this.removeEventListener('frigate-card:media-show', this._boundTitleHandler); this.removeEventListener('frigate-card:media:loaded', this._boundTitleHandler);
this._resizeObserver.disconnect(); this._resizeObserver.disconnect();
this._intersectionObserver.disconnect(); this._intersectionObserver.disconnect();
@@ -312,7 +347,7 @@ export class FrigateCardMediaCarousel extends LitElement {
// Hack: This method attempts to measure the height of the selected slide in // Hack: This method attempts to measure the height of the selected slide in
// order to set the overall carousel height to match. This method is // order to set the overall carousel height to match. This method is
// triggered from `frigate-card:media-show` events, which are usually in // triggered from `frigate-card:media:loaded` events, which are usually in
// turn triggered from media/metadata load events from media players. // turn triggered from media/metadata load events from media players.
// Sufficient time needs to be allowed after these metadata load events to // Sufficient time needs to be allowed after these metadata load events to
// allow the browser to repaint the element heights, so that we can get the // allow the browser to repaint the element heights, so that we can get the
@@ -323,43 +358,57 @@ export class FrigateCardMediaCarousel extends LitElement {
/** /**
* Fire a media show event when a slide is selected. * Fire a media show event when a slide is selected.
*/ */
protected _dispatchMediaShowInfo(): void { protected _dispatchMediaLoadedInfo(): void {
const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index; const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index;
if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) { if (slideIndex !== undefined && slideIndex in this._mediaLoadedInfo) {
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]); dispatchExistingMediaLoadedInfoAsEvent(this, this._mediaLoadedInfo[slideIndex]);
} }
} }
/** /**
* Handle a media-show event that is generated by a child component, saving the * Handle a media:loaded event that is generated by a child component, saving the
* contents for future use when the relevant slide is actually shown. * contents for future use when the relevant slide is actually shown.
* @param slideIndex The relevant slide index. * @param slideIndex The relevant slide index.
* @param event The media-show event from the child component. * @param event The media:loaded event from the child component.
*/ */
protected _storeMediaShowInfo(event: CustomEvent<CarouselMediaShowInfo>): void { protected _storeMediaLoadedInfo(event: CustomEvent<CarouselMediaLoadedInfo>): void {
// Don't allow the inbound event to propagate upwards, that will be // Don't allow the inbound event to propagate upwards, that will be
// automatically done at the appropriate time as the slide is shown. // automatically done at the appropriate time as the slide is shown.
event.stopPropagation(); event.stopPropagation();
const mediaShowInfo = event.detail.mediaShowInfo; const mediaLoadedInfo = event.detail.mediaLoadedInfo;
const slideIndex = event.detail.slide; const slideIndex = event.detail.slide;
// isValidMediaShowInfo is used to prevent saving media info that will be // isValidMediaLoadedInfo is used to prevent saving media info that will be
// rejected upstream (empty 1x1 images will be rejected here). // rejected upstream (empty 1x1 images will be rejected here).
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) { if (mediaLoadedInfo && isValidMediaLoadedInfo(mediaLoadedInfo)) {
this._mediaShowInfo[slideIndex] = mediaShowInfo; this._mediaLoadedInfo[slideIndex] = mediaLoadedInfo;
if (this.frigateCardCarousel()?.getCarouselSelected()?.index === slideIndex) { if (this.frigateCardCarousel()?.getCarouselSelected()?.index === slideIndex) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo); dispatchExistingMediaLoadedInfoAsEvent(this, mediaLoadedInfo);
} }
} }
} }
/**
* Remove a media loaded info (i.e. a media item has unloaded).
* @param event The CarouselMediaUnloadedInfo event.
*/
protected _removeMediaLoadedInfo(event: CustomEvent<CarouselMediaUnloadedInfo>): void {
const slideIndex = event.detail.slide;
delete this._mediaLoadedInfo[slideIndex];
// If the slide that unloaded is not visible, don't propagate the event upwards.
if (this.frigateCardCarousel()?.getCarouselSelected()?.index !== slideIndex) {
event.stopPropagation();
}
}
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` <frigate-card-carousel return html` <frigate-card-carousel
${ref(this._refCarousel)} ${ref(this._refCarousel)}
.carouselOptions=${this.carouselOptions} .carouselOptions=${this.carouselOptions}
.carouselPlugins=${this.carouselPlugins} .carouselPlugins=${this.carouselPlugins}
transitionEffect=${ifDefined(this.transitionEffect)} transitionEffect=${ifDefined(this.transitionEffect)}
@frigate-card:carousel:init=${this._dispatchMediaShowInfo.bind(this)} @frigate-card:carousel:init=${this._dispatchMediaLoadedInfo.bind(this)}
@frigate-card:carousel:select=${(ev: CustomEvent<CarouselSelect>) => { @frigate-card:carousel:select=${(ev: CustomEvent<CarouselSelect>) => {
this._slideResizeObserver.disconnect(); this._slideResizeObserver.disconnect();
this._slideResizeObserver.observe(ev.detail.element); this._slideResizeObserver.observe(ev.detail.element);
@@ -373,9 +422,10 @@ export class FrigateCardMediaCarousel extends LitElement {
); );
// Dispatch media info. // Dispatch media info.
this._dispatchMediaShowInfo(); this._dispatchMediaLoadedInfo();
}} }}
@frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)} @frigate-card:carousel:media:loaded=${this._storeMediaLoadedInfo.bind(this)}
@frigate-card:carousel:media:unloaded=${this._removeMediaLoadedInfo.bind(this)}
> >
<slot slot="previous" name="previous"></slot> <slot slot="previous" name="previous"></slot>
<slot></slot> <slot></slot>
+8 -11
View File
@@ -13,10 +13,7 @@ import { guard } from 'lit/directives/guard.js';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { import { renderProgressIndicator } from '../components/message.js';
dispatchFrigateCardErrorEvent,
renderProgressIndicator,
} from '../components/message.js';
import viewerStyle from '../scss/viewer.scss'; import viewerStyle from '../scss/viewer.scss';
import viewerCarouselStyle from '../scss/viewer-carousel.scss'; import viewerCarouselStyle from '../scss/viewer-carousel.scss';
import { import {
@@ -27,7 +24,7 @@ import {
FrigateBrowseMediaSource, FrigateBrowseMediaSource,
frigateCardConfigDefaults, frigateCardConfigDefaults,
FrigateCardMediaPlayer, FrigateCardMediaPlayer,
MediaShowInfo, MediaLoadedInfo,
TransitionEffect, TransitionEffect,
ViewerConfig, ViewerConfig,
} from '../types.js'; } from '../types.js';
@@ -48,8 +45,8 @@ import { Lazyload } from './embla-plugins/lazyload.js';
import { import {
FrigateCardMediaCarousel, FrigateCardMediaCarousel,
IMG_EMPTY, IMG_EMPTY,
wrapMediaLoadEventForCarousel, wrapRawMediaLoadedEventForCarousel,
wrapMediaShowEventForCarousel, wrapMediaLoadedEventForCarousel,
} from './media-carousel.js'; } from './media-carousel.js';
import './next-prev-control.js'; import './next-prev-control.js';
import './title-control.js'; import './title-control.js';
@@ -619,7 +616,7 @@ export class FrigateCardViewerCarousel extends LitElement {
.titlePopupConfig=${this.viewerConfig?.controls.title} .titlePopupConfig=${this.viewerConfig?.controls.title}
transitionEffect=${this._getTransitionEffect()} transitionEffect=${this._getTransitionEffect()}
@frigate-card:media-carousel:select=${this._setViewHandler.bind(this)} @frigate-card:media-carousel:select=${this._setViewHandler.bind(this)}
@frigate-card:media-show=${this._recordingSeekHandler.bind(this)} @frigate-card:media:loaded=${this._recordingSeekHandler.bind(this)}
> >
<frigate-card-next-previous-control <frigate-card-next-previous-control
slot="previous" slot="previous"
@@ -706,8 +703,8 @@ export class FrigateCardViewerCarousel extends LitElement {
)} )}
.media=${mediaToRender} .media=${mediaToRender}
.hass=${this.hass} .hass=${this.hass}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => { @frigate-card:media:loaded=${(e: CustomEvent<MediaLoadedInfo>) => {
wrapMediaShowEventForCarousel(slideIndex, e); wrapMediaLoadedEventForCarousel(slideIndex, e);
}} }}
> >
</frigate-card-ha-hls-player>` </frigate-card-ha-hls-player>`
@@ -743,7 +740,7 @@ export class FrigateCardViewerCarousel extends LitElement {
!lazyLoad || !lazyLoad ||
lazyloadPlugin?.hasLazyloaded(slideIndex) lazyloadPlugin?.hasLazyloaded(slideIndex)
) { ) {
wrapMediaLoadEventForCarousel(slideIndex, e); wrapRawMediaLoadedEventForCarousel(slideIndex, e);
} }
}}" }}"
/>`} />`}
+1 -1
View File
@@ -1203,7 +1203,7 @@ export interface BrowseMediaNeighbors {
nextIndex: number | null; nextIndex: number | null;
} }
export interface MediaShowInfo { export interface MediaLoadedInfo {
width: number; width: number;
height: number; height: number;
} }
+26 -16
View File
@@ -1,15 +1,17 @@
import { MediaShowInfo } from '../types.js'; import { MediaLoadedInfo } from '../types.js';
import { dispatchFrigateCardEvent } from './basic.js'; import { dispatchFrigateCardEvent } from './basic.js';
const MEDIA_INFO_HEIGHT_CUTOFF = 50; const MEDIA_INFO_HEIGHT_CUTOFF = 50;
const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF;
/** /**
* Create a MediaShowInfo object. * Create a MediaLoadedInfo object.
* @param source An event or HTMLElement that should be used as a source. * @param source An event or HTMLElement that should be used as a source.
* @returns A new MediaShowInfo object or null if one could not be created. * @returns A new MediaLoadedInfo object or null if one could not be created.
*/ */
export function createMediaShowInfo(source: Event | HTMLElement): MediaShowInfo | null { export function createMediaLoadedInfo(
source: Event | HTMLElement,
): MediaLoadedInfo | null {
let target: HTMLElement | EventTarget; let target: HTMLElement | EventTarget;
if (source instanceof Event) { if (source instanceof Event) {
target = source.composedPath()[0]; target = source.composedPath()[0];
@@ -37,7 +39,7 @@ export function createMediaShowInfo(source: Event | HTMLElement): MediaShowInfo
} }
/** /**
* Dispatch a Frigate card media show event. * Dispatch a Frigate card media loaded event.
* @param element The element to send the event. * @param element The element to send the event.
* @param source An event or HTMLElement that should be used as a source. * @param source An event or HTMLElement that should be used as a source.
*/ */
@@ -45,31 +47,39 @@ export function dispatchMediaShowEvent(
element: HTMLElement, element: HTMLElement,
source: Event | HTMLElement, source: Event | HTMLElement,
): void { ): void {
const mediaShowInfo = createMediaShowInfo(source); const mediaLoadedInfo = createMediaLoadedInfo(source);
if (mediaShowInfo) { if (mediaLoadedInfo) {
dispatchExistingMediaShowInfoAsEvent(element, mediaShowInfo); dispatchExistingMediaLoadedInfoAsEvent(element, mediaLoadedInfo);
} }
} }
/** /**
* Dispatch a pre-existing MediaShowInfo object as an event. * Dispatch a pre-existing MediaLoadedInfo object as an event.
* @param element The element to send the event. * @param element The element to send the event.
* @param mediaShowInfo The MediaShowInfo object to send. * @param MediaLoadedInfo The MediaLoadedInfo object to send.
*/ */
export function dispatchExistingMediaShowInfoAsEvent( export function dispatchExistingMediaLoadedInfoAsEvent(
element: HTMLElement, element: HTMLElement,
mediaShowInfo: MediaShowInfo, MediaLoadedInfo: MediaLoadedInfo,
): void { ): void {
dispatchFrigateCardEvent<MediaShowInfo>(element, 'media-show', mediaShowInfo); dispatchFrigateCardEvent<MediaLoadedInfo>(element, 'media:loaded', MediaLoadedInfo);
} }
/** /**
* Determine if a MediaShowInfo object is valid/acceptable. * Determine if a MediaLoadedInfo object is valid/acceptable.
* @param info The MediaShowInfo object. * @param info The MediaLoadedInfo object.
* @returns True if the object is valid, false otherwise. * @returns True if the object is valid, false otherwise.
*/ */
export function isValidMediaShowInfo(info: MediaShowInfo): boolean { export function isValidMediaLoadedInfo(info: MediaLoadedInfo): boolean {
return ( return (
info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF
); );
} }
/**
* Dispatch a media unloaded event.
* @param element The element to send the event.
*/
export function dispatchMediaUnloadedEvent(element: HTMLElement): void {
dispatchFrigateCardEvent(element, 'media:unloaded');
}
+1 -1
View File
@@ -152,7 +152,7 @@ export class View {
/** /**
* Determine if a view is of a piece of media (including the media viewer, * Determine if a view is of a piece of media (including the media viewer,
* live view, image view -- anything that can create a MediaShowInfo event). * live view, image view -- anything that can create a MediaLoadedInfo event).
*/ */
public isAnyMediaView(): boolean { public isAnyMediaView(): boolean {
return this.isViewerView() || this.is('live') || this.is('image'); return this.isViewerView() || this.is('live') || this.is('image');