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>
- 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>
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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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
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>
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>
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>