Desktop NVR sample¶
amdva-nvr CONFIG.json records every registered RTSP source and displays up to
16 selected sources in a Qt desktop window. YOLOv8n runs on the selected sources
with a shared MIGraphX batch executor, optionally also analyzing keyframes from
hidden sources. Source registration has no fixed count
limit; camera, storage, network and decoder capacity still limit a deployment.
The supported input format is progressive, even-sized, 8-bit NV12 up to
2560×1440, BT.709 limited range, from H.264 or H.265 at 30 FPS. Audio is ignored.
Build and start¶
Run python3 scripts/bootstrap.py to build the locked dependencies, including
Qt 6, SQLite, RTSP/RTP and Matroska plugins. The sample is enabled by default;
-DAMDVA_BUILD_NVR_GUI=OFF keeps the backend and the headless amdva-streams
service but needs no Qt, and -DAMDVA_BUILD_NVR=OFF skips the sample. Dependencies
remain under .deps; no system packages are installed or replaced. The sample
also configures on its own against an installed SDK:
cmake -S examples/nvr -B build-nvr -DCMAKE_PREFIX_PATH=/path/to/amdva-sdk.
Prepare the same YOLOv8n COCO weights in a separate CPU-only export environment:
source scripts/env.sh
python3 scripts/setup_model_export.py
export PATH="$AMDVA_ROOT/build/src/gst:$PATH"
.deps/model-export/bin/python examples/nvr/scripts/export_nvr_model.py \
/path/to/yolov8n.pt artifacts/nvr-model --batch=4
cp examples/nvr/config/nvr.json artifacts/nvr-local.json
# Edit paths and camera URLs in the copied config; paths are relative to that config.
WAYLAND_DISPLAY=wayland-0 QT_QPA_PLATFORM=wayland-egl \
build/examples/nvr/amdva-nvr artifacts/nvr-local.json
Alternatively run under an X11 desktop with QT_QPA_PLATFORM=xcb; the environment
selects the EGL integration. An AMD hardware OpenGL ES 3 context and DMA-BUF
import are required. Software rendering is rejected. No CPU image upload fallback
is provided.
Add --fullscreen before the config path to start in full-screen mode, for
example for demonstrations. F11 switches between full-screen and windowed mode.
Use Live to assign cameras to fixed 1/4/9/16-view tiles and double-click a
tile for single-view focus with a coverage timeline, in-place hover preview,
recording playback and a Back to Live action. Cameras provides add/edit/remove,
status, retention settings and recording deletion. Every
registered camera records regardless of its live-view selection. See the
VMS GUI guide for operation, playback boundaries and catalog migration.
Configuration changes are saved atomically with owner-only permissions; the
UI arrangement is stored separately in <config>.ui.json. Status messages omit
URLs that may contain credentials.
The config keys are demonstrated in examples/nvr/config/nvr.json. Batch size is one of
1, 2, 4, 8 or 16; model must match that size and single_model must be a
batch-1 model for partial batches of one. Model-info describes one image. Schema-1 models remain readable; new models use the general schema-2
GPU/hash/device-I/O manifest and record the batch size. Dynamic-batch ONNX is exported from the
weights; changing a fixed-batch ONNX input alone is insufficient.
Layout, shared services, tracking and publication¶
| Directory | Contents |
|---|---|
backend/ |
Qt-free amdva_nvr_backend library (amdva::nvr): camera controller, config persistence, AI scheduling (AiRuntime), person search and tracking, in-process MobileCLIP runtime |
app/ |
Qt desktop (amdva-nvr) |
headless.cpp |
amdva-streams, the backend without a display |
tools/, tests/, scripts/ |
validation programs, contract tests, model export and benchmarks |
config/, benchmarks/, docs/ |
example configurations, reviewed fixtures, guides and validation records |
The backend uses only the public framework: RTSP ingest, segment recording,
the recording catalog, keyframe inspection and playback come from
amdva_streaming. The person-search table lives in the recording catalog's
database through Catalog::extend, so its rows are removed with recordings and
counted in the quota. GPU image packing, batch execution, imageless tracking,
overlay geometry and result publication use shared framework libraries. See
framework interfaces for SDK and headless usage.
tracking_enabled defaults to true. Selected channels run imageless DL Streamer
tracking across decoded frames and draw class labels and tracking IDs. A source
reconnect, selection change or incompatible caps resets its tracker. Hidden
keyframe detections remain independent untracked results.
Optional results configuration uses the same asynchronous publisher as
amdmetapublish:
"results": {
"method": "file", "file_path": "events.jsonl",
"address": "", "topic": "amdva/analytics",
"queue_limit": 256, "byte_limit": 4194304
}
Use method=none to disable output (default), mqtt with an address such as
tcp://127.0.0.1:1883, or kafka with 127.0.0.1:9092. JSON contains selected
tracked observations and background observations with source generation and
PTS. File paths are relative to the config. Full queues discard oldest waiting
records; delivery errors do not stop recording/display. The UI reports delivered,
dropped and error counts. Output is best effort and is not a durable event log.
Background AI for hidden sources¶
Set background_ai_enabled to true to analyze every source that is not selected
by the active live layout. background_ai_interval_seconds is an integer from 1
through 3600 and defaults to 5. An omitted enable flag defaults to false for
existing configurations; examples/nvr/config/nvr.json enables the feature. Selected
sources continue using their normal display inference scheduler.
Each hidden source takes the next independently decodable access unit after its
sampling deadline. H.264 accepts IDR pictures; H.265 accepts IDR, BLA and CRA
pictures. The compressed parser caches parameter sets by ID, capped at 1 MiB
per source, and validates slice-header references before admission. An ordinary
I picture or the absence of DELTA_UNIT alone is insufficient. Corrupted units,
H.264 extensions and multilayer H.265 are excluded. The existing progressive,
8-bit NV12, size and colorimetry restrictions still apply.
A fresh VA decoder receives cached parameters and exactly one selected AU, then EOS to drain reordered output. No dependent pictures are decoded. Decode timeout is two seconds; errors affect background AI only. There is no full-stream or CPU decode fallback. Recording always receives the original complete compressed stream. Compressed headers may be copied on the CPU; decoded pixels remain in VAMemory through VPE, HIP and MIGraphX.
Four background slots bound the entire decode-to-inference workload. Each owns only a reusable AI surface and mapping, with no preview/EGL resources. When all slots are occupied, a candidate is skipped rather than queued. Older sampling deadlines have priority, with rotating ties. The five-second deadline starts at admission; long GOPs or contention increase the actual interval. Sampling does not request extra keyframes from cameras.
Background and displayed images share the existing batch executor. A mixed batch admits at most one background image, and background-only batches may fill all available batch positions. Partial batches retain the existing batch-1 and GPU-padding behavior. Selection, removal and reconnection cancel pending background jobs; generation checks reject obsolete completions. Background boxes are never reused as display overlays.
The status column reports background mode, completed inference count, detection
count and result age. Controller::status() and the validation JSON telemetry
also expose decoded/attempt counts, active jobs, capture timestamps and the
latest class/score/box coordinates. Results are retained in memory only; this
feature does not create a persistent detection-event database.
Pipelines and failure handling¶
Each source has three independently controlled pipelines:
rtspsrc → depay → parse → compressed appsink
├─ buffer references → recorder appsrc → parse → splitmuxsink
└─ selected only → decoder appsrc → VA decoder → VAMemory appsink
├─ VPE → EGL tiles
└─ VPE → HIP → batch MIGraphX → GPU NMS
Compressed queues are limited to 16 MiB and approximately two seconds per receiver. Appsrc queues are also bounded. No source callback waits for disk I/O, AI or the GUI. A compressed backlog restarts the affected recording or decoder at a keyframe instead of dropping dependent pictures silently. Recording errors and quota exhaustion are visible; reception and selected display continue.
RTSP uses TCP, a 100 ms jitter latency and a five-second no-video watchdog. Reconnect delays grow from one to thirty seconds with jitter and repeat until the source is removed. Every reconnect gets a new generation and recording session. Decoder format changes are renegotiated; unsupported formats produce a visible decoder error. Old-generation detections are rejected. Resolution changes invalidate overlays, and results older than 500 ms are hidden.
Recording is the original encoded video, with parser framing converted as
required by Matroska. Files split at keyframes around segment_seconds, so a
long GOP can make a segment longer than the configured target. Filenames contain
UTC epoch microseconds, channel ID, session UUID and fragment number. Window close, SIGINT and SIGTERM perform graceful shutdown. Shutdown
sends EOS and allows up to five seconds for asynchronous muxers to finish.
SQLite catalogues files created by this application. The recorder root has an
exclusive writer lock. Once the managed files exceed quota_gib or available
space drops below reserve_gib, the oldest closed managed files are deleted.
Active recordings, unregistered files and symlinks are excluded. If no eligible
files can restore capacity, recording pauses and resumes at a keyframe when
space becomes available. After an abrupt shutdown, previous active entries
become deletion candidates; they are not claimed to be valid finalized MKVs.
retention_minutes defaults to 60 and removes inactive files by their known
end time; 0 disables time-based removal. Files spanning the cutoff or with
unknown duration are excluded. Playback/extraction handles protect files until
released. The VMS recording controls
provide retention editing and confirmed deletion of closed recordings.
GPU ownership and scheduling¶
Startup requires an observed increase of the dedicated VPE engine counter. The application keeps one shared GstVaDisplay and 16 persistent view slots. Preview and AI use separate VA processing contexts. The preview context is renewed when source or output geometry changes; AI does the same on source geometry changes. Preview downscaling is limited to the validated 6:1 range, with further window reduction performed by GL sampling. A minimized window therefore does not submit extreme reduction ratios to VPE.
Each slot owns a 640×640 linear BGRA VPE output with a persistent HIP mapping, plus three 2560×1440 linear BGRA preview surfaces imported into EGL once. Only the occupied rectangle is sampled; tile size changes do not reimport GPU allocations. The larger 16-slot preview pool adds about 295 MiB of pixel storage versus the former 1080p pool, before decoder/allocator overhead. Detector, ReID and embedding tensor sizes are independent of source resolution. GL fences protect preview surfaces from reuse while rendering. Selection changes reuse these pools, including across camera reconnects.
Preview conversion runs separately from inference. Each source keeps only its latest decoded buffer. HIP packs RGB NCHW directly into the corresponding slice of a reusable device batch arena; the shared batch executor then copies each slice GPU-to-GPU into its own batch input. There is no host-to-device tensor copy. The scheduler rotates across eligible sources, waits at most 20 ms for a batch, uses batch 1 for a single image, and fills unused slots of a larger batch with a HIP kernel. Only valid slots produce results. Postprocessing is performed independently per image on the GPU. Allocator layout probes may derive a newly-created, never-rendered test surface to discover pitches; they do not map or transfer frame pixels. The copy auditor counts these separately and still rejects derivation of any rendered surface. Each valid image reads back an 8404-byte compact detection structure (up to 300 boxes with their candidate index) as marked metadata; frame pixels remain on the GPU.
AI starts at 5 FPS per selected channel. Measured batch service time and frame
age adjust sampling, up to 30 FPS per channel, with a 200 ms age target. These
are scheduling targets, not throughput or maximum-latency guarantees. Each
result retains channel ID, a globally increasing generation and its geometry.
Inference does not retain a growing frame queue. The original amdpreproc and
amdmigraphx elements remain usable and share the same inference implementation.
Verification¶
ctest --test-dir build --output-on-failure includes the existing hardware and
metadata tests and catalog tests for ownership, quota, active protection,
symlinks, path traversal and concurrent-writer exclusion.
amdva-batch-reference and examples/nvr/tools/compare_batch_reference.py compare distinct
images and valid batch occupancies 1–4 against CPU FP32 ONNX. Fixture upload and
framebuffer capture are explicitly offline test operations, outside the live
video processing path.
amdva-nvr-rtsp-server H264_FILE H265_FILE [PORT] serves 32 looped local camera
paths, alternating H.264/H.265. Use pre-encoded 1080p30 files with a starting
keyframe and no B frames; the test server does not encode or decode video.
For arbitrary MP4/MKV files and per-file camera URLs, use
amdva-rtsp-files. It preserves file timing instead of assuming
1080p30 and accepts a text file list or positional paths.
For a 20-source configuration with the first 16 selected:
source scripts/env.sh
python3 examples/nvr/tools/nvr_soak.py /path/to/20-source-config.json \
/path/to/1080p30.h264 /path/to/1080p30.h265 \
--seconds=260 --cycles=100 --restart-at=100 \
--output=artifacts/nvr-dynamic-validation
python3 examples/nvr/tools/nvr_soak.py /path/to/20-source-config.json \
/path/to/1080p30.h264 /path/to/1080p30.h265 \
--seconds=3600 --restart-at=600 --output=artifacts/nvr-soak
The harness verifies the view limit, repeated add/remove/select/unselect,
recovery after server restart, live copy audit, recording finalization and MKV
metadata. It captures an actual EGL-rendered tile image and reports per-channel
rates and GPU batch timing. Resource monitoring covers RSS, deduplicated DRM
allocation counters and FDs, with a 64 MiB growth guard for RSS/GPU allocation
and a bounded allowance for asynchronous recording finalizers. Telemetry also
reports live and free glibc heap bytes. NVR entry points initialize GStreamer with
a default limit of four glibc heap arenas, before worker creation; an explicit
MALLOC_ARENA_MAX environment setting takes precedence. Native callers should
call initialize_runtime() before creating Qt or GStreamer workers. After
pipelines are retired or reconnected, the controller
waits for frame owners to drain and trims free heap pages, with a five-second
cooldown, to limit RSS retained by transient GStreamer worker arenas. It aborts before
unbounded growth can exhaust host memory. Recorded validation results are in
docs/dev/nvr/nvr-validation-results.json.
On Ryzen AI 9 HX 370 / Radeon 890M, the one-hour local RTSP test passed with 20 recording sources and 16 selected views, 100 dynamic source/selection cycles, and a server restart that swapped H.264/H.265 at the same URLs. All sources recovered in seven seconds. Median per-view display and AI rates after recovery were 29.88 and 5.98 FPS. All 1,304 finalized MKVs passed metadata validation; 60 frames from the latest recording of each of the 20 channels also decoded successfully. Live copy audit reported zero violations. After warm-up, maximum RSS and GPU allocation growth were 11,200 and 7,124 KiB, with no FD growth.
amdva-nvr-faults CONFIG.json uses the first source to exercise failed RTSP
registration, credential masking, 64×64 window resizing, recording-directory
permission failure and recovery. Use a dedicated recording directory; this test
temporarily changes its permissions.
This sample has no playback, ONVIF discovery, audio recording or 4K performance claim. A production NVR should additionally define crash repair, retention policy, monitoring, access control and camera-specific qualification.
The framework's optional keyframe-tests H264|H265 FILE OUTPUT_PREFIX executable
(AMDVA_HARDWARE_TESTS=ON) checks
single-AU VA decoding and writes extracted AUs for offline reference work.
Use B-frame/open-GOP fixtures as well as closed-GOP fixtures to distinguish
ordinary intra pictures from valid random-access pictures. Extracted AUs and
amdva-dump-tensor readbacks are offline test artifacts, not live processing.
examples/nvr/tools/compare_background_reference.py compares live background boxes with CPU
FP32 ONNX detections from matching offline keyframe tensors.
The background reference comparator checks class/count agreement, IoU >= 0.95
and score delta <= 0.03. With --allow-fp16-nms-ties, it also accepts a
CPU-suppressed overlapping candidate whose score is within 0.005 of the CPU
winner, retaining those geometric/confidence checks. This addresses measured
FP16 ranking changes rather than lowering the IoU threshold; the report records
both the canonical post-NMS IoU and the number of alternate winners. On the
second bus IDR, one low-confidence box differs from the FP32 NMS winner (IoU
about 0.93), while matching the alternate CPU candidate above 0.994. The existing
offline GPU executor reproduces the background result on that same tensor.
Background AI completed the one-hour 20-source / 16-view test with 100 dynamic cycles and codec-swapping reconnection, recovering receive/record/display in 7.5 seconds. All 1,309 MKVs passed metadata checks, and 60 frames from the latest recording of every channel decoded successfully. Copy-audit violations were zero; maximum RSS/GPU growth was 22.3/21.2 MiB, with at most four background jobs. Sequential 70-second runs with background AI enabled/disabled measured display medians of 29.86/29.90 FPS and selected AI medians of 5.98/5.98 FPS. These are local-fixture observations. Full results and reference-comparison conditions are in background AI validation results.
Decode and display timing¶
The optional telemetry configuration defaults to
{ "enabled": true, "stall_ms": 1000 }. NVR shows actual decoder-output and
new-image swap FPS plus input→decode/input→swap P99 columns; the FPS tooltip
contains detailed statistics. Hidden/minimized display observation pauses while
decode continues. Legacy SDK decoded/display_fps fields still describe
preview processing. Use Controller::telemetry() for actual stage counts,
latency, update intervals, stops and explicit discards. The headless command and
GUI validator add a top-level telemetry array to periodic JSON. There is no
latency/FPS pass/fail policy. See telemetry for exact boundaries
and the limitations of buffer-swap notification as a display measurement.
Person search¶
See NVR semantic search for native EmbeddingGemma 2 setup, collection policy, text and region queries, and inline result playback.
1440p validation (2026-10-22)¶
The original sixteen 2560×1440, 30 FPS, 8-bit HEVC files in ~/work/videos/
were served over RTSP without transcoding. A three-minute run exercised 16
visible cameras, four visible cameras and all cameras hidden in turn. All
inputs retained their original resolution; tracking settled at 1.98–2.02 Hz
per camera, with no tracking queue drops or camera errors. Process RSS was
about 6.3 GiB (peak 7.1 GiB). This is a short functional check, not a long soak.
The existing bounded YOLO, ReID and embedding input sizes were unchanged.
Text search, recording-region search, inline playback and deletion passed. Full-size playback produced 2560×1440 output; 100 alternating H.264 1080p / HEVC 1440p hover previews passed with zero copy-audit or VPE errors (p95 89 ms). At that revision, 1440p hover previews used a 428×240 GPU texture to respect VPE's 6:1 reduction limit, sampled into the former 320×180 popup by OpenGL. All 32 CTest tests also passed.
The prepared local launcher is bash artifacts/1440p-demo/run-demo.sh.
It serves the original videos on port 18570 and uses isolated demo settings
and recordings under artifacts/1440p-demo/ (these local artifacts are not
committed).