Add instrumentation to trace which stream type and workload precede the
occasional runaway memory growth in nph-zms.
- zm_utils: add zm_get_rss_kb() reading resident set size from
/proc/self/statm
- zms: log a one-shot Debug summary of the decoded request (source, mode,
monitor/event, connkey, scale, buffer, ttl, rss) so a runaway process
can be tied to its stream parameters
- MonitorStream/EventStream: emit a periodic Debug RSS trace every 30s.
The EventStream trace includes frames-vector size and frame_count,
which scale with reloaded event length and will show whether the
in-process event reload path drives the growth
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The command_processor thread was started in runStream() before
temp_image_buffer_count, the read/write indices and temp_image_buffer were
assigned, so a command arriving in that window observed a half-initialised
stream (the root cause behind the divide-by-zero guarded in the previous
commit). Keep the thread object declared up front for the join(), but defer
its launch until after the playback_buffer setup so processCommand can only
run against fully initialised state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MonitorStream::processCommand computed buffer_level by dividing by
temp_image_buffer_count (and via MOD_ADD modulo) while only guarding on
playback_buffer. processCommand runs on the command_processor thread,
started in runStream before temp_image_buffer_count is assigned from
playback_buffer. In that window temp_image_buffer_count is still 0 while
playback_buffer is already > 0, so a command arriving then divided by
zero and raised SIGFPE, crashing nph-zms.
Extract the percentage math into MonitorStreamBufferLevel which returns 0
when the buffer count is not positive, and add Catch2 coverage for the
zero-count and wrap-around cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avoid sleeping up to MAX_SLEEP (500ms) at the end of runStream when
termination has been requested, so zms shuts down more promptly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zm_terminate is written from signal handlers and read from daemon loops.
A plain bool is undefined to access from a signal handler and may be hoisted
out of a loop by the optimizer. std::atomic<bool> is lock-free on supported
platforms and matches the already-atomic per-object terminate_ flags. Update
the three Debug() vararg sites to call .load() since atomics can't pass
through ...
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added zm_get_pix_fmt_name() in zm_pixformat.h: av_get_pix_fmt_name()
returns nullptr for unknown formats (including AV_PIX_FMT_NONE), and
passing nullptr to %s is undefined and can crash. Wraps with an
"unknown" fallback. Replaced the five raw uses flagged by review
(RemoteCameraRtsp/FfmpegCamera/VncCamera/LocalCamera constructors and
MonitorStream::sendFrame's Debug log) with the wrapper. Each can hit
AV_PIX_FMT_NONE before the first frame or via an unexpected
(colours, subpixelorder) pair.
- Monitor::GetImage and Monitor::getSnapshot now route the slot read
through ReadShmFrame so the per-slot AVPixelFormat zmc recorded is
adopted on image_buffer[index] before its bytes are interpreted.
Without this, the JPEG encode in GetImage and the ZMPacket returned
by getSnapshot would interpret SHM bytes using the placeholder format
set at attach time and produce garbled output when the slot's actual
format differs (the contract previously relied on callers calling
ReadShmFrame themselves). getSnapshot/GetTimestamp drop const since
ReadShmFrame mutates image_buffer[index]; callers in this codebase
already use a non-const Monitor pointer.
- Monitor::connect: updated the misleading "+64 bytes reserved" comment
to match the actual reservation of 63 + (alignof(AVPixelFormat) - 1).
refs #4788
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When CMD_STOP is sent via AJAX using ZMS MJPEG streaming, the response
was incorrectly returning paused=true instead of indicating a stopped
state (paused=false).
Changes:
- Add `stopped` boolean to StreamBase (zm_stream.h)
- MonitorStream: CMD_STOP now sets stopped=true, paused=false instead
of paused=true; run loop skips frame sending when stopped
- EventStream: CMD_STOP sets stopped=true (was already setting
paused=false); run loop skips frame sending when stopped
- All other play/pause commands reset stopped=false
- Both streams include stopped field in the status response struct
- stream.php unpacks the new stopped field from MSG_DATA_WATCH and
MSG_DATA_EVENT responses
- MonitorStream.js handles stopped status in UI (shows 'Stopped' mode)
- EventStream.js tracks stopped state from server response
Fixes issue: CMD_STOP response paused=true should be paused=false
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/ba9cb47a-a3e8-4e13-aec7-c9cd258e2a3d
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
The deprecated Monitors.Colours column was driving the on-disk SHM
image_buffer slot format end-to-end: zmc allocated each slot in
camera->Colours()/SubpixelOrder() shape, zms attached and built its
own per-process Image objects assuming the same shape. Anything that
wrote a different format into a slot — i.e. anything that wasn't
already RGB32 on a Colours=4 monitor — left zmc's local Image with
the new format but zms reading the bytes as RGB32. The visible
artefacts varied with the format pair (4 small images per row for
YUV420P-into-RGB32, 3 tiles for the planar-as-RGB24 fallthrough,
fully garbled colours for image_pixelformats overlapping alarm_image,
etc.) but the underlying contract was just broken — the SHM had no
representation of the per-slot format.
Make the SHM transport any format Image supports without an upfront
convert step:
- Place image_pixelformats[] correctly. mem_size already reserved
image_buffer_count*sizeof(AVPixelFormat) at the end of the SHM
region, but the pointer was set to shared_images +
image_buffer_count*image_size — overlapping the alarm_image
region. zmc's writes scribbled into alarm_image; zms read alarm
pixel data back as enum values. Move it to
+2*image_buffer_count*image_size, after both image regions.
- Add Monitor::WriteShmFrame(index, capture_image): no conversion,
just Assign and record image_pixelformats[index] =
capture_image->AVPixFormat(). Used by both the normal Decode path
and the signal-loss path so they share the same cross-process
format-consistency contract.
- Add Monitor::ReadShmFrame(index): the read-side counterpart. Calls
image_buffer[index]->AVPixFormat(image_pixelformats[index]) before
returning so other-process Image objects interpret the SHM bytes
correctly per-frame. zm_monitorstream's image_buffer[index]
accesses go through this accessor.
- image_buffer[i] / alarm_image: initial format is now just a
placeholder (YUV420P), with allocation sized from
camera->ImageSize() as the upper bound. The actual format carried
in each slot is the one zmc wrote, recorded in image_pixelformats
and adopted on read.
This means the typical capture pipelines run with zero sws_scale
calls in the SHM hot path:
- H.264/H.265 sw decode -> YUV420P -> identity copy via
av_image_copy in Image::Assign(AVFrame*).
- LocalCamera/MJPEG -> RGB32 (DecodeJpeg target) -> SHM holds RGB32;
zms reads RGB32 directly.
- H.265 hwaccel -> NV12 -> falls through to YUV420P (NV12 isn't in
zm_pixformat) — one convert at decode time, then passthrough
through the rest of the pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When setStreamStart()->loadMonitor() failed (e.g. monitor id not found,
shm not yet mapped), zms continued into runStream() for non-SINGLE
stream types and dereferenced a null Monitor shared_ptr at
monitor->GetFPS(), crashing in get_capture_fps with fault address 0x618.
Bail early after the STREAM_SINGLE branch with a "Not connected" text
frame, mirroring the SINGLE-type recovery. For STREAM_JPEG, emit the
multipart Content-Type first so sendTextFrame produces a well-formed
response. Placed before openComms()/command_processor spawn so there
is no resource teardown to do on the bail path.
When a browser closes a streaming connection, fwrite to stdout raises
SIGPIPE. Without a handler the default action terminates the process
immediately, skipping exit_zm() and leaving the DB handle and log
unclosed.
Add zm_pipe_handler that sets zm_terminate so zms falls out of its
streaming loop and exits through the normal shutdown path. Clarify
the sendFrame comment to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a boolean analysis_image field to the CMD_QUERY status response that
reports whether zms is actually sending analysis images (with motion zone
overlays) or regular capture images. This lets MonitorStream.js detect
when the stream state is out of sync with what the client requested and
re-send the CMD_ANALYZE_ON/OFF command to correct it.
The field is true only when frame_type is FRAME_ANALYSIS, shared memory
is valid, and the monitor has analysis enabled — matching the same
condition used to select the image in the streaming loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The command processor thread could crash dereferencing monitor->shared_data
or monitor->trigger_data while the main thread was in loadMonitor() calling
disconnect() which munmaps shared memory and nulls those pointers.
Three fixes:
- Guard against null monitor at the top of processCommand
- Add monitor_mutex to StreamBase, held during disconnect/connect in
loadMonitor and during shared memory reads in processCommand, so the
command thread cannot access shared memory while it is being remapped
- Initialize status_data.analysing and status_data.score in the
!ShmValid branch where they were previously sent uninitialized
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SharedData field and methods to track when analysis images are being
viewed, mirroring the existing last_viewed_time functionality. This allows
the analysis process to know if someone is actively viewing analysis images.
- Add last_analysis_viewed_time to SharedData struct
- Add getLastAnalysisViewed(), setLastAnalysisViewed(), hasAnalysisViewers()
- Update MonitorStream to set flag when streaming FRAME_ANALYSIS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>