feat: Add experimental reolink media support (#1694)
* feat: Add experimental rich reolink support. * Formatting fix
This commit is contained in:
@@ -48,35 +48,31 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const renderIcon =
|
||||
!this.thumbnail || ['chevrons', 'icons'].includes(this._controlConfig.style);
|
||||
|
||||
const classes = {
|
||||
controls: true,
|
||||
previous: this.direction === 'previous',
|
||||
next: this.direction === 'next',
|
||||
thumbnails: this._controlConfig.style === 'thumbnails',
|
||||
icons: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
||||
button: ['chevrons', 'icons'].includes(this._controlConfig.style),
|
||||
thumbnails: !renderIcon,
|
||||
icons: renderIcon,
|
||||
button: renderIcon,
|
||||
};
|
||||
|
||||
if (['chevrons', 'icons'].includes(this._controlConfig.style)) {
|
||||
let icon: string;
|
||||
if (this._controlConfig.style === 'chevrons') {
|
||||
icon = this.direction === 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
||||
} else {
|
||||
if (!this.icon) {
|
||||
return html``;
|
||||
}
|
||||
icon = this.icon;
|
||||
}
|
||||
if (renderIcon) {
|
||||
const icon =
|
||||
!this.thumbnail || !this.icon || this._controlConfig.style === 'chevrons'
|
||||
? this.direction === 'previous'
|
||||
? 'mdi:chevron-left'
|
||||
: 'mdi:chevron-right'
|
||||
: this.icon;
|
||||
|
||||
return html` <ha-icon-button class="${classMap(classes)}" .label=${this.label}>
|
||||
<ha-icon icon=${icon}></ha-icon>
|
||||
</ha-icon-button>`;
|
||||
}
|
||||
|
||||
if (!this.thumbnail) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return renderTask(
|
||||
this,
|
||||
this._embedThumbnailTask,
|
||||
|
||||
+44
-29
@@ -11,11 +11,13 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
|
||||
import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss';
|
||||
import thumbnailFeatureTextStyle from '../scss/thumbnail-feature-text.scss';
|
||||
import thumbnailFeatureThumbnailStyle from '../scss/thumbnail-feature-thumbnail.scss';
|
||||
import thumbnailStyle from '../scss/thumbnail.scss';
|
||||
import type { ExtendedHomeAssistant } from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
@@ -31,13 +33,12 @@ import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumb
|
||||
import { ViewMediaClassifier } from '../view/media-classifier.js';
|
||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
|
||||
// The minimum width of a thumbnail with details enabled.
|
||||
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||
|
||||
@customElement('frigate-card-thumbnail-feature-event')
|
||||
export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||
@customElement('frigate-card-thumbnail-feature-thumbnail')
|
||||
export class FrigateCardThumbnailFeatureThumbnail extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public thumbnail?: string;
|
||||
|
||||
@@ -101,14 +102,15 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._embedThumbnailTask) {
|
||||
return;
|
||||
}
|
||||
const imageOff = html`<ha-icon
|
||||
icon="mdi:image-off"
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
></ha-icon> `;
|
||||
|
||||
if (!this._embedThumbnailTask) {
|
||||
return imageOff;
|
||||
}
|
||||
|
||||
return html`${this.thumbnail
|
||||
? renderTask(
|
||||
this,
|
||||
@@ -121,31 +123,41 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureEventStyle);
|
||||
return unsafeCSS(thumbnailFeatureThumbnailStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-thumbnail-feature-recording')
|
||||
export class FrigateCardThumbnailFeatureRecording extends LitElement {
|
||||
@customElement('frigate-card-thumbnail-feature-text')
|
||||
export class FrigateCardThumbnailFeatureText extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public date?: Date;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraTitle?: string;
|
||||
public cameraMetadata?: CameraManagerCameraMetadata;
|
||||
|
||||
@property({ attribute: false })
|
||||
public showCameraTitle?: boolean;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.date) {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
<div class="title">${format(this.date, 'HH:mm')}</div>
|
||||
<div class="subtitle">${format(this.date, 'MMM do')}</div>
|
||||
${this.cameraTitle ? html`<div class="camera">${this.cameraTitle}</div>` : html``}
|
||||
${this.cameraMetadata?.engineLogo
|
||||
? html`<img class="background" src="${this.cameraMetadata.engineLogo}" />`
|
||||
: ''}
|
||||
<div class="content">
|
||||
<div class="title">${format(this.date, 'HH:mm')}</div>
|
||||
<div class="subtitle">${format(this.date, 'MMM do')}</div>
|
||||
${this.showCameraTitle && this.cameraMetadata?.title
|
||||
? html`<div class="camera">${this.cameraMetadata.title}</div>`
|
||||
: html``}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureRecordingStyle);
|
||||
return unsafeCSS(thumbnailFeatureTextStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,25 +417,28 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
this.media.getID() &&
|
||||
mediaCapabilities?.canDownload;
|
||||
|
||||
const cameraTitle = this.cameraManager.getCameraMetadata(
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(
|
||||
this.media.getCameraID(),
|
||||
)?.title;
|
||||
);
|
||||
|
||||
return html`
|
||||
${ViewMediaClassifier.isEvent(this.media)
|
||||
? html`<frigate-card-thumbnail-feature-event
|
||||
${ViewMediaClassifier.isEvent(this.media) && thumbnail
|
||||
? html`<frigate-card-thumbnail-feature-thumbnail
|
||||
aria-label="${title ?? ''}"
|
||||
title=${title}
|
||||
.hass=${this.hass}
|
||||
.date=${this.media.getStartTime() ?? undefined}
|
||||
.thumbnail=${thumbnail ?? undefined}
|
||||
></frigate-card-thumbnail-feature-event>`
|
||||
: ViewMediaClassifier.isRecording(this.media)
|
||||
? html`<frigate-card-thumbnail-feature-recording
|
||||
></frigate-card-thumbnail-feature-thumbnail>`
|
||||
: ViewMediaClassifier.isEvent(this.media) ||
|
||||
ViewMediaClassifier.isRecording(this.media)
|
||||
? html`<frigate-card-thumbnail-feature-text
|
||||
aria-label="${title ?? ''}"
|
||||
title="${title ?? ''}"
|
||||
.cameraTitle=${this.details ? undefined : cameraTitle}
|
||||
.cameraMetadata=${cameraMetadata}
|
||||
.showCameraTitle=${!this.details}
|
||||
.date=${this.media.getStartTime() ?? undefined}
|
||||
></frigate-card-thumbnail-feature-recording>`
|
||||
></frigate-card-thumbnail-feature-text>`
|
||||
: html``}
|
||||
${shouldShowFavoriteControl
|
||||
? html` <ha-icon
|
||||
@@ -450,13 +465,13 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
${this.details && ViewMediaClassifier.isEvent(this.media)
|
||||
? html`<frigate-card-thumbnail-details-event
|
||||
.media=${this.media ?? undefined}
|
||||
.cameraTitle=${cameraTitle}
|
||||
.cameraTitle=${cameraMetadata?.title}
|
||||
.seek=${this.seek}
|
||||
></frigate-card-thumbnail-details-event>`
|
||||
: this.details && ViewMediaClassifier.isRecording(this.media)
|
||||
? html`<frigate-card-thumbnail-details-recording
|
||||
.media=${this.media ?? undefined}
|
||||
.cameraTitle=${cameraTitle}
|
||||
.cameraTitle=${cameraMetadata?.title}
|
||||
.seek=${this.seek}
|
||||
></frigate-card-thumbnail-details-recording>`
|
||||
: html``}
|
||||
@@ -516,7 +531,7 @@ declare global {
|
||||
'frigate-card-thumbnail': FrigateCardThumbnail;
|
||||
'frigate-card-thumbnail-details-recording': FrigateCardThumbnailDetailsRecording;
|
||||
'frigate-card-thumbnail-details-event': FrigateCardThumbnailDetailsEvent;
|
||||
'frigate-card-thumbnail-feature-recording': FrigateCardThumbnailFeatureRecording;
|
||||
'frigate-card-thumbnail-feature-event': FrigateCardThumbnailFeatureEvent;
|
||||
'frigate-card-thumbnail-feature-text': FrigateCardThumbnailFeatureText;
|
||||
'frigate-card-thumbnail-feature-thumbnail': FrigateCardThumbnailFeatureThumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
@@ -16,11 +16,24 @@ import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-
|
||||
import { CardWideConfig, ViewerConfig } from '../../config/types.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardMediaPlayer,
|
||||
ResolvedMedia,
|
||||
} from '../../types.js';
|
||||
import { mayHaveAudio } from '../../utils/audio.js';
|
||||
import { aspectRatioToString } from '../../utils/basic.js';
|
||||
import { canonicalizeHAURL } from '../../utils/ha/index.js';
|
||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
||||
import {
|
||||
canonicalizeHAURL,
|
||||
homeAssistantSignPath,
|
||||
isHARelativeURL,
|
||||
} from '../../utils/ha/index.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media.js';
|
||||
import {
|
||||
addDynamicProxyURL,
|
||||
getWebProxiedURL,
|
||||
shouldUseWebProxy,
|
||||
} from '../../utils/ha/web-proxy.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
@@ -77,6 +90,9 @@ export class FrigateCardViewerProvider
|
||||
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
||||
protected _refImageProvider: Ref<HTMLImageElement> = createRef();
|
||||
|
||||
@state()
|
||||
protected _url: string | null = null;
|
||||
|
||||
public async play(): Promise<void> {
|
||||
await playMediaMutingIfNecessary(
|
||||
this,
|
||||
@@ -191,21 +207,77 @@ export class FrigateCardViewerProvider
|
||||
});
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
const mediaContentID = this.media ? this.media.getContentID() : null;
|
||||
|
||||
protected async _setURL(): Promise<void> {
|
||||
const mediaContentID = this.media?.getContentID();
|
||||
if (
|
||||
(changedProps.has('load') ||
|
||||
changedProps.has('media') ||
|
||||
changedProps.has('viewerConfig') ||
|
||||
changedProps.has('resolvedMediaCache') ||
|
||||
changedProps.has('hass')) &&
|
||||
this.hass &&
|
||||
mediaContentID &&
|
||||
!this.resolvedMediaCache?.has(mediaContentID) &&
|
||||
(!this.viewerConfig?.lazy_load || this.load)
|
||||
!this.media ||
|
||||
!mediaContentID ||
|
||||
!this.hass ||
|
||||
(this.viewerConfig?.lazy_load && !this.load)
|
||||
) {
|
||||
resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache).then(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedMedia: ResolvedMedia | null =
|
||||
this.resolvedMediaCache?.get(mediaContentID) ?? null;
|
||||
if (!resolvedMedia) {
|
||||
resolvedMedia = await resolveMedia(
|
||||
this.hass,
|
||||
mediaContentID,
|
||||
this.resolvedMediaCache,
|
||||
);
|
||||
}
|
||||
|
||||
if (!resolvedMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsignedURL = resolvedMedia.url;
|
||||
if (isHARelativeURL(unsignedURL)) {
|
||||
// No need to proxy or sign local resolved URLs.
|
||||
this._url = canonicalizeHAURL(this.hass, unsignedURL);
|
||||
return;
|
||||
}
|
||||
|
||||
const camera = this.cameraManager?.getStore().getCamera(this.media.getCameraID());
|
||||
const proxyConfig = camera?.getProxyConfig();
|
||||
|
||||
if (proxyConfig && shouldUseWebProxy(this.hass, proxyConfig, 'media')) {
|
||||
if (proxyConfig.dynamic) {
|
||||
// Don't use URL() parsing, since that will strip the port number if
|
||||
// it's the default, just need to strip any hash part of the URL.
|
||||
const urlWithoutQSorHash = unsignedURL.split(/#/)[0];
|
||||
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
|
||||
sslVerification: proxyConfig.ssl_verification,
|
||||
sslCiphers: proxyConfig.ssl_ciphers,
|
||||
|
||||
// The link may need to be opened multiple times.
|
||||
openLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this._url = await homeAssistantSignPath(
|
||||
this.hass,
|
||||
getWebProxiedURL(unsignedURL),
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
} else {
|
||||
this._url = unsignedURL;
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
changedProps.has('load') ||
|
||||
changedProps.has('media') ||
|
||||
changedProps.has('viewerConfig') ||
|
||||
changedProps.has('resolvedMediaCache') ||
|
||||
changedProps.has('hass')
|
||||
) {
|
||||
this._setURL().then(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
@@ -262,13 +334,7 @@ export class FrigateCardViewerProvider
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaContentID = this.media.getContentID();
|
||||
const resolvedMedia = mediaContentID
|
||||
? this.resolvedMediaCache?.get(mediaContentID)
|
||||
: null;
|
||||
if (!resolvedMedia) {
|
||||
// Media will be resolved with the call in willUpdate() then this will be
|
||||
// re-rendered.
|
||||
if (!this._url) {
|
||||
return renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
});
|
||||
@@ -288,7 +354,7 @@ export class FrigateCardViewerProvider
|
||||
muted
|
||||
playsinline
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
url=${this._url}
|
||||
.hass=${this.hass}
|
||||
?controls=${this.viewerConfig.controls.builtin}
|
||||
>
|
||||
@@ -325,16 +391,13 @@ export class FrigateCardViewerProvider
|
||||
@play=${() => dispatchMediaPlayEvent(this)}
|
||||
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||
>
|
||||
<source
|
||||
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
type="video/mp4"
|
||||
/>
|
||||
<source src=${this._url} type="video/mp4" />
|
||||
</video>
|
||||
`
|
||||
: html`<img
|
||||
${ref(this._refImageProvider)}
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
src="${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}"
|
||||
src="${this._url}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
@click=${() => {
|
||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import viewsStyle from '../scss/views.scss';
|
||||
import { ExtendedHomeAssistant } from '../types.js';
|
||||
import { DeviceRegistryManager } from '../utils/ha/registry/device/index.js';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||
import './surround.js';
|
||||
|
||||
// As a special case: The diagnostics view is not dynamically loaded in case
|
||||
// something goes wrong.
|
||||
|
||||
Reference in New Issue
Block a user