From c83299a2e6ebe097a4b3b2796573410009146737 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 21 Dec 2021 20:53:33 -0800 Subject: [PATCH] Implement adaptive height to support live cameras with different dimensions. --- src/components/live.ts | 6 ++- src/components/media-carousel.ts | 75 ++++++++++++++++++++++++-------- src/components/viewer.ts | 6 +-- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/components/live.ts b/src/components/live.ts index 78b7c3c8..ea8e403a 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,8 +1,10 @@ -// TODO lazy loading -// TODO media events // TODO height adapting // TODO can height adapting remove need for 16x9 dummy in the media carousel? // TODO controls +// TODO lazy loading configuration +// TODO editor +// TODO readme +// TODO search for TODOs import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import type { diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index 1cd59118..90f6a629 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -13,10 +13,9 @@ 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); +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`; +export const IMG_EMPTY = getEmptyImageSrc(16, 9); @customElement('frigate-card-media-carousel') export class FrigateCardMediaCarousel extends FrigateCardCarousel { @@ -47,18 +46,49 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { // 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 the view object as the carousel is moved. carousel?.on('select', this._selectSlideSetViewHandler.bind(this)); + // Dispatch MediaShow events as the carousel is moved. carousel?.on('init', 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) { + // Load media as the carousel is moved (if lazy loading is in use). carousel?.on('init', this._lazyLoadMediaHandler.bind(this)); carousel?.on('select', 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. */ @@ -173,33 +203,40 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) { 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 * 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 + * 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 * 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, - // ); + const firstMediaLoad = !Object.keys(this._mediaShowInfo).length; + if (firstMediaLoad && this._getLazyLoadCount() != null) { + 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; - // } - // }); - // } + this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => { + const imageElement = img as HTMLImageElement; + if (imageElement.src === IMG_EMPTY) { + imageElement.src = replacementImageSrc; + } + }); + } } } diff --git a/src/components/viewer.ts b/src/components/viewer.ts index c8dd067a..0020b760 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -15,7 +15,7 @@ import type { MediaShowInfo, ViewerConfig, } 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 { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; @@ -35,10 +35,6 @@ import './next-prev-control.js'; import viewerStyle from '../scss/viewer.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') export class FrigateCardViewer extends LitElement { @property({ attribute: false })