Add an 'image' live provider.
This commit is contained in:
+22
-12
@@ -6,7 +6,7 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
@@ -15,13 +15,17 @@ import { CachedValueController } from '../cached-value-controller.js';
|
|||||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { CameraConfig, ImageViewConfig } from '../types.js';
|
import { CameraConfig, ImageViewConfig, MediaLoadedInfo } from '../types.js';
|
||||||
import { isHassDifferent } from '../utils/ha';
|
import { isHassDifferent } from '../utils/ha';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
import {
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
|
} from '../utils/media-info.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import { dispatchErrorMessageEvent } from './message.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
// 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;
|
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
||||||
@@ -48,6 +52,8 @@ export class FrigateCardImage extends LitElement {
|
|||||||
protected _cachedValueController?: CachedValueController<string>;
|
protected _cachedValueController?: CachedValueController<string>;
|
||||||
protected _boundVisibilityHandler = this._visibilityHandler.bind(this);
|
protected _boundVisibilityHandler = this._visibilityHandler.bind(this);
|
||||||
|
|
||||||
|
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the camera entity for the current camera configuration.
|
* Get the camera entity for the current camera configuration.
|
||||||
* @returns The entity or undefined if no camera entity is available.
|
* @returns The entity or undefined if no camera entity is available.
|
||||||
@@ -234,7 +240,13 @@ export class FrigateCardImage extends LitElement {
|
|||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
src=${live(src)}
|
src=${live(src)}
|
||||||
@load=${(ev: Event) => {
|
@load=${(ev: Event) => {
|
||||||
dispatchMediaLoadedEvent(this, ev);
|
const mediaLoadedInfo = createMediaLoadedInfo(ev);
|
||||||
|
// Avoid the media being reported as repeatedly loading unless the
|
||||||
|
// media info changes.
|
||||||
|
if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) {
|
||||||
|
this._mediaLoadedInfo = mediaLoadedInfo;
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent(this, mediaLoadedInfo);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
@error=${() => {
|
@error=${() => {
|
||||||
if (this.imageConfig?.mode === 'camera') {
|
if (this.imageConfig?.mode === 'camera') {
|
||||||
@@ -246,11 +258,9 @@ export class FrigateCardImage extends LitElement {
|
|||||||
} else if (this.imageConfig?.mode === 'url') {
|
} else if (this.imageConfig?.mode === 'url') {
|
||||||
// In url mode, the user likely specified a URL that cannot be
|
// In url mode, the user likely specified a URL that cannot be
|
||||||
// resolved. Show an error message.
|
// resolved. Show an error message.
|
||||||
dispatchErrorMessageEvent(
|
dispatchErrorMessageEvent(this, localize('error.image_load_error'), {
|
||||||
this,
|
context: this.imageConfig,
|
||||||
localize('error.image_load_error'),
|
});
|
||||||
{ context: this.imageConfig },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>`
|
/>`
|
||||||
@@ -263,7 +273,7 @@ export class FrigateCardImage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"frigate-card-image": FrigateCardImage
|
'frigate-card-image': FrigateCardImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
|
||||||
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
||||||
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
import { getCameraTitle } from '../../utils/camera.js';
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
import { dispatchErrorMessageEvent, dispatchMessageEvent } from '../message.js';
|
|
||||||
import '../../patches/ha-camera-stream';
|
import '../../patches/ha-camera-stream';
|
||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import '../../patches/ha-web-rtc-player.ts';
|
import '../../patches/ha-web-rtc-player.ts';
|
||||||
@@ -66,30 +64,9 @@ export class FrigateCardLiveHA extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.cameraConfig?.camera_entity) {
|
const stateObj = getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
||||||
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
|
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_not_found'), {
|
return;
|
||||||
context: this.cameraConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateObj.state === 'unavailable') {
|
|
||||||
// Don't treat state unavailability as an error per se.
|
|
||||||
return dispatchMessageEvent(
|
|
||||||
this,
|
|
||||||
localize('error.live_camera_unavailable'),
|
|
||||||
'info',
|
|
||||||
{
|
|
||||||
icon: 'mdi:connection',
|
|
||||||
context: getCameraTitle(this.hass, this.cameraConfig),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <frigate-card-ha-camera-stream
|
return html` <frigate-card-ha-camera-stream
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import liveFrigateStyle from '../../scss/live-frigate.scss';
|
||||||
|
import { CameraConfig } from '../../types.js';
|
||||||
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
|
import '../image.js';
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-image')
|
||||||
|
export class FrigateCardLiveImage extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _playing = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the video.
|
||||||
|
*/
|
||||||
|
public play(): void {
|
||||||
|
this._playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the video.
|
||||||
|
*/
|
||||||
|
public pause(): void {
|
||||||
|
this._playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the video.
|
||||||
|
*/
|
||||||
|
public mute(): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute the video.
|
||||||
|
*/
|
||||||
|
public unmute(): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public seek(_seconds: number): void {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateObjOrDispatchError(this, this.hass, this.cameraConfig);
|
||||||
|
|
||||||
|
return html` <frigate-card-image
|
||||||
|
.imageConfig=${{
|
||||||
|
mode: 'camera' as const,
|
||||||
|
refresh_seconds: this._playing ? 1 : 0,
|
||||||
|
}}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-image>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(liveFrigateStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-live-image': FrigateCardLiveImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-19
@@ -53,6 +53,47 @@ import { EmblaCarouselPlugins } from '../carousel.js';
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
import { DataManager } from '../../utils/data-manager.js';
|
import { DataManager } from '../../utils/data-manager.js';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
|
||||||
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state object or dispatch an error. Used in `ha` and `image` live
|
||||||
|
* providers.
|
||||||
|
* @param element HTMLElement to dispatch errors from.
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param cameraConfig Camera configuration.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getStateObjOrDispatchError = (
|
||||||
|
element: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig?: CameraConfig,
|
||||||
|
): HassEntity | null => {
|
||||||
|
if (!cameraConfig?.camera_entity) {
|
||||||
|
dispatchErrorMessageEvent(element, localize('error.no_live_camera'), {
|
||||||
|
context: cameraConfig,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateObj = hass.states[cameraConfig.camera_entity];
|
||||||
|
if (!stateObj) {
|
||||||
|
dispatchErrorMessageEvent(element, localize('error.live_camera_not_found'), {
|
||||||
|
context: cameraConfig,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateObj.state === 'unavailable') {
|
||||||
|
dispatchMessageEvent(element, localize('error.live_camera_unavailable'), 'info', {
|
||||||
|
icon: 'mdi:connection',
|
||||||
|
context: getCameraTitle(hass, cameraConfig),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stateObj;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement('frigate-card-live')
|
@customElement('frigate-card-live')
|
||||||
export class FrigateCardLive extends LitElement {
|
export class FrigateCardLive extends LitElement {
|
||||||
@@ -708,15 +749,17 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
) {
|
) {
|
||||||
return 'webrtc-card';
|
return 'webrtc-card';
|
||||||
} else if (this.cameraConfig?.camera_entity) {
|
} else if (this.cameraConfig?.camera_entity) {
|
||||||
return 'ha';
|
if (this.cardWideConfig?.performance?.profile === 'low') {
|
||||||
|
return 'image';
|
||||||
|
} else {
|
||||||
|
return 'ha';
|
||||||
|
}
|
||||||
} else if (this.cameraConfig?.frigate.camera_name) {
|
} else if (this.cameraConfig?.frigate.camera_name) {
|
||||||
return 'frigate-jsmpeg';
|
return 'frigate-jsmpeg';
|
||||||
}
|
}
|
||||||
return frigateCardConfigDefaults.cameras.live_provider;
|
return frigateCardConfigDefaults.cameras.live_provider;
|
||||||
}
|
}
|
||||||
return (
|
return this.cameraConfig?.live_provider || 'image';
|
||||||
this.cameraConfig?.live_provider || frigateCardConfigDefaults.cameras.live_provider
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -759,16 +802,19 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
if (changedProps.has('liveConfig')) {
|
if (changedProps.has('liveConfig')) {
|
||||||
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
||||||
if (this.liveConfig?.show_image_during_load) {
|
if (this.liveConfig?.show_image_during_load) {
|
||||||
import('../image.js');
|
import('./live-image.js');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedProps.has('cameraConfig')) {
|
if (changedProps.has('cameraConfig')) {
|
||||||
if (this._getResolvedProvider() === 'frigate-jsmpeg') {
|
const provider = this._getResolvedProvider();
|
||||||
|
if (provider === 'frigate-jsmpeg') {
|
||||||
import('./live-jsmpeg.js');
|
import('./live-jsmpeg.js');
|
||||||
} else if (this._getResolvedProvider() === 'ha') {
|
} else if (provider === 'ha') {
|
||||||
import('./live-ha.js');
|
import('./live-ha.js');
|
||||||
} else if (this._getResolvedProvider() === 'webrtc-card') {
|
} else if (provider === 'webrtc-card') {
|
||||||
import('./live-webrtc.js');
|
import('./live-webrtc.js');
|
||||||
|
} else if (provider === 'image') {
|
||||||
|
import('./live-image.js');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -787,22 +833,28 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
this.ariaLabel = this.label;
|
this.ariaLabel = this.label;
|
||||||
|
|
||||||
const provider = this._getResolvedProvider();
|
const provider = this._getResolvedProvider();
|
||||||
const showImage = !this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
|
const showImageDuringLoading =
|
||||||
|
!this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
|
||||||
const providerClasses = {
|
const providerClasses = {
|
||||||
hidden: showImage,
|
hidden: showImageDuringLoading,
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${showImage
|
${showImageDuringLoading || provider === 'image'
|
||||||
? html`<frigate-card-image
|
? html`<frigate-card-live-image
|
||||||
.imageConfig=${{
|
${ref(this._providerRef)}
|
||||||
mode: 'camera' as const,
|
|
||||||
refresh_seconds: 1,
|
|
||||||
}}
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
@frigate-card:media:loaded=${() => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-image>`
|
</frigate-card-live-image>`
|
||||||
: html``}
|
: html``}
|
||||||
${provider === 'ha'
|
${provider === 'ha'
|
||||||
? html` <frigate-card-live-ha
|
? html` <frigate-card-live-ha
|
||||||
@@ -824,7 +876,8 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-webrtc-card>`
|
</frigate-card-live-webrtc-card>`
|
||||||
: html` <frigate-card-live-jsmpeg
|
: provider === 'frigate-jsmpeg'
|
||||||
|
? html` <frigate-card-live-jsmpeg
|
||||||
${ref(this._providerRef)}
|
${ref(this._providerRef)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(providerClasses)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -833,7 +886,8 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-live-jsmpeg>`}
|
</frigate-card-live-jsmpeg>`
|
||||||
|
: html``}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1107,6 +1107,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
||||||
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
||||||
|
{
|
||||||
|
value: 'image',
|
||||||
|
label: localize('config.cameras.live_providers.image'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'frigate-jsmpeg',
|
value: 'frigate-jsmpeg',
|
||||||
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
|
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"live_providers": {
|
"live_providers": {
|
||||||
"auto": "Automatic",
|
"auto": "Automatic",
|
||||||
"frigate-jsmpeg": "Frigate JSMpeg",
|
"frigate-jsmpeg": "Frigate JSMpeg",
|
||||||
"ha": "Home Assistant (i.e. HLS, LL-HLS, WebRTC native)",
|
"ha": "Home Assistant video stream (i.e. HLS, LL-HLS, WebRTC native)",
|
||||||
|
"image": "Home Assistant images",
|
||||||
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
||||||
},
|
},
|
||||||
"title": "Title for this camera (Autodetected from entity)",
|
"title": "Title for this camera (Autodetected from entity)",
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
|
|||||||
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
||||||
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
||||||
|
|
||||||
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
|
const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
|
||||||
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
||||||
|
|
||||||
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
|
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user