Videostream Analytics HTTP API Reference#
The Videostream Analytics microservice (VSA) is the standalone RTSP-processing service that pulls camera streams, runs motion detection and optional NPU-based YOLO prefiltering, cuts qualifying segments into MP4 clips, and pushes the resulting events to a configured webhook consumer (typically the MCP server).
1. Service Overview#
Item |
Value |
|---|---|
Service name |
|
Framework |
FastAPI (served by uvicorn with the strict |
Default bind |
|
Default webhook target |
|
Content-Type |
|
Character encoding |
UTF-8 |
Auth |
None. Because every endpoint (register/delete a source, rewrite |
The service is a long-running single process. Each registered source spawns background threads for the motion pipeline, an optional continuous recorder, and a shared keepalive watchdog. Multiple sources coexist in a single VSA instance.
2. Endpoint Summary#
Method |
Path |
Purpose |
See |
|---|---|---|---|
|
|
Liveness probe. |
§3.1 |
|
|
Report the deployed prefilter model and available devices. |
§3.2 |
|
|
List all registered sources (bare array). |
§3.3 |
|
|
Get source status. |
§3.4 |
|
|
Alias of |
§3.4 |
|
|
Register and start a new source. |
§3.5 |
|
|
Unregister a source. |
§3.6 |
|
|
Stop and unregister (equivalent to |
§3.6 |
|
|
Stop and start the pipeline while preserving the bundle. |
§3.7 |
|
|
Pause the pipeline; keep the source registered. |
§3.8 |
|
|
Resume a paused pipeline. |
§3.8 |
|
|
Refresh the keepalive timestamp. |
§3.9 |
|
|
Hot-update supported pipeline configuration. |
§3.10 |
3. Control Plane#
All responses are JSON. The service follows a small, uniform status-code convention:
Status |
Meaning |
|---|---|
|
The requested state change succeeded, or the idempotent no-op response ( |
|
The request is schema-valid but cannot be honoured — a |
|
The referenced |
|
Another request is mid-registration for the same |
|
The request body failed schema validation (missing required fields, unknown fields due to |
|
A server-side dependency is missing (currently: ffmpeg absent from |
|
Reserved for genuine defects; any request reaching this is a bug. See §5.6. |
3.1 GET /health#
Liveness probe.
Request: no body.
Response 200:
{ "status": "ok", "service": "videostream-analytics" }
3.2 GET /capabilities/prefilter#
Report the prefilter capabilities of the deployed model.
Request: no body.
Response 200:
{
"enabled": true,
"model_path": "/models/yolo11s.xml",
"class_names": ["person", "car"],
"labels_source": "embedded",
"available_devices": ["CPU", "GPU"]
}
labels_source is embedded when the model exposes authoritative labels. It
may be fallback_coco or unavailable; clients must not treat those values as
an authoritative model class list. When the configured model is unavailable,
class_names is an empty array.
3.3 GET /sources#
List all registered sources.
Request: no body.
Response 200: a bare JSON array of SourceStatus objects (see §3.4). The array is [] when no sources are registered.
[
{
"source_id": "cam_demo",
"source_url": "rtsp://localhost:8554/live/child",
"data_dir": "/data/cam_demo",
"status": "online",
"running": true,
"recording_enabled": true,
"health": { "...": "..." },
"keepalive_enabled": false,
"last_keepalive_at": null
}
]
Note: The
/sourcesresponse is intentionally a bare array, not{"sources": [...]}.
3.4 GET /sources/{source_id} and GET /sources/{source_id}/status#
Return the status of a single source. Both paths dispatch to the same handler and return identical bodies. The MCP server’s analyticsSourceExists probe calls the /status variant.
Path parameter: source_id — the id used at registration.
Response 200 (SourceStatus):
Field |
Type |
Description |
|---|---|---|
|
|
Same as the path parameter. |
|
|
The stream URL supplied at registration. |
|
|
Absolute path of the per-source output directory. |
|
|
Pipeline lifecycle state. See §3.8.1 for the full state machine. |
|
|
Whether the pipeline background thread is alive. |
|
|
Whether the continuous recorder is enabled for this source. |
|
|
Health sub-object; see below. |
|
|
Whether keepalive watchdog is enabled for this source. |
|
|
ISO 8601 UTC timestamp of the most recent keepalive; |
health sub-object:
Field |
Type |
Description |
|---|---|---|
|
|
Consecutive RTSP read failures; reset to |
|
|
ISO 8601 timestamp of the most recent failure. |
|
|
Cumulative successful reconnect attempts. |
|
|
Health-recovery strategy taken from |
|
|
Failure threshold that triggers the recovery strategy. |
|
|
ISO 8601 timestamp of pipeline startup. |
Response 404:
{ "detail": "Source not found: cam_xxx" }
3.5 POST /register_source#
Register a new source and start its pipeline. Idempotent: re-registering an id that is already running returns {"status": "already_running"} without changes.
Request body (RegisterSourceRequest, extra="forbid")#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
✅ |
Unique identifier. Must match |
|
|
✅ |
Stream URL from which VSA pulls. Non-empty, at most 2048 characters, no control characters. The scheme must be in |
|
|
Optional |
Overrides the destination for this source’s events. Must be an |
|
|
Optional |
Absolute path for this source’s outputs, which must resolve inside |
|
|
Optional |
Nested pipeline configuration; see below. |
The schema is extra="forbid": any field not listed above is rejected with 422, and its name appears in the unknown_fields array of the error body (see §5.2).
PipelineConfig#
The nested pipeline object (extra="forbid"). Every sub-block is optional. Explicit fields in a
supplied sub-block are merged onto the corresponding defaults.<sub_block>; omitted fields retain
their defaults, so a request may override only the values that need to differ.
The values below are the defaults in the shipped videostream-analytics/config/config.yaml used
by setup_docker.sh. A deployment that loads a different VIDEOSTREAM_CONFIG inherits the
defaults blocks from that file instead.
Sub-block |
Model |
Purpose |
|---|---|---|
|
|
Frame-difference motion detector parameters. |
|
|
Motion-clip segmentation parameters. |
|
|
Quiet-period close-out event parameters. |
|
|
Optional NPU / OpenVINO™ YOLO prefilter. |
|
|
ROI crop and trajectory-region emission. |
|
|
Fixed-cadence continuous recording branch. |
|
|
RTSP failure-handling policy. |
|
|
Keepalive protocol. |
MotionConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
When |
|
|
|
Per-pixel frame-difference threshold. |
|
|
|
Minimum fraction of frame area required to declare motion. |
|
|
|
Consecutive static frames required to end a motion event. |
SegmentConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Hard ceiling on segment length, in seconds. Must be > 0. |
|
|
|
Cut-frequency guard: forced cuts (ROI early-split) are rejected while the running segment is younger than this. Finished segments are always emitted — short motion-end tails are never discarded. Must be ≥ 0, and must not exceed the effective |
StaticConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Emit a |
|
|
|
Suppress quiet periods shorter than this many seconds. |
PrefilterConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Enable OpenVINO™ YOLO prefilter on motion clips. |
|
|
Generated from |
Absolute path to the OpenVINO™ |
|
|
|
Class labels that count as a hit. |
|
|
|
Minimum detection confidence. Must be within 0–1. |
|
|
|
Number of hits within a clip required for PASS. Must be ≥ 1. |
|
|
|
YOLO inference rate; inference does not run on every frame. Must be > 0. |
|
|
|
OpenVINO™ device ( |
|
|
|
Resize the frame’s longest side before inference; |
RoiConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
When enabled, prefilter PASS produces a |
|
|
|
|
|
|
|
Fractional outward expansion of the union bbox. Must be ≥ 0. |
|
|
|
When the union bbox covers more than this fraction of the frame, the current motion segment is cut early to prevent oversize ROIs. |
RecordingConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Enable the continuous recorder branch (independent of motion). |
|
|
|
Duration of each recording segment. Must be within 1–3600. |
|
|
|
Recording output frame rate. Must be within 1–120. |
HealthConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Consecutive failure threshold that triggers the recovery strategy. Must be ≥ 1. |
|
|
|
|
|
|
|
Base of the exponential backoff sequence, in seconds. Must be ≥ 1. |
|
|
|
Upper bound on backoff delay. Must be > 0. |
KeepaliveConfig#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Enable keepalive watchdog for this source. |
|
|
|
Auto-pause when no keepalive arrives within this window. |
|
|
|
Watchdog polling interval. |
Example request body#
{
"source_id": "cam_child",
"source_url": "rtsp://localhost:8554/live/child",
"webhook_url": "http://localhost:3101/events",
"data_dir": "/data/cam_child",
"pipeline": {
"motion": { "diff_threshold": 15, "area_ratio": 0.005, "stable_frames": 45 },
"segment": { "max_duration": 10, "min_duration": 1.0 },
"prefilter": {
"enabled": true,
"model_path": "/models/yolo11s.xml",
"target_classes": ["person"],
"detect_fps": 2.0,
"device": "NPU"
},
"roi": { "enabled": true, "mode": "crop", "expand": 0.25, "auto_split_area": 0.35 },
"recording": { "enabled": true, "interval_seconds": 60 },
"health": { "max_failures": 30, "recovery_strategy": "retry" },
"keepalive": { "enabled": true, "timeout_seconds": 90.0, "check_interval_seconds": 10.0 }
}
}
Response#
Case |
Response 200 body |
|---|---|
Fresh registration |
|
Same id already running (idempotent) |
|
Same id previously registered but not running |
Fresh |
Concurrent registrations for the same id are refused rather than queued: while one registration is in flight, others receive 409 (§5.4). Re-registering a stopped source is not affected — that is a sequential operation.
Validation failures return 422 with the shape described in §5.2.
3.6 DELETE /sources/{source_id}, POST /sources/{source_id}/stop#
The two endpoints are semantically equivalent: stop the pipeline and the recorder, close the per-source webhook sink, and remove the bundle from the in-memory registry. The source’s data_dir is not deleted — file retention is the MCP server’s responsibility.
DELETE /sources/{source_id}— RESTful path form; used by the MCP server’s startup reconciliation.POST /sources/{source_id}/stop— convenience form for clients that cannot issueDELETE.
Response 200:
{ "status": "stopped", "source_id": "cam_child" }
Response 404 when the source is not registered.
3.7 POST /sources/{source_id}/restart#
Stop and start the pipeline (and the recorder, if present) while preserving the registration entry. The source bundle and the webhook sink are reused.
Response 200:
{ "status": "restarted", "source_id": "cam_child" }
Response 404 when the source is not registered.
3.8 POST /sources/{source_id}/pause and /resume#
/pause transitions the pipeline to paused: frame capture continues in order to keep the RTSP connection alive, but motion detection and webhook emission are suspended. /resume returns the pipeline to online.
The response is idempotent: pausing a source that is not_running returns {"status": "not_running"} with HTTP 200.
Response 200:
// pause
{ "status": "paused", "source_id": "cam_child" }
// resume
{ "status": "online", "source_id": "cam_child" }
Response 404 when the source is not registered.
Both endpoints emit a type="status" webhook event with payload {"status": "paused"} or {"status": "online"}; see §4.4.
3.8.1 Source Lifecycle State Machine#
The status field returned by §3.4 evolves according to the following state machine, driven by the StreamPipeline._run() loop and the external control endpoints.
status |
Trigger |
Terminal |
Exit condition |
|---|---|---|---|
|
|
No (transient) |
Success → |
|
RTSP connected and frames are flowing. |
No (event-driven) |
RTSP failure → |
|
A single RTSP read failed. |
No (transient) |
Enters the |
|
Failure with |
No |
Successful reconnect → |
|
Cumulative failures ≥ |
No (transient) |
Depending on |
|
|
Yes |
Only |
|
|
Yes |
The source is no longer in the registry; a fresh |
|
Graceful shutdown / |
Yes |
Same as |
Invariants:
reconnectingis not terminal.recovery_strategy=pausedoes not pause the source on the first failure; VSA accumulatesfailure_countfailures with exponential backoff, and only whenfailure_countreachesmax_failuresdoes the strategy fire. With defaultsmax_failures=30, backoff_base=2.0, backoff_max=120.0the backoff schedule is:failure_count delay (s) cumulative (s) 1 2 2 2 4 6 3 8 14 4 16 30 5 32 62 6 64 126 7 … 120 (cap) 246, 366, 486, … 30 120 → unhealthy → recovery_strategy
pausedis terminal. The state persists across RTSP idle-disconnect and silent reconnects. If a source appears to leavepausedon its own, an external/resumecall is the cause (a lingering test script, another orchestrator, or a re-register_sourceteardown-and-rebuild).failure_countresets to zero on a successful reconnect. Recovery from a transient RTSP glitch therefore restarts the failure budget from scratch.
To shorten the reproduction window for the paused transition during verification, hot-update the health block via §3.10:
curl -X PUT http://localhost:8999/sources/cam_demo/pipeline \
-H "Content-Type: application/json" \
-d '{"pipeline": {"health": {"max_failures": 3, "recovery_strategy": "pause", "backoff_base": 1.0, "backoff_max": 5.0}}}'
Kill the upstream RTSP producer afterwards; the source transitions to paused within roughly seven seconds and remains there.
3.9 POST /sources/{source_id}/keepalive#
This endpoint is intended for an MCP keepalive sender to call at a regular cadence (typically every 30 seconds) to prove liveness. VSA refreshes last_keepalive_at on the source; a background watchdog polls every check_interval_seconds and auto-pauses the source when the timestamp is older than timeout_seconds. Keepalive is disabled by default. The current MCP-side keepalive sender is not wired, so an external caller must send these requests and the source must have pipeline.keepalive.enabled=true at registration to activate the watchdog.
Request: body is ignored ({}, empty body, and arbitrary JSON are all accepted).
Response 200:
{
"status": "ok",
"source_id": "cam_child",
"last_keepalive_at": "2026-06-30T12:34:56.789012+00:00"
}
Response 404 when the source is not registered.
Watchdog behaviour:
The watchdog reuses the standard
pause_source()path. The source becomespausedin the VSA control plane and is visible throughGET /sources/{source_id}/status; VSA does not emit status webhook events.Watchdog-triggered pauses are terminal for the same reason
/pauseis; the MCP server must explicitly/resumethe source. See §3.8.1.At registration, if
keepalive.enabled=true,last_keepalive_atis initialised to the current time, granting atimeout_secondsgrace period before the first heartbeat is required.
3.10 PUT /sources/{source_id}/pipeline#
Hot-update pipeline configuration without unregistering the source.
The request body wraps a PipelineConfig object (§3.5):
{
"pipeline": {
"motion": { "diff_threshold": 10 },
"recording": { "enabled": false }
}
}
Explicit fields in the supported supplied sub-blocks are merged onto the source’s current
configuration; omitted fields and sub-blocks retain their current values. The endpoint currently
supports motion, segment, prefilter, roi, recording, and health. Although static and
keepalive are accepted by the shared PipelineConfig schema, this endpoint does not apply those
two blocks; configure them when registering the source instead. A change to recording.enabled
creates or destroys the ContinuousRecorder; other supported changes are applied by stopping and
restarting the pipeline.
Response 200:
{ "status": "updated", "source_id": "cam_child" }
Response 404 when the source is not registered; response 422 on schema validation failure (§5.2).
4. Event Plane — VSA → Downstream Webhook#
Every event produced by a running source is delivered as an HTTP POST to the configured webhook URL. The envelope shape is aligned with the MCP events-endpoint (see MCP Webhook Event API Reference for the receiving contract).
4.1 Envelope#
{
"sourceId": "cam_child",
"type": "motion | static | recording",
"timestamp": "2026-06-30T14:30:15",
"payload": { ... }
}
Field |
Type |
Description |
|---|---|---|
|
|
The |
|
|
Event category; determines the payload schema. |
|
|
Time the event was emitted on VSA. |
|
|
Body specific to the |
4.2 type=motion payload#
A motion segment is emitted only after the underlying MP4 has been written. When prefilter is enabled, only clips with prefilter_passed=1 are emitted; the file for a skipped clip is deleted and no event is produced.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
✅ |
Absolute path of the original motion clip, under |
|
|
✅ |
Path of the clip to feed into the video-summary service. When prefilter passes and |
|
|
✅ |
Clip start time. |
|
|
Optional |
Clip end time. |
|
|
✅ |
Clip duration in seconds. |
|
|
Optional |
Present only when prefilter is enabled on this source. |
|
|
Optional |
Hit class names as a JSON string. |
|
|
Optional |
Maximum detection confidence within the clip. |
|
|
Optional |
The union bbox accumulated by prefilter, serialised as a JSON string of four floats. |
4.3 type=static payload#
A static event is emitted when a quiet period closes and its duration meets pipeline.static.min_duration. Static events record activity timing but do not create a video-summary task.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
✅ |
Quiet-period start time. |
|
|
Optional |
Quiet-period end time. |
|
|
✅ |
Quiet-period duration in seconds. |
4.4 type=recording payload#
A recording event is emitted after each fixed-cadence recording segment is written to disk.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
✅ |
Absolute path of the recording, under |
|
|
✅ |
Segment start time. |
|
|
✅ |
Segment end time. |
|
|
Optional |
Actual segment duration; may differ slightly from |
|
|
Optional |
File size in bytes. |
5. Error Responses#
5.1 404 Not Found#
Standard FastAPI HTTPException(404) used whenever the referenced source is not registered:
{ "detail": "Source not found: cam_xxx" }
5.2 422 Unprocessable Entity#
Validation errors use a machine-readable response format. Unknown fields are rejected on all request models and collected into an unknown_fields array to help clients pinpoint the drift.
{
"detail": [
{ "type": "extra_forbidden", "loc": ["body", "rtsp_url"], "...": "..." },
{ "type": "extra_forbidden", "loc": ["body", "motion"], "...": "..." }
],
"unknown_fields": ["rtsp_url", "motion"],
"hint": "request body must match the nested-pipeline schema (source_id/source_url/webhook_url/data_dir/pipeline.{motion,segment,prefilter,recording,health})"
}
unknown_fields is populated only for extra_forbidden errors. Other 422 causes (missing required fields, type mismatches, out-of-range numbers, values outside a closed set, a source_id that does not match ^[A-Za-z0-9_-]{1,128}$, a relative data_dir, a non-HTTP webhook_url) populate detail but leave unknown_fields empty.
5.3 400 Bad Request#
Returned when the request is schema-valid but the business layer cannot honour it — a data_dir outside the permitted roots, an unwritable output directory, or segment durations that are inverted once merged with the per-source defaults.
{
"detail": {
"error": "invalid_request",
"reason": "PermissionError",
"operation": "register_source"
}
}
reason is the exception class name only. The underlying message is written to the service log rather than the response, because it routinely contains absolute server paths and this API is unauthenticated.
5.4 409 Conflict#
Returned only by POST /register_source: another request is already mid-registration for the same source_id. Refusing is deliberate — the alternative is queueing, which would park the caller through a potential 10–20 second teardown of the previous bundle. The client should wait for the in-flight registration to settle and retry.
{ "detail": { "error": "registration_in_progress", "source_id": "cam_child" } }
5.6 500 Internal Server Error#
Reserved for genuine defects. Any request that produces a 500 is a bug: user input reaches either a deliberate 4xx or a successful response. The body carries no server-side detail; the traceback goes to the log.
{ "error": "internal_error" }
6. Data Directory Layout#
VSA writes all per-source outputs under the resolved data_dir. The layout is stable and forms an implicit contract with the MCP server’s cleanup job.
<data_dir>/
├── latest.jpg # Periodically overwritten snapshot; read by smart_community_scene_query.
├── motion_events/<YYYY-MM-DD>/
│ ├── <source_id>_HHMMSS.mp4 # Original motion clip (payload.event_file_path).
│ └── <source_id>_HHMMSS_input.mp4 # ROI-cropped clip (payload.summary_clip_input).
└── recordings/<YYYY-MM-DD>/
└── <source_id>_HHMMSS.mp4 # Fixed-cadence recording segment (payload.recording_path).
Retention responsibilities:
Old date directories under
motion_events/andrecordings/are pruned by the MCP server according tostorage.retention_days.latest.jpgis atomically overwritten by VSA and does not require cleanup.Unregistering a source stops the pipeline but does not delete the
data_dir.
7. Ports and Environment#
Env / Config |
Default |
Purpose |
|---|---|---|
|
|
HTTP bind address. Loopback by default because the service is unauthenticated; widen only for a cross-host deployment that adds its own authentication. |
|
|
Default webhook target; the per-source |
|
|
Global segment root; the |
|
|
Additional roots a request-supplied |
|
|
Schemes accepted in a register |
|
|
Also allow |
|
|
Passed to ffmpeg as |
|
|
Additional roots a request-supplied |
|
|
Overrides the platform data root; VSA writes under its |
|
|
Alternate configuration file path. |
|
|
OpenVINO™ prefilter XML. |
|
Generated by |
Host-side runtime configuration mounted at |
|
|
OpenVINO™ model cache used by the YOLO prefilter. |
Ports used by the service and local verification recipes:
:8554— MediaMTX RTSP server (upstream).:8999— VSA service (this document).:9999— Mock webhook receiver used by integration tests.:3101— Production MCPevents-endpoint(replaces the mock in end-to-end scenarios).
8. Verification#
Use the following test suites for unit and integration verification:
Unit tests:
pytest tests/unit/ --timeout=60— expected210 passed.Integration tests:
pytest tests/integration/ -m integration --timeout=300— expected27 passed(requires MediaMTX, mock webhook, and ffmpeg producer).Manual verification (
V1–V12) covers the/healthprobe, source registration, lifecycle transitions, motion / recording event delivery, health strategies, CLI, real MCP integration, keepalive, and trajectory + ROI crop.
Appendix A: MCP Interoperability#
Contract-level correspondence between the MCP server’s outbound calls and the VSA endpoints defined here.
MCP call |
VSA endpoint |
Status |
|---|---|---|
|
|
Aligned. |
|
|
Aligned. |
|
|
Aligned. |
|
|
Aligned. |
|
|
Aligned. |
|
|
Aligned (bare array). |
Keepalive sender loop |
|
VSA endpoint ready; the MCP-side |
Webhook receiver ( |
VSA |
Aligned envelope. |
|
VSA writes |
VSA producer ready; the MCP-side resource handler still returns a stub. |