feat: Add proxying support for images (#2427)

- Closes #2418
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 9384785d37
commit 1cd5520154
51 changed files with 2404 additions and 687 deletions
+32 -10
View File
@@ -2,6 +2,7 @@ import { ActionsExecutor } from '../card-controller/actions/types';
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
import { CameraConfig } from '../config/schema/cameras';
import { EnabledProxyConfig, resolveProxyConfig } from '../config/schema/common/proxy';
import { isTriggeredState } from '../ha/is-triggered-state';
import { HassStateDifference, HomeAssistant } from '../ha/types';
import { localize } from '../localize/localize';
@@ -100,6 +101,8 @@ export class Camera {
}
protected async _has2WayAudioCapability(hass: HomeAssistant): Promise<boolean> {
// Check disable/disableExcept/force early to short-circuit the expensive
// network call to fetch go2rtc metadata.
if (this._config.capabilities?.disable?.includes('2-way-audio')) {
return false;
}
@@ -115,7 +118,7 @@ export class Camera {
this.getConfig(),
this.getConfig().go2rtc.metadata_fetch_timeout_seconds,
this._getGo2RTCMetadataEndpoint(),
this.getProxyConfig(),
this.getLiveProxyConfig(),
);
}
@@ -137,7 +140,7 @@ export class Camera {
}
public async destroy(): Promise<void> {
this._destroyCallbacks.forEach((callback) => callback());
await Promise.all(this._destroyCallbacks.map((callback) => callback()));
}
public getConfig(): CameraConfig {
@@ -204,6 +207,7 @@ export class Camera {
public getProxyConfig(): CameraProxyConfig {
return {
...resolveProxyConfig(this._config.proxy),
live:
this._config.proxy.live === 'auto'
? // Live is proxied if the live provider is go2rtc and if a go2rtc
@@ -211,13 +215,32 @@ export class Camera {
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'
? 'default'
: this._config.proxy.ssl_ciphers,
public getLiveProxyConfig(): EnabledProxyConfig {
const config = this.getProxyConfig();
return {
...config,
// `enabled` uses the resolved engine decision (so `auto` may become
// true), whereas `enforce` uses the raw user setting so only an explicit
// `true` means "fail instead of falling back" if the proxy is unavailable.
enabled: config.live,
enforce: this._config.proxy.live === true,
};
}
public getMediaProxyConfig(): EnabledProxyConfig {
const config = this.getProxyConfig();
return {
...config,
// `enabled` uses the resolved engine decision (so `auto` may become
// true), whereas `enforce` uses the raw user setting so only an explicit
// `true` means "fail instead of falling back" if the proxy is unavailable.
enabled: config.media,
enforce: this._config.proxy.media === true,
};
}
@@ -250,11 +273,10 @@ export class Camera {
this._destroyCallbacks.push(callback);
}
protected _subscribeBasedOnCapabilities(
private _subscribeBasedOnCapabilities(
stateWatcher: StateWatcherSubscriptionInterface,
): void {
if (this._capabilities?.has('trigger')) {
stateWatcher.unsubscribe(this._stateChangeHandler);
stateWatcher.subscribe(this._stateChangeHandler, this._config.triggers.entities);
}
}