Merge pull request #274 from dermotduffy/pause-clips
Support autoplay & autopause for live views and event viewer
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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';
|
||||
|
||||
@@ -11,6 +15,7 @@ export interface CarouselSelect {
|
||||
|
||||
export class FrigateCardCarousel extends LitElement {
|
||||
protected _carousel?: EmblaCarouselType;
|
||||
protected _plugins: Record<string, EmblaPluginType> = {};
|
||||
|
||||
/**
|
||||
* Scroll to a particular slide.
|
||||
@@ -53,10 +58,19 @@ 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();
|
||||
}
|
||||
this._plugins = {};
|
||||
this._carousel = undefined;
|
||||
}
|
||||
|
||||
@@ -69,7 +83,13 @@ export class FrigateCardCarousel extends LitElement {
|
||||
) as HTMLElement;
|
||||
|
||||
if (carouselNode) {
|
||||
this._carousel = EmblaCarousel(carouselNode, this._getOptions());
|
||||
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();
|
||||
|
||||
@@ -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<LazyloadOptionsType> = {
|
||||
count: 0,
|
||||
};
|
||||
|
||||
export type LazyloadType = EmblaPluginType<LazyloadOptionsType> & {
|
||||
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<number, boolean> = {};
|
||||
|
||||
/**
|
||||
* 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<number>();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel';
|
||||
import { FrigateCardMediaPlayer } from '../../types';
|
||||
|
||||
export type MediaAutoPlayPauseOptionsType = {
|
||||
autoplay?: boolean;
|
||||
autopause?: boolean;
|
||||
playerSelector: string;
|
||||
};
|
||||
|
||||
export const defaultOptions: Partial<MediaAutoPlayPauseOptionsType> = {
|
||||
// Frigate card media autoplays when the media loads, not necessarily when the
|
||||
// slide is selected.
|
||||
autoplay: false,
|
||||
autopause: true,
|
||||
};
|
||||
|
||||
export type MediaAutoPlayPauseType = EmblaPluginType<MediaAutoPlayPauseOptionsType> & {
|
||||
play: () => void;
|
||||
}
|
||||
|
||||
export function MediaAutoPlayPause(
|
||||
userOptions?: MediaAutoPlayPauseOptionsType,
|
||||
): MediaAutoPlayPauseType {
|
||||
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', pausePrevious);
|
||||
}
|
||||
|
||||
if (options.autoplay) {
|
||||
carousel.on('select', play);
|
||||
carousel.on('init', play);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the plugin.
|
||||
*/
|
||||
function destroy(): void {
|
||||
if (options.autopause) {
|
||||
carousel.off('destroy', pauseAllHandler);
|
||||
carousel.off('select', pausePrevious);
|
||||
}
|
||||
|
||||
if (options.autoplay) {
|
||||
carousel.off('select', play);
|
||||
carousel.off('init', play);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media player from a slide.
|
||||
* @param slide
|
||||
* @returns A FrigateCardMediaPlayer object or `null`.
|
||||
*/
|
||||
function getPlayer(slide: HTMLElement | undefined): 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 play(): void {
|
||||
getPlayer(slides[carousel.selectedScrollSnap()])?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Autopause the previous slide.
|
||||
*/
|
||||
function pausePrevious(): void {
|
||||
getPlayer(slides[carousel.previousScrollSnap()])?.pause();
|
||||
}
|
||||
|
||||
const self: MediaAutoPlayPauseType = {
|
||||
name: 'MediaAutoPlayPause',
|
||||
options,
|
||||
init,
|
||||
destroy,
|
||||
play,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
+103
-4
@@ -19,16 +19,18 @@ 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 { 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';
|
||||
@@ -291,6 +293,25 @@ 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 [
|
||||
...(this.liveConfig?.lazy_load
|
||||
? [
|
||||
Lazyload({
|
||||
lazyloadCallback: this._lazyLoadSlide.bind(this),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
MediaAutoPlayPause({
|
||||
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
|
||||
@@ -381,7 +402,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
|
||||
return html` <div class="embla__slide">
|
||||
<frigate-card-live-provider
|
||||
?disabled=${this._isLazyLoading()}
|
||||
?disabled=${this.liveConfig.lazy_load}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.label=${getCameraTitle(this.hass, cameraConfig)}
|
||||
.liveConfig=${config}
|
||||
@@ -511,6 +532,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 +584,21 @@ export class FrigateCardLiveProvider extends LitElement {
|
||||
return html`
|
||||
${provider == 'frigate'
|
||||
? html` <frigate-card-live-frigate
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraEntity=${this.cameraConfig.camera_entity}
|
||||
>
|
||||
</frigate-card-live-frigate>`
|
||||
: provider == 'webrtc'
|
||||
? html`<frigate-card-live-webrtc
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.webRTCConfig=${this.liveConfig.webrtc}
|
||||
>
|
||||
</frigate-card-live-webrtc>`
|
||||
: html` <frigate-card-live-jsmpeg
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
||||
@@ -574,6 +616,22 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected cameraEntity?: string;
|
||||
|
||||
protected _playerRef: Ref<FrigateCardLiveFrigate> = 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 +649,7 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
);
|
||||
}
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
${ref(this._playerRef)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this.cameraEntity]}
|
||||
.controls=${true}
|
||||
@@ -619,6 +678,32 @@ export class FrigateCardLiveWebRTC extends LitElement {
|
||||
|
||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||
|
||||
/**
|
||||
* Play the video.
|
||||
*/
|
||||
public play(): void {
|
||||
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.
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the video.
|
||||
*/
|
||||
public pause(): void {
|
||||
this._getPlayer()?.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying video player.
|
||||
* @returns The player or `null` if not found.
|
||||
*/
|
||||
protected _getPlayer(): HTMLVideoElement | null {
|
||||
return this.renderRoot.querySelector('#video') as HTMLVideoElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the WebRTC element. May throw.
|
||||
*/
|
||||
@@ -675,7 +760,7 @@ export class FrigateCardLiveWebRTC extends LitElement {
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
const video = this.renderRoot.querySelector('#video') as HTMLVideoElement;
|
||||
const video = this._getPlayer();
|
||||
if (video) {
|
||||
const onloadedmetadata = video.onloadedmetadata;
|
||||
const onplay = video.onplay;
|
||||
@@ -725,6 +810,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.
|
||||
|
||||
@@ -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,31 @@ export const IMG_EMPTY = getEmptyImageSrc(16, 9);
|
||||
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> = {};
|
||||
|
||||
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
|
||||
protected _previousControlRef: Ref<FrigateCardNextPreviousControl> = 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. May be overridden to control when
|
||||
* autoplay should happen.
|
||||
*/
|
||||
protected _getLazyLoadCount(): number | null {
|
||||
// Defaults to fully-lazy loading.
|
||||
return 0;
|
||||
protected _autoplayHandler(): 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._autoplayHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('frigate-card:media-show', this._autoplayHandler);
|
||||
}
|
||||
|
||||
protected _destroyCarousel(): void {
|
||||
@@ -61,9 +62,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 +90,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 +156,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<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(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 +225,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,
|
||||
|
||||
+40
-77
@@ -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,8 @@ import {
|
||||
FrigateCardThumbnailCarousel,
|
||||
ThumbnailCarouselTap,
|
||||
} from './thumbnail-carousel.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 {
|
||||
@@ -215,7 +217,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
async ([target]: (BrowseMediaSource | undefined)[]): Promise<void> => {
|
||||
for (
|
||||
let i = 0;
|
||||
!this._isLazyLoading() &&
|
||||
!this.viewerConfig?.lazy_load &&
|
||||
this.hass &&
|
||||
target &&
|
||||
target.children &&
|
||||
@@ -263,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();
|
||||
|
||||
@@ -294,15 +306,28 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Get the Embla plugins to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
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),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
||||
// Don't need autoplay/pause for snapshots.
|
||||
...(this.view?.is('clip')
|
||||
? [
|
||||
MediaAutoPlayPause({
|
||||
playerSelector: 'frigate-card-ha-hls-player',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,70 +557,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
|
||||
@@ -644,7 +605,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(),
|
||||
@@ -711,7 +672,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;
|
||||
@@ -754,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._slideHasBeenLazyLoaded[slideIndex]
|
||||
(this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded(
|
||||
slideIndex,
|
||||
)
|
||||
) {
|
||||
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
||||
}
|
||||
|
||||
@@ -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<HTMLElement> = 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`
|
||||
<frigate-card-ha-hls-player
|
||||
${ref(this._playerRef)}
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
|
||||
@@ -764,6 +764,11 @@ export interface StateParameters {
|
||||
style?: StyleInfo;
|
||||
}
|
||||
|
||||
export interface FrigateCardMediaPlayer {
|
||||
play(): void;
|
||||
pause(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Home Assistant API types.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user