Dynamically extract dimensions from loaded media.
This commit is contained in:
@@ -27,7 +27,6 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.15.4",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-multi-entry": "^4.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"eslint": "^7.32.0",
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@ import { terser } from 'rollup-plugin-terser';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
import json from '@rollup/plugin-json';
|
||||
import styles from 'rollup-plugin-styles';
|
||||
import multi from '@rollup/plugin-multi-entry';
|
||||
|
||||
const dev = process.env.ROLLUP_WATCH;
|
||||
|
||||
@@ -21,7 +20,6 @@ const serveopts = {
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
multi(),
|
||||
styles({
|
||||
modules: false,
|
||||
// Behavior of inject mode, without actually injecting style
|
||||
@@ -44,7 +42,7 @@ const plugins = [
|
||||
|
||||
export default [
|
||||
{
|
||||
input: ['src/card.ts'],
|
||||
input: 'src/card.ts',
|
||||
output: {
|
||||
file: 'dist/frigate-hass-card.js',
|
||||
format: 'es',
|
||||
|
||||
+83
-7
@@ -9,6 +9,7 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import {
|
||||
HomeAssistant,
|
||||
LovelaceCardEditor,
|
||||
@@ -17,14 +18,12 @@ import {
|
||||
stateIcon,
|
||||
} from 'custom-card-helpers';
|
||||
|
||||
import {
|
||||
MenuButton,
|
||||
frigateCardConfigSchema,
|
||||
} from './types';
|
||||
import { MenuButton, frigateCardConfigSchema } from './types';
|
||||
import type {
|
||||
BrowseMediaQueryParameters,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardConfig,
|
||||
MediaLoadInfo,
|
||||
} from './types';
|
||||
|
||||
import { CARD_VERSION } from './const';
|
||||
@@ -39,9 +38,14 @@ import './components/live';
|
||||
import './components/menu';
|
||||
import './components/message';
|
||||
import './components/viewer';
|
||||
import './patches/ha-camera-stream';
|
||||
import './patches/ha-hls-player';
|
||||
|
||||
import cardStyle from './scss/card.scss';
|
||||
|
||||
const MEDIA_HEIGHT_CUTOFF = 50;
|
||||
const MEDIA_WIDTH_CUTOFF = MEDIA_HEIGHT_CUTOFF;
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
console.info(
|
||||
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
||||
@@ -109,6 +113,9 @@ export class FrigateCard extends LitElement {
|
||||
// a hass update arrives.
|
||||
protected _entitiesToMonitor: string[] | null = null;
|
||||
|
||||
// Information about the most recently loaded media item.
|
||||
protected _mediaInfo: MediaLoadInfo | null = null;
|
||||
|
||||
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
|
||||
this._hass = hass;
|
||||
this._updateMenu();
|
||||
@@ -357,6 +364,55 @@ export class FrigateCard extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
protected _mediaLoadHandler(e: CustomEvent<MediaLoadInfo>): void {
|
||||
const mediaInfo = e.detail;
|
||||
|
||||
// In Safari, with WebRTC, 0x0 is occasionally returned during loading,
|
||||
// so treat anything less than a safety cutoff as bogus.
|
||||
if (mediaInfo.height < MEDIA_HEIGHT_CUTOFF || mediaInfo.width < MEDIA_WIDTH_CUTOFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
let requestRefresh = false;
|
||||
if (
|
||||
this.config.dimensions?.aspect_ratio_mode != 'static' &&
|
||||
(mediaInfo.width != this._mediaInfo?.width ||
|
||||
mediaInfo.height != this._mediaInfo?.height)
|
||||
) {
|
||||
requestRefresh = true;
|
||||
}
|
||||
|
||||
this._mediaInfo = mediaInfo;
|
||||
if (requestRefresh) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
protected _getAspectRatioPadding(): number | null {
|
||||
const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'auto';
|
||||
|
||||
// Do not constrain aspect ratio if it's not a gallery (clips or snapshots),
|
||||
// if the aspect ratio is not static and if there is a loaded media item.
|
||||
if (
|
||||
!this._view.isGalleryView() &&
|
||||
aspect_ratio_mode != 'static' &&
|
||||
this._mediaInfo
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (aspect_ratio_mode == 'auto' && this._mediaInfo) {
|
||||
return (this._mediaInfo.height / this._mediaInfo.width) * 100;
|
||||
}
|
||||
|
||||
const default_aspect_ratio = this.config.dimensions?.aspect_ratio;
|
||||
if (default_aspect_ratio) {
|
||||
return (default_aspect_ratio[1] / default_aspect_ratio[0]) * 100;
|
||||
} else {
|
||||
return (9 / 16) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the call (master render method).
|
||||
protected render(): TemplateResult | void {
|
||||
if (this.config.show_warning) {
|
||||
@@ -365,10 +421,24 @@ export class FrigateCard extends LitElement {
|
||||
if (this.config.show_error) {
|
||||
return this._showError(localize('common.show_error'));
|
||||
}
|
||||
|
||||
const padding = this._getAspectRatioPadding();
|
||||
let container_style_map = {};
|
||||
if (padding != null) {
|
||||
container_style_map = {
|
||||
'padding-top': `${padding}%`,
|
||||
};
|
||||
}
|
||||
|
||||
const content_classes = {
|
||||
'frigate-card-contents': true,
|
||||
absolute: (padding != null),
|
||||
};
|
||||
|
||||
return html` <ha-card @click=${this._interactionHandler}>
|
||||
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
|
||||
<div class="container_16_9 outer">
|
||||
<div class="frigate-card-contents">
|
||||
<div class="container outer" style="${styleMap(container_style_map)}">
|
||||
<div class="${classMap(content_classes)}">
|
||||
${this._view.is('clips') || this._view.is('snapshots')
|
||||
? html` <frigate-card-gallery
|
||||
.hass=${this._hass}
|
||||
@@ -383,9 +453,11 @@ export class FrigateCard extends LitElement {
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
|
||||
.nextPreviousControlStyle=${this.config.controls?.nextprev ??
|
||||
'thumbnails'}
|
||||
.autoplayClip=${this.config.autoplay_clip}
|
||||
@frigate-card:change-view=${this._changeViewHandler}
|
||||
@frigate-card:media-load=${this._mediaLoadHandler}
|
||||
>
|
||||
</frigate-card-viewer>`
|
||||
: ``}
|
||||
@@ -393,6 +465,7 @@ export class FrigateCard extends LitElement {
|
||||
? html` <frigate-card-live
|
||||
.hass=${this._hass}
|
||||
.config=${this.config}
|
||||
@frigate-card:media-load=${this._mediaLoadHandler}
|
||||
>
|
||||
</frigate-card-live>`
|
||||
: ``}
|
||||
@@ -426,6 +499,9 @@ export class FrigateCard extends LitElement {
|
||||
|
||||
// Get the Lovelace card size.
|
||||
public getCardSize(): number {
|
||||
if (this._mediaInfo) {
|
||||
return this._mediaInfo.height / 50;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-10
@@ -6,6 +6,7 @@ import type {
|
||||
BrowseMediaQueryParameters,
|
||||
BrowseMediaSource,
|
||||
ExtendedHomeAssistant,
|
||||
MediaLoadInfo,
|
||||
} from './types';
|
||||
import { browseMediaSourceSchema } from './types';
|
||||
|
||||
@@ -96,20 +97,46 @@ export async function browseMediaQuery(
|
||||
);
|
||||
}
|
||||
|
||||
export function dispatchPlayEvent(node: HTMLElement): void {
|
||||
node.dispatchEvent(
|
||||
new CustomEvent<void>('frigate-card:play', {
|
||||
export function dispatchEvent<T>(element: HTMLElement, name: string, detail?: T): void {
|
||||
element.dispatchEvent(
|
||||
new CustomEvent<T>(`frigate-card:${name}`, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: detail,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function dispatchPauseEvent(node: HTMLElement): void {
|
||||
node.dispatchEvent(
|
||||
new CustomEvent<void>('frigate-card:pause', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
export function dispatchPlayEvent(element: HTMLElement): void {
|
||||
dispatchEvent(element, 'play')
|
||||
}
|
||||
|
||||
export function dispatchPauseEvent(element: HTMLElement): void {
|
||||
dispatchEvent(element, 'pause')
|
||||
}
|
||||
|
||||
export function dispatchMediaLoadEvent(element: HTMLElement, source: Event | HTMLElement): void {
|
||||
let target: HTMLElement | EventTarget;
|
||||
if (source instanceof Event) {
|
||||
target = source.composedPath()[0];
|
||||
} else {
|
||||
target = source;
|
||||
}
|
||||
|
||||
if (target instanceof HTMLImageElement) {
|
||||
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
|
||||
width: (target as HTMLImageElement).naturalWidth,
|
||||
height: (target as HTMLImageElement).naturalHeight,
|
||||
});
|
||||
} else if (target instanceof HTMLVideoElement) {
|
||||
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
|
||||
width: (target as HTMLVideoElement).videoWidth,
|
||||
height: (target as HTMLVideoElement).videoHeight,
|
||||
});
|
||||
} else if (target instanceof HTMLCanvasElement) {
|
||||
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
|
||||
width: (target as HTMLCanvasElement).width,
|
||||
height: (target as HTMLCanvasElement).height,
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
-9
@@ -7,7 +7,7 @@ import { signedPathSchema } from '../types';
|
||||
import type { ExtendedHomeAssistant, FrigateCardConfig } from '../types';
|
||||
|
||||
import { localize } from '../localize/localize';
|
||||
import { homeAssistantWSRequest } from '../common';
|
||||
import { dispatchMediaLoadEvent, homeAssistantWSRequest } from '../common';
|
||||
import {
|
||||
renderMessage,
|
||||
renderErrorMessage,
|
||||
@@ -68,13 +68,13 @@ export class FrigateCardViewerFrigate extends LitElement {
|
||||
if (!(this.cameraEntity in this.hass.states)) {
|
||||
return renderMessage(localize('error.no_live_camera'), 'mdi:camera-off');
|
||||
}
|
||||
return html` <ha-camera-stream
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this.cameraEntity]}
|
||||
.controls=${true}
|
||||
.muted=${true}
|
||||
>
|
||||
</ha-camera-stream>`;
|
||||
</frigate-card-ha-camera-stream>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -119,6 +119,19 @@ export class FrigateCardViewerWebRTC extends LitElement {
|
||||
return html`${this._webRTCElement}`;
|
||||
}
|
||||
|
||||
public updated(): void {
|
||||
// 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;
|
||||
if (video) {
|
||||
video.onloadedmetadata = () => {
|
||||
dispatchMediaLoadEvent(this, video);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveStyle);
|
||||
}
|
||||
@@ -135,7 +148,7 @@ export class FrigateCardViewerJSMPEG extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected clientId!: string;
|
||||
|
||||
protected _jsmpegCanvasElement: HTMLElement | null = null;
|
||||
protected _jsmpegCanvasElement: HTMLCanvasElement | null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected _jsmpegVideoPlayer: any | null = null;
|
||||
@@ -187,10 +200,7 @@ export class FrigateCardViewerJSMPEG extends LitElement {
|
||||
return renderErrorMessage('Could not retrieve or sign JSMPEG websocket path');
|
||||
}
|
||||
|
||||
// Return the html canvas node only after the JSMPEG video has loaded and
|
||||
// is playing, to reduce the amount of time the user is staring at a blank
|
||||
// white canvas (instead they get the progress spinner until this promise
|
||||
// resolves).
|
||||
let videoDecoded = false;
|
||||
return new Promise<TemplateResult>((resolve) => {
|
||||
this._jsmpegVideoPlayer = new JSMpeg.VideoElement(
|
||||
this,
|
||||
@@ -198,12 +208,26 @@ export class FrigateCardViewerJSMPEG extends LitElement {
|
||||
{
|
||||
canvas: this._jsmpegCanvasElement,
|
||||
hooks: {
|
||||
// Don't resolve the promise until it's playing to minimize the
|
||||
// amount of time the canvas is empty (and show the spinner
|
||||
// instead).
|
||||
play: () => {
|
||||
resolve(html`${this._jsmpegCanvasElement}`);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ protocols: [], videoBufferSize: 1024 * 1024 * 4 },
|
||||
{ protocols: [],
|
||||
videoBufferSize: 1024 * 1024 * 4,
|
||||
onVideoDecode: () => {
|
||||
// This is the only callback that is called after the dimensions
|
||||
// are available. It's called on every frame decode, so just
|
||||
// ignore any subsequent calls.
|
||||
if (!videoDecoded && this._jsmpegCanvasElement) {
|
||||
videoDecoded = true;
|
||||
dispatchMediaLoadEvent(this, this._jsmpegCanvasElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
import { localize } from '../localize/localize';
|
||||
import {
|
||||
browseMediaQuery,
|
||||
dispatchMediaLoadEvent,
|
||||
dispatchPauseEvent,
|
||||
dispatchPlayEvent,
|
||||
getFirstTrueMediaChildIndex,
|
||||
@@ -199,7 +200,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
|
||||
const neighbors = this._getMediaNeighbors(parent, childIndex);
|
||||
|
||||
return html`
|
||||
return html` <div>
|
||||
${neighbors?.previousIndex != null
|
||||
? html`<frigate-card-next-previous-control
|
||||
.control=${'previous'}
|
||||
@@ -211,7 +212,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
: ``}
|
||||
${this.view.is('clip')
|
||||
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
|
||||
? html`<ha-hls-player
|
||||
? html`<frigate-card-ha-hls-player
|
||||
.hass=${this.hass}
|
||||
.url=${resolvedMedia.url}
|
||||
title="${mediaToRender.title}"
|
||||
@@ -221,13 +222,14 @@ export class FrigateCardViewer extends LitElement {
|
||||
allow-exoplayer
|
||||
?autoplay="${autoplay}"
|
||||
>
|
||||
</ha-hls-player>`
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`<video
|
||||
title="${mediaToRender.title}"
|
||||
muted
|
||||
controls
|
||||
playsinline
|
||||
?autoplay="${autoplay}"
|
||||
@loadedmetadata=${(e) => dispatchMediaLoadEvent(this, e)}a
|
||||
@play=${() => dispatchPlayEvent(this)}
|
||||
@pause=${() => dispatchPauseEvent(this)}
|
||||
>
|
||||
@@ -247,6 +249,9 @@ export class FrigateCardViewer extends LitElement {
|
||||
}
|
||||
});
|
||||
}}
|
||||
@load=${(e) => {
|
||||
dispatchMediaLoadEvent(this, e);
|
||||
}}
|
||||
/>`}
|
||||
${neighbors?.nextIndex != null
|
||||
? html`<frigate-card-next-previous-control
|
||||
@@ -257,7 +262,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
.view=${this.view}
|
||||
></frigate-card-next-previous-control>`
|
||||
: ``}
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
// ====================================================================
|
||||
// ** Keep modifications to this file to a minimum **
|
||||
//
|
||||
// Type checking is disabled since this is a modified copy-and-paste of
|
||||
// underlying render() function, but the rest of the class source it not
|
||||
// available as compilation time.
|
||||
// ====================================================================
|
||||
|
||||
import { TemplateResult, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { dispatchMediaLoadEvent } from '../common';
|
||||
|
||||
customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
// ========================================================================================
|
||||
// From:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/data/camera.ts
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_state_name.ts
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_object_id.ts
|
||||
// ========================================================================================
|
||||
const computeMJPEGStreamUrl = (entity: CameraEntity): string =>
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||
|
||||
const computeObjectId = (entityId: string): string =>
|
||||
entityId.substr(entityId.indexOf('.') + 1);
|
||||
|
||||
const computeStateName = (stateObj: HassEntity): string =>
|
||||
stateObj.attributes.friendly_name === undefined
|
||||
? computeObjectId(stateObj.entity_id).replace(/_/g, ' ')
|
||||
: stateObj.attributes.friendly_name || '';
|
||||
|
||||
@customElement('frigate-card-ha-camera-stream')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class FrigateCardHaCameraStream extends customElements.get('ha-camera-stream') {
|
||||
// ========================================================================================
|
||||
// Minor modifications from:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-camera-stream.ts
|
||||
// ========================================================================================
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._shouldRenderMJPEG
|
||||
? html`
|
||||
<img
|
||||
@load=${(e) => {
|
||||
this._elementResized();
|
||||
dispatchMediaLoadEvent(this, e);
|
||||
}}
|
||||
.src=${computeMJPEGStreamUrl(this.stateObj)}
|
||||
.alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
|
||||
/>
|
||||
`
|
||||
: this._url
|
||||
? html`
|
||||
<frigate-card-ha-hls-player
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.url=${this._url}
|
||||
></frigate-card-ha-hls-player>
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
// ====================================================================
|
||||
// ** Keep modifications to this file to a minimum **
|
||||
//
|
||||
// Type checking is disabled since this is a modified copy-and-paste of
|
||||
// underlying render() function, but the rest of the class source is not
|
||||
// available as compilation time.
|
||||
// ====================================================================
|
||||
|
||||
import {
|
||||
TemplateResult,
|
||||
html,
|
||||
} from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { dispatchMediaLoadEvent } from '../common';
|
||||
|
||||
customElements.whenDefined("ha-hls-player").then(() => {
|
||||
@customElement("frigate-card-ha-hls-player")
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
class FrigateCardHaHlsPlayer extends customElements.get("ha-hls-player") {
|
||||
// =====================================================================================
|
||||
// Minor modifications from:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
||||
// =====================================================================================
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
.muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
@loadedmetadata=${(e) => dispatchMediaLoadEvent(this, e)}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})
|
||||
+10
-9
@@ -1,16 +1,8 @@
|
||||
.container_16_9 {
|
||||
/* 16:9 Aspect Ratio. When Safari supports 'aspect-ratio' this should not be
|
||||
necessary */
|
||||
.container {
|
||||
position: relative;
|
||||
padding-top: 56.25%; // 9 / 16 == 0.5625
|
||||
}
|
||||
|
||||
.frigate-card-contents {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
@@ -24,6 +16,15 @@
|
||||
}
|
||||
|
||||
/* The 'hover' menu mode is styling applied outside of the menu itself */
|
||||
.frigate-card-contents.absolute {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
/* Support the 'hover' menu mode. */
|
||||
.hover-menu {
|
||||
z-index: 1;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Don't drop shadow or have radius for nested webrtc card */
|
||||
|
||||
@@ -4,4 +4,9 @@ ha-hls-player {
|
||||
img,video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
div {
|
||||
// Keep the controls positioned relative to the video.
|
||||
position: relative;
|
||||
}
|
||||
+15
-1
@@ -97,6 +97,15 @@ export const frigateCardConfigSchema = z.object({
|
||||
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
|
||||
})
|
||||
.optional(),
|
||||
dimensions: z.object({
|
||||
aspect_ratio_mode: z.enum(['dynamic', 'static']).default('dynamic'),
|
||||
aspect_ratio:
|
||||
z.number().array().length(2).or(
|
||||
z.string()
|
||||
.regex(/^\s*\d+\s*\/\s*\d+\s*$/)
|
||||
.transform((input) => input.split("/").map((d) => Number(d)))
|
||||
).default([16, 9]),
|
||||
}).optional(),
|
||||
|
||||
// Stock lovelace card config.
|
||||
type: z.string(),
|
||||
@@ -126,8 +135,13 @@ export interface BrowseMediaQueryParameters {
|
||||
after?: number;
|
||||
}
|
||||
|
||||
export interface MediaLoadInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Browser API types.
|
||||
* Home Assistant API types.
|
||||
*/
|
||||
|
||||
// Recursive type, cannot use type interference:
|
||||
|
||||
@@ -24,6 +24,10 @@ export class View {
|
||||
return this.view == name;
|
||||
}
|
||||
|
||||
public isGalleryView(): boolean {
|
||||
return this.view == 'clips' || this.view == 'snapshots';
|
||||
}
|
||||
|
||||
get media(): BrowseMediaSource | undefined {
|
||||
if (this.target) {
|
||||
if (this.target.children && this.childIndex !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user