diff --git a/web/public/locales/en/config/validation.json b/web/public/locales/en/config/validation.json index 6f3b5f6864..f3d98a65e9 100644 --- a/web/public/locales/en/config/validation.json +++ b/web/public/locales/en/config/validation.json @@ -28,5 +28,8 @@ "detectRequired": "At least one input stream must be assigned the 'detect' role.", "hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments." } + }, + "detect": { + "dimensionMustBeEven": "Must be an even number." } } diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index ec37ca7f17..11fcd92123 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1794,7 +1794,9 @@ }, "detect": { "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.", - "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function." + "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function.", + "resolutionShouldBeMultipleOfFour": "For best results, detect width and height should be multiples of 4. Other even values may produce visual artifacts or slight distortion in the detect stream.", + "aspectRatioMismatch": "The width and height you've entered don't match the aspect ratio of your current detect resolution. This may produce a stretched or distorted image." }, "objects": { "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated." diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index 964a802d3f..74d170edc6 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -11,6 +11,50 @@ const detect: SectionConfigOverrides = { condition: (ctx) => ctx.level === "camera" && ctx.formData?.enabled === false, }, + { + key: "detect-resolution-not-multiple-of-four", + messageKey: "configMessages.detect.resolutionShouldBeMultipleOfFour", + severity: "warning", + condition: (ctx) => { + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + const isEvenButNotFour = (v: unknown) => + typeof v === "number" && v % 2 === 0 && v % 4 !== 0; + return isEvenButNotFour(width) || isEvenButNotFour(height); + }, + }, + { + key: "detect-aspect-ratio-mismatch", + messageKey: "configMessages.detect.aspectRatioMismatch", + severity: "warning", + condition: (ctx) => { + const newWidth = ctx.formData?.width as number | null | undefined; + const newHeight = ctx.formData?.height as number | null | undefined; + if (typeof newWidth !== "number" || typeof newHeight !== "number") { + return false; + } + const saved = + ctx.level === "camera" + ? ctx.fullCameraConfig?.detect + : ctx.fullConfig?.detect; + const savedWidth = saved?.width; + const savedHeight = saved?.height; + if ( + typeof savedWidth !== "number" || + typeof savedHeight !== "number" || + savedWidth <= 0 || + savedHeight <= 0 + ) { + return false; + } + if (newWidth === savedWidth && newHeight === savedHeight) { + return false; + } + const newRatio = newWidth / newHeight; + const savedRatio = savedWidth / savedHeight; + return Math.abs(newRatio - savedRatio) > 0.01; + }, + }, ], fieldMessages: [ { diff --git a/web/src/components/config-form/section-validations/detect.ts b/web/src/components/config-form/section-validations/detect.ts new file mode 100644 index 0000000000..7ecc805b72 --- /dev/null +++ b/web/src/components/config-form/section-validations/detect.ts @@ -0,0 +1,36 @@ +import type { FormValidation } from "@rjsf/utils"; +import type { TFunction } from "i18next"; +import { isJsonObject } from "@/lib/utils"; +import type { JsonObject } from "@/types/configForm"; + +export function validateDetectDimensions( + formData: unknown, + errors: FormValidation, + t: TFunction, +): FormValidation { + if (!isJsonObject(formData as JsonObject)) { + return errors; + } + + const data = formData as JsonObject; + const width = data.width; + const height = data.height; + + const widthErrors = errors.width as + | { addError?: (message: string) => void } + | undefined; + const heightErrors = errors.height as + | { addError?: (message: string) => void } + | undefined; + + const message = t("detect.dimensionMustBeEven", { ns: "config/validation" }); + + if (typeof width === "number" && width % 2 !== 0) { + widthErrors?.addError?.(message); + } + if (typeof height === "number" && height % 2 !== 0) { + heightErrors?.addError?.(message); + } + + return errors; +} diff --git a/web/src/components/config-form/section-validations/index.ts b/web/src/components/config-form/section-validations/index.ts index 31a02a1d10..33c02b1c7a 100644 --- a/web/src/components/config-form/section-validations/index.ts +++ b/web/src/components/config-form/section-validations/index.ts @@ -1,5 +1,6 @@ import type { FormValidation } from "@rjsf/utils"; import type { TFunction } from "i18next"; +import { validateDetectDimensions } from "./detect"; import { validateFfmpegInputRoles } from "./ffmpeg"; import { validateProxyRoleHeader } from "./proxy"; @@ -19,6 +20,10 @@ export function getSectionValidation({ level, t, }: SectionValidationOptions): SectionValidation | undefined { + if (sectionPath === "detect") { + return (formData, errors) => validateDetectDimensions(formData, errors, t); + } + if (sectionPath === "ffmpeg" && level === "camera") { return (formData, errors) => validateFfmpegInputRoles(formData, errors, t); }