Commit Graph

1447 Commits

Author SHA1 Message Date
Isaac Connor
fb7b1d1c77 fix: SHM padding covers both alignment adjustments (PR #4788)
mem_size only reserved 64 bytes of slack for the 64-byte alignment of
shared_images, but the code subsequently rounds image_pixelformats up
to alignof(AVPixelFormat) too. In the worst case shared_images shifts
by 63 bytes and image_pixelformats shifts by alignof(AVPixelFormat)-1
more, which could push image_pixelformats / alarm_image_pixelformat
past the end of the mapped region. Reserve 63 + (alignof(AVPixelFormat)
- 1) bytes so both adjustments fit.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 08:27:12 -04:00
Isaac Connor
f794e260cc fix: per-row Overlay; align image_pixelformats SHM pointer (PR #4788)
- Image::Overlay (no-offset variant): rewrite all eight format branches
  to walk row-by-row using each image's own linesize. The previous
  linear walk with `buffer + size` as the end pointer drifted across
  rows whenever this->linesize != image.linesize (which can happen for
  images constructed via held-buffer ctors at a different stride) and
  also walked into the destination's chroma planes for planar YUV
  destinations whose `size` covers chroma. Inner loops bound by `width`
  per row in every branch, so per-row padding is left untouched.
- Monitor::connect: align the image_pixelformats SHM base address up
  to alignof(AVPixelFormat) before casting. shared_images +
  2*image_buffer_count*image_size can be misaligned when image_size
  isn't a multiple of alignof(AVPixelFormat) (e.g. GRAY8 with odd
  width sourced from camera->ImageSize() before the SHM upper-bound
  applies). An unaligned AVPixelFormat* is undefined behaviour on
  strict-alignment ISAs and slow even on x86. The +64-byte padding
  reserved in mem_size already covers the small shift.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:54:31 -04:00
Isaac Connor
77edeca57c fix: use linesize for row stride in pixel ops (PR #4788)
The FFALIGN(linesize, 32) introduced by the AVPixelFormat migration adds
per-row padding for non-32-aligned widths. Functions that walked pixel
buffers using width*bytes_per_pixel as the row stride wrote into padding,
shifted rows sideways, or under-allocated output buffers for those
widths. Convert each affected pixel op to use the Image's linesize as
the per-row stride. Alignment is kept for SIMD/perf; only the consumer
addressing is fixed.

- Image::MaskPrivacy: reset the row pointer to `buffer + y*linesize`
  each iteration instead of monotonically incrementing across rows.
- Image::Annotate (GRAY8/RGB24/RGB32 branches): use linesize for the
  per-row advance and char-block reset. The RGB32 branch additionally
  uses `linesize / sizeof(Rgb)` as the Rgb-typed stride.
- Image::Fill and Image::Fill(colour, density, ...): compute each row's
  base address as `buffer + y*linesize + lo_x*colours` instead of
  `colours*(y*width + lo_x)`.
- Image::Colourise: allocate the output buffer with
  av_image_get_buffer_size(target_fmt, w, h, 32) and copy row by row
  using src_linesize / dst_linesize. The previous tightly-packed
  allocation undersized the buffer (so AssignDirect would reject it)
  and smeared pixels across row boundaries for non-aligned widths.
- Image::Deinterlace_Discard (all three colour branches): index source
  and destination rows by y*linesize instead of (y*width)*bytes.
- Monitor::CheckSignal: convert the random linear index to (x, y) and
  address as y*LineSize() + x*bytes_per_pixel, so samples don't read
  per-row padding or land on the wrong row.
- Monitor::Capture signal-loss path: rewrote the publish-order comment
  so it matches the actual code (WriteShmFrame, then timestamp, then
  last_write_time, then last_write_index as the commit step).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:17:56 -04:00
Isaac Connor
766b3c72fb fix: cross-process AVPixelFormat sync for alarm_image (PR #4788)
alarm_image's bytes live in SHM but its Image metadata
(imagePixFormat/linesize/size) was per-process. Readers (zms) constructed
alarm_image with a hardcoded YUV420P placeholder at attach time and there
was no path for the writer (zmc/zma) to publish the actual format it
wrote. The earlier comment pointed at image_pixelformats[alarm_index],
but image_pixelformats[] only tracks the ring-buffer slots and no
alarm_index exists. The result: when the writer assigned an RGB24/RGBA
frame, reader streaming/encoding interpreted the bytes as YUV420P and
produced garbage.

Fix:
- Allocate one extra AVPixelFormat slot at the end of SHM
  (alarm_image_pixelformat) and point a new Monitor member at it.
- Add Monitor::WriteAlarmImage(const Image&) helper that mirrors
  WriteShmFrame's contract for the alarm slot: copy bytes, then publish
  the canonical PixFormat(). Replace the two direct alarm_image.Assign()
  call sites in Analyse() with WriteAlarmImage().
- Make Monitor::GetAlarmImage() sync alarm_image's format from
  *alarm_image_pixelformat before returning, with the same defensive
  validation ReadShmFrame uses (recognised enum + required size fits
  shm_slot_size, otherwise keep current format with a warning).
- Initialise both image_pixelformats[] and alarm_image_pixelformat to
  AV_PIX_FMT_NONE in the CAPTURE-side memset path, since memset's 0
  collides with AV_PIX_FMT_YUV420P rather than the NONE sentinel readers
  expect for "format not yet published".

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:02:16 -04:00
Isaac Connor
158d9433d3 fix: address PR #4788 review comments on planar sizing and SHM bounds
- Image::Assign(raw buffer overload): derive new_size from
  av_image_get_buffer_size(pix_fmt, w, h, 32) instead of
  width*height*colours, which undercounts planar formats (YUV420P/J420P/
  YUV422P/J422P) and would silently truncate U/V planes. Update linesize
  in the same branch so LineSize() consumers (JPEG encode, overlay) stay
  in sync with the new format. Reject AV_PIX_FMT_NONE and negative
  av_image_get_buffer_size returns up-front.
- Image::AVPixFormat(AVPixelFormat) setter: validate the requested
  format via zm_colours_from_pixformat() and check both av_image_*
  return values before mutating any state. On failure, leave
  imagePixFormat/size/linesize untouched so callers can recover instead
  of inheriting wrapped-unsigned junk.
- Image::Assign(const Image&) stride-mismatch branch: refuse planar
  formats (YUV420P/J420P/YUV422P/J422P) explicitly. The per-row copy
  only touches the Y plane; doing it silently on planar input leaves
  U/V uninitialised in the destination and produces solid-green output
  downstream. Caller must reconvert or reallocate.
- Monitor::connect: size SHM slots to an upper bound across every
  supported AVPixelFormat (RGBA at w*h, align=32) instead of the
  monitor's configured camera->ImageSize(). Prevents "Held buffer is
  undersized" failures when the no-conversion pipeline transports a
  format larger than the legacy DB Colours selection (e.g. YUV422P or
  RGBA into a GRAY8-configured monitor). Store the chosen capacity on
  the Monitor as shm_slot_size.
- Monitor::ReadShmFrame: validate image_pixelformats[index] both as a
  recognised enum AND that its av_image_get_buffer_size fits
  shm_slot_size before calling AVPixFormat(fmt). Treats a corrupted or
  torn SHM value as ignorable instead of letting it overrun the held
  slot. Single zm_colours_from_pixformat() call (no longer duplicated).
- web/.../monitor.php: inline fallback for
  translate('DeprecatedColoursSetting') so non-en_gb locales render an
  English string instead of the literal key name.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:10:22 -04:00
Isaac Connor
d6a146e1d7 fix: address PR #4788 review comments on SHM publish ordering and overlay bounds
- Image::Overlay: in the 1-byte-per-pixel branch, bound the loop by
  std::min(height*linesize, image.height*image.linesize) instead of
  `buffer + size`. For planar YUV destinations `size` includes chroma
  planes, while GRAY8 sources have only Y bytes; the old loop read past
  image.buffer and clobbered the destination's chroma. Restricting to the
  smaller of the two Y-plane spans keeps the overlay in the luma plane
  and never over-reads the source.
- Monitor::ReadShmFrame: image_pixelformats[] lives in SHM and is written
  by another process, so its value must be treated as untrusted. Validate
  via zm_colours_from_pixformat() before passing into AVPixFormat(fmt) —
  an unsupported enum value would make av_image_get_buffer_size return
  negative and wrap into the Image's unsigned size/linesize members.
  Compare against PixFormat() (canonical imagePixFormat) instead of the
  deprecated AVPixFormat() getter.
- Monitor::Capture signal-loss path: reorder SHM publishing so the slot
  bytes and per-slot shared_timestamps[index] are written first, then
  last_write_time is set from packet->timestamp (the fresh value, not
  the stale tv_sec the slot held from its previous occupant), and
  last_write_index is assigned last as the commit step. Readers gate on
  last_write_index, so all per-slot state must be visible before that
  final assignment.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:40:00 -04:00
Isaac Connor
1fe29202f5 fix: address PR #4788 review comments on AVPixelFormat migration
- Image::Assign: when source linesize > destination linesize, copy only
  the destination's row capacity per line; previously copied
  image.linesize bytes into rows of smaller linesize, overflowing the
  destination buffer on the last row.
- Monitor::CheckSignal: dispatch the 1-byte-per-pixel branch via
  zm_bytes_per_pixel(pix_fmt) == 1 so YUV422P/J422P are sampled on the
  Y plane like GRAY8/YUV420P, instead of falling through and reporting
  "no signal".
- Monitor::WriteShmFrame: record image_pixelformats[index] via
  capture_image->PixFormat() (canonical imagePixFormat) instead of
  AVPixFormat(), which re-derives from the deprecated
  (colours, subpixelorder) pair and could propagate stale metadata.
- Monitor::connect: rewrite stale comment that claimed readers don't
  need per-slot pixformat adoption; readers MUST call ReadShmFrame()
  to adopt the actual format zmc wrote.
- Camera::Camera: guard linesize/imagesize derivation against
  AV_PIX_FMT_NONE and negative returns from av_image_get_linesize /
  av_image_get_buffer_size, falling back to width * colours stride so
  unsigned wrap-around can't break SHM sizing.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 19:36:20 -04:00
Isaac Connor
a7c3fb7ce6 Merge branch 'master' into fix/avpixelformat-respin 2026-05-21 21:11:40 -04:00
Isaac Connor
5153dc68ee fix: flush decoder_queue on decoder thread exit to avoid stale latency offset across reconnect
The decoder thread holds a raw AVCodecContext* obtained from
camera->getVideoCodecContext() and pushes packet locks into Monitor's
decoder_queue for each send_packet() that hasn't yet been matched by a
receive_frame(). Monitor::PrimeCapture() used to call camera->PrimeCapture()
(which Close()s the camera and frees the codec context) without first
stopping the decoder thread. Two problems followed:

1. Use-after-free race between the decoder thread and the camera teardown.
2. The stale decoder_queue entries survived the reconnect. The new codec
   context produced frames in send-order, so we popped the oldest stale
   entries to attribute frames that actually came from packets sent later.
   The net effect was a permanent N-packet offset between capture and
   decode (~92 packets observed in the field). Analysis blocks on
   !packet->decoded for those packets, so the packetqueue saturates at
   max_video_packet_count and stays there, spamming the "max video packets
   in the queue" warning forever.

Fix:
- Stop+Join the decoder in Monitor::PrimeCapture() before tearing down the
  codec context.
- Add Monitor::flushDecoderQueue() which marks in-flight packets decoded,
  notifies waiters, and clears the queue.
- Call it at the end of DecoderThread::Run() so any Stop()+Join() (including
  the existing one in Pause()) naturally releases stale entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:38:58 -04:00
copilot-swe-agent[bot]
b799962e2a fix: ensure trigger_state written last in zmTriggerEventOn to fix 1/3 event trigger rate
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/68794b2b-7137-4888-b66e-20c629112a57

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-04 13:22:13 +00:00
Isaac Connor
c8d8cc384a feat: SHM cross-process format consistency via per-slot AVPixelFormat
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>
2026-05-03 13:03:53 -04:00
Isaac Connor
7b21371083 Reapply "fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch"
This reverts commit 60b6b37c60.
2026-05-03 12:24:43 -04:00
Isaac Connor
60b6b37c60 Revert "fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch" 2026-05-02 18:26:48 -04:00
Isaac Connor
81519fb2f1 Merge pull request #4742 from ZoneMinder/fix/deprecate-zm-colour-use-avpixelformat
fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch
2026-05-02 18:24:37 -04:00
Isaac Connor
11544d86e3 fix: PR #4742 round 2 review feedback
Eight Copilot comments from the second review pass:

1. monitor.php (#3): "Deprecated - will be auto-detected..." note next
   to TargetColorspace bypassed translate(). Added DeprecatedColoursSetting
   key to en_gb and routed the deprecation note through translate() so it
   localises with the rest of the form.

2. tests/zm_pixformat.cpp (#4): zm_colours_from_pixformat / round-trip
   tests didn't cover the new YUV422P/YUVJ422P entries (added by
   02e6be6b4). Added explicit assertions in both test cases — bumps
   pixformat coverage from 105 to 115 assertions.

3. zm_image.cpp WriteBuffer (#5): linesize and size were derived from
   p_width * p_colours, which undercounts planar YUV* (where p_colours=1
   via the GRAY8 alias collision but actual buffer needs ~1.5x/2x for
   chroma). Use av_image_get_buffer_size and av_image_get_linesize for
   the AVPixelFormat instead, with bail-out on either failing.

4. zm_image.cpp AssignDirect (#6, #7): av_image_get_buffer_size returns
   int and can be negative; assigning that into unsigned size/allocation
   wrapped to a huge value. Check the return first, treat negative as the
   same "unsupported format" failure as zm_colours_from_pixformat
   returning false, and reset size/allocation/linesize/pixels to 0
   (alongside imagePixFormat=NONE/colours=0/subpixelorder=0) so the
   Image is left in a single coherent invalid state instead of partially
   stale.

5. zm_image.cpp Assign (#8): av_get_pix_fmt_name(format) can return
   nullptr (e.g. AV_PIX_FMT_NONE / unknown); passing that into
   Debug(..., "%s", ...) would segfault. Capture once with a fallback
   string before logging.

6. zm_monitor.cpp Capture path (#9): same nullptr issue with two Debug
   calls — capture native_fmt_name once with fallback.

7. zm_monitor.cpp can_passthrough comment (#10): comment claimed
   YUVJ422P would be converted to YUV420P because Image drops chroma,
   but can_passthrough now allows YUV422P/YUVJ422P passthrough since
   02e6be6b4 added 4:2:2 support. Updated the comment to describe the
   current behavior (full 4:2:0 + 4:2:2 planar passthrough plus GRAY8
   and RGB24/32) so the code and the rationale agree.

Tests: 76 cases, 788 assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:59:51 -04:00
Isaac Connor
b2b50aba5d perf: skip clearPackets early-returns and allow drop-to-iterator-keyframe
Two related changes to PacketQueue::clearPackets, called by the analysis
thread on every video packet:

1. Lock-free call-site gate (should_try_clear) on the analysis path.
   In keep_keyframes mode the existing early-return at the top of
   clearPackets discards most non-keyframe video packets after acquiring
   the queue mutex. Add an inline lock-free check at the call site so
   non-keyframe packets skip the mutex acquire entirely. clear_packets_pending_
   is now std::atomic<bool> so it can be read without the lock; a stale
   read is harmless (at worst we make one extra cheap early-returning call).
   The !keep_keyframes path always returns true from the gate because that
   mode pops one packet at a time on every video packet.

2. Iterator boundary in the scan loop changed from >= to >. Setting
   next_front to a packet that an iterator points at is safe because
   clearPackets deletes strictly before next_front, so the iterator's
   own packet stays in the queue. Previously, an event-start (or other)
   iterator landing exactly on a keyframe blocked the leading GOP from
   being dropped until the iterator advanced; now we can include that
   keyframe as next_front while the iterator continues to point at it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:39:04 -04:00
Isaac Connor
390903ebaf fix: YUV422P passthrough and libjpeg grayscale for planar YUV
- Add YUV422P/YUVJ422P subpixelorder constants and proper mapping in
  zm_pixformat.h so MJPEG cameras keep full chroma through the pipeline
- Fix decoder to only passthrough formats Image handles with full color
- Fix libjpeg EncodeJpeg/WriteJpeg: use zm_bytes_per_pixel==1 instead
  of GRAY8-only check so planar YUV images encode as grayscale JPEG
  (fixes "split in 3" artifact on inactive monitor placeholder)
- Fix sendTextFrame: use GRAY8 (not YUV420P) for text-on-black frames

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 10:52:40 -04:00
Isaac Connor
cfabcbaf8c fix: use decoded frame's native pixel format instead of Monitor.Colours
The decoder was creating packet->image using camera->Colours() from the
DB (typically RGBA), forcing an expensive conversion from the decoded
frame's native format (typically YUV420P). This was wasteful — the
camera already decoded to a usable format — and caused 3x larger shared
memory frames, worsening the live-view race condition.

Use the in_frame's native pixel format directly. Only fall back to the
DB format if the native format is unrecognised. For a typical h264
camera producing YUV420P, this eliminates the YUV420P->RGBA conversion
entirely and keeps frames at 5.5MB instead of 14.7MB (for 2560x1440).

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 09:23:06 -04:00
Isaac Connor
b416aa5293 fix: replace ZM_COLOUR_*/ZM_SUBPIX_ORDER_* with AVPixelFormat for format dispatch
ZM_COLOUR_GRAY8, ZM_COLOUR_YUV420P, and ZM_COLOUR_YUVJ420P were all
defined to 1, making format identification via colours ambiguous.
LocalCamera misidentified YUV420P as GRAY8, causing V4L2 MJPEG cameras
to decode to grayscale via expensive sws_scale conversion.

Replace the legacy ZM_COLOUR_*/ZM_SUBPIX_ORDER_* integer pair with
AVPixelFormat as the single source of truth for pixel format dispatch:

- Add src/zm_pixformat.h with central format helpers:
  zm_pixformat_from_colours, zm_colours_from_pixformat,
  zm_bytes_per_pixel, zm_db_colours_to_pixformat, zm_is_rgb32,
  zm_is_rgb24, zm_is_yuv420
- Add AVPixelFormat pixelFormat member + PixelFormat() accessor to Camera
- Add PixFormat() accessor to Image, delegate AVPixFormat methods
  to shared helpers
- Migrate all ~100 format dispatch comparisons in zm_image.cpp,
  zm_local_camera.cpp, zm_ffmpeg_camera.cpp, zm_remote_camera_rtsp.cpp,
  zm_libvlc_camera.cpp, zm_libvnc_camera.cpp, zm_monitor.cpp,
  zm_mpeg.cpp from colours/subpixelorder checks to imagePixFormat/
  AVPixelFormat checks
- Deprecate GetFFMPEGPixelFormat, delegate to zm_pixformat_from_colours
- Fix DeColourise bug: imagePixFormat was not updated to GRAY8
- Deprecate ZM_COLOUR_* and ZM_SUBPIX_ORDER_* constants in zm_rgb.h
- Add deprecation notice on Monitor.Colours web UI dropdown
- Add 13 Catch2 test cases (105 assertions) for format mapping helpers

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:42:06 -04:00
Isaac Connor
33a2148c7c Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-04-02 14:17:21 -04:00
Isaac Connor
af33926aa3 fix: reset last_write_index in Pause to restore DECODING_ONDEMAND bootstrap
Pause() did not restore last_write_index to the sentinel value
(image_buffer_count). After a Pause/Play cycle, the DECODING_ONDEMAND
fallback condition (last_write_index == image_buffer_count) was dead,
making decoding depend entirely on hasViewers(). This created a timing
gap where the decoder skipped packets after Play before zms called
setLastViewed, causing the decoder to fall behind capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:45:08 -04:00
Isaac Connor
1ec9c449b4 fix: null-check StorageId column in Monitor::Load to prevent segfault
dbrow[col] for StorageId can be NULL when the database column contains
a NULL value. Passing NULL to atoi causes a segfault in strtol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:13:50 -04:00
Isaac Connor
0e78b5a6c6 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-03-26 17:20:23 -04:00
Isaac Connor
277eece633 Log the value when event_close_mode is unkown 2026-03-26 17:19:59 -04:00
Isaac Connor
9adbed166e fix: add missing CLOSE_DURATION event close mode handling
- Add "duration" string mapping in constructor and DB loader — was
  silently falling back to CLOSE_IDLE
- Add CLOSE_DURATION handler in analysis logic: close event when
  duration >= section_length regardless of alarm state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:01:14 -04:00
Isaac Connor
1b689d6e67 fix: include alarm_frame_count in ready_count calculation
ready_count only considered warmup_count and pre_event_count, but
openEvent walks back max(pre_event_count, alarm_frame_count) frames.
When pre_event_count=0 and alarm_frame_count=2, analysis started before
the queue had enough packets, causing spurious "Hit end of packetqueue
before satisfying pre_event_count" warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:32:28 -05:00
Isaac Connor
635439eb5b feat: add zma utility for offline event re-analysis
Add a new zma binary that re-analyses recorded events using current zone
settings. It decodes frames from stored video (FFmpeg) or JPEG files and
runs the full motion detection pipeline (DetectMotion + ref_image blending
+ state machine).

Two modes of operation:
- Default: updates the original event's motion stats (AlarmFrames,
  TotScore, AvgScore, MaxScore) in the database
- --create-events (-c): creates new events from detected motion regions,
  with video files hard linked (copy fallback) to the new event directory

Additional features:
- --save-analysis (-a): writes analysis JPEGs with zone alarm overlays
  to the event directory for visual inspection
- --monitor (-m): override which monitor's zone config to use
- --verbose (-v): increase debug verbosity

Adds Monitor::AnalyseFrame() public methods that encapsulate DetectMotion
+ ref_image initialization and blending, with an optional analysis_image
output parameter for zone overlay rendering. Also guards shared_data
access in DetectMotion to allow offline use without shared memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:19:20 -05:00
Isaac Connor
66628ae163 perf: replace sleep polling with condition wait in Event::Run()
The event thread was sleeping 33ms (ZM_SAMPLE_RATE) between checks for
packet decoded/analyzed status. Replace with a condition variable wait
on the packet itself, using a 2ms timeout as safety net for the race
between flag check and wait entry.

Add packet->notify_all() at every site where decoded or analyzed is set
to true, so the event thread wakes up near-instantly. Add wait_for()
to ZMPacketLock to support timed waits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:38:07 -05:00
Pliable Pixels
e97a37df29 fix: preserve legacy execlp() behavior for commands without % tokens
If EventStartCommand/EventEndCommand contains a % character, use the
new token substitution (%EID%, %MID%, %EC%) with sh -c execution.
Otherwise, fall back to the original execlp() behavior that passes
event_id and monitor_id as $1 and $2, so existing installs are not
broken.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:38:07 -05:00
Pliable Pixels
cf3f44a466 fix: shell-escape %EC% token to prevent command injection
cause can include trigger_data->trigger_cause (writable via zmtrigger
over the network) and zone labels (user-configured). Without escaping,
shell metacharacters in cause would be interpreted by sh -c.

Wraps cause in single quotes (with embedded single-quote escaping)
before substitution. %EID% and %MID% are safe as they are always
numeric from std::to_string.

Note on backward compatibility: the old execlp() passed event_id and
monitor_id as argv[1]/argv[2]. This PR intentionally does not preserve
that behavior — the old execlp() treated the entire command string as
an executable path, making it impossible to pass arguments, so any
working setup was already a simple path with no args. Users should
migrate to %EID%/%MID% tokens which are more explicit and flexible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:38:07 -05:00
Pliable Pixels
7f9fc2ee2f feat: support token substitution in EventStartCommand/EventEndCommand
Replace execlp() with execl("/bin/sh") and substitute %EID%, %MID%,
and %EC% tokens before execution. This allows users to pass arguments
directly in the command string, e.g.:

  /path/to/zm_detect.py -c /etc/zm/config.yml -e %EID% -m %MID% -r "%EC%" -n

Previously execlp() treated the entire command string as the executable
path, making it impossible to pass arguments without a wrapper script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:38:07 -05:00
Isaac Connor
0f7aceb19e perf: replace sleep polling with condition wait in Event::Run()
The event thread was sleeping 33ms (ZM_SAMPLE_RATE) between checks for
packet decoded/analyzed status. Replace with a condition variable wait
on the packet itself, using a 2ms timeout as safety net for the race
between flag check and wait entry.

Add packet->notify_all() at every site where decoded or analyzed is set
to true, so the event thread wakes up near-instantly. Add wait_for()
to ZMPacketLock to support timed waits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 09:44:42 -05:00
Isaac Connor
08144613fc Merge pull request #4618 from pliablepixels/fix_event_start_stop_commands
feat: support token substitution in EventStartCommand/EventEndCommand
2026-02-13 14:40:18 -05:00
Pliable Pixels
c4fa045d1e fix: preserve legacy execlp() behavior for commands without % tokens
If EventStartCommand/EventEndCommand contains a % character, use the
new token substitution (%EID%, %MID%, %EC%) with sh -c execution.
Otherwise, fall back to the original execlp() behavior that passes
event_id and monitor_id as $1 and $2, so existing installs are not
broken.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 14:35:17 -05:00
Pliable Pixels
20f0d9f6f9 fix: shell-escape %EC% token to prevent command injection
cause can include trigger_data->trigger_cause (writable via zmtrigger
over the network) and zone labels (user-configured). Without escaping,
shell metacharacters in cause would be interpreted by sh -c.

Wraps cause in single quotes (with embedded single-quote escaping)
before substitution. %EID% and %MID% are safe as they are always
numeric from std::to_string.

Note on backward compatibility: the old execlp() passed event_id and
monitor_id as argv[1]/argv[2]. This PR intentionally does not preserve
that behavior — the old execlp() treated the entire command string as
an executable path, making it impossible to pass arguments, so any
working setup was already a simple path with no args. Users should
migrate to %EID%/%MID% tokens which are more explicit and flexible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 10:51:04 -05:00
Isaac Connor
0882a3ad1e fix: resolve Event::Run thread hang preventing zmc clean shutdown
Event::Run could block indefinitely in PacketQueue methods during normal
event closing (closeEvent from analysis thread), because their wait
predicates only check deleting/zm_terminate, not Event's terminate_ flag.

Three changes fix this:
- get_packet_no_wait: return immediately when iterator at end instead of
  blocking on condition variable (makes it truly non-blocking)
- Event::Run: use increment_it(wait=false) since deletePacket can advance
  the iterator to end() during AddPacket_ without the queue lock
- Event::Stop: call packetqueue->notify_all() to wake timed waits so
  Run() checks terminate_ promptly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:00:59 -05:00
Isaac Connor
d5b4709584 fix: reduce packet queue backpressure from event thread blocking
Three changes to prevent the analysis thread from stalling and the
packet queue from filling up:

1. Replace blind sleep_for/usleep in Event::Run() with
   packetqueue->wait_for() condition variable waits. The event thread
   now wakes immediately when decoder/analysis completes or new packets
   are queued, instead of always sleeping the full 33ms/10ms.

2. Add missing packetqueue.notify_all() calls after setting
   packet->analyzed (Monitor::Analyse) and packet->decoded
   (DECODING_NONE path in Monitor::Capture) so the event thread's
   condition waits actually get signaled.

3. Replace synchronous zmDbDoUpdate() calls in Event::~Event() with
   async dbQueue.push(). The two Events UPDATE queries (with Name
   fallback logic) are combined into a single query using MySQL IF().
   This eliminates blocking DB I/O from the close_event_thread, which
   the analysis thread joins on the next closeEvent() call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:17:45 -05:00
Pliable Pixels
5ffc2120c7 feat: support token substitution in EventStartCommand/EventEndCommand
Replace execlp() with execl("/bin/sh") and substitute %EID%, %MID%,
and %EC% tokens before execution. This allows users to pass arguments
directly in the command string, e.g.:

  /path/to/zm_detect.py -c /etc/zm/config.yml -e %EID% -m %MID% -r "%EC%" -n

Previously execlp() treated the entire command string as the executable
path, making it impossible to pass arguments without a wrapper script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:29:10 -05:00
Isaac Connor
dc74cb928f perf: reduce logging overhead and string temporaries in Analyse()
Raise ZoneStats::DumpToLog() from Debug level 1 to 4 since
per-zone stats are detailed diagnostics, not basic debug info.
Remove redundant DumpToLog call in zone loop (GetStats() already
calls it). Remove std::string temporaries in alarm cause building.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:52:15 -05:00
Isaac Connor
3907eb1687 fix: update last_write_time when skipping decode in KEYFRAMESONDEMAND
In KEYFRAMESONDEMAND mode without viewers, only keyframes are decoded.
Non-keyframe packets skip decoding and never reach the Phase 5 code
that updates last_write_time.  If the keyframe interval exceeds
ZM_WATCH_MAX_DELAY, zmwatch sees the stale timestamp and restarts
the capture daemon unnecessarily.

Update last_write_time for skipped video packets so zmwatch knows the
decode thread is still processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 13:23:24 -05:00
Isaac Connor
7f97f2b77e fix: flush decoder when on-demand decoding is no longer needed
When using DECODING_ONDEMAND or DECODING_KEYFRAMESONDEMAND, packets
accumulate in the decoder_queue while a viewer is connected. When the
viewer disconnects, should_decode becomes false but stale packets
remain queued in the decoder indefinitely — Phase 1 tries
receive_frame (gets EAGAIN), Phase 2 skips sending new packets, and
the cycle repeats.

Flush the decoder via avcodec_flush_buffers in both Phase 1 (before
attempting receive_frame) and Phase 2 (after determining decoding is
not needed), marking queued packets as decoded and clearing the queue.
This releases held packet locks and resets the decoder so it starts
clean when a viewer reconnects.

Also rename the 'dominated' variable to 'already_decoded' for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 22:47:37 -05:00
Isaac Connor
94f3a5771b fix: release GPU surfaces immediately after hw transfer
The nvidia-vaapi-driver would fail with "list argument exceeds maximum
number" when decoding HEVC because GPU surfaces were being held in the
packet queue after transfer, exhausting the VAAPI surface pool.

Changes:
- Transfer hw frames to software immediately in receive_frame() while
  the VA context is still valid, then release the GPU surface
- Check hw_frames_ctx in needs_hw_transfer() to detect already-transferred
  frames
- Remove extra_hw_frames and thread_count settings (not needed with
  immediate surface release)
- Fix EAGAIN handling in send_packet to wait instead of busy-loop

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:51:21 -05:00
Isaac Connor
2892db312f Check return from transfer_hwframe so as not to crash 2026-02-04 21:34:42 -05:00
Isaac Connor
ae42c3c94d Wait on the packetqueue condition instead of sleeping. Should help analysis keep up with decoding better and offer faster shutdown. 2026-02-04 20:44:17 -05:00
Isaac Connor
a96b776949 fix: apply credentials to secondary URL and fix buffer overflow in DumpSettings
- Apply credentials to secondary stream URL in FFmpegCamera (was causing 401 Unauthorized)
- Add empty check for rtsp_second_path in RTSP2WebManager before applying credentials
- Replace unsafe sprintf pattern in Monitor::DumpSettings with std::string + stringtf
- Refactor Zone::DumpSettings to return std::string instead of writing to char buffer
- Add decimal precision to event duration debug output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 17:40:26 -05:00
Isaac Connor
c6ab1c143b fix: address multiple bugs in eventstream, fifo, and monitor
zm_eventstream.cpp:
- Fix null pointer dereference when video_file or scheme column is NULL
- Fix out-of-bounds array access in CMD_SEEK handler when curr_frame_id
  decrements to 0
- Fix setStreamStart calling wrong loadInitialEventData overload (event_id
  was being truncated and used as monitor_id)

zm_fifo.cpp:
- Fix close(-1) call when file creation fails
- Fix use of uninitialized raw_fd when on_blocking_abort is false
- Reset outfile and raw_fd in close() to prevent use-after-close

zm_monitor.cpp:
- Fix shm_id being zeroed before use in shmctl() call
2026-02-02 23:24:08 -05:00
Isaac Connor
eae89025ee refactor: rename RTSP2WebStream to StreamChannel
Rename applies to Go2RTC, Janus, and RTSP2Web streaming options.
Update enum values from Primary/Secondary to Restream/CameraDirectPrimary/CameraDirectSecondary.

- Add db migration zm_update-1.37.79.sql to rename column and migrate data
- Update C++ enum StreamChannelOption and member stream_channel
- Update PHP getStreamChannelOptions() method
- Update all JavaScript references
- Auto-select CameraDirectPrimary when Restream option becomes disabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:37:08 -05:00
Isaac Connor
d89f2e59db refactor: rename Janus_Use_RTSP_Restream to Restream
Rename Janus-specific restream fields to be more generic since they are
now used by Go2RTC and RTSP2Web as well:
- Janus_Use_RTSP_Restream → Restream
- Janus_RTSP_User → RTSP_User

Update visibility logic so the Restream checkbox appears when RTSPServer
is enabled AND any streaming service (Janus, Go2RTC, or RTSP2Web) is
selected, rather than only when Janus is enabled.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 18:47:37 -05:00
Isaac Connor
b76a7c9640 fix: add null checks for y_image before dereferencing in Analyse
When analysis_image is set to ANALYSISIMAGE_YCHANNEL but in_frame is
not populated (e.g., LocalCamera which captures directly to image),
get_y_image() returns nullptr. The code was dereferencing this null
pointer in DetectMotion and Blend calls, causing a segfault.

Now checks if y_image is valid before use and skips the operation
with a debug message if unavailable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 14:29:04 -05:00
Isaac Connor
1d23bd297f Warn about too slow send_packet only if debug is on 2026-01-28 14:51:15 -05:00