Initial code for major engine refactor.

This commit is contained in:
Dermot Duffy
2023-01-24 19:36:54 -08:00
parent 03de7b473a
commit 093925cb40
39 changed files with 3506 additions and 2459 deletions
+187 -304
View File
@@ -16,30 +16,21 @@ import { renderProgressIndicator } from '../components/message.js';
import viewerStyle from '../scss/viewer.scss';
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
import {
BrowseMediaNeighbors,
BrowseMediaQueryParameters,
CameraConfig,
CardWideConfig,
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
frigateCardConfigDefaults,
FrigateCardMediaPlayer,
MediaLoadedInfo,
ResolvedMedia,
TransitionEffect,
ViewerConfig,
} from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { contentsChanged } from '../utils/basic.js';
import {
fetchLatestMediaAndDispatchViewChange,
getEventStartTime,
getFullDependentBrowseMediaQueryParametersOrDispatchError,
isTrueMedia,
multipleBrowseMediaQueryMerged,
overrideMultiBrowseMediaQueryParameters,
} from '../utils/ha/browse-media.js';
import { getFullDependentBrowseMediaQueryParametersOrDispatchError } from '../utils/ha/browse-media.js';
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
import { View } from '../view.js';
import { MediaQueriesResults, View } from '../view.js';
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
import { Lazyload } from './embla-plugins/lazyload.js';
import {
@@ -55,8 +46,13 @@ import '../patches/ha-hls-player';
import './surround.js';
import { renderTask } from '../utils/task.js';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { DataManager } from '../utils/data-manager.js';
import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
import { DataManager } from '../utils/data/data-manager.js';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
} from '../utils/media-to-view.js';
import { ViewMedia, ViewMediaClassifier } from '../view-media.js';
import { guard } from 'lit/directives/guard.js';
export interface MediaSeek {
// Specifies the point at which this recording should be played, the
@@ -123,10 +119,10 @@ export class FrigateCardViewer extends LitElement {
this.view.camera,
);
if (!this.view.target) {
// If the target is not specified, the view must tell us which mediaType
// to search for. When the target *is* specified, the view is not required
// to indicate the media type (e.g. the mixed 'events' view from the
if (!this.view.queryResults?.hasResults()) {
// If the query is not specified, the view must tell us which mediaType to
// search for. When the query *is* specified, the view is not required to
// indicate the media type (e.g. the mixed 'media' view from the
// timeline).
const mediaType = this.view.getMediaType();
if (!browseMediaQueryParameters || !mediaType) {
@@ -145,13 +141,15 @@ export class FrigateCardViewer extends LitElement {
},
);
} else {
fetchLatestMediaAndDispatchViewChange(
changeViewToRecentEventsForCameraAndDependents(
this,
this.hass,
this.dataManager,
this.cameras,
this.view,
overrideMultiBrowseMediaQueryParameters(browseMediaQueryParameters, {
mediaType: mediaType,
}),
{
targetView: mediaType === 'clips' ? 'clip' : 'snapshot',
},
);
}
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
@@ -160,7 +158,6 @@ export class FrigateCardViewer extends LitElement {
return html` <frigate-card-surround
.hass=${this.hass}
.view=${this.view}
.fetch=${false}
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline}
.dataManager=${this.dataManager}
@@ -169,8 +166,8 @@ export class FrigateCardViewer extends LitElement {
<frigate-card-viewer-carousel
.hass=${this.hass}
.view=${this.view}
.cameras=${this.cameras}
.viewerConfig=${this.viewerConfig}
.browseMediaQueryParameters=${browseMediaQueryParameters}
.resolvedMediaCache=${this.resolvedMediaCache}
.cardWideConfig=${this.cardWideConfig}
>
@@ -204,46 +201,52 @@ export class FrigateCardViewerCarousel extends LitElement {
@property({ attribute: false, hasChanged: contentsChanged })
public viewerConfig?: ViewerConfig;
@property({ attribute: false })
public browseMediaQueryParameters?: BrowseMediaQueryParameters[] | null;
@property({ attribute: false })
public resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
// Mapping of slide # to FrigateBrowseMediaSource child #.
// (Folders are not media items that can be rendered).
protected _slideToChild: Record<number, number> = {};
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
protected _carouselOptions?: EmblaOptionsType;
protected _carouselPlugins?: EmblaPluginType[];
// A task to resolve target media if lazy loading is disabled.
protected _mediaResolutionTask = new Task<
[FrigateBrowseMediaSource | null | undefined],
[ViewerConfig | undefined, Map<string, CameraConfig> | undefined, View | undefined],
void
>(
this,
async ([target]: (FrigateBrowseMediaSource | null | undefined)[]): Promise<void> => {
for (
let i = 0;
!this.viewerConfig?.lazy_load &&
this.hass &&
target &&
target.children &&
i < (target.children || []).length;
++i
async ([viewerConfig, cameras, view]: [
ViewerConfig | undefined,
Map<string, CameraConfig> | undefined,
View | undefined,
]): Promise<void> => {
if (
!this.hass ||
!viewerConfig?.lazy_load ||
!cameras ||
!view ||
!view.queryResults?.hasResults()
) {
if (isTrueMedia(target.children[i])) {
await resolveMedia(this.hass, target.children[i], this.resolvedMediaCache);
}
return;
}
const promises: Promise<ResolvedMedia | null>[] = [];
view.queryResults?.getResults()?.forEach((media: ViewMedia) => {
const mediaContentID = media.getContentID(cameras.get(media.getCameraID()));
if (this.hass && mediaContentID) {
promises.push(
resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache),
);
}
});
await Promise.all(promises);
},
() => [this.view?.target],
() => [this.viewerConfig, this.cameras, this.view],
);
/**
@@ -251,27 +254,8 @@ export class FrigateCardViewerCarousel extends LitElement {
* @param changedProperties The properties that were changed in this render.
*/
updated(changedProperties: PropertyValues): void {
const frigateCardCarousel = this._refMediaCarousel.value?.frigateCardCarousel();
if (frigateCardCarousel && changedProperties.has('view')) {
if (changedProperties.has('view')) {
const oldView = changedProperties.get('view') as View | undefined;
if (oldView) {
if (
oldView.target === this.view?.target &&
oldView.childIndex !== this.view.childIndex
) {
const slide = this._getSlideForChild(this.view.childIndex);
if (
slide !== null &&
slide !== frigateCardCarousel.getCarouselSelected()?.index
) {
// If the media target is the same as already loaded, but isn't of
// the selected slide, scroll to that slide.
frigateCardCarousel.carouselScrollTo(slide);
}
}
}
// Seek into the video if the seek time has changed (this is also called
// on media load, since the media may or may not have been loaded at
// this point).
@@ -282,21 +266,6 @@ export class FrigateCardViewerCarousel extends LitElement {
super.updated(changedProperties);
}
/**
* Get the slide number given a media child number.
* @param childIndex The child index (relative to `view.target`)
* @returns A number or null if the child is not found.
*/
protected _getSlideForChild(childIndex: number | null | undefined): number | null {
if (childIndex === undefined || childIndex === null) {
return null;
}
const slideIndex = Object.keys(this._slideToChild).find(
(key) => this._slideToChild[key] === childIndex,
);
return slideIndex !== undefined ? Number(slideIndex) : null;
}
/**
* Get the transition effect to use.
* @returns An TransitionEffect object.
@@ -308,18 +277,6 @@ export class FrigateCardViewerCarousel extends LitElement {
);
}
/**
* Get the Embla options to use.
* @returns An EmblaOptionsType object or undefined for no options.
*/
protected _getOptions(): EmblaOptionsType {
return {
// Start the carousel on the selected child number.
startIndex: this._getSlideForChild(this.view?.childIndex) ?? 0,
draggable: this.viewerConfig?.draggable ?? true,
};
}
/**
* The the HLS player on a slide (or current slide if not provided.)
* @param slide An optional slide.
@@ -344,10 +301,7 @@ export class FrigateCardViewerCarousel extends LitElement {
protected _getPlugins(): EmblaPluginType[] {
return [
// Only enable wheel plugin if there is more than one media item.
...(this.view &&
this.view.target &&
this.view.target.children &&
this.view.target.children.length > 1
...(this.view?.queryResults?.getResultsCount() ?? 0 > 1
? [
WheelGesturesPlugin({
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
@@ -384,42 +338,20 @@ export class FrigateCardViewerCarousel extends LitElement {
* @returns A BrowseMediaNeighbors with indices and objects of true media
* neighbors.
*/
protected _getMediaNeighbors(): BrowseMediaNeighbors | null {
if (
!this.view ||
!this.view.target ||
!this.view.target.children ||
this.view.childIndex === null
) {
return null;
protected _getMediaNeighbors(): [ViewMedia | null, ViewMedia | null] {
const selectedIndex = this.view?.queryResults?.getSelectedIndex() ?? null;
const resultCount = this.view?.queryResults?.getResultsCount() ?? 0;
if (!this.view || !this.view.queryResults || selectedIndex === null) {
return [null, null];
}
// Work backwards from the index to get the previous real media.
let prevIndex: number | null = null;
for (let i = this.view.childIndex - 1; i >= 0; i--) {
const media = this.view.target.children[i];
if (media && isTrueMedia(media)) {
prevIndex = i;
break;
}
}
// Work forwards from the index to get the next real media.
let nextIndex: number | null = null;
for (let i = this.view.childIndex + 1; i < this.view.target.children.length; i++) {
const media = this.view.target.children[i];
if (media && isTrueMedia(media)) {
nextIndex = i;
break;
}
}
return {
previousIndex: prevIndex,
previous: prevIndex != null ? this.view.target.children[prevIndex] : null,
nextIndex: nextIndex,
next: nextIndex != null ? this.view.target.children[nextIndex] : null,
};
const previous: ViewMedia | null =
selectedIndex > 0 ? this.view.queryResults.getResult(selectedIndex - 1) : null;
const next: ViewMedia | null =
selectedIndex + 1 < resultCount
? this.view.queryResults.getResult(selectedIndex + 1)
: null;
return [previous, next];
}
/**
@@ -428,91 +360,55 @@ export class FrigateCardViewerCarousel extends LitElement {
* @param snapshot The snapshot to find a matching clip for.
* @returns The view that would show the matching clip.
*/
protected async _findRelatedClipView(
snapshot: FrigateBrowseMediaSource,
): Promise<View | null> {
protected async _createRelatedClipView(targetIndex: number): Promise<View | null> {
const media = this.view?.queryResults?.getResult(targetIndex);
if (
!this.hass ||
!this.view ||
!this.view.target ||
!this.view.target.children ||
!this.view.target.children.length ||
!this.browseMediaQueryParameters
!media ||
// If this specific media item has no clip, then do nothing (even if all
// the other media items do).
!ViewMediaClassifier.isFrigateEvent(media) ||
!media.hasClip() ||
!this.view.query?.areEventQueries()
) {
return null;
}
const snapshotStartTime = getEventStartTime(snapshot);
if (!snapshotStartTime) {
return null;
}
const newResults: ViewMedia[] = [];
let newSelectedIndex: number | null = null;
// Heuristic: At this point, the user has a particular snapshot that they
// are interested in and want to see a related clip, yet the viewer code
// does not know the exact search criteria that led to that snapshot (e.g.
// it could be a 10-deep folder in the gallery). To give the user to ability
// to 'navigate' in the clips view once they change into that mode, this
// heuristic finds the earliest and latest snapshot that the user is
// currently viewing and mirrors that range into the clips view. Then,
// within the results see if there's a clip that matches the same time as
// the snapshot.
let earliest: number | null = null;
let latest: number | null = null;
for (let i = 0; i < this.view.target.children.length; i++) {
const child = this.view.target.children[i];
if (!isTrueMedia(child)) {
// Convert the query to a clips equivalent.
const newQuery = this.view.query.clone();
newQuery.convertToClipsQueries();
// Regenerate the whole results stack.
for (let i = 0; i < (this.view.queryResults?.getResultsCount() ?? 0); ++i) {
const media = this.view.queryResults?.getResult(i);
if (!media || !ViewMediaClassifier.isFrigateEvent(media)) {
continue;
}
const startTime = getEventStartTime(child);
if (startTime && (earliest === null || startTime < earliest)) {
earliest = startTime;
}
if (startTime && (latest === null || startTime > latest)) {
latest = startTime;
const clipMedia = media.getClipEquivalent();
if (clipMedia) {
newResults.push(clipMedia);
if (i === targetIndex) {
newSelectedIndex = i;
}
}
}
if (!earliest || !latest) {
if (newSelectedIndex === null) {
return null;
}
let clips: FrigateBrowseMediaSource | null;
const newQueryResults = new MediaQueriesResults(newResults);
newQueryResults.selectResult(newSelectedIndex);
const params = overrideMultiBrowseMediaQueryParameters(
this.browseMediaQueryParameters,
{
mediaType: 'clips',
before: latest,
after: earliest,
},
);
try {
clips = await multipleBrowseMediaQueryMerged(this.hass, params);
} catch (e) {
// This is best effort.
return null;
}
if (!clips || !clips.children || !clips.children.length) {
return null;
}
for (let i = 0; i < clips.children.length; i++) {
const child = clips.children[i];
if (!isTrueMedia(child)) {
continue;
}
const clipStartTime = getEventStartTime(child);
if (clipStartTime && clipStartTime === snapshotStartTime) {
return this.view.evolve({
view: 'clip',
target: clips,
childIndex: i,
});
}
}
return null;
return this.view.evolve({
view: 'clip',
query: newQuery,
queryResults: newQueryResults,
});
}
/**
@@ -523,13 +419,15 @@ export class FrigateCardViewerCarousel extends LitElement {
return;
}
// Update the childIndex in the view.
const childIndex = this._slideToChild[ev.detail.index];
if (childIndex !== undefined) {
// The slide may already be selected on load, so don't dispatch a new view
// unless necessary.
if (ev.detail.index !== this.view.queryResults?.getSelectedIndex()) {
this.view
.evolve({
childIndex: childIndex,
queryResults: this.view.queryResults?.clone().selectResult(ev.detail.index),
})
// Ensure the timeline is able to update its position.
.mergeInContext({ timeline: { noSetWindow: false } })
.dispatchChangeEvent(this);
}
}
@@ -539,11 +437,11 @@ export class FrigateCardViewerCarousel extends LitElement {
* default location will be the Chromecast receiver, not HA).
* @param url The media URL
*/
protected _canonicalizeHAURL(url?: string): string | undefined {
protected _canonicalizeHAURL(url?: string): string | null {
if (this.hass && url && url.startsWith('/')) {
return this.hass.hassUrl(url);
}
return url;
return url ?? null;
}
/**
@@ -551,44 +449,40 @@ export class FrigateCardViewerCarousel extends LitElement {
* @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 {
const childIndex: number | undefined = this._slideToChild[index];
if (
childIndex === undefined ||
!this.hass ||
!this.view ||
!this.view.target ||
!this.view.target.children ||
!isTrueMedia(this.view.target.children[childIndex])
) {
if (!this.hass || !this.view || !this.view.query || !this.cameras) {
return;
}
resolveMedia(
this.hass,
this.view.target.children[childIndex],
this.resolvedMediaCache,
).then((resolvedMedia) => {
if (!resolvedMedia) {
return;
}
const media = this.view.queryResults?.getResult(index);
const mediaContentID = media
? media.getContentID(this.cameras.get(media.getCameraID()))
: null;
if (!mediaContentID) {
return;
}
// Snapshots.
const img = slide.querySelector('img') as HTMLImageElement;
resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache).then(
(resolvedMedia) => {
if (!resolvedMedia) {
return;
}
// Frigate >= 0.9.0+ clips.
const hls_player = this._getPlayer(slide) as FrigateCardMediaPlayer & {
url: string;
};
// Snapshots.
const img = slide.querySelector('img') as HTMLImageElement;
if (img) {
img.src = this._canonicalizeHAURL(resolvedMedia.url) || '';
} else if (hls_player) {
hls_player.url = this._canonicalizeHAURL(resolvedMedia.url) || '';
}
});
// Frigate >= 0.9.0+ clips.
const hls_player = this._getPlayer(slide) as FrigateCardMediaPlayer & {
url: string;
};
if (img) {
img.src = this._canonicalizeHAURL(resolvedMedia.url) ?? '';
} else if (hls_player) {
hls_player.url = this._canonicalizeHAURL(resolvedMedia.url) ?? '';
}
},
);
}
/**
@@ -596,21 +490,18 @@ export class FrigateCardViewerCarousel extends LitElement {
* @returns The slides to include in the render.
*/
protected _getSlides(): TemplateResult[] {
if (
!this.view ||
!this.view.target ||
!this.view.target.children ||
!this.view.target.children.length
) {
if (!this.view || !this.view.queryResults) {
return [];
}
const slides: TemplateResult[] = [];
for (let i = 0; i < this.view.target.children?.length; ++i) {
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
if (slide) {
slides.push(slide);
for (let i = 0; i < this.view.queryResults.getResultsCount(); ++i) {
const media = this.view.queryResults.getResult(i);
if (media) {
const slide = this._renderMediaItem(media, i);
if (slide) {
slides[i] = slide;
}
}
}
return slides;
@@ -620,8 +511,12 @@ export class FrigateCardViewerCarousel extends LitElement {
* Determine if all the media in the carousel are resolved.
*/
protected _isMediaFullyResolved(): boolean {
for (const child of this.view?.target?.children || []) {
if (!this.resolvedMediaCache?.has(child.media_content_id)) {
if (!this.resolvedMediaCache || !this.cameras) {
return false;
}
for (const media of this.view?.queryResults?.getResults() ?? []) {
const mediaContentID = media.getContentID(this.cameras.get(media.getCameraID()));
if (mediaContentID && !this.resolvedMediaCache.has(mediaContentID)) {
return false;
}
}
@@ -633,29 +528,20 @@ export class FrigateCardViewerCarousel extends LitElement {
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
// Pre-populate a map between real media slides and view child indicies.
if (changedProps.has('view')) {
this._slideToChild = {};
let i = 0;
(this.view?.target?.children ?? []).forEach((child, index) => {
if (isTrueMedia(child) && ['video', 'image'].includes(child.media_content_type)) {
this._slideToChild[i++] = index;
}
})
}
if (changedProps.has('viewerConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.viewerConfig?.layout);
}
if (!this._carouselOptions || changedProps.has('viewerConfig')) {
this._carouselOptions = this._getOptions();
this._carouselOptions = {
draggable: this.viewerConfig?.draggable ?? true,
};
}
if (
!this._carouselPlugins ||
changedProps.has('viewerConfig') ||
(changedProps.has('view') &&
this.view?.target?.children?.length !==
changedProps.get('view')?.target?.children?.length)
this.view?.queryResults?.getResultsCount() !==
changedProps.get('view')?.queryResults?.getResultsCount())
) {
this._carouselPlugins = this._getPlugins();
}
@@ -680,21 +566,20 @@ export class FrigateCardViewerCarousel extends LitElement {
* @returns A template to display to the user.
*/
protected _render(): TemplateResult | void {
const slides = this._getSlides();
if (!slides.length || !this.view?.media) {
const media = this.view?.queryResults?.getSelectedResult();
if (!media || !this.cameras) {
return;
}
const neighbors = this._getMediaNeighbors();
const [prev, next] = [neighbors?.previous, neighbors?.next];
const [prev, next] = this._getMediaNeighbors();
return html` <frigate-card-media-carousel
${ref(this._refMediaCarousel)}
.carouselOptions=${this._carouselOptions}
.carouselPlugins=${this._carouselPlugins}
.label="${this.view.media.title}"
.label=${media.getTitle() ?? undefined}
.titlePopupConfig=${this.viewerConfig?.controls.title}
.selected=${this.view?.queryResults?.getSelectedIndex() ?? 0}
transitionEffect=${this._getTransitionEffect()}
@frigate-card:media-carousel:select=${this._setViewHandler.bind(this)}
@frigate-card:media:loaded=${this._recordingSeekHandler.bind(this)}
@@ -704,22 +589,24 @@ export class FrigateCardViewerCarousel extends LitElement {
.hass=${this.hass}
.direction=${'previous'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
.label=${prev ? prev.title : ''}
.thumbnail=${prev?.getThumbnail(this.cameras.get(prev.getCameraID())) ??
undefined}
.label=${prev?.getTitle() ?? ''}
?disabled=${!prev}
@click=${(ev) => {
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollPrevious();
stopEventFromActivatingCardWideActions(ev);
}}
></frigate-card-next-previous-control>
${slides}
${guard(this.view?.queryResults?.getResults(), () => this._getSlides())}
<frigate-card-next-previous-control
slot="next"
.hass=${this.hass}
.direction=${'next'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
.label=${next ? next.title : ''}
.thumbnail=${next?.getThumbnail(this.cameras.get(next.getCameraID())) ??
undefined}
.label=${next?.getTitle() ?? ''}
?disabled=${!next}
@click=${(ev) => {
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
@@ -733,10 +620,12 @@ export class FrigateCardViewerCarousel extends LitElement {
* Fire a media show event when a slide is selected.
*/
protected _recordingSeekHandler(): void {
const player = this._getPlayer();
const childIndex = this.view?.childIndex ?? null;
const selectedIndex = this.view?.queryResults?.getSelectedIndex() ?? null;
const seek =
childIndex !== null ? this.view?.context?.mediaViewer?.seek.get(childIndex) : null;
selectedIndex !== null
? this.view?.context?.mediaViewer?.seek.get(selectedIndex)
: null;
const player = this._getPlayer();
if (player && seek) {
player.seek(seek.seekSeconds);
}
@@ -744,59 +633,53 @@ export class FrigateCardViewerCarousel extends LitElement {
/**
* Render a single media item in the viewer carousel.
* @param mediaToRender The FrigateBrowseMediaSource to render.
* @param slideIndex The index of the slide to render.
* @param media The ViewMedia to render.
* @param index The (slide|queryResult) index of the item to render.
* @returns A rendered template.
*/
protected _renderMediaItem(
mediaToRender: FrigateBrowseMediaSource,
slideIndex: number,
): TemplateResult | void {
protected _renderMediaItem(media: ViewMedia, index: number): TemplateResult | null {
// Skip folders as they cannot be rendered by this viewer.
if (
!this.hass ||
!this.view ||
!this.viewerConfig ||
!isTrueMedia(mediaToRender) ||
!['video', 'image'].includes(mediaToRender.media_content_type)
) {
return;
if (!this.hass || !this.view || !this.viewerConfig || !this.cameras) {
return null;
}
const lazyLoad = this.viewerConfig.lazy_load;
const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id);
const mediaContentID = media.getContentID(this.cameras.get(media.getCameraID()));
const resolvedMedia = mediaContentID
? this.resolvedMediaCache?.get(mediaContentID)
: null;
if (!resolvedMedia && !lazyLoad) {
return;
return null;
}
// The media is attached to the player as '.media' which is used in
// `_selectSlideMediaShowHandler` (and not used by the player itself).
return html`
<div class="embla__slide">
${mediaToRender.media_content_type === 'video'
${media.isVideo()
? html`<frigate-card-ha-hls-player
allow-exoplayer
aria-label="${mediaToRender.title}"
aria-label="${media.getTitle() ?? ''}"
?autoplay=${false}
controls
muted
playsinline
title="${mediaToRender.title}"
title="${media.getTitle() ?? ''}"
url=${ifDefined(
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url),
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url) ?? '',
)}
.hass=${this.hass}
@frigate-card:media:loaded=${(e: CustomEvent<MediaLoadedInfo>) => {
wrapMediaLoadedEventForCarousel(slideIndex, e);
wrapMediaLoadedEventForCarousel(index, e);
}}
>
</frigate-card-ha-hls-player>`
: html`<img
aria-label="${mediaToRender.title}"
aria-label="${media.getTitle() ?? ''}"
src=${ifDefined(
lazyLoad ? IMG_EMPTY : this._canonicalizeHAURL(resolvedMedia?.url),
lazyLoad ? IMG_EMPTY : this._canonicalizeHAURL(resolvedMedia?.url) ?? '',
)}
title="${mediaToRender.title}"
title="${media.getTitle() ?? ''}"
@click=${() => {
if (
this._refMediaCarousel.value
@@ -804,7 +687,7 @@ export class FrigateCardViewerCarousel extends LitElement {
?.carouselClickAllowed() &&
this.viewerConfig?.snapshot_click_plays_clip
) {
this._findRelatedClipView(mediaToRender).then((view) => {
this._createRelatedClipView(index).then((view) => {
if (view) {
view.dispatchChangeEvent(this);
}
@@ -822,9 +705,9 @@ export class FrigateCardViewerCarousel extends LitElement {
// images in media-carousel.ts). Here we need to only call the
// media load handler on a 'real' load.
!lazyLoad ||
lazyloadPlugin?.hasLazyloaded(slideIndex)
lazyloadPlugin?.hasLazyloaded(index)
) {
wrapRawMediaLoadedEventForCarousel(slideIndex, e);
wrapRawMediaLoadedEventForCarousel(index, e);
}
}}"
/>`}