Initial attempt to break media carousel out separately from viewer.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent 677f0d511b
commit 986add8463
5 changed files with 267 additions and 223 deletions
+16 -30
View File
@@ -1,7 +1,8 @@
// TODO controls
// TODO media events
// TODO lazy loading // TODO lazy loading
// TODO media events
// TODO height adapting // TODO height adapting
// TODO can height adapting remove need for 16x9 dummy in the media carousel?
// TODO controls
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import type { import type {
@@ -13,13 +14,13 @@ import type {
MediaShowInfo, MediaShowInfo,
WebRTCConfig, WebRTCConfig,
} from '../types.js'; } from '../types.js';
import { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
import { BrowseMediaUtil } from '../browse-media-util.js'; import { BrowseMediaUtil } from '../browse-media-util.js';
import { FrigateCardCarousel } from './carousel.js'; import { FrigateCardMediaCarousel } from './media-carousel.js';
import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
import { View } from '../view.js'; import { View } from '../view.js';
import { localize } from '../localize/localize.js'; import { localize } from '../localize/localize.js';
@@ -40,7 +41,6 @@ import liveStyle from '../scss/live.scss';
import liveFrigateStyle from '../scss/live-frigate.scss'; import liveFrigateStyle from '../scss/live-frigate.scss';
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss'; import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
import liveWebRTCStyle from '../scss/live-webrtc.scss'; import liveWebRTCStyle from '../scss/live-webrtc.scss';
import mediaCarouselStyle from '../scss/media-carousel.scss';
// Number of seconds a signed URL is valid for. // Number of seconds a signed URL is valid for.
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -184,7 +184,7 @@ export class FrigateCardLive extends LitElement {
} }
@customElement('frigate-card-live-carousel') @customElement('frigate-card-live-carousel')
export class FrigateCardLiveCarousel extends FrigateCardCarousel { export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
@property({ attribute: false }) @property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant; protected hass?: HomeAssistant & ExtendedHomeAssistant;
@@ -213,18 +213,6 @@ export class FrigateCardLiveCarousel extends FrigateCardCarousel {
}; };
} }
/**
* Load the carousel.
*/
protected _loadCarousel(): void {
super._loadCarousel();
// Necessary because typescript local type narrowing is not paying attention
// to the side-effect of the call to super._loadCarousel().
const carousel = this._carousel as EmblaCarouselType | undefined;
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
}
/** /**
* Get slides to include in the render. * Get slides to include in the render.
* @returns The slides to include in the render. * @returns The slides to include in the render.
@@ -233,12 +221,12 @@ export class FrigateCardLiveCarousel extends FrigateCardCarousel {
if (!this.cameras) { if (!this.cameras) {
return []; return [];
} }
return Array.from(this.cameras.values()).map(this._renderLive.bind(this)); return Array.from(this.cameras.values()).map((cameraConfig, index) => this._renderLive(cameraConfig, index));
} }
// /** /**
// * Handle the user selecting a new slide in the carousel. * Handle the user selecting a new slide in the carousel.
// */ */
protected _selectSlideSetViewHandler(): void { protected _selectSlideSetViewHandler(): void {
if (!this._carousel || !this.view || !this.cameras) { if (!this._carousel || !this.view || !this.cameras) {
return; return;
@@ -248,13 +236,18 @@ export class FrigateCardLiveCarousel extends FrigateCardCarousel {
this.view.camera = Array.from(this.cameras.keys())[selectedSnap]; this.view.camera = Array.from(this.cameras.keys())[selectedSnap];
} }
protected _renderLive(cameraConfig: CameraConfig): TemplateResult { protected _renderLive(
cameraConfig: CameraConfig,
slideIndex: number,
): TemplateResult {
// TODO: Add lazy load // TODO: Add lazy load
return html` <div class="embla__slide"> return html` <div class="embla__slide">
<frigate-card-live-provider <frigate-card-live-provider
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${cameraConfig} .cameraConfig=${cameraConfig}
.liveConfig=${this.liveConfig} .liveConfig=${this.liveConfig}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
this._mediaShowEventHandler(slideIndex, e)}
> >
</frigate-card-live-provider> </frigate-card-live-provider>
</div>`; </div>`;
@@ -307,13 +300,6 @@ export class FrigateCardLiveCarousel extends FrigateCardCarousel {
// ></frigate-card-next-previous-control>` // ></frigate-card-next-previous-control>`
// : ``} // : ``}
} }
/**
* Get element styles.
*/
static get styles(): CSSResultGroup {
return [super.styles, unsafeCSS(mediaCarouselStyle)];
}
} }
@customElement('frigate-card-live-provider') @customElement('frigate-card-live-provider')
+212
View File
@@ -0,0 +1,212 @@
import { CSSResultGroup, unsafeCSS } from 'lit';
import { EmblaCarouselType } from 'embla-carousel';
import { customElement } from 'lit/decorators.js';
import { FrigateCardCarousel } from './carousel.js';
import type { MediaShowInfo } from '../types.js';
import {
dispatchExistingMediaShowInfoAsEvent,
isValidMediaShowInfo,
} from '../common.js';
import './next-prev-control.js';
import mediaCarouselStyle from '../scss/media-carousel.scss';
// TODO Remove this if not needed (and below)
// 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`;
// const IMG_EMPTY = getEmptyImageSrc(16, 9);
@customElement('frigate-card-media-carousel')
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// A "map" from slide number to MediaShowInfo object.
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
// Whether or not a given slide has been successfully lazily loaded.
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
/**
* Returns the number of slides to lazily load. 0 means all slides are lazy
* loaded, 1 means that 1 slide on each side of the currently selected slide
* should lazy load, etc. `null` means lazy loading is disabled and everything
* should load simultaneously.
* @returns
*/
protected _getLazyLoadCount(): number | null {
// Defaults to fully-lazy loading.
return 0;
}
/**
* Load the carousel with "slides" (clips or snapshots).
*/
protected _loadCarousel(): void {
super._loadCarousel();
// Necessary because typescript local type narrowing is not paying attention
// to the side-effect of the call to super._loadCarousel().
const carousel = this._carousel as EmblaCarouselType | undefined;
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
carousel?.on('init', this._selectSlideMediaShowHandler.bind(this));
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
if (this._getLazyLoadCount() != null) {
carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
}
}
/**
* Handle the user selecting a new slide in the carousel.
*/
protected _selectSlideSetViewHandler(): void {
// To be overridden in children.
}
/**
* Handle a next/previous control interaction.
* @param direction The direction requested, previous or next.
*/
protected _nextPreviousHandler(direction: 'previous' | 'next'): void {
if (direction == 'previous') {
this._carousel?.scrollPrev();
} else if (direction == 'next') {
this._carousel?.scrollNext();
}
}
/**
* Lazily load media in the carousel.
*/
protected _lazyLoadMediaHandler(): void {
if (!this._carousel) {
return;
}
const lazyLoadCount = this._getLazyLoadCount();
if (lazyLoadCount === null) {
return;
}
const slides = this._carousel.slideNodes();
const slidesInView = this._carousel.slidesInView(true);
const slidesToLoad = new Set<number>();
const minSlide = Math.min(...slidesInView);
const maxSlide = Math.max(...slidesInView);
// Lazily load 'lazyLoadCount' slides on either side of the slides in view.
for (let i = 1; i <= lazyLoadCount && minSlide - i >= 0; i++) {
slidesToLoad.add(minSlide - i);
}
slidesInView.forEach((index) => slidesToLoad.add(index));
for (let i = 1; i <= lazyLoadCount && maxSlide + i < slides.length; i++) {
slidesToLoad.add(maxSlide + i);
}
slidesToLoad.forEach((index) => {
// Only lazy load slides that are not already loaded.
if (this._slideHasBeenLazyLoaded[index]) {
return;
}
this._slideHasBeenLazyLoaded[index] = true;
this._lazyLoadSlide(slides[index]);
});
}
/**
* Lazy load a slide.
* @param _slide The slide to lazy load.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _lazyLoadSlide(_slide: HTMLElement): void {
// To be overridden in children.
}
/**
* Fire a media show event when a slide is selected.
*/
protected _selectSlideMediaShowHandler(): void {
if (!this._carousel) {
return;
}
this._carousel.slidesInView(true).forEach((slideIndex) => {
if (slideIndex in this._mediaShowInfo) {
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
}
});
}
/**
* Handle a media-show event that is generated by a child component, saving the
* contents for future use when the relevant slide is actually shown.
* @param slideIndex The relevant slide index.
* @param event The media-show event from the child component.
*/
protected _mediaShowEventHandler(
slideIndex: number,
event: CustomEvent<MediaShowInfo>,
): void {
// Don't allow the inbound event to propagate upwards, that will be
// automatically done at the appropriate time as the slide is shown.
event.stopPropagation();
this._mediaLoadedHandler(slideIndex, event.detail);
}
/**
* Handle a MediaShowInfo object that is generated on media load, by saving it
* for future, or immediate use, when the relevant slide is displayed.
* @param slideIndex The relevant slide index.
* @param mediaShowInfo The MediaShowInfo object generated by the media.
*/
protected _mediaLoadedHandler(
slideIndex: number,
mediaShowInfo?: MediaShowInfo | null,
): void {
// isValidMediaShowInfo is used to prevent saving media info that will be
// rejected upstream.
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
this._mediaShowInfo[slideIndex] = mediaShowInfo;
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
}
/**
* Images need a width/height from initial load, and browsers will assume
* that the aspect ratio of the initial dummy-image load will persist. In
* lazy-loading, this can cause a 1x1 pixel dummy image to cause the
* browser to assume all images will be square, so the whole carousel will
* have the wrong aspect-ratio until every single image has been lazily
* loaded. To avoid this, we use a 16:9 dummy image at first (most
* likely?) and once the first piece of real media has been loaded, all
* dummy images are replaced with dummy images that match the aspect ratio
* of the real image. It still might be wrong, but it's the best option
* available.
*/
// TODO remove this
// const firstMediaLoad = !Object.keys(this._mediaShowInfo).length;
// if (firstMediaLoad && this.viewerConfig.lazy_load) {
// const replacementImageSrc = getEmptyImageSrc(
// mediaShowInfo.width,
// mediaShowInfo.height,
// );
// this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => {
// const imageElement: HTMLImageElement = img as HTMLImageElement;
// if (imageElement.src === IMG_EMPTY) {
// imageElement.src = replacementImageSrc;
// }
// });
// }
}
}
/**
* Get element styles.
*/
static get styles(): CSSResultGroup {
return [super.styles, unsafeCSS(mediaCarouselStyle)];
}
}
+37 -191
View File
@@ -1,6 +1,6 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { BrowseMediaUtil } from '../browse-media-util.js'; import { BrowseMediaUtil } from '../browse-media-util.js';
import { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
@@ -15,7 +15,7 @@ import type {
MediaShowInfo, MediaShowInfo,
ViewerConfig, ViewerConfig,
} from '../types.js'; } from '../types.js';
import { FrigateCardCarousel } from './carousel.js'; import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js'; import { View } from '../view.js';
@@ -26,8 +26,6 @@ import {
dispatchMessageEvent, dispatchMessageEvent,
dispatchPauseEvent, dispatchPauseEvent,
dispatchPlayEvent, dispatchPlayEvent,
dispatchExistingMediaShowInfoAsEvent,
isValidMediaShowInfo,
} from '../common.js'; } from '../common.js';
import { localize } from '../localize/localize.js'; import { localize } from '../localize/localize.js';
import { renderProgressIndicator } from '../components/message.js'; import { renderProgressIndicator } from '../components/message.js';
@@ -36,7 +34,6 @@ import './next-prev-control.js';
import viewerStyle from '../scss/viewer.scss'; import viewerStyle from '../scss/viewer.scss';
import viewerCoreStyle from '../scss/viewer-core.scss'; import viewerCoreStyle from '../scss/viewer-core.scss';
import mediaCarouselStyle from '../scss/media-carousel.scss';
const getEmptyImageSrc = (width: number, height: number) => 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`;
@@ -164,11 +161,11 @@ export class FrigateCardViewerCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache; protected resolvedMediaCache?: ResolvedMediaCache;
protected _mediaCarouselRef: Ref<FrigateCardMediaCarousel> = createRef(); protected _viewerCarouselRef: Ref<FrigateCardViewerCarousel> = createRef();
protected _thumbnailCarouselRef: Ref<FrigateCardThumbnailCarousel> = createRef(); protected _thumbnailCarouselRef: Ref<FrigateCardThumbnailCarousel> = createRef();
protected _syncThumbnailCarousel(): void { protected _syncThumbnailCarousel(): void {
const mediaSelected = this._mediaCarouselRef.value?.carouselSelected(); const mediaSelected = this._viewerCarouselRef.value?.carouselSelected();
if (mediaSelected !== undefined) { if (mediaSelected !== undefined) {
this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected); this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected);
} }
@@ -184,7 +181,7 @@ export class FrigateCardViewerCore extends LitElement {
.target=${this.view.target} .target=${this.view.target}
.config=${this.viewerConfig.controls.thumbnails} .config=${this.viewerConfig.controls.thumbnails}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => { @frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.slideIndex); this._viewerCarouselRef.value?.carouselScrollTo(ev.detail.slideIndex);
}} }}
@frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)}
> >
@@ -199,8 +196,8 @@ export class FrigateCardViewerCore extends LitElement {
this.viewerConfig.controls.thumbnails.mode === 'above' this.viewerConfig.controls.thumbnails.mode === 'above'
? this._renderThumbnails() ? this._renderThumbnails()
: ''} : ''}
<frigate-card-media-carousel <frigate-card-viewer-carousel
${ref(this._mediaCarouselRef)} ${ref(this._viewerCarouselRef)}
.hass=${this.hass} .hass=${this.hass}
.view=${this.view} .view=${this.view}
.viewerConfig=${this.viewerConfig} .viewerConfig=${this.viewerConfig}
@@ -208,7 +205,7 @@ export class FrigateCardViewerCore extends LitElement {
.resolvedMediaCache=${this.resolvedMediaCache} .resolvedMediaCache=${this.resolvedMediaCache}
@frigate-card:carousel:select=${this._syncThumbnailCarousel.bind(this)} @frigate-card:carousel:select=${this._syncThumbnailCarousel.bind(this)}
> >
</frigate-card-media-carousel> </frigate-card-viewer-carousel>
${this.viewerConfig && this.viewerConfig.controls.thumbnails.mode === 'below' ${this.viewerConfig && this.viewerConfig.controls.thumbnails.mode === 'below'
? this._renderThumbnails() ? this._renderThumbnails()
: ''}`; : ''}`;
@@ -222,8 +219,8 @@ export class FrigateCardViewerCore extends LitElement {
} }
} }
@customElement('frigate-card-media-carousel') @customElement('frigate-card-viewer-carousel')
export class FrigateCardMediaCarousel extends FrigateCardCarousel { export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
@property({ attribute: false }) @property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant; protected hass?: HomeAssistant & ExtendedHomeAssistant;
@@ -243,12 +240,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// (Folders are not media items that can be rendered). // (Folders are not media items that can be rendered).
protected _slideToChild: Record<number, number> = {}; protected _slideToChild: Record<number, number> = {};
// A "map" from slide number to MediaShowInfo object.
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
// Whether or not a given slide has been successfully lazily loaded.
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
/** /**
* Get the Embla options to use. * Get the Embla options to use.
* @returns An EmblaOptionsType object or undefined for no options. * @returns An EmblaOptionsType object or undefined for no options.
@@ -267,28 +258,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
}; };
} }
/**
* Load the carousel with "slides" (clips or snapshots).
*/
protected _loadCarousel(): void {
super._loadCarousel();
// Necessary because typescript local type narrowing is not paying attention
// to the side-effect of the call to super._loadCarousel().
const carousel = this._carousel as EmblaCarouselType | undefined;
// Update views and dispatch media-show events based on slide selections.
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
// Lazily load media that is displayed. These handlers are registered
// regardless of the value of this.lazyLoad to allow that value to change
// after the carousel has been initialized.
carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
}
/** /**
* Get the previous and next true media items from the current view. * Get the previous and next true media items from the current view.
* @returns A BrowseMediaNeighbors with indices and objects of true media * @returns A BrowseMediaNeighbors with indices and objects of true media
@@ -438,79 +407,39 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
if (childIndex !== undefined) { if (childIndex !== undefined) {
// Update the currently live view in place. // Update the currently live view in place.
this.view.childIndex = childIndex; this.view.childIndex = childIndex;
this.requestUpdate();
} }
} }
} }
/** /**
* Handle a next/previous control interaction. * Lazy load a slide.
* @param direction The direction requested, previous or next. * @param slide The slide to lazy load.
*/ */
protected _nextPreviousHandler(direction: 'previous' | 'next'): void { protected _lazyLoadSlide(slide: HTMLElement): void {
if (direction == 'previous') {
this._carousel?.scrollPrev(); // Snapshots.
} else if (direction == 'next') { const img = slide.querySelector('img') as HTMLImageElement;
this._carousel?.scrollNext();
// Frigate >= 0.9.0+ clips.
const hls_player = slide.querySelector(
'frigate-card-ha-hls-player',
) as HTMLElement & { url: string };
// Frigate < 0.9.0 clips. frigate-card-ha-hls-player will also have a
// video source element, so search for that first.
const video_source = slide.querySelector('video source') as HTMLElement & {
src: string;
};
if (img) {
img.src = img.getAttribute('data-src') || img.src;
} else if (hls_player) {
hls_player.url = hls_player.getAttribute('data-url') || hls_player.url;
} else if (video_source) {
video_source.src = video_source.getAttribute('data-src') || video_source.src;
} }
} }
/**
* Lazily load media in the carousel.
*/
protected _lazyLoadMediaHandler(): void {
if (!this.viewerConfig?.lazy_load || !this._carousel) {
return;
}
const slides = this._carousel.slideNodes();
const slidesInView = this._carousel.slidesInView(true);
const slidesToLoad = new Set<number>();
// Lazily load the selected slide and the one on each side of it to improve
// the user navigation experience.
for (let i = 0; i < slidesInView.length; i++) {
const index = slidesInView[i];
if (index > 0) {
slidesToLoad.add(index - 1);
}
slidesToLoad.add(index);
if (index < slides.length - 1) {
slidesToLoad.add(index + 1);
}
}
slidesToLoad.forEach((index) => {
// Only lazy load slides that are not already loaded.
if (this._slideHasBeenLazyLoaded[index]) {
return;
}
this._slideHasBeenLazyLoaded[index] = true;
const slide = slides[index];
// Snapshots.
const img = slide.querySelector('img') as HTMLImageElement;
// Frigate >= 0.9.0+ clips.
const hls_player = slide.querySelector(
'frigate-card-ha-hls-player',
) as HTMLElement & { url: string };
// Frigate < 0.9.0 clips. frigate-card-ha-hls-player will also have a
// video source element, so search for that first.
const video_source = slide.querySelector('video source') as HTMLElement & {
src: string;
};
if (img) {
img.src = img.getAttribute('data-src') || img.src;
} else if (hls_player) {
hls_player.url = hls_player.getAttribute('data-url') || hls_player.url;
} else if (video_source) {
video_source.src = video_source.getAttribute('data-src') || video_source.src;
}
});
}
/** /**
* Get slides to include in the render. * Get slides to include in the render.
* @returns The slides to include in the render. * @returns The slides to include in the render.
@@ -587,82 +516,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
</div>`; </div>`;
} }
/**
* Fire a media show event when a slide is selected.
*/
protected _selectSlideMediaShowHandler(): void {
if (!this._carousel || !this.view) {
return;
}
this._carousel.slidesInView(true).forEach((slideIndex) => {
if (slideIndex in this._mediaShowInfo) {
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
}
});
}
/**
* Handle a media-show event that is generated by a child component, saving the
* contents for future use when the relevant slide is shown.
* @param slideIndex The relevant slide index.
* @param event The media-show event from the child component.
*/
protected _mediaShowEventHandler(
slideIndex: number,
event: CustomEvent<MediaShowInfo>,
): void {
// Don't allow the inbound event to propagate upwards, that will be
// automatically done at the appropriate time as the slide is shown.
event.stopPropagation();
this._mediaShowInfoHandler(slideIndex, event.detail);
}
/**
* Handle a MediaShowInfo object that is generated on media load, by saving it
* for future, or immediate use, when the relevant slide is displayed.
* @param slideIndex The relevant slide index.
* @param mediaShowInfo The MediaShowInfo object generated by the media.
*/
protected _mediaShowInfoHandler(
slideIndex: number,
mediaShowInfo?: MediaShowInfo | null,
): void {
// isValidMediaShowInfo is used to prevent saving media info that will be
// rejected upstream.
if (this.viewerConfig && mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
const firstMediaLoad = !Object.keys(this._mediaShowInfo).length;
this._mediaShowInfo[slideIndex] = mediaShowInfo;
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
}
/**
* Images need a width/height from initial load, and browsers will assume
* that the aspect ratio of the initial dummy-image load will persist. In
* lazy-loading, this can cause a 1x1 pixel dummy image to cause the
* browser to assume all images will be square, so the whole carousel will
* have the wrong aspect-ratio until every single image has been lazily
* loaded. To avoid this, we use a 16:9 dummy image at first (most
* likely?) and once the first piece of real media has been loaded, all
* dummy images are replaced with dummy images that match the aspect ratio
* of the real image. It still might be wrong, but it's the best option
* available.
*/
if (firstMediaLoad && this.viewerConfig.lazy_load) {
const replacementImageSrc = getEmptyImageSrc(
mediaShowInfo.width,
mediaShowInfo.height,
);
this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => {
const imageElement: HTMLImageElement = img as HTMLImageElement;
if (imageElement.src === IMG_EMPTY) {
imageElement.src = replacementImageSrc;
}
});
}
}
}
protected _renderMediaItem( protected _renderMediaItem(
mediaToRender: BrowseMediaSource, mediaToRender: BrowseMediaSource,
@@ -720,7 +573,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
playsinline playsinline
?autoplay="${autoplay}" ?autoplay="${autoplay}"
@loadedmetadata="${(e: Event) => { @loadedmetadata="${(e: Event) => {
this._mediaShowInfoHandler(slideIndex, createMediaShowInfo(e)); this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
}}" }}"
@play=${() => dispatchPlayEvent(this)} @play=${() => dispatchPlayEvent(this)}
@pause=${() => dispatchPauseEvent(this)} @pause=${() => dispatchPauseEvent(this)}
@@ -754,18 +607,11 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
(!this.viewerConfig.lazy_load || (!this.viewerConfig.lazy_load ||
this._slideHasBeenLazyLoaded[slideIndex]) this._slideHasBeenLazyLoaded[slideIndex])
) { ) {
this._mediaShowInfoHandler(slideIndex, createMediaShowInfo(e)); this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
} }
}}" }}"
/>`} />`}
</div> </div>
`; `;
} }
/**
* Get element styles.
*/
static get styles(): CSSResultGroup {
return [super.styles, unsafeCSS(mediaCarouselStyle)];
}
} }
+1 -1
View File
@@ -6,7 +6,7 @@
flex: 0 0 100%; flex: 0 0 100%;
} }
frigate-card-ha-hls-player { * {
height: 100%; height: 100%;
width: 100%; width: 100%;
transform-style: preserve-3d; transform-style: preserve-3d;
+1 -1
View File
@@ -7,7 +7,7 @@
width: 100%; width: 100%;
} }
frigate-card-media-carousel { frigate-card-viewer-carousel {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }