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
+3 -16
View File
@@ -5,6 +5,7 @@ import { ptzCameraConfigDefaults, ptzCameraConfigSchema } from './camera/ptz';
import { aspectRatioSchema } from './common/aspect-ratio';
import { eventsMediaTypeSchema } from './common/events-media';
import { imageBaseConfigDefault, imageBaseConfigSchema } from './common/image';
import { proxyBaseConfigDefault, proxyBaseConfigSchema } from './common/proxy';
import { severitySchema } from './common/severity';
const CAMERA_TRIGGER_EVENT_TYPES = [
@@ -143,32 +144,18 @@ export const cameraConfigDefault = {
},
},
proxy: {
dynamic: true,
...proxyBaseConfigDefault,
live: 'auto' as const,
media: 'auto' as const,
ssl_ciphers: 'auto' as const,
ssl_verification: 'auto' as const,
},
go2rtc: go2rtcConfigDefault,
image: imageBaseConfigDefault,
always_error_if_entity_unavailable: false,
};
const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const;
export type SSLCiphers = (typeof SSL_CIPHERS)[number];
const proxyConfigSchema = z.object({
const proxyConfigSchema = proxyBaseConfigSchema.extend({
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
.boolean()
.or(z.literal('auto'))
.default(cameraConfigDefault.proxy.ssl_verification),
ssl_ciphers: z
.enum(SSL_CIPHERS)
.or(z.literal('auto'))
.default(cameraConfigDefault.proxy.ssl_ciphers),
});
const rotationSchema = z
-5
View File
@@ -5,11 +5,6 @@ export const imageBaseConfigDefault = {
refresh_seconds: 1,
};
export const imageConfigDefault = {
...imageBaseConfigDefault,
zoomable: true,
};
const IMAGE_MODES = ['auto', 'camera', 'entity', 'screensaver', 'url'] as const;
export type ImageMode = (typeof IMAGE_MODES)[number];
+45
View File
@@ -0,0 +1,45 @@
import { z } from 'zod';
const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const;
type SSLCiphers = (typeof SSL_CIPHERS)[number];
export const proxyBaseConfigDefault = {
dynamic: true,
ssl_ciphers: 'auto' as const,
ssl_verification: 'auto' as const,
};
export const proxyBaseConfigSchema = z.object({
dynamic: z.boolean().default(proxyBaseConfigDefault.dynamic),
ssl_verification: z
.boolean()
.or(z.literal('auto'))
.default(proxyBaseConfigDefault.ssl_verification),
ssl_ciphers: z
.enum(SSL_CIPHERS)
.or(z.literal('auto'))
.default(proxyBaseConfigDefault.ssl_ciphers),
});
type UnresolvedProxyConfig = z.output<typeof proxyBaseConfigSchema>;
export interface ResolvedProxyConfig {
dynamic: boolean;
ssl_verification: boolean;
ssl_ciphers: SSLCiphers;
}
export interface EnabledProxyConfig extends ResolvedProxyConfig {
enabled: boolean;
// Whether proxying is a strict requirement. When false, callers may fall
// back to the original URL if the proxy integration is unavailable.
enforce?: boolean;
}
export const resolveProxyConfig = (
config: UnresolvedProxyConfig,
): ResolvedProxyConfig => ({
dynamic: config.dynamic,
ssl_verification: config.ssl_verification === 'auto' ? true : config.ssl_verification,
ssl_ciphers: config.ssl_ciphers === 'auto' ? 'default' : config.ssl_ciphers,
});
+17 -1
View File
@@ -1,9 +1,25 @@
import { z } from 'zod';
import { actionsSchema } from './actions/types';
import { imageBaseConfigSchema, imageConfigDefault } from './common/image';
import { imageBaseConfigDefault, imageBaseConfigSchema } from './common/image';
import { proxyBaseConfigDefault, proxyBaseConfigSchema } from './common/proxy';
export const imageConfigDefault = {
...imageBaseConfigDefault,
proxy: {
...proxyBaseConfigDefault,
enabled: false,
},
zoomable: true,
};
const imageProxyConfigSchema = proxyBaseConfigSchema.extend({
enabled: z.boolean().default(imageConfigDefault.proxy.enabled),
});
export type ImageViewProxyConfig = z.infer<typeof imageProxyConfigSchema>;
export const imageConfigSchema = imageBaseConfigSchema
.extend({
proxy: imageProxyConfigSchema.optional(),
zoomable: z.boolean().default(imageConfigDefault.zoomable),
})
.extend(actionsSchema.shape)
+1 -2
View File
@@ -3,12 +3,11 @@ import { deepRemoveDefaults } from '../../utils/zod/deep-remove-defaults';
import { automationsSchema } from './automations';
import { cameraConfigDefault, cameraConfigSchema, camerasConfigSchema } from './cameras';
import { cardIDRegex } from './common/const';
import { imageConfigDefault } from './common/image';
import { DebugConfig, debugConfigDefault, debugConfigSchema } from './debug';
import { dimensionsConfigSchema } from './dimensions';
import { pictureElementsSchema } from './elements/types';
import { foldersConfigSchema } from './folders';
import { imageConfigSchema } from './image';
import { imageConfigDefault, imageConfigSchema } from './image';
import { liveConfigDefault, liveConfigSchema } from './live';
import { mediaGalleryConfigDefault, mediaGalleryConfigSchema } from './media-gallery';
import { menuConfigDefault, menuConfigSchema } from './menu';