From 05bcbbb16becdf29fd3ed131e7b17d4bff8cdc72 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 21 Jan 2022 22:44:30 -0800 Subject: [PATCH 1/3] Rework autoplay/pause into a plugin, apply to viewer & live. --- src/components/carousel.ts | 12 ++- .../embla-plugins/media-autoplay.ts | 95 +++++++++++++++++++ src/components/live.ts | 93 +++++++++++++++++- src/components/viewer.ts | 77 +++------------ src/patches/ha-camera-stream.ts | 23 +++++ src/types.ts | 5 + 6 files changed, 235 insertions(+), 70 deletions(-) create mode 100644 src/components/embla-plugins/media-autoplay.ts diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 11e4bab0..ae8f2c7c 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,5 +1,5 @@ import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; -import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel'; +import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { dispatchFrigateCardEvent } from '../common'; @@ -53,6 +53,14 @@ export class FrigateCardCarousel extends LitElement { return undefined; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return undefined; + } + protected _destroyCarousel(): void { if (this._carousel) { this._carousel.destroy(); @@ -69,7 +77,7 @@ export class FrigateCardCarousel extends LitElement { ) as HTMLElement; if (carouselNode) { - this._carousel = EmblaCarousel(carouselNode, this._getOptions()); + this._carousel = EmblaCarousel(carouselNode, this._getOptions(), this._getPlugins()); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('select', () => { const selected = this.carouselSelected(); diff --git a/src/components/embla-plugins/media-autoplay.ts b/src/components/embla-plugins/media-autoplay.ts new file mode 100644 index 00000000..d4a1a7e8 --- /dev/null +++ b/src/components/embla-plugins/media-autoplay.ts @@ -0,0 +1,95 @@ +import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel'; +import { FrigateCardMediaPlayer } from '../../types'; + +export type MediaAutoplayOptionsType = { + autoplay?: boolean; + autopause?: boolean; + playerSelector: string; +}; + +export const defaultOptions: Partial = { + autoplay: true, + autopause: true, +}; + +export type MediaAutoplayType = EmblaPluginType; + +export function MediaAutoplay( + userOptions?: MediaAutoplayOptionsType, +): MediaAutoplayType { + const options = Object.assign({}, defaultOptions, userOptions); + + let carousel: EmblaCarouselType; + let slides: HTMLElement[]; + + /** + * Initialize the plugin. + */ + function init(embla: EmblaCarouselType): void { + carousel = embla; + slides = carousel.slideNodes(); + + if (options.autopause) { + carousel.on('destroy', pauseAllHandler); + carousel.on('select', autopausePreviousHandler); + } + + if (options.autoplay) { + carousel.on('select', autoplayCurrentHandler); + carousel.on('init', autoplayCurrentHandler); + } + } + + /** + * Destroy the plugin. + */ + function destroy(): void { + if (options.autopause) { + carousel.off('destroy', pauseAllHandler); + carousel.off('select', autopausePreviousHandler); + } + + if (options.autoplay) { + carousel.off('select', autoplayCurrentHandler); + carousel.off('init', autoplayCurrentHandler); + } + } + + /** + * Get the media player from a slide. + * @param slide + * @returns A FrigateCardMediaPlayer object or `null`. + */ + function getPlayer(slide: HTMLElement): FrigateCardMediaPlayer | null { + return slide.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null; + } + + /** + * Pause all clips. + */ + function pauseAllHandler(): void { + slides.forEach((slide) => getPlayer(slide)?.pause()); + } + + /** + * Autoplay the current slide. + */ + function autoplayCurrentHandler(): void { + getPlayer(slides[carousel.selectedScrollSnap()])?.play(); + } + + /** + * Autopause the previous slide. + */ + function autopausePreviousHandler(): void { + getPlayer(slides[carousel.previousScrollSnap()])?.pause(); + } + + const self: MediaAutoplayType = { + name: 'MediaAutoplay', + options, + init, + destroy, + }; + return self; +} diff --git a/src/components/live.ts b/src/components/live.ts index 3f05c191..718ae0ac 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -19,16 +19,17 @@ import { LiveProvider, frigateCardConfigDefaults, } from '../types.js'; -import { EmblaOptionsType } from 'embla-carousel'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; +import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { customElement, property, state } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref'; import { until } from 'lit/directives/until.js'; import { BrowseMediaUtil } from '../browse-media-util.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js'; import { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { View } from '../view.js'; import { localize } from '../localize/localize.js'; @@ -291,6 +292,18 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { }; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return [ + MediaAutoplay({ + playerSelector: 'frigate-card-live-provider', + }), + ]; + } + /** * 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 @@ -511,6 +524,24 @@ export class FrigateCardLiveProvider extends LitElement { @property({ attribute: false }) public label = ''; + protected _providerRef: Ref< + FrigateCardLiveFrigate | FrigateCardLiveJSMPEG | FrigateCardLiveWebRTC + > = createRef(); + + /** + * Play the video. + */ + public play(): void { + this._providerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._providerRef.value?.pause(); + } + protected _getResolvedProvider(): LiveProvider { if (this.cameraConfig?.live_provider === 'auto') { if (this.cameraConfig?.webrtc?.entity || this.cameraConfig?.webrtc?.url) { @@ -545,18 +576,21 @@ export class FrigateCardLiveProvider extends LitElement { return html` ${provider == 'frigate' ? html` ` : provider == 'webrtc' ? html` ` : html` = createRef(); + + /** + * Play the video. + */ + public play(): void { + this._playerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._playerRef.value?.pause(); + } + /** * Master render method. * @returns A rendered template. @@ -591,6 +641,7 @@ export class FrigateCardLiveFrigate extends LitElement { ); } return html` { - const video = this.renderRoot.querySelector('#video') as HTMLVideoElement; + const video = this._getPlayer(); if (video) { const onloadedmetadata = video.onloadedmetadata; const onplay = video.onplay; @@ -725,6 +798,20 @@ export class FrigateCardLiveJSMPEG extends LitElement { protected _jsmpegVideoPlayer?: JSMpeg.VideoElement; protected _refreshPlayerTimerID?: number; + /** + * Play the video. + */ + public play(): void { + this._jsmpegVideoPlayer?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._jsmpegVideoPlayer?.stop(); + } + /** * Get a signed player URL. * @returns A URL or null. diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 1d4686d8..920d331e 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -7,7 +7,7 @@ import { unsafeCSS, } from 'lit'; import { BrowseMediaUtil } from '../browse-media-util.js'; -import { EmblaOptionsType } from 'embla-carousel'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; import { Task } from '@lit-labs/task'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; @@ -29,6 +29,7 @@ import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap, } from './thumbnail-carousel.js'; +import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; import { @@ -293,6 +294,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { }; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return [MediaAutoplay({ + autoplay: this.viewerConfig?.autoplay_clip, + playerSelector: 'frigate-card-ha-hls-player' })]; + } + /** * 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 @@ -532,70 +543,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { } } - protected _playOrPauseClip(action: 'play' | 'pause', slide: HTMLElement): void { - const player = slide.querySelector('frigate-card-ha-hls-player') as - | (HTMLElement & { play: () => void; pause: () => void }) - | undefined; - if (player) { - if (action === 'play') { - player.play(); - } else if (action === 'pause') { - player.pause(); - } - } - } - - /** - * Pause all clips. - */ - protected _pauseAllHandler(): void { - if (this._carousel) { - this._carousel - .slideNodes() - .forEach((slide) => this._playOrPauseClip('pause', slide)); - } - } - - /** - * Play the clip being shown to the user and pause the prior. - */ - protected _autoplayPauseHandler(pausePrevious: boolean): void { - if (!this._carousel) { - return; - } - - const slides = this._carousel.slideNodes(); - - // Pause the previous/current slide. - if (pausePrevious) { - this._carousel - .slidesInView(false) - .forEach((slide) => { - this._playOrPauseClip('pause', slides[slide]) - }); - } - - // Play the target slide. - this._carousel - .slidesInView(true) - .forEach((slide) => { - this._playOrPauseClip('play', slides[slide]) - }); - } - - /** - * Initialize the carousel. - */ - protected _initCarousel(): void { - super._initCarousel(); - - if (this._carousel && this.viewerConfig && this.viewerConfig.autoplay_clip) { - this._carousel.on('destroy', () => this._pauseAllHandler()); - this._carousel.on('init', () => this._autoplayPauseHandler(false)); - this._carousel.on('select', () => this._autoplayPauseHandler(true)); - } - } - /** * Get slides to include in the render. * @returns The slides to include in the render and an index keyed by slide diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index 83408a43..a94874d9 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -9,6 +9,7 @@ // available as compilation time. // ==================================================================== +import { Ref, createRef, ref } from 'lit/directives/ref'; import { TemplateResult, css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { dispatchMediaShowEvent } from '../common.js'; @@ -34,10 +35,31 @@ customElements.whenDefined('ha-camera-stream').then(() => { @customElement('frigate-card-ha-camera-stream') // eslint-disable-next-line @typescript-eslint/no-unused-vars class FrigateCardHaCameraStream extends customElements.get('ha-camera-stream') { + protected _playerRef: Ref = createRef(); + // ======================================================================================== // Minor modifications from: // - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-camera-stream.ts // ======================================================================================== + + /** + * Play the video. + */ + public play(): void { + this._playerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._playerRef.value?.pause(); + } + + /** + * Master render method. + * @returns A rendered template. + */ protected render(): TemplateResult { if (!this.stateObj) { return html``; @@ -59,6 +81,7 @@ customElements.whenDefined('ha-camera-stream').then(() => { : this._url ? html` Date: Sat, 22 Jan 2022 18:23:30 -0800 Subject: [PATCH 2/3] Make autoplay/pause work correctly with lazyloading. --- package.json | 2 +- src/components/carousel.ts | 16 +++- src/components/embla-plugins/lazyload.ts | 91 ++++++++++++++++++ .../embla-plugins/media-autoplay.ts | 43 +++++---- src/components/live.ts | 20 +++- src/components/media-carousel.ts | 92 ++++--------------- src/components/viewer.ts | 40 ++++---- 7 files changed, 182 insertions(+), 122 deletions(-) create mode 100644 src/components/embla-plugins/lazyload.ts diff --git a/package.json b/package.json index 55963fd0..1e3a10d0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@material/rtl": "^13.0.0", "custom-card-helpers": "^1.8.0", "dayjs": "^1.10.7", - "embla-carousel": "^6.1.0", + "embla-carousel": "^6.1.1", "home-assistant-js-websocket": "^5.11.1", "lit": "^2.0.2", "lodash-es": "^4.17.21", diff --git a/src/components/carousel.ts b/src/components/carousel.ts index ae8f2c7c..02bf84ea 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,5 +1,9 @@ import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; -import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; +import EmblaCarousel, { + EmblaCarouselType, + EmblaOptionsType, + EmblaPluginType, +} from 'embla-carousel'; import { dispatchFrigateCardEvent } from '../common'; @@ -11,6 +15,7 @@ export interface CarouselSelect { export class FrigateCardCarousel extends LitElement { protected _carousel?: EmblaCarouselType; + protected _plugins: Record = {}; /** * Scroll to a particular slide. @@ -65,6 +70,7 @@ export class FrigateCardCarousel extends LitElement { if (this._carousel) { this._carousel.destroy(); } + this._plugins = {}; this._carousel = undefined; } @@ -77,7 +83,13 @@ export class FrigateCardCarousel extends LitElement { ) as HTMLElement; if (carouselNode) { - this._carousel = EmblaCarousel(carouselNode, this._getOptions(), this._getPlugins()); + const plugins = this._getPlugins() ?? []; + this._plugins = plugins.reduce((acc, cur) => { + acc[cur.name] = cur; + return acc; + }, {}); + + this._carousel = EmblaCarousel(carouselNode, this._getOptions(), plugins); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('select', () => { const selected = this.carouselSelected(); diff --git a/src/components/embla-plugins/lazyload.ts b/src/components/embla-plugins/lazyload.ts new file mode 100644 index 00000000..a78e6ca9 --- /dev/null +++ b/src/components/embla-plugins/lazyload.ts @@ -0,0 +1,91 @@ +import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel'; + +export type LazyloadOptionsType = { + count?: number; + lazyloadCallback: (index: number, slide: HTMLElement) => void; +}; + +export const defaultOptions: Partial = { + count: 0, +}; + +export type LazyloadType = EmblaPluginType & { + hasLazyloaded: (index: number) => boolean; +}; + +export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType { + const options = Object.assign({}, defaultOptions, userOptions); + + let carousel: EmblaCarouselType; + let slides: HTMLElement[]; + const isSlideLazyloaded: Record = {}; + + /** + * Initialize the plugin. + */ + function init(embla: EmblaCarouselType): void { + carousel = embla; + slides = carousel.slideNodes(); + + carousel.on('init', lazyLoadHandler); + carousel.on('select', lazyLoadHandler); + carousel.on('resize', lazyLoadHandler); + } + + /** + * Destroy the plugin. + */ + function destroy(): void { + carousel.off('init', lazyLoadHandler); + carousel.off('select', lazyLoadHandler); + carousel.off('resize', lazyLoadHandler); + } + + /** + * Determine if a slide index has been lazily loaded. + * @param index Slide index. + * @returns `true` if the slide has been lazily loaded. + */ + function hasLazyloaded(index: number): boolean { + return !!isSlideLazyloaded[index]; + } + + /** + * Lazily load media in the carousel. + */ + function lazyLoadHandler(): void { + const lazyLoadCount = options.count ?? 0; + const slidesInView = carousel.slidesInView(true); + const slidesToLoad = new Set(); + + const minSlide = Math.min(...slidesInView); + const maxSlide = Math.max(...slidesInView); + + // Lazily load 'count' 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 (isSlideLazyloaded[index]) { + return; + } + isSlideLazyloaded[index] = true; + options.lazyloadCallback(index, slides[index]); + }); + } + + const self: LazyloadType = { + name: 'Lazyload', + options, + init, + destroy, + hasLazyloaded, + }; + return self; +} diff --git a/src/components/embla-plugins/media-autoplay.ts b/src/components/embla-plugins/media-autoplay.ts index d4a1a7e8..b8c5bf25 100644 --- a/src/components/embla-plugins/media-autoplay.ts +++ b/src/components/embla-plugins/media-autoplay.ts @@ -1,22 +1,26 @@ import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel'; import { FrigateCardMediaPlayer } from '../../types'; -export type MediaAutoplayOptionsType = { +export type MediaAutoPlayPauseOptionsType = { autoplay?: boolean; autopause?: boolean; playerSelector: string; }; -export const defaultOptions: Partial = { - autoplay: true, +export const defaultOptions: Partial = { + // Frigate card media autoplays when the media loads, not necessarily when the + // slide is selected. + autoplay: false, autopause: true, }; -export type MediaAutoplayType = EmblaPluginType; +export type MediaAutoPlayPauseType = EmblaPluginType & { + play: () => void; +} -export function MediaAutoplay( - userOptions?: MediaAutoplayOptionsType, -): MediaAutoplayType { +export function MediaAutoPlayPause( + userOptions?: MediaAutoPlayPauseOptionsType, +): MediaAutoPlayPauseType { const options = Object.assign({}, defaultOptions, userOptions); let carousel: EmblaCarouselType; @@ -31,12 +35,12 @@ export function MediaAutoplay( if (options.autopause) { carousel.on('destroy', pauseAllHandler); - carousel.on('select', autopausePreviousHandler); + carousel.on('select', pausePrevious); } if (options.autoplay) { - carousel.on('select', autoplayCurrentHandler); - carousel.on('init', autoplayCurrentHandler); + carousel.on('select', play); + carousel.on('init', play); } } @@ -46,12 +50,12 @@ export function MediaAutoplay( function destroy(): void { if (options.autopause) { carousel.off('destroy', pauseAllHandler); - carousel.off('select', autopausePreviousHandler); + carousel.off('select', pausePrevious); } if (options.autoplay) { - carousel.off('select', autoplayCurrentHandler); - carousel.off('init', autoplayCurrentHandler); + carousel.off('select', play); + carousel.off('init', play); } } @@ -60,8 +64,8 @@ export function MediaAutoplay( * @param slide * @returns A FrigateCardMediaPlayer object or `null`. */ - function getPlayer(slide: HTMLElement): FrigateCardMediaPlayer | null { - return slide.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null; + function getPlayer(slide: HTMLElement | undefined): FrigateCardMediaPlayer | null { + return slide?.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null; } /** @@ -74,22 +78,23 @@ export function MediaAutoplay( /** * Autoplay the current slide. */ - function autoplayCurrentHandler(): void { + function play(): void { getPlayer(slides[carousel.selectedScrollSnap()])?.play(); } /** * Autopause the previous slide. */ - function autopausePreviousHandler(): void { + function pausePrevious(): void { getPlayer(slides[carousel.previousScrollSnap()])?.pause(); } - const self: MediaAutoplayType = { - name: 'MediaAutoplay', + const self: MediaAutoPlayPauseType = { + name: 'MediaAutoPlayPause', options, init, destroy, + play, }; return self; } diff --git a/src/components/live.ts b/src/components/live.ts index 718ae0ac..e3d9b9fe 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -29,7 +29,8 @@ import { BrowseMediaUtil } from '../browse-media-util.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js'; import { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; -import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; +import { Lazyload } from './embla-plugins/lazyload.js'; +import { MediaAutoPlayPause } from './embla-plugins/media-autoplay.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { View } from '../view.js'; import { localize } from '../localize/localize.js'; @@ -298,7 +299,14 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { */ protected _getPlugins(): EmblaPluginType[] | undefined { return [ - MediaAutoplay({ + ...(this.liveConfig?.lazy_load + ? [ + Lazyload({ + lazyloadCallback: this._lazyLoadSlide.bind(this), + }), + ] + : []), + MediaAutoPlayPause({ playerSelector: 'frigate-card-live-provider', }), ]; @@ -394,7 +402,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { return html`
{ + // WebRTC appears to generate additional spurious load events, which may + // result in loads after a play() call, which causes the browser to spam + // the logs unless the promise rejection is handled here. + }) } /** diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index 5b20e55a..09f5b676 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -15,6 +15,7 @@ import './next-prev-control.js'; import mediaCarouselStyle from '../scss/media-carousel.scss'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import { MediaAutoPlayPauseType } from './embla-plugins/media-autoplay.js'; 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`; @@ -24,31 +25,30 @@ export const IMG_EMPTY = getEmptyImageSrc(16, 9); export class FrigateCardMediaCarousel extends FrigateCardCarousel { // A "map" from slide number to MediaShowInfo object. protected _mediaShowInfo: Record = {}; - - // Whether or not a given slide has been successfully lazily loaded. - protected _slideHasBeenLazyLoaded: Record = {}; - protected _nextControlRef: Ref = createRef(); protected _previousControlRef: Ref = createRef(); /** - * 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 + * Play the media on the selected slide. */ - protected _getLazyLoadCount(): number | null { - // Defaults to fully-lazy loading. - return 0; + protected _playSelectedMediaHandler(): void { + (this._plugins['MediaAutoPlayPause'] as MediaAutoPlayPauseType | undefined)?.play(); } /** - * Determine if lazy loading is being used. - * @returns `true` is lazy loading is in use. + * Component connected callback. */ - protected _isLazyLoading(): boolean { - return this._getLazyLoadCount() !== null; + connectedCallback(): void { + super.connectedCallback(); + this.addEventListener('frigate-card:media-show', this._playSelectedMediaHandler); + } + + /** + * Component disconnected callback. + */ + disconnectedCallback(): void { + super.disconnectedCallback(); + this.removeEventListener('frigate-card:media-show', this._playSelectedMediaHandler); } protected _destroyCarousel(): void { @@ -61,9 +61,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { // media would not reload) -- as such, leave this alone on carousel // destroy. New media in that slide will replace the prior contents on // load. - // * this._slideHasBeenLazyLoaded: This is a performance optimization and - // can be safely reset. - this._slideHasBeenLazyLoaded = {}; } /** @@ -92,13 +89,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { carousel?.on('init', this._adaptiveHeightSetHandler.bind(this)); carousel?.on('select', this._adaptiveHeightSetHandler.bind(this)); carousel?.on('resize', this._adaptiveHeightSetHandler.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)); - } } /** @@ -165,54 +155,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { } } - /** - * 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(); - - 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(index, slides[index]); - }); - } - - /** - * Lazy load a slide. - * @param _index The index of the slide to lazy load. - * @param _slide The slide to lazy load. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected _lazyLoadSlide(_index: number, _slide: HTMLElement): void { - // To be overridden in children. - } - /** * Fire a media show event when a slide is selected. */ @@ -282,7 +224,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { * available. */ const firstMediaLoad = !Object.keys(this._mediaShowInfo).length; - if (firstMediaLoad && this._getLazyLoadCount() != null) { + if (firstMediaLoad) { const replacementImageSrc = getEmptyImageSrc( mediaShowInfo.width, mediaShowInfo.height, diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 920d331e..6483a0d4 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -29,7 +29,8 @@ import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap, } from './thumbnail-carousel.js'; -import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; +import { MediaAutoPlayPause } from './embla-plugins/media-autoplay.js'; +import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; import { @@ -216,7 +217,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { async ([target]: (BrowseMediaSource | undefined)[]): Promise => { for ( let i = 0; - !this._isLazyLoading() && + !this.viewerConfig?.lazy_load && this.hass && target && target.children && @@ -298,22 +299,19 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { * Get the Embla plugins to use. * @returns An EmblaOptionsType object or undefined for no options. */ - protected _getPlugins(): EmblaPluginType[] | undefined { - return [MediaAutoplay({ - autoplay: this.viewerConfig?.autoplay_clip, - playerSelector: 'frigate-card-ha-hls-player' })]; - } - - /** - * 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 this.viewerConfig?.lazy_load === false ? null : 0; + protected _getPlugins(): EmblaPluginType[] | undefined { + return [ + ...(this.viewerConfig?.lazy_load + ? [ + Lazyload({ + lazyloadCallback: this._lazyLoadSlide.bind(this), + }), + ] + : []), + MediaAutoPlayPause({ + playerSelector: 'frigate-card-ha-hls-player', + }), + ]; } /** @@ -591,7 +589,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { // If lazy loading is not enabled, wait for the media resolver task to // complete and show a progress indictator until this. - if (!this._isLazyLoading() && !this._isMediaFullyResolved()) { + if (!this.viewerConfig?.lazy_load && !this._isMediaFullyResolved()) { return html`${this._mediaResolutionTask.render({ initial: () => renderProgressIndicator(), pending: () => renderProgressIndicator(), @@ -658,7 +656,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { return; } - const lazyLoad = this._isLazyLoading(); + const lazyLoad = this.viewerConfig.lazy_load; const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id); if (!resolvedMedia && !lazyLoad) { return; @@ -701,7 +699,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { // images in media-carousel.ts). Here we need to only call the // media load handler on a 'real' load. !lazyLoad || - this._slideHasBeenLazyLoaded[slideIndex] + (this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded(slideIndex) ) { this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e)); } From 20ea8054a704932c54dcfb34784f0e98f3c4cfbf Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 22 Jan 2022 19:38:57 -0800 Subject: [PATCH 3/3] Don't use autoplay logic for snapshots. --- src/components/live.ts | 2 +- src/components/media-carousel.ts | 9 +++++---- src/components/viewer.ts | 26 ++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/live.ts b/src/components/live.ts index e3d9b9fe..be86e2a2 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -682,7 +682,7 @@ export class FrigateCardLiveWebRTC extends LitElement { * Play the video. */ public play(): void { - this._getPlayer()?.play().catch((_) => { + this._getPlayer()?.play().catch(() => { // WebRTC appears to generate additional spurious load events, which may // result in loads after a play() call, which causes the browser to spam // the logs unless the promise rejection is handled here. diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index 09f5b676..a7aa7454 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -29,9 +29,10 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { protected _previousControlRef: Ref = createRef(); /** - * Play the media on the selected slide. + * Play the media on the selected slide. May be overridden to control when + * autoplay should happen. */ - protected _playSelectedMediaHandler(): void { + protected _autoplayHandler(): void { (this._plugins['MediaAutoPlayPause'] as MediaAutoPlayPauseType | undefined)?.play(); } @@ -40,7 +41,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { */ connectedCallback(): void { super.connectedCallback(); - this.addEventListener('frigate-card:media-show', this._playSelectedMediaHandler); + this.addEventListener('frigate-card:media-show', this._autoplayHandler); } /** @@ -48,7 +49,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { */ disconnectedCallback(): void { super.disconnectedCallback(); - this.removeEventListener('frigate-card:media-show', this._playSelectedMediaHandler); + this.removeEventListener('frigate-card:media-show', this._autoplayHandler); } protected _destroyCarousel(): void { diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 6483a0d4..406dd764 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -265,6 +265,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { super.updated(changedProperties); } + /** + * Play the media on the selected slide. May be overridden to control when + * autoplay should happen. + */ + protected _autoplayHandler(): void { + if (this.viewerConfig?.autoplay_clip) { + super._autoplayHandler(); + } + } + protected _destroyCarousel(): void { super._destroyCarousel(); @@ -308,9 +318,15 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { }), ] : []), - MediaAutoPlayPause({ - playerSelector: 'frigate-card-ha-hls-player', - }), + + // Don't need autoplay/pause for snapshots. + ...(this.view?.is('clip') + ? [ + MediaAutoPlayPause({ + playerSelector: 'frigate-card-ha-hls-player', + }), + ] + : []), ]; } @@ -699,7 +715,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { // images in media-carousel.ts). Here we need to only call the // media load handler on a 'real' load. !lazyLoad || - (this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded(slideIndex) + (this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded( + slideIndex, + ) ) { this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e)); }