Capture paths that deliver a raw Image without an ffmpeg decode (e.g.
LocalCamera/V4L2) left packet->in_frame null even though the pixels were
already present, so anything expecting a decoded frame failed. In
particular YChannel analysis called get_y_image(), which needs
in_frame->data[0], and logged "Can't get y_image without frame".
At the end of Monitor::Decode(), when a packet has an image but no
in_frame, wrap the image's planes in an AVFrame via Image::PopulateFrame
(av_image_fill_arrays over a dont_free buffer ref: pointers, no copy).
Any format is populated; consumers that need a specific layout check for
themselves (get_y_image now reports RGB has no Y plane rather than "no
frame").
Done after PHASE 5 so the frame reflects the oriented/masked image and we
don't re-orient a shared Y plane, and after the codec phases so
transfer_hwframe is never called with the null codec context a
non-decoding camera has. videostore is unaffected: it prefers
packet->image for frame data and derives pts from packet->timestamp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single alarm_image slot with an analysis_image_buffer ring of
image_buffer_count Images living in the already-reserved alarm_images SHM
region. Successive WriteAlarmImage calls rotate through the ring and
publish last_analysis_index last (after the bytes and per-slot format),
so a reader sampling last_analysis_index always sees a fully written
slot. GetAlarmImage returns that slot, syncing its AVPixelFormat from the
per-slot analysis_image_pixelformats array.
SharedData gains last_analysis_index and analysis_image_count (plus 8
bytes of padding to keep the 16-byte-multiple layout), making it 888
bytes. The Perl (Memory.pm) and PHP (Monitor.php) SHM readers are updated
in lockstep, and a static_assert(sizeof(SharedData)==888) in zm_monitor.h
guards the layout against silent drift.
This lets multiple in-flight analysis/annotated frames be buffered and
streamed in sync rather than always overwriting one slot, and gives the
AI object-detection work a place to publish annotated frames.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The C++ SharedData struct is naturally aligned, not packed, so the
compiler inserts a 4-byte pad before capture_fps (after state) and
another before the startup_time union (after audio_channels). Monitor.php
used the naive packed offsets, so every field from capture_fps onward
(capture_fps/analysis_fps, latitude/longitude, the time fields,
alarm_cause and all of TriggerData) was read from an address 4-8 bytes
too low, yielding garbage.
Correct the offsets to the real aligned layout (SharedData is 872 bytes,
TriggerData starts at 872), matching what ZoneMinder::Memory computes and
what the C++ writes. No struct change; this is a reader-side fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the Add/Delete buttons on the AI Datasets, Models and Classes
option tabs the same treatment as the console: an add_circle icon on
the add button and a delete (trash) icon on the delete button, each
with a labelled text span.
Add the missing AddNewClass/AddNewDataset/AddNewModel language strings
so the add buttons read "Add New Class/Dataset/Model" instead of the
raw translation keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add three Options tabs (AI Datasets, AI Models, AI Classes) with full
CRUD, backed by the AI_* tables:
- List views (_options_ai_{datasets,models,classes}.php), edit modals
(ajax/modals/ai_{dataset,model,class}.php) and action handlers
(actions/ai_{dataset,model,class}.php).
- options.js loads the modals over ajax, wires the Add/edit buttons and
the AI Classes dataset filter.
- options.php dispatches the new tab includes; functions.php registers
the three tabs in the Options sub-menu with readable labels (they are
not Config categories, so they need explicit entries).
- actions/options.php routes object=ai_* deletes to the matching
handler; saves post directly to view=ai_*.
All tabs and actions are gated on System permission.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the schema backing per-monitor object detection and the AI dataset/
model/class management UI:
- Monitors gains AnalysisImageOpacity and ObjectDetection,
ObjectDetectionModel, ObjectDetectionObjectThreshold,
ObjectDetectionNMSThreshold columns.
- New tables AI_Datasets, AI_Models, AI_Object_Classes,
AI_Detection_Settings and AI_Detections.
- Seed the COCO 2017 dataset (80 classes) and default per-class
detection settings via db/coco_dataset.sql.
Existing installs get zm_update-1.39.17.sql, which is idempotent and
adds the columns with their final VARCHAR(16) ObjectDetection shape
directly (no enum-churn intermediates). Fresh installs create the same
objects from zm_create.sql.in sourcing AI_Models.sql and
coco_dataset.sql. Both paths were verified to produce identical schema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert the Level and Component filter dropdowns on the system log view
into Chosen multi-selects so several values can be filtered at once. An
empty selection means "All" (shown via the placeholder), replacing the
former explicit All option.
- log.php: emit filterLevel[]/filterComponent[] with multiple + placeholder,
read the remembered selection as an array (tolerating the legacy scalar),
and drop the leftover ZM\Debug dumps of the component list
- js/log.js: send level/Component as arrays when non-empty; drop the
redundant filterComponent change binding now that data-on-change wires it
- ajax/log.php: match with Level IN (...) / Component IN (...) using
parameterized placeholders, and persist the selections as arrays in the
session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "realtime=1" (alias "re=1") ffmpeg input option, modeled on the
existing "loop" option and equivalent to ffmpeg's -re flag. When set on a
file source, FfmpegCamera throttles packet delivery to the rate implied by
the stream timestamps instead of reading the file as fast as possible.
The option is parsed out of the ffmpeg Options string in OpenFfmpeg and
consumed so it is not passed to the demuxer or decoder. Capture() anchors
wall-clock time to the first delivered packet and sleeps before each
subsequent packet so it is not delivered ahead of schedule. Pacing uses dts
(monotonic in read order) with a pts fallback, runs after the existing
drop/jump filters, and re-anchors on backward jumps or gaps beyond a 10s cap
to avoid stalling on a discontinuity. Works alongside loop, whose
offset-adjusted timestamps stay monotonic across restarts.
The pacing decision is factored into a pure ComputeRealtimePace() function
and unit-tested in tests/zm_ffmpeg_camera.cpp (8 cases covering full/partial
interval waits, behind-schedule, on-schedule, backward jump, over-cap, and
the cap boundary).
Ported from the ai_server branch onto master's OpenFfmpeg structure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an ffmpeg monitor reads a seekable file source (e.g. file:///x.mp4)
and reaches the end, it previously failed and reconnected. Add a loop
mode, enabled with loop=1 in the monitor Options, that seeks back to the
start and continues instead.
Packet pts/dts are shifted by a per-stream absolute offset recomputed on
each loop (last emitted dts + frame duration - stream start_time) so
timestamps stay monotonically increasing for analysis and recording. The
loop option is consumed before passing options to ffmpeg (both the demuxer
in OpenFfmpeg and the decoder), and non-seekable inputs fall through to the
normal reconnect path.
Ported from the ai_server branch onto master's OpenFfmpeg structure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a go2rtc player connects but the source video codec cannot be decoded by
the browser (e.g. an H.265 camera viewed in Chrome, which supports HEVC over
neither WebRTC nor MSE), go2rtc negotiates the video track as inactive and
sends only audio. The video element "plays" audio with no picture and stays at
0x0, so the normal 'error' handler never fires and the player hangs on
"Loading..." indefinitely.
Add a watchdog armed when a go2rtc stream starts: if no video frame is decoded
within NO_VIDEO_TIMEOUT, register a playback error and advance through the
existing player-priority fallback (eventually ZMS MJPEG), which can render the
stream server-side. The watchdog is cleared on stop()/restart() so it never
fires against a subsequently selected player.
Verified in-browser on a 3840x2160 H.265 monitor: go2rtc_webrtc -> go2rtc_mse
-> zms, ending on a visible MJPEG image instead of an endless "Loading...".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the Watch page with Scale=Auto, the live status bar is constrained to the
scaled video width. For a wide/high-resolution camera shown via the go2rtc
player, the Auto fit shrank the video, which narrowed the status bar, which
wrapped its items onto more lines and grew taller. scaleToFit() counts that
height as overhead and shrank the video further on the next ResizeObserver
pass, so the video collapsed in visible steps (e.g. 249->181->29px, then
snapping back to full width via the negative-height fallback, and repeating).
Keep each status item on a single line and truncate with an ellipsis so the
status bar has a fixed height. This breaks the feedback loop; the Auto fit now
converges on the first pass.
Verified in-browser on a 3840x2160 go2rtc monitor: height stabilises instead
of oscillating.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The REST API archive action toggled an event's Archived
(retention-protection) flag with no authorization beyond the
controller's coarse "Events permission is not None" gate. Any
authenticated read-only user, including one restricted to a subset of
monitors, could flip the retention state of any event by enumerating
event ids, and the action was reachable over GET (CSRF-able).
Gate the write by direction: archiving (protects from purge) requires
view access via Event::canView(); un-archiving (re-exposes to purge)
requires edit access via Event::canEdit(). Both enforce the per-monitor
object-level ACL. Restrict the action to POST/PUT to block CSRF.
Addresses GHSA-5v9h-ww7p-hxgv.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the menu customization work from the ai_server branch onto master:
- Add/edit/delete custom navbar/sidebar menu entries via Options > Menu
- Per-entry Link column (new Menu_Items.Link) with ?view= fallback derived
the same way as built-in items; custom entries render via buildMenuItem
- Live icon preview (material/font awesome) with fixed-width preview cell
- Per-row delete icon; add/reset buttons with tooltips
- Surface DB errors when saving options config: capture the swallowed PDO
error via dbLastError() and show it in the page error banner instead of
redirecting past it
Menu_Items.Link is added to zm_create.sql.in and as an idempotent ALTER in
zm_update-1.39.16.sql.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
avformat_new_stream() can return NULL, and the encode/transcode branch
dereferenced it immediately via ->codecpar when calling
avcodec_parameters_from_context, segfaulting zmc on the Event recording
thread (SIGSEGV, fault address 0xc). The encoder opens fine; it is the
output stream allocation that comes back NULL.
Check the return value and fail open() gracefully, matching the guard
already present on the PASSTHROUGH path a few lines above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The C++ and PHP parsers for zm.conf and /etc/zm/conf.d/*.conf used fgets
with a 512-byte buffer, silently truncating any line longer than that.
There was also no way to split a value across multiple lines, so editors
hit the cap with no workaround.
Accept a trailing backslash (with optional whitespace before the newline)
as a line-continuation marker. Leading whitespace on continuation lines
is stripped so users can indent for readability without it leaking into
the value. Three parsers all read the same files and must agree:
- src/zm_config.cpp: switch to std::ifstream + std::getline so a single
physical line is no longer capped at 512 bytes, then join continuation
lines before running the existing pointer-based parser
- scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in: accumulate a logical
line across trailing-backslash physical lines
- web/includes/config.php.in: drop the 512-byte fgets cap and accumulate
in the same way
tests/zm_config.cpp covers three cases against the C++ parser: a
three-segment continuation joins to "firstsecondthird" with leading
whitespace stripped, a bare backslash inside a value is preserved
(C:\Users\zm), and a 1500-byte single line survives intact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The insert branch only logged via ZM\Error and returned, leaving
$error_message empty. The dispatcher then re-rendered views/monitor.php
with no banner and the user saw an apparent success that silently
discarded their new monitor.
Append $monitor->get_last_error() to $error_message so the existing
<div id="error"> in getBodyTopHTML() shows the actual DB message, and
keep the early return so the user stays on the edit view with the form
values preserved by views/monitor.php's $_REQUEST['newMonitor'] handling.
Matches the pattern already used on the update path at line 273.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix: When Go2RTC generates an error message (not a Go2RTC error itself!), also register the error using the streamErrorRegistration() method and restart the stream. (video-stream.js)
zm_eventstream.cpp uses std::filesystem, which compiles into libzm.a.
On Rocky 8 (GCC 8) std::filesystem lives in a separate libstdc++fs that
must be linked explicitly. FILESYSTEM_LIBRARY was only attached to zma,
so zmc, zms, zmu, zmbenchmark and zm_rtsp_server failed to link with
undefined references to std::filesystem symbols.
Attach FILESYSTEM_LIBRARY to the zm library as PUBLIC so all consumers
inherit it transitively, and drop the redundant entry on zma.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Target image is already colourised, colours: 1" warning fired from
Image::Colourise() during zone alarm overlay on YUV420P monitors, even with
ZM_COLOUR_JPEG_FILES off.
monitor->Colours() returns 1 for both GRAY8 and planar YUV420P (the GRAY8
alias), so zm_zone.cpp misclassified YUV420P monitors as grayscale and built
an RGB24 alarm highlight. Overlaying that RGB24 highlight onto the YUV420P
analysis_image hit the RGB-on-mono branch, which calls Colourise() - valid
only for GRAY8. Colourise() warned and bailed, after which the per-pixel loop
walked the 1.5x planar buffer as 3x packed RGB (out-of-bounds write).
Resolve the real capture format via zm_pixformat_from_colours() so true GRAY8
still upgrades to an RGB24 highlight while YUV420P keeps its format. Add a
YUV420P output branch to HighlightEdges (single alarm colour written to luma
and the shared chroma sample, transparent elsewhere) and a YUV420P-on-YUV420P
branch to Overlay (copies luma where non-zero, copies each chroma sample when
any covered luma pixel is marked). Colourise() is now only reached for GRAY8.
Add Catch2 coverage for the YUV420P Overlay masking and HighlightEdges output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First, this will speed up the display of the actual status.
Second, it will prevent incorrect status indication if rows were selected while executing the next AJAX request.
Also, update the status to "awaiting" only if the previous status was "stopped." This will ensure the status is displayed correctly on the first page refresh.