Skip to content

feat(detection): add DOODS2 request format for API detection - #627

Merged
matteius merged 2 commits into
mainfrom
feat/api-detection-doods2-format
Oct 5, 2026
Merged

matteius merged 2 commits into
mainfrom
feat/api-detection-doods2-format

Conversation

@matteius

@matteius matteius commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #626. The stream "custom API endpoint" only changed where LightNVR sent detection requests; the wire format was hard-coded to light-object-detect (multipart file upload plus backend/confidence_threshold/return_image query params, x_min-style reply). Pointing it at a DOODS2 server got HTTP 422 on every frame, and nothing in the UI or docs said why.

This adds a selectable request format and makes the API detector configurable globally and per stream.

What changed

Request format

  • [api_detection] format = light-object-detect | doods2 and detector_name (DOODS2 only) in lightnvr.ini, in GET/POST /api/settings (api_detection_format, api_detection_detector_name; an unknown format is rejected with 400), and in the Settings → Detection tab (format select; the backend select is shown only for light-object-detect, the detector name only for DOODS2).
  • doods2 posts {"id","detector_name","detect":{"*":pct},"data":<base64 JPEG>} as JSON to the URL verbatim, with no query parameters appended. The reply's top/left/bottom/right boxes and 0–100 confidence are converted to LightNVR's normalized 0–1 scale; a non-empty error field fails the request.
  • Per-stream overrides: an api engine's config JSON ({"format","backend","detector_name"}) is honored on the single-engine dispatch path, settable through PUT /api/streams/{name}/detection-engines.

Refactor

  • api_detection.c: the frame and snapshot entry points now share one transport → parse → persist path instead of two ~350-line copies. The pure URL/body/response helpers are exposed for unit tests. light-object-detect behavior is unchanged.
  • Non-200 replies now log a sanitized body preview, so server-side rejections like the 422 in the issue are diagnosable from the log.
  • New shared base64 encoder in src/utils/base64.c.

Docs and defaults

  • docs/CONFIGURATION.md documents both wire contracts; docs/DETECTION_ENGINES.md documents the per-engine config keys; README mentions DOODS2.
  • Default URL aligned with light-object-detect's real endpoint (/api/v1/detect). The stream modal no longer shows a hard-coded localhost:9001/detect.
  • The old [api_detection] doc entries confidence_threshold / filter_classes were never parsed by LightNVR; they are replaced with the keys that are.

Testing

  • cmake --build build for all targets is clean, as is cd web && npx vite build.
  • test_api_detection: 22 tests covering format parsing, option overlay from engine config, URL building for both formats, the DOODS2 body (base64, threshold scaling, JSON escaping, defaults), response parsing for both box shapes, error/null handling, and the MAX_DETECTIONS cap.
  • test_config: defaults, name normalization and rejection, ini save/load round-trip, unknown format falling back to the default.
  • Full ctest: 142/143. The one failure is test_mp4_segment_boundaries, a known local FFmpeg header/library mismatch unrelated to this change.
  • Not exercised against a live DOODS2 server. The request and response shapes follow the DOODS2 README and the transport is a plain libcurl JSON POST, so a report back from @betweenbrain would be welcome.

Follow-ups (not in this PR)

  • Per-stream format selection from the stream modal (needs a stream-row field; today per-stream is engine-config/API only).
  • Passing a custom DOODS2 detect map or regions through engine config.

🤖 Generated with Claude Code

The "custom API endpoint" only ever changed where the request went; the
wire format was hard-coded to light-object-detect (multipart "file" upload
plus backend/confidence_threshold/return_image query params, x_min-style
reply). Pointing it at a DOODS2 server returned HTTP 422 on every frame.

Add a selectable request format:

- [api_detection] format = light-object-detect | doods2, plus
  detector_name for DOODS2. Exposed through GET/POST /api/settings and the
  Settings > Detection tab (format select, backend only for
  light-object-detect, detector name only for DOODS2).
- Per-stream overrides via the api engine's config JSON
  ({"format","backend","detector_name"}) on the single-engine path.
- doods2 posts {"id","detector_name","detect":{"*":pct},"data":b64} as
  JSON to the URL verbatim; the reply's top/left/bottom/right boxes and
  0-100 confidence are converted to LightNVR's normalized 0-1 scale and a
  non-empty "error" fails the request. Non-200 replies now log a body
  preview so server-side rejections are diagnosable.
- Refactor api_detection.c so the frame and snapshot entry points share
  one transport/parse/persist path instead of two ~350 line copies, and
  expose the pure URL/body/response helpers for unit tests.
- New shared base64 encoder in src/utils.
- Align the default URL with light-object-detect's real endpoint
  (/api/v1/detect) and document both wire contracts.

Closes #626

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Invalid-format requests partially modify runtime settings, and detection shutdown no longer releases cached JPEG encoders.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds selectable DOODS2 support to LightNVR’s external detection pipeline, addressing #626’s incompatible request format.

Changes:

  • Adds global settings and per-engine request overrides.
  • Shares transport, response parsing, and persistence between detection paths.
  • Adds configuration controls, documentation, and unit coverage.
File Description
web/​public/​locales/​en.json Adds format labels and endpoint guidance.
web/​js/​components/​preact/​StreamConfigModal.jsx Removes the hard-coded endpoint display.
web/​js/​components/​preact/​SettingsView.jsx Loads and saves detector options.
web/​js/​components/​preact/​settings/​DetectionTab.jsx Adds format-specific controls.
tests/​unit/​test_config.c Tests format configuration handling.
tests/​unit/​test_api_detection.c Tests request construction and response parsing.
src/​web/​api_handlers_settings.c Exposes new detector settings.
src/​video/​unified_detection_thread.c Applies per-engine request overrides.
src/​video/​api_detection.c Implements DOODS2 and shared request handling.
src/​utils/​base64.c Implements base64 encoding.
src/​core/​config.c Loads and persists detector options.
README.md Introduces DOODS2 support.
include/​video/​api_detection.h Declares request options and helpers.
include/​utils/​base64.h Declares base64 utilities.
include/​core/​config.h Defines format types and configuration fields.
docs/​DETECTION_ENGINES.md Documents per-engine overrides.
docs/​CONFIGURATION.md Documents both detection protocols.
config/​lightnvr.ini Adds format and detector-name defaults.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/video/api_detection.c Outdated
// The global cleanup will happen at program shutdown

log_info("API detection system shutdown complete");
log_info("API detection system shutdown");
Comment thread src/web/api_handlers_settings.c Outdated
Comment on lines +1562 to +1566
if (!config_set_api_detection_format(&g_config, api_detection_format->valuestring)) {
log_warn("Rejected invalid api_detection_format setting");
cJSON_Delete(settings);
http_response_set_json_error(res, 400, "Invalid api_detection_format: expected light-object-detect or doods2");
return;
- Restore jpeg_encoder_cleanup_all() in shutdown_api_detection_system();
  the refactor dropped it, leaving the cached encoder contexts allocated
  after shutdown with no remaining caller.
- Preflight-validate api_detection_format in POST /api/settings before
  any g_config mutation so a bad format cannot leave earlier settings in
  the same request half-applied.
- Use a literal format string for the DOODS2 body prefix
  (-Wformat-nonliteral).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@matteius
matteius merged commit 9b9b404 into main Oct 5, 2026
4 checks passed
@matteius
matteius deleted the feat/api-detection-doods2-format branch October 5, 2026 05:05
@betweenbrain

Copy link
Copy Markdown

@matteius Thanks for this! I would love to test it. Is there a way to do it via Home Assistant? If not, I'll look into standing up a test server.

@betweenbrain

Copy link
Copy Markdown

@matteius I've installed the main branch, following https://github.com/opensensor/lightNVR#installation and #68 (comment), and am confused by which Detection Model should be selected in the stream (e.g. Custom API Detection, API Detection (light-object-detect)...etc).

I've tried setting the stream to both options, both with the defaults as well as overriding the detection endpoint, and keep getting the following loggwd:

[Detection] API Detection: API request failed with HTTP code 400 (format: doods2, response: {"detections":[],"error":"unknown detector name: DOODS2"})

Also, does Light NVR send images to the API for detection or does it use a stream?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom API detection request structure

3 participants