Add initial zoom support.
This commit is contained in:
+12
-2
@@ -26,6 +26,7 @@ import { View } from '../view/view.js';
|
||||
import { dispatchErrorMessageEvent } from './message.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import './zoomer.js';
|
||||
|
||||
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
||||
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
||||
@@ -41,6 +42,9 @@ export class FrigateCardImage extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public supportZoom = false;
|
||||
|
||||
// Using contentsChanged to ensure overridden configs (e.g. when the
|
||||
// 'show_image_during_load' option is true for live views, an overridden
|
||||
// config may be used here).
|
||||
@@ -231,12 +235,18 @@ export class FrigateCardImage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderZoom(contents: TemplateResult): TemplateResult {
|
||||
return this.supportZoom
|
||||
? html` <frigate-card-zoomer>${contents}</frigate-card-zoomer>`
|
||||
: contents;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const src = this._cachedValueController?.value;
|
||||
// Note the use of live() below to ensure the update will restore the image
|
||||
// src if it's been changed via _forceSafeImage().
|
||||
return src
|
||||
? html` <img
|
||||
? this._renderZoom(html` <img
|
||||
${ref(this._refImage)}
|
||||
src=${live(src)}
|
||||
@load=${(ev: Event) => {
|
||||
@@ -263,7 +273,7 @@ export class FrigateCardImage extends LitElement {
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>`
|
||||
/>`)
|
||||
: html``;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,12 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls: boolean): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.controls = controls;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._player = undefined;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
||||
this._playerRef.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls: boolean): Promise<void> {
|
||||
this._playerRef.value?.setControls(controls);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,11 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async setControls(_controls: boolean): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.cameraConfig) {
|
||||
return;
|
||||
|
||||
@@ -70,6 +70,11 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
// JSMPEG does not support seeking.
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async setControls(_controls: boolean): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSMPEG player.
|
||||
* @param url The URL for the player to connect to.
|
||||
|
||||
@@ -73,6 +73,13 @@ export class FrigateCardLiveWebRTCCard
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls: boolean): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
if (player) {
|
||||
player.controls = controls;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
|
||||
+66
-55
@@ -47,6 +47,7 @@ import {
|
||||
import '../next-prev-control.js';
|
||||
import '../title-control.js';
|
||||
import '../surround.js';
|
||||
import '../zoomer.js';
|
||||
import { CarouselSelect, EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
@@ -772,6 +773,12 @@ export class FrigateCardLiveProvider
|
||||
this._refProvider.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls: boolean): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
this._refProvider.value?.setControls(controls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully resolved live provider.
|
||||
* @returns A live provider (that is not 'auto').
|
||||
@@ -886,35 +893,41 @@ export class FrigateCardLiveProvider
|
||||
};
|
||||
|
||||
return html`
|
||||
${showImageDuringLoading || provider === 'image'
|
||||
? html`<frigate-card-live-image
|
||||
${ref(this._refProvider)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
@frigate-card:media:loaded=${(ev: Event) => {
|
||||
if (provider === 'image') {
|
||||
// Only count the media has loaded if the required provider is
|
||||
// the image (not just the temporary image shown during
|
||||
// loading).
|
||||
this._videoMediaShowHandler();
|
||||
} else {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
</frigate-card-live-image>`
|
||||
: html``}
|
||||
${provider === 'ha'
|
||||
? html` <frigate-card-live-ha
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-ha>`
|
||||
: provider === 'go2rtc'
|
||||
? html`<frigate-card-live-go2rtc
|
||||
<frigate-card-zoomer
|
||||
@frigate-card:zoom:zoomed=${() => this.setControls(false)}
|
||||
@frigate-card:zoom:unzoomed=${() => this.setControls(true)}
|
||||
>
|
||||
${showImageDuringLoading || provider === 'image'
|
||||
? html`
|
||||
<frigate-card-live-image
|
||||
${ref(this._refProvider)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
@frigate-card:media:loaded=${(ev: Event) => {
|
||||
if (provider === 'image') {
|
||||
// Only count the media has loaded if the required provider is
|
||||
// the image (not just the temporary image shown during
|
||||
// loading).
|
||||
this._videoMediaShowHandler();
|
||||
} else {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
</frigate-card-live-image
|
||||
></frigate-card-zoomer>`
|
||||
: html``}
|
||||
${provider === 'ha'
|
||||
? html` <frigate-card-live-ha
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-ha>`
|
||||
: provider === 'go2rtc'
|
||||
? html`<frigate-card-live-go2rtc
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
@@ -923,35 +936,33 @@ export class FrigateCardLiveProvider
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-webrtc-card>`
|
||||
: provider === 'webrtc-card'
|
||||
? html`<frigate-card-live-webrtc-card
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-webrtc-card>`
|
||||
: provider === 'jsmpeg'
|
||||
? html` <frigate-card-live-jsmpeg
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-jsmpeg>`
|
||||
: html``}
|
||||
: provider === 'webrtc-card'
|
||||
? html`<frigate-card-live-webrtc-card
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-webrtc-card>`
|
||||
: provider === 'jsmpeg'
|
||||
? html` <frigate-card-live-jsmpeg
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(providerClasses)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-live-jsmpeg>`
|
||||
: html``}
|
||||
</frigate-card-zoomer>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveProviderStyle);
|
||||
}
|
||||
|
||||
+88
-72
@@ -9,8 +9,12 @@ import {
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } 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';
|
||||
import { dispatchMessageEvent, renderProgressIndicator } from '../components/message.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import '../patches/ha-hls-player';
|
||||
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
||||
import viewerProviderStyle from '../scss/viewer-provider.scss';
|
||||
import viewerStyle from '../scss/viewer.scss';
|
||||
@@ -25,38 +29,35 @@ import {
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged, errorToConsole } from '../utils/basic.js';
|
||||
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||
import { View } from '../view/view.js';
|
||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
} from '../utils/media-to-view.js';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
playMediaMutingIfNecessary,
|
||||
} from '../utils/media.js';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results.js';
|
||||
import { VideoContentType, ViewMedia } from '../view/media.js';
|
||||
import { View } from '../view/view.js';
|
||||
import './zoomer.js';
|
||||
import type { CarouselSelect } from './carousel.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import {
|
||||
FrigateCardMediaCarousel,
|
||||
wrapMediaLoadedEventForCarousel,
|
||||
} from './media-carousel.js';
|
||||
import type { CarouselSelect } from './carousel.js';
|
||||
import './next-prev-control.js';
|
||||
import './title-control.js';
|
||||
import '../patches/ha-hls-player';
|
||||
import './surround.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
} from '../utils/media-to-view.js';
|
||||
import { VideoContentType, ViewMedia } from '../view/media.js';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results.js';
|
||||
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
||||
import { playMediaMutingIfNecessary } from '../utils/media.js';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
} from '../utils/media.js';
|
||||
import './title-control.js';
|
||||
|
||||
export interface MediaViewerViewContext {
|
||||
seek?: Date;
|
||||
@@ -602,6 +603,14 @@ export class FrigateCardViewerProvider
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls: boolean): Promise<void> {
|
||||
if (this._refFrigateCardMediaPlayer.value) {
|
||||
return this._refFrigateCardMediaPlayer.value.setControls(controls);
|
||||
} else if (this._refVideoProvider.value) {
|
||||
this._refVideoProvider.value.controls = controls;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a clip view that matches the current (snapshot) query.
|
||||
*/
|
||||
@@ -693,61 +702,68 @@ export class FrigateCardViewerProvider
|
||||
});
|
||||
}
|
||||
|
||||
return ViewMediaClassifier.isVideo(this.media)
|
||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||
? html`<frigate-card-ha-hls-player
|
||||
${ref(this._refFrigateCardMediaPlayer)}
|
||||
allow-exoplayer
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
?autoplay=${false}
|
||||
controls
|
||||
muted
|
||||
playsinline
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`
|
||||
<video
|
||||
${ref(this._refVideoProvider)}
|
||||
return html`
|
||||
<frigate-card-zoomer
|
||||
@frigate-card:zoom:zoomed=${() => this.setControls(false)}
|
||||
@frigate-card:zoom:unzoomed=${() => this.setControls(true)}
|
||||
>
|
||||
${ViewMediaClassifier.isVideo(this.media)
|
||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||
? html`<frigate-card-ha-hls-player
|
||||
${ref(this._refFrigateCardMediaPlayer)}
|
||||
allow-exoplayer
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
?autoplay=${false}
|
||||
controls
|
||||
muted
|
||||
playsinline
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`
|
||||
<video
|
||||
${ref(this._refVideoProvider)}
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
muted
|
||||
controls
|
||||
playsinline
|
||||
?autoplay=${false}
|
||||
@loadedmetadata=${(ev: Event) => {
|
||||
if (ev.target) {
|
||||
hideMediaControlsTemporarily(
|
||||
ev.target as HTMLVideoElement,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@loadeddata=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev);
|
||||
}}
|
||||
>
|
||||
<source
|
||||
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
`
|
||||
: html`<img
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
src="${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
muted
|
||||
controls
|
||||
playsinline
|
||||
?autoplay=${false}
|
||||
@loadedmetadata=${(ev: Event) => {
|
||||
if (ev.target) {
|
||||
hideMediaControlsTemporarily(
|
||||
ev.target as HTMLVideoElement,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
);
|
||||
@click=${() => {
|
||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||
this._dispatchRelatedClipView();
|
||||
}
|
||||
}}
|
||||
@loadeddata=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev);
|
||||
@load=${(e: Event) => {
|
||||
dispatchMediaLoadedEvent(this, e);
|
||||
}}
|
||||
>
|
||||
<source
|
||||
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
`
|
||||
: html`<img
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
src="${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
@click=${() => {
|
||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||
this._dispatchRelatedClipView();
|
||||
}
|
||||
}}
|
||||
@load=${(e: Event) => {
|
||||
dispatchMediaLoadedEvent(this, e);
|
||||
}}
|
||||
/>`;
|
||||
/>`}
|
||||
</frigate-card-zoomer>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
@@ -72,7 +72,7 @@ export class FrigateCardViews extends LitElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected shouldUpdate(_: PropertyValues): boolean {
|
||||
// Future: Updates to `hass` and `conditionState` here will be frequent.
|
||||
@@ -150,6 +150,7 @@ export class FrigateCardViews extends LitElement {
|
||||
.view=${this.view}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.supportZoom=${true}
|
||||
>
|
||||
</frigate-card-image>`
|
||||
: ``}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { Zoom } from '../utils/zoom/zoom.js';
|
||||
|
||||
@customElement('frigate-card-zoomer')
|
||||
export class FrigateCardZoomer extends LitElement {
|
||||
protected _zoom = new Zoom(this);
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._zoom.activate();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._zoom.deactivate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <slot></slot> `;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-zoomer': FrigateCardZoomer;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user