feat: Enable go2rtc live stream proxying (#2159)

- Closes #1984
This commit is contained in:
Dermot Duffy
2025-08-28 21:19:19 -07:00
committed by GitHub
parent c3d95a5b32
commit bc5022ef1a
29 changed files with 479 additions and 282 deletions
+8 -1
View File
@@ -77,8 +77,15 @@ export class Camera {
public getProxyConfig(): CameraProxyConfig {
return {
dynamic: this._config.proxy.dynamic,
live:
this._config.proxy.live === 'auto'
? // Live is proxied if the live provider is go2rtc and if a go2rtc
// URL is manually set.
this._config.live_provider === 'go2rtc' && !!this._config.go2rtc?.url
: this._config.proxy.live,
media: this._config.proxy.media === 'auto' ? false : this._config.proxy.media,
dynamic: this._config.proxy.dynamic,
ssl_verification: this._config.proxy.ssl_verification !== false,
ssl_ciphers:
this._config.proxy.ssl_ciphers === 'auto'
+1
View File
@@ -123,6 +123,7 @@ export interface CameraEndpoints {
export interface CameraProxyConfig {
dynamic: boolean;
live: boolean;
media: boolean;
ssl_verification: boolean;
ssl_ciphers: SSLCiphers;
+6 -19
View File
@@ -17,7 +17,6 @@ import { MediaActionsController } from '../../components-lib/media-actions-contr
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
import { CameraConfig } from '../../config/schema/cameras.js';
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
import { LiveConfig } from '../../config/schema/live.js';
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
@@ -170,18 +169,8 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
const slides: TemplateResult[] = [];
const cameraToSlide: Record<string, number> = {};
for (const [cameraID, cameraConfig] of this.cameraManager
.getStore()
.getCameraConfigEntries(cameraIDs)) {
const liveCameraID = this._getSubstreamCameraID(cameraID, view);
const liveCameraConfig =
cameraID === liveCameraID
? cameraConfig
: this.cameraManager?.getStore().getCameraConfig(liveCameraID);
const slide = liveCameraConfig
? this._renderLive(liveCameraID, liveCameraConfig)
: null;
for (const cameraID of cameraIDs ?? []) {
const slide = this._renderLive(this._getSubstreamCameraID(cameraID, view));
if (slide) {
cameraToSlide[cameraID] = slides.length;
slides.push(slide);
@@ -207,11 +196,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
}
}
protected _renderLive(
cameraID: string,
cameraConfig: CameraConfig,
): TemplateResult | void {
if (!this.liveConfig || !this.hass || !this.cameraManager) {
protected _renderLive(cameraID: string): TemplateResult | void {
const camera = this.cameraManager?.getStore().getCamera(cameraID);
if (!this.liveConfig || !this.hass || !this.cameraManager || !camera) {
return;
}
@@ -224,7 +211,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
.microphoneState=${view?.camera === cameraID
? this.microphoneState
: undefined}
.cameraConfig=${cameraConfig}
.camera=${camera}
.cameraEndpoints=${guard(
[this.cameraManager, cameraID],
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
+30 -29
View File
@@ -10,12 +10,13 @@ import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { Camera } from '../../camera-manager/camera.js';
import { CameraEndpoints } from '../../camera-manager/types.js';
import { MicrophoneState } from '../../card-controller/types.js';
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
import { LiveProvider } from '../../config/schema/cameras.js';
import { LiveConfig } from '../../config/schema/live.js';
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
@@ -39,7 +40,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
public hass?: HomeAssistant;
@property({ attribute: false })
public cameraConfig?: CameraConfig;
public camera?: Camera;
@property({ attribute: false })
public cameraEndpoints?: CameraEndpoints;
@@ -105,20 +106,18 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
* @returns A live provider (that is not 'auto').
*/
protected _getResolvedProvider(): Omit<LiveProvider, 'auto'> {
if (this.cameraConfig?.live_provider === 'auto') {
if (
this.cameraConfig?.webrtc_card?.entity ||
this.cameraConfig?.webrtc_card?.url
) {
const config = this.camera?.getConfig();
if (config?.live_provider === 'auto') {
if (config?.webrtc_card?.entity || config?.webrtc_card?.url) {
return 'webrtc-card';
} else if (this.cameraConfig?.camera_entity) {
} else if (config?.camera_entity) {
return 'ha';
} else if (this.cameraConfig?.frigate.camera_name) {
} else if (config?.frigate.camera_name) {
return 'jsmpeg';
}
return configDefaults.cameras.live_provider;
}
return this.cameraConfig?.live_provider || 'image';
return config?.live_provider || 'image';
}
/**
@@ -129,7 +128,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
protected _shouldShowImageDuringLoading(): boolean {
return (
!this._isVideoMediaLoaded &&
!!this.cameraConfig?.camera_entity &&
!!this.camera?.getConfig()?.camera_entity &&
!!this.hass &&
!!this.liveConfig?.show_image_during_load &&
!this._showStreamTroubleshooting &&
@@ -172,7 +171,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
}
}
if (changedProps.has('cameraConfig')) {
if (changedProps.has('camera')) {
const provider = this._getResolvedProvider();
if (provider === 'jsmpeg') {
this._importPromises.push(import('./providers/jsmpeg.js'));
@@ -198,8 +197,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
}
protected _renderContainer(template: TemplateResult): TemplateResult {
const config = this.camera?.getConfig();
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
.dimensionsConfig=${this.cameraConfig?.dimensions}
.dimensionsConfig=${config?.dimensions}
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
if (ev.detail.placeholder) {
ev.stopPropagation();
@@ -213,11 +213,11 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
return html` ${this.liveConfig?.zoomable
? html` <advanced-camera-card-zoomer
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
this.cameraConfig?.dimensions?.layout
.defaultSettings=${guard([config?.dimensions?.layout], () =>
config?.dimensions?.layout
? {
pan: this.cameraConfig.dimensions.layout.pan,
zoom: this.cameraConfig.dimensions.layout.zoom,
pan: config.dimensions.layout.pan,
zoom: config.dimensions.layout.zoom,
}
: undefined,
)}
@@ -233,11 +233,13 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
}
protected render(): TemplateResult | void {
const cameraConfig = this.camera?.getConfig();
if (
!this._lazyLoadController?.isLoaded() ||
!this.hass ||
!this.liveConfig ||
!this.cameraConfig
!this.camera ||
!cameraConfig
) {
return;
}
@@ -251,27 +253,26 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
if (
provider === 'ha' ||
provider === 'image' ||
(this.cameraConfig?.camera_entity &&
this.cameraConfig.always_error_if_entity_unavailable)
(cameraConfig?.camera_entity && cameraConfig.always_error_if_entity_unavailable)
) {
if (!this.cameraConfig?.camera_entity) {
if (!cameraConfig?.camera_entity) {
dispatchLiveErrorEvent(this);
return renderMessage({
message: localize('error.no_live_camera'),
type: 'error',
icon: 'mdi:camera',
context: this.cameraConfig,
context: cameraConfig,
});
}
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
const stateObj = this.hass.states[cameraConfig.camera_entity];
if (!stateObj) {
dispatchLiveErrorEvent(this);
return renderMessage({
message: localize('error.live_camera_not_found'),
type: 'error',
icon: 'mdi:camera',
context: this.cameraConfig,
context: cameraConfig,
});
}
@@ -301,7 +302,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
? html` <advanced-camera-card-live-image
${ref(this._refProvider)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.cameraConfig=${cameraConfig}
class=${classMap({
...classes,
// The image provider is providing the temporary loading image,
@@ -320,7 +321,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
${ref(this._refProvider)}
class=${classMap(classes)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.cameraConfig=${cameraConfig}
?controls=${this.liveConfig.controls.builtin}
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
>
@@ -330,7 +331,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
${ref(this._refProvider)}
class=${classMap(classes)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.camera=${this.camera}
.cameraEndpoints=${this.cameraEndpoints}
.microphoneState=${this.microphoneState}
.microphoneConfig=${this.liveConfig.microphone}
@@ -343,7 +344,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
${ref(this._refProvider)}
class=${classMap(classes)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.cameraConfig=${cameraConfig}
.cameraEndpoints=${this.cameraEndpoints}
.cardWideConfig=${this.cardWideConfig}
?controls=${this.liveConfig.controls.builtin}
@@ -355,7 +356,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
${ref(this._refProvider)}
class=${classMap(classes)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.cameraConfig=${cameraConfig}
.cameraEndpoints=${this.cameraEndpoints}
.cardWideConfig=${this.cardWideConfig}
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
+89 -27
View File
@@ -7,17 +7,23 @@ import {
unsafeCSS,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Camera } from '../../../../camera-manager/camera.js';
import { CameraEndpoints } from '../../../../camera-manager/types.js';
import { MicrophoneState } from '../../../../card-controller/types.js';
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js';
import { CameraConfig } from '../../../../config/schema/cameras.js';
import { MicrophoneConfig } from '../../../../config/schema/live.js';
import { homeAssistantSignPath } from '../../../../ha/sign-path.js';
import { HomeAssistant } from '../../../../ha/types.js';
import {
addDynamicProxyURL,
getWebProxiedURL,
shouldUseWebProxy,
} from '../../../../ha/web-proxy.js';
import { localize } from '../../../../localize/localize.js';
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
import { MediaPlayer, MediaPlayerController, Message } from '../../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
import { errorToConsole } from '../../../../utils/basic.js';
import { renderMessage } from '../../../message.js';
import { VideoRTC } from './video-rtc.js';
@@ -36,7 +42,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
public hass?: HomeAssistant;
@property({ attribute: false })
public cameraConfig?: CameraConfig;
public camera?: Camera;
@property({ attribute: false })
public cameraEndpoints?: CameraEndpoints;
@@ -79,46 +85,102 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
this.requestUpdate();
}
protected async _createPlayer(): Promise<void> {
if (!this.hass) {
return;
protected _handleError(message: Message, e?: Error): void {
if (e) {
errorToConsole(e as Error);
}
this._message = {
type: 'error',
...message,
};
dispatchLiveErrorEvent(this);
return;
}
protected async _getPlayerSource(): Promise<string | null> {
const cameraConfig = this.camera?.getConfig();
if (!this.hass || !cameraConfig) {
return null;
}
const endpoint = this.cameraEndpoints?.go2rtc;
if (!endpoint) {
this._message = {
type: 'error',
this._handleError({
message: localize('error.live_camera_no_endpoint'),
context: this.cameraConfig,
};
dispatchLiveErrorEvent(this);
return;
context: cameraConfig,
});
return null;
}
const address = await convertEndpointAddressToSignedWebsocket(
this.hass,
endpoint,
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
);
if (!address) {
this._message = {
type: 'error',
message: localize('error.failed_sign'),
context: this.cameraConfig,
};
dispatchLiveErrorEvent(this);
const proxyConfig = this.camera?.getProxyConfig();
let src: string | null = endpoint.endpoint;
let sign: boolean = endpoint.sign ?? false;
if (proxyConfig && shouldUseWebProxy(this.hass, proxyConfig, 'live')) {
if (proxyConfig.dynamic) {
try {
await addDynamicProxyURL(this.hass, endpoint.endpoint, {
proxyConfig,
ttl: GO2RTC_URL_SIGN_EXPIRY_SECONDS,
// The link may need to be opened multiple times.
openLimit: 0,
});
} catch (e) {
this._handleError(
{
message: localize('error.failed_proxy'),
context: cameraConfig,
},
e as Error,
);
return null;
}
}
src = getWebProxiedURL(endpoint.endpoint, { websocket: true });
sign = true;
}
if (src && sign) {
try {
src = await homeAssistantSignPath(
this.hass,
src,
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
);
} catch (e) {
this._handleError(
{
message: localize('error.failed_sign'),
context: cameraConfig,
},
e as Error,
);
return null;
}
}
return src;
}
protected async _createPlayer(): Promise<void> {
const src = await this._getPlayerSource();
if (!src) {
return;
}
this._player = new VideoRTC();
this._player.mediaPlayerController = this._mediaPlayerController;
this._player.microphoneStream = this.microphoneState?.stream ?? null;
this._player.src = address;
this._player.src = src;
this._player.visibilityCheck = false;
this._player.setControls(this.controls);
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
const cameraConfig = this.camera?.getConfig();
if (cameraConfig?.go2rtc?.modes && cameraConfig.go2rtc.modes.length) {
this._player.mode = cameraConfig.go2rtc.modes.join(',');
}
this.requestUpdate();
+14 -6
View File
@@ -14,11 +14,12 @@ import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispa
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
import { CameraConfig } from '../../../config/schema/cameras.js';
import { CardWideConfig } from '../../../config/schema/types.js';
import { homeAssistantSignPath } from '../../../ha/sign-path.js';
import { HomeAssistant } from '../../../ha/types.js';
import { localize } from '../../../localize/localize.js';
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
import { MediaPlayer, MediaPlayerController, Message } from '../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
import { convertHTTPAdressToWebsocket, errorToConsole } from '../../../utils/basic.js';
import {
dispatchMediaLoadedEvent,
dispatchMediaPauseEvent,
@@ -183,11 +184,18 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
return;
}
const address = await convertEndpointAddressToSignedWebsocket(
this.hass,
endpoint,
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
);
let response: string | null | undefined;
try {
response = await homeAssistantSignPath(
this.hass,
endpoint.endpoint,
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
);
} catch (e) {
errorToConsole(e as Error);
}
const address = response ? convertHTTPAdressToWebsocket(response) : null;
if (!address) {
this._message = {
type: 'error',
+10 -6
View File
@@ -158,13 +158,17 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
// 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 {
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
proxyConfig,
// The link may need to be opened multiple times.
openLimit: 0,
});
} catch (e) {
errorToConsole(e as Error);
}
}
try {
+4 -3
View File
@@ -121,10 +121,11 @@ export const cameraConfigDefault = {
entities: [],
},
proxy: {
media: 'auto' as const,
dynamic: true,
ssl_verification: 'auto' as const,
live: 'auto' as const,
media: 'auto' as const,
ssl_ciphers: 'auto' as const,
ssl_verification: 'auto' as const,
},
always_error_if_entity_unavailable: false,
};
@@ -133,6 +134,7 @@ const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const;
export type SSLCiphers = (typeof SSL_CIPHERS)[number];
const proxyConfigSchema = z.object({
live: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.live),
media: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.media),
dynamic: z.boolean().default(cameraConfigDefault.proxy.dynamic),
ssl_verification: z
@@ -144,7 +146,6 @@ const proxyConfigSchema = z.object({
.or(z.literal('auto'))
.default(cameraConfigDefault.proxy.ssl_ciphers),
});
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
const rotationSchema = z
.literal(0)
+1
View File
@@ -92,6 +92,7 @@ export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR =
`${CONF_CAMERAS}.#.dimensions.layout.zoom` as const;
export const CONF_CAMERAS_ARRAY_PROXY_DYNAMIC =
`${CONF_CAMERAS}.#.proxy.dynamic` as const;
export const CONF_CAMERAS_ARRAY_PROXY_LIVE = `${CONF_CAMERAS}.#.proxy.live` as const;
export const CONF_CAMERAS_ARRAY_PROXY_MEDIA = `${CONF_CAMERAS}.#.proxy.media` as const;
export const CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS =
`${CONF_CAMERAS}.#.proxy.ssl_ciphers` as const;
+15 -7
View File
@@ -77,6 +77,7 @@ import {
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_FILE_PATTERN,
CONF_CAMERAS_ARRAY_MOTIONEYE_URL,
CONF_CAMERAS_ARRAY_PROXY_DYNAMIC,
CONF_CAMERAS_ARRAY_PROXY_LIVE,
CONF_CAMERAS_ARRAY_PROXY_MEDIA,
CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS,
CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION,
@@ -871,19 +872,19 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
},
];
protected _proxyMedia: EditorSelectOption[] = [
protected _proxyModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'auto',
label: localize('config.cameras.proxy.media.auto'),
label: localize('config.cameras.proxy.modes.auto'),
},
{
value: true,
label: localize('config.cameras.proxy.media.true'),
label: localize('config.cameras.proxy.modes.true'),
},
{
value: false,
label: localize('config.cameras.proxy.media.false'),
label: localize('config.cameras.proxy.modes.false'),
},
];
@@ -2563,10 +2564,17 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
'mdi:arrow-decision',
html`
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex),
this._proxyMedia,
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_LIVE, cameraIndex),
this._proxyModes,
{
label: localize('config.cameras.proxy.media.editor_label'),
label: localize('config.cameras.proxy.live'),
},
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex),
this._proxyModes,
{
label: localize('config.cameras.proxy.media'),
},
)}
${this._renderSwitch(
+21 -13
View File
@@ -7,30 +7,37 @@ const hasWebProxyAvailable = (hass: HomeAssistant): boolean => {
return hass.config.components.includes(HASS_WEB_PROXY_DOMAIN);
};
export const getWebProxiedURL = (url: string, v?: number): string => {
return `/api/${HASS_WEB_PROXY_DOMAIN}/v${v ?? 0}/?url=${encodeURIComponent(url)}`;
interface ProxiedURLOptions {
version?: number;
websocket?: boolean;
}
/**
* Get a proxied URL for the given URL.
* @param url The URL to proxy.
* @param options Options for the proxied URL.
* @returns The proxied URL.
*/
export const getWebProxiedURL = (url: string, options?: ProxiedURLOptions): string => {
return (
`/api/${HASS_WEB_PROXY_DOMAIN}/v${options?.version ?? 0}/` +
`${options?.websocket ? 'ws' : ''}?url=${encodeURIComponent(url)}`
);
};
export const shouldUseWebProxy = (
hass: HomeAssistant,
proxyConfig: CameraProxyConfig,
context: 'media' = 'media',
context: 'media' | 'live' = 'media',
): boolean => {
return hasWebProxyAvailable(hass) && !!proxyConfig[context];
};
/**
* Request that HA sign a path. May throw.
* @param hass The HomeAssistant object used to request the signature.
* @param path The path to sign.
* @param expires An optional number of seconds to sign the path for (by default
* HA will sign for 30 seconds).
* @returns The signed URL, or null if the response was malformed.
*/
export async function addDynamicProxyURL(
hass: HomeAssistant,
url_pattern: string,
options?: {
proxyConfig?: CameraProxyConfig;
urlID?: string;
sslVerification?: boolean;
sslCiphers?: string;
@@ -43,8 +50,9 @@ export async function addDynamicProxyURL(
url_pattern: url_pattern,
...(options && {
url_id: options.urlID,
ssl_verification: options.sslVerification,
ssl_ciphers: options.sslCiphers,
ssl_verification:
options.sslVerification ?? options?.proxyConfig?.ssl_verification,
ssl_ciphers: options.sslCiphers ?? options?.proxyConfig?.ssl_ciphers,
open_limit: options.openLimit,
ttl: options.ttl,
allow_unauthenticated: options.allowUnauthenticated,
+6 -4
View File
@@ -83,9 +83,9 @@
"rotation": "",
"rotations": {
"0": "",
"90": "",
"180": "",
"270": ""
"270": "",
"90": ""
}
},
"engines": {
@@ -145,9 +145,10 @@
"proxy": {
"dynamic": "",
"editor_label": "",
"media": {
"live": "",
"media": "",
"modes": {
"auto": "",
"editor_label": "",
"false": "",
"true": ""
},
@@ -679,6 +680,7 @@
"duplicate_camera_id": "Duplica l'identificador de la càmera Frigate per a la següent càmera, utilitza el paràmetre 'id' per identificar les càmeres de manera única",
"duplicate_folder_id": "",
"empty_response": "S'ha rebut una resposta buida de Home Assistant per a la sol·licitud",
"failed_proxy": "",
"failed_response": "No s'ha pogut rebre la resposta de Home Assistant per a la sol·licitud",
"failed_retain": "No s'ha pogut retenir l'esdeveniment",
"failed_sign": "No s'ha pogut signar l'URL de Home Assistant",
+12 -10
View File
@@ -83,9 +83,9 @@
"rotation": "Rotation",
"rotations": {
"0": "No rotation",
"90": "90 degrees clockwise",
"180": "180 degrees clockwise",
"270": "270 degrees clockwise"
"270": "270 degrees clockwise",
"90": "90 degrees clockwise"
}
},
"engines": {
@@ -145,11 +145,12 @@
"proxy": {
"dynamic": "Dynamic proxying",
"editor_label": "Camera proxying",
"media": {
"auto": "Media proxying automatically configured",
"editor_label": "Media proxying",
"false": "Media proxying disabled",
"true": "Media proxying enabled"
"live": "Live proxying",
"media": "Media proxying",
"modes": {
"auto": "Proxying automatically configured",
"false": "Proxying disabled",
"true": "Proxying enabled"
},
"ssl_ciphers": {
"auto": "SSL ciphers automatically configured",
@@ -330,8 +331,8 @@
"folders": {
"ha": {
"editor_label": "Home Assistant Media Folder Options",
"url": "URL of media folder",
"path_info": "Additional path matching functionality is available in the text editor"
"path_info": "Additional path matching functionality is available in the text editor",
"url": "URL of media folder"
},
"icon": "Icon for this folder",
"id": "Unique ID for this folder in this card",
@@ -665,8 +666,8 @@
}
},
"error": {
"awaiting_live": "Waiting for live stream to load ...",
"awaiting_folder": "Waiting for folder to load",
"awaiting_live": "Waiting for live stream to load ...",
"awaiting_media": "Waiting for media to load",
"camera_initialization": "Camera initialization failed",
"camera_initialization_reolink": "Could not initialize Reolink camera",
@@ -679,6 +680,7 @@
"duplicate_camera_id": "Duplicate camera id for the following camera, use the 'id' parameter to uniquely identify cameras",
"duplicate_folder_id": "Duplicate folder id for the following folder, use the 'id' parameter to uniquely identify folders",
"empty_response": "Received empty response from Home Assistant for request",
"failed_proxy": "Could not proxy via Home Assistant",
"failed_response": "Failed to receive response from Home Assistant for request",
"failed_retain": "Could not retain event",
"failed_sign": "Could not sign Home Assistant URL",
+9 -7
View File
@@ -83,9 +83,9 @@
"rotation": "",
"rotations": {
"0": "",
"90": "",
"180": "",
"270": ""
"270": "",
"90": ""
}
},
"engines": {
@@ -145,11 +145,12 @@
"proxy": {
"dynamic": "Proxy dynamique",
"editor_label": "Proxy de la caméra",
"media": {
"auto": "Proxy média automatique",
"editor_label": "Proxy média",
"false": "Proxy média désactivé",
"true": "Proxy média activé"
"live": "",
"media": "",
"modes": {
"auto": "Proxy automatique",
"false": "Proxy désactivé",
"true": "Proxy activé"
},
"ssl_ciphers": {
"auto": "Chiffrement SSL automatique",
@@ -679,6 +680,7 @@
"duplicate_camera_id": "ID de caméra Frigate en double pour la caméra suivante, utilisez le paramètre « ID » pour identifier de manière unique les caméras",
"duplicate_folder_id": "",
"empty_response": "Réponse vide reçue de Home Assistant pour la demande",
"failed_proxy": "",
"failed_response": "Échec de la réponse de Home Assistant à la demande",
"failed_retain": "Impossible de conserver l'événement",
"failed_sign": "Impossible de signer l'URL de Home Assistant",
+6 -4
View File
@@ -83,9 +83,9 @@
"rotation": "",
"rotations": {
"0": "",
"90": "",
"180": "",
"270": ""
"270": "",
"90": ""
}
},
"engines": {
@@ -145,9 +145,10 @@
"proxy": {
"dynamic": "",
"editor_label": "",
"media": {
"live": "",
"media": "",
"modes": {
"auto": "",
"editor_label": "",
"false": "",
"true": ""
},
@@ -679,6 +680,7 @@
"duplicate_camera_id": "Duplicato ID dellla telecamera Frigate, utilizzare il parametro 'ID' per identificare in modo univoco le telecamere",
"duplicate_folder_id": "",
"empty_response": "Ricevuto risposta vuota da Home Assistant per la richiesta",
"failed_proxy": "",
"failed_response": "Impossibile ricevere risposta da Home Assistant per la richiesta",
"failed_retain": "Impossibile conservare l'evento",
"failed_sign": "Impossibile firmare l'URL ad Home Assistant",
+6 -4
View File
@@ -83,9 +83,9 @@
"rotation": "",
"rotations": {
"0": "",
"90": "",
"180": "",
"270": ""
"270": "",
"90": ""
}
},
"engines": {
@@ -145,9 +145,10 @@
"proxy": {
"dynamic": "",
"editor_label": "",
"media": {
"live": "",
"media": "",
"modes": {
"auto": "",
"editor_label": "",
"false": "",
"true": ""
},
@@ -679,6 +680,7 @@
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
"duplicate_folder_id": "",
"empty_response": "Sem resposta do Home Assistant para a solicitação",
"failed_proxy": "",
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
"failed_retain": "Não foi possível reter o evento",
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
+6 -4
View File
@@ -83,9 +83,9 @@
"rotation": "",
"rotations": {
"0": "",
"90": "",
"180": "",
"270": ""
"270": "",
"90": ""
}
},
"engines": {
@@ -145,9 +145,10 @@
"proxy": {
"dynamic": "",
"editor_label": "",
"media": {
"live": "",
"media": "",
"modes": {
"auto": "",
"editor_label": "",
"false": "",
"true": ""
},
@@ -679,6 +680,7 @@
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
"duplicate_folder_id": "",
"empty_response": "Sem resposta do Home Assistant para a solicitação",
"failed_proxy": "",
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
"failed_retain": "Não foi possível reter o evento",
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
+4
View File
@@ -323,3 +323,7 @@ export const generateFloatApproximatelyEqualsCustomizer = (
: undefined;
};
};
export const convertHTTPAdressToWebsocket = (url: string): string => {
return url.replace(/^http/i, 'ws');
};
-23
View File
@@ -1,23 +0,0 @@
import { homeAssistantSignPath } from '../ha/sign-path';
import { HomeAssistant } from '../ha/types';
import { Endpoint } from '../types';
import { errorToConsole } from './basic';
export const convertEndpointAddressToSignedWebsocket = async (
hass: HomeAssistant,
endpoint: Endpoint,
expires?: number,
): Promise<string | null> => {
if (!endpoint.sign) {
return endpoint.endpoint;
}
let response: string | null | undefined;
try {
response = await homeAssistantSignPath(hass, endpoint.endpoint, expires);
} catch (e) {
errorToConsole(e as Error);
}
return response ? response.replace(/^http/i, 'ws') : null;
};