Implement adaptive height to support live cameras with different dimensions.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent da3851f790
commit c83299a2e6
3 changed files with 61 additions and 26 deletions
+4 -2
View File
@@ -1,8 +1,10 @@
// 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 can height adapting remove need for 16x9 dummy in the media carousel?
// TODO controls // TODO controls
// TODO lazy loading configuration
// TODO editor
// TODO readme
// TODO search for TODOs
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import type { import type {
+56 -19
View File
@@ -13,10 +13,9 @@ import './next-prev-control.js';
import mediaCarouselStyle from '../scss/media-carousel.scss'; import mediaCarouselStyle from '../scss/media-carousel.scss';
// TODO Remove this if not needed (and below) 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`; export const IMG_EMPTY = getEmptyImageSrc(16, 9);
// const IMG_EMPTY = getEmptyImageSrc(16, 9);
@customElement('frigate-card-media-carousel') @customElement('frigate-card-media-carousel')
export class FrigateCardMediaCarousel extends FrigateCardCarousel { export class FrigateCardMediaCarousel extends FrigateCardCarousel {
@@ -47,18 +46,49 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// Necessary because typescript local type narrowing is not paying attention // Necessary because typescript local type narrowing is not paying attention
// to the side-effect of the call to super._loadCarousel(). // to the side-effect of the call to super._loadCarousel().
const carousel = this._carousel as EmblaCarouselType | undefined; const carousel = this._carousel as EmblaCarouselType | undefined;
// Update the view object as the carousel is moved.
carousel?.on('select', this._selectSlideSetViewHandler.bind(this)); carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
// Dispatch MediaShow events as the carousel is moved.
carousel?.on('init', this._selectSlideMediaShowHandler.bind(this)); carousel?.on('init', this._selectSlideMediaShowHandler.bind(this));
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this)); carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
// Adapt the height of the container to the media as the carousel is moved.
carousel?.on('init', this._adaptiveHeightHandler.bind(this));
carousel?.on('select', this._adaptiveHeightHandler.bind(this));
carousel?.on('resize', this._adaptiveHeightHandler.bind(this));
if (this._getLazyLoadCount() != null) { if (this._getLazyLoadCount() != null) {
// Load media as the carousel is moved (if lazy loading is in use).
carousel?.on('init', this._lazyLoadMediaHandler.bind(this)); carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
carousel?.on('select', this._lazyLoadMediaHandler.bind(this)); carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this)); carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
} }
} }
/**
* Adapt the height of the container to the height of the media (for cases
* where the carousel has different media heights, e.g. live cameras with
* different aspect ratios).
*/
protected _adaptiveHeightHandler(): void {
if (!this._carousel) {
return;
}
const slides = this._carousel.slideNodes();
const heights = this._carousel.slidesInView(true).map((index) => {
const firstChild = slides[index].querySelector("*");
return firstChild ? firstChild.getBoundingClientRect().height : 0;
})
const targetHeight = Math.max(...heights);
if (targetHeight > 0) {
this._carousel.containerNode().style.maxHeight = `${targetHeight}px`;
} else {
this._carousel.containerNode().style.removeProperty('max-height')
}
}
/** /**
* Handle the user selecting a new slide in the carousel. * Handle the user selecting a new slide in the carousel.
*/ */
@@ -173,33 +203,40 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) { if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo); dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
} }
// After media has been loaded, the height of the container may need to be
// re-adjusted.
this._adaptiveHeightHandler();
/** /**
* Images need a width/height from initial load, and browsers will assume * 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 * 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 * 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 * 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 * 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 * loaded. Adaptive height helps in that the carousel gets resized on each
* img display to the correct size, but it still causes a minor noticeable
* flicker until the height change is complete.
*
* 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 * 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 * 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 * of the real image. It still might be wrong, but it's the best option
* available. * available.
*/ */
// TODO remove this const firstMediaLoad = !Object.keys(this._mediaShowInfo).length;
// const firstMediaLoad = !Object.keys(this._mediaShowInfo).length; if (firstMediaLoad && this._getLazyLoadCount() != null) {
// if (firstMediaLoad && this.viewerConfig.lazy_load) { const replacementImageSrc = getEmptyImageSrc(
// const replacementImageSrc = getEmptyImageSrc( mediaShowInfo.width,
// mediaShowInfo.width, mediaShowInfo.height,
// mediaShowInfo.height, );
// );
// this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => { this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => {
// const imageElement: HTMLImageElement = img as HTMLImageElement; const imageElement = img as HTMLImageElement;
// if (imageElement.src === IMG_EMPTY) { if (imageElement.src === IMG_EMPTY) {
// imageElement.src = replacementImageSrc; imageElement.src = replacementImageSrc;
// } }
// }); });
// } }
} }
} }
+1 -5
View File
@@ -15,7 +15,7 @@ import type {
MediaShowInfo, MediaShowInfo,
ViewerConfig, ViewerConfig,
} from '../types.js'; } from '../types.js';
import { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardMediaCarousel, IMG_EMPTY } 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';
@@ -35,10 +35,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';
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-viewer') @customElement('frigate-card-viewer')
export class FrigateCardViewer extends LitElement { export class FrigateCardViewer extends LitElement {
@property({ attribute: false }) @property({ attribute: false })