Commit Graph

6485 Commits

Author SHA1 Message Date
Isaac Connor
69aea10d27 Merge pull request #4805 from ZoneMinder/copilot/fix-zms-mjpeg-response-status
fix: introduce stopped state for CMD_STOP, separating it from paused
2026-05-17 09:23:28 -04:00
copilot-swe-agent[bot]
03801a9e23 fix: reset step/send_twice in MonitorStream CMD_STOP; reset stopped in EventStream.js lifecycle
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/8c14d453-7815-44fd-956a-0cda13c218f9

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-17 12:37:39 +00:00
copilot-swe-agent[bot]
1bce09f4e8 fix: address review feedback - endian decode, send_twice init, setLastViewed, curr_frame_id gating
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/b457dcf6-616f-4df9-bc28-76640bf5dd39

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-17 03:29:41 +00:00
Isaac Connor
3b03ec5f12 refactor: route VideoStore destructor through finalize() refs #4757
The destructor previously duplicated the queue drain, trailer write,
and avio close from finalize(), gated on !finalized_. That drifts.
Have the destructor just call finalize() — it is idempotent (early
returns when finalized_ is already set or when oc was never allocated)
— so the shutdown logic lives in one place and resource deallocation
stays in the destructor.

Reported by Copilot on PR #4835.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:30:19 -04:00
Isaac Connor
4aa62a4d82 fix: VideoStore filename use-after-free + null-oc destructor guard refs #4757
Two crash-class bugs reported by Copilot on PR #4835:

1. filename was held as const char* pointing into a caller-owned
   std::string. Event::AddPacket_() can rename the underlying file and
   reassign video_incomplete_path, invalidating that pointer; finalize()
   then fopens through the dangling pointer to parse the mfro trailer.
   Store filename as std::string inside VideoStore so the storage lives
   as long as the object does.

2. VideoStore::~VideoStore() dereferenced oc->pb and ran the reorder-
   queue drain without checking oc. If open() bailed before allocating
   oc, both paths would crash. Guard on oc being non-null.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:15:20 -04:00
Isaac Connor
2e83b8b074 revert: stop forcing DefaultVideo='index.m3u8' for HLS events refs #4757
47d3af70c rewrote DefaultVideo to 'index.m3u8' on close so event.php
would pick HLS for closed events. But several PHP call sites use
DefaultVideo as a local file path passed to ffmpeg or to filesize() —
Event.php's getImageSrc(), Length(), Filesize(), and Video() all rely
on it being a real video filename. ffmpeg can't resolve our manifest's
relative "index.php?…" segment URLs, so thumbnail/frame generation
would break for events with SaveJPEGs disabled.

The PHP-side fallback (10a4f9af1, 375e82fd8) keys HLS selection off the
on-disk index.m3u8 plus the codec choice, which is enough on its own to
route closed events through HLS without touching DefaultVideo.

Reported by Copilot on PR #4835.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:01:19 -04:00
Isaac Connor
4182a829db fix: drain VideoStore reorder_queues inside finalize() refs #4757
The destructor drained reorder_queues by calling writeVideoFramePacket /
writeAudioFramePacket, which call av_interleaved_write_frame on oc.
finalize() closes oc->pb, so when Event::~Event() calls finalize() and
then deletes the VideoStore, the destructor's queue drain would write
into a closed output — undefined behavior at best, silently dropping
the trailing reordered frames at worst.

Move the drain into finalize() ahead of the trailer write, and gate the
destructor's drain on !finalized_ so it still runs in the legacy path
where nobody called finalize().

Reported by Copilot on PR #4835.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:01:19 -04:00
Isaac Connor
47d3af70c7 fix: keep DefaultVideo='index.m3u8' for events with an HLS manifest refs #4757
Event::~Event() rewrote DefaultVideo to the renamed mp4 filename on every
close, which made event.php's HLS detection (str_ends_with DefaultVideo,
'.m3u8') return false for every closed event. The HLS player only kicked
in for in-progress events or when MP4HLS was manually picked from the
codec dropdown.

Detect index.m3u8 in the same dirent walk that already computes video_size,
and write 'index.m3u8' to DefaultVideo when it's present. mp4-only events
keep using the renamed mp4 as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:54:32 -04:00
Isaac Connor
0e532cd77b fix: correct HLS fragment byte-range and duration tracking refs #4757
The m3u8 manifest written by VideoStore had two interlocking bugs that
produced duplicate entries with bogus 0.040s durations and a missing or
mis-pointed final fragment, which made video.js seek backwards.

Root cause: with movflags=frag_keyframe the mov muxer flushes fragment N
to disk *inside* av_interleaved_write_frame() when keyframe N+1 arrives.
The previous code snapshotted avio_tell() *before* that call, so its
last_fragment_offset_ described the next fragment's start while the
fragment at that offset hadn't actually been flushed yet. writeM3U8
then push_back'd a tentative entry off that stale state on every live
update, and the next keyframe push pushed a second entry at the same
offset+size with the correct duration.

Now we snapshot avio_tell() *after* av_interleaved_write_frame(), which
gives the actual end of the just-flushed fragment, and record fragment
N-1 there. writeM3U8 no longer mutates fragments_ — it just emits the
list.

The final fragment (no later keyframe to close it) is recorded by a new
finalize() method on VideoStore that runs av_interleaved_write_frame
flush + av_write_trailer, then parses the trailing mfro box to subtract
the mfra trailer size from the file length. Event::~Event() calls
finalize() before writeM3U8(true); the VideoStore destructor skips its
own trailer write when finalize() has already run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:28:00 -04:00
Isaac Connor
2be9c86c99 Merge pull request #4834 from abishai/fix-arm-build
FreeBSD - arm fix
2026-05-15 18:33:13 -04:00
abi
52cfaff3fc typo 2026-05-16 01:27:23 +03:00
abi
878a9dab14 Since kFreeBSD was amd64 and i386 archs only, remove unnecessary checks 2026-05-15 22:53:27 +03:00
abi
6faa106af1 Fix FreeBSD arm builds 2026-05-15 22:53:22 +03:00
Isaac Connor
327b8d4a89 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-05-14 07:40:14 -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
Isaac Connor
e1eb7876c6 fix: clamp percentage polygon to width-1/height-1
ParsePercentagePolygon clamped to [0, width] / [0, height], so a new
zone created at 100% produced pixel coords equal to the monitor
dimensions. The rasterizer requires hi_x < width and hi_y < height,
so the runtime then logged warnings like
  "polygon hi_x (1920) >= image width (1920), clamping".

Clamp to width-1 / height-1 instead. Updates existing tests that
encoded the previous behavior.
2026-05-13 11:38:42 -04:00
copilot-swe-agent[bot]
ea11498dc1 docs: document CMD_PLAY vs CMD_VARPLAY and CMD_STOP in MsgCommand enum
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/f82e2bf4-0e4e-48a4-bccd-42c61e66c6b3

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-06 02:41:57 +00:00
copilot-swe-agent[bot]
35b453c935 fix: improve stopped state handling in EventStream run loop
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/ba9cb47a-a3e8-4e13-aec7-c9cd258e2a3d

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-06 02:11:15 +00:00
copilot-swe-agent[bot]
1be352c790 fix: add stopped state for CMD_STOP, fixing paused=true response bug
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>
2026-05-06 01:58:48 +00:00
Isaac Connor
21c68cc839 Merge pull request #4794 from ZoneMinder/copilot/fix-triggered-events-issue
fix: triggered events only fire ~1/3 of the time due to hash iteration ordering race
2026-05-05 21:30:07 -04:00
copilot-swe-agent[bot]
5b51d086e1 fix: use HTTP_X_FORWARDED_FOR in auth hash validation to fix AUTH_HASH_IPS with reverse proxy
When AUTH_HASH_IPS is enabled and ZoneMinder is behind a reverse proxy
(e.g. Nginx in front of Apache), the hash is generated using
HTTP_X_FORWARDED_FOR (the real client IP) but was validated using only
REMOTE_ADDR (the proxy's IP), causing all authentication to fail.

Fix by consistently using HTTP_X_FORWARDED_FOR (first IP only, to guard
against spoofed multi-value headers) with REMOTE_ADDR as fallback in
all three places:
- web/includes/session.php: where remoteAddr is stored for hash generation
- web/includes/auth.php: getAuthUser() validation (PHP, also used by zms CGI)
- src/zm_user.cpp: zmLoadAuthUser() validation (C++ zms binary)

refs #4758

Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/959dfe9d-edea-4de5-a3a0-f90b758e5628

Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
2026-05-04 14:53:38 +00: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
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
0eb36d0c52 fix: silence spurious Overlay subpixel-order warning post-AVPixelFormat
Image::Overlay() warned when (colours == image.colours &&
subpixelorder != image.subpixelorder), which made sense when
(colours, subpixelorder) was the canonical format identifier.

Now that imagePixFormat is canonical, the check produces false
positives in a normal code path: zm_monitor.cpp's analysis pass calls
analysis_image->Overlay(*(zone.AlarmImage())) where the destination is
YUV420P (colours=1 via the GRAY8/YUV420P=1 alias collision in
zm_rgb.h, subpixelorder=ZM_SUBPIX_ORDER_YUV420P=11) and the source is
the zone's GRAY8 alarm mask (colours=1, subpixelorder=NONE=2). The
overlay dispatch below already handles this correctly via
zm_bytes_per_pixel(imagePixFormat) == 1 on both sides — only the Y
plane of the dest is touched, leaving chroma untouched, which is
exactly the intent. The warning was just noise.

Reframe the check around imagePixFormat: warn only when the
AVPixelFormat actually matches but the ZM (colours, subpixelorder)
metadata diverges, which would indicate a real format-tracking bug.
The new message also names the AVPixelFormat for context, instead of
two opaque integers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:33:20 -04:00
Isaac Connor
e74916ad0e fix: PR #4742 review feedback + drop stray u_buffer assignments
Two issues flagged by Copilot review on the AVPixelFormat-migration PR,
plus one build fix that was needed to land them:

1. zm_local_camera.cpp set subpixelorder to BGR for V4L2_PIX_FMT_RGB24
   captures. V4L2_PIX_FMT_RGB24 is byte-order R,G,B in memory and is
   mapped to AV_PIX_FMT_RGB24 by getFfPixFormatFromV4lPalette earlier
   in the same file, so the matching ZM subpixel order is RGB. Setting
   BGR meant red and blue were swapped in the captured image whenever
   a V4L2 camera was configured with the RGB24 palette. Long-standing
   bug — preserved unchanged through the AVPixelFormat migration —
   now fixed to ZM_SUBPIX_ORDER_RGB.

2. Image::AssignDirect(const AVFrame*) called zm_colours_from_pixformat
   without checking the return value, leaving colours/subpixelorder at
   their previous values for any unsupported AVPixelFormat. Wrap the
   call and on failure put the Image into an explicit invalid state
   (AV_PIX_FMT_NONE plus zeroed colours/subpixelorder) so the
   inconsistency surfaces immediately instead of producing wrong-format
   reads downstream.

3. Drop the u_buffer = ... / v_buffer = ... assignments inside
   Image::Assign()'s identity-copy path. Those members exist on the
   ai_server lineage but not on master, so the PR branch did not
   compile against master as-is. av_image_copy reads the planes
   directly out of temp_frame->data, so the assignments were not
   load-bearing — they look like leftover state-tracking that didn't
   survive the upstreaming. Comment notes why the lines were removed.

Tests pass: 76 cases, 778 assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:36:43 -04:00
Isaac Connor
8b4c93ea1f fix: bail from MonitorStream::runStream when monitor failed to load
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.
2026-04-30 18:11:55 -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
257ecafa4d fix: clean up ONVIF subscription before re-Subscribe to stop NotAuthorized loop
When PullMessages failed, Run() looped back into Subscribe() and called
CreatePullPointSubscription on the same soap context without unsubscribing
the previous one. Cameras with per-user pull-point caps (Hikvision,
Reolink, etc.) only release a slot when the originating socket closes, so
each cycle leaked a slot until the camera started rejecting new
subscriptions with ter:NotAuthorized. Only a zmc restart (which closed
sockets and ran the destructor) recovered.

- Subscribe() now calls cleanup_subscription() and tears down the soap
  context up front when has_valid_subscription_ is set, forcing a fresh
  TCP connection.
- WaitForMessage()'s failure path calls cleanup_subscription() before
  going unhealthy so the camera-side slot is released as soon as we know
  we're abandoning it.
- try_usernametoken_auth is reset at the start of each subscription cycle
  so a transient plain-auth fallback doesn't pin for the lifetime of the
  process.
- Auth-error detection in CreatePullPointSubscription now checks
  soap_fault_string in addition to soap_fault_detail; the observed fault
  carries the "...not authorized" wording in fault_string with detail=null,
  so the existing plain-UsernameToken fallback never fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:36:49 -04:00
Isaac Connor
509d8b4d3f feat: add G.711 μ-law (PCMU) audio support to zm_rtsp_server
Mirror the existing pcm_alaw branch with a pcm_mulaw branch that
creates an xop::G711USource. Cameras emitting PCMU audio can now
be re-streamed via the built-in RTSP server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:06:36 -04:00
Isaac Connor
76ec0ae1de fix: skip sws_scale for identity conversions in Image::Assign
Use av_image_copy instead of sws_scale when source and dest format +
dimensions match. Eliminates unnecessary per-frame pixel processing
for passthrough formats (e.g. YUVJ422P from MJPEG cameras).

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:04:44 -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
49498f4076 fix: compute Camera linesize/imagesize from pixelFormat, not hardcoded YUV420P
linesize and imagesize were still hardcoded to AV_PIX_FMT_YUV420P
sizing despite colours/pixelFormat being set correctly from DB.
For RGBA monitors, shared memory slots were 3.7x too small, causing
buffer overflow and intermittent corruption in the live stream.

Use the Camera's pixelFormat member (set from p_colours/p_subpixelorder)
for linesize and imagesize computation.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 21:30:47 -04:00
Isaac Connor
53683cff79 fix: use zm_bytes_per_pixel for pixel-rendering dispatch instead of GRAY8-only
The migration from colours==ZM_COLOUR_GRAY8 to imagePixFormat==GRAY8
broke YUV420P handling: the old check matched both GRAY8 and YUV420P
(both had colours==1) but the new check only matched GRAY8.

For pixel-rendering operations (Annotate, Fill, Outline, DrawLine,
Rotate, Flip, Delta, MaskPrivacy, Deinterlace, Overlay) that work on
the Y-plane of any 1-byte-per-pixel format, replace the GRAY8-only
check with zm_bytes_per_pixel(imagePixFormat)==1. This covers GRAY8,
YUV420P, YUVJ420P, YUV422P, and YUVJ422P.

Format-identification sites (JPEG encoding, Colourise, DeColourise)
that genuinely distinguish grayscale from YUV are left unchanged.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 21:15:48 -04:00
Isaac Connor
02e6be6b47 fix: handle YUVJ422P/YUV422P in zm_colours_from_pixformat
MJPEG decoder outputs AV_PIX_FMT_YUVJ422P which was not handled by
zm_colours_from_pixformat, causing "Unknown pixelformat 13 yuvj422p"
errors and leaving imagePixFormat stale — resulting in random image
corruption.

Add YUV422P and YUVJ422P cases to zm_colours_from_pixformat and
zm_bytes_per_pixel in zm_pixformat.h.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:38:10 -04:00
Isaac Connor
4c999c0fe8 fix: sync imagePixFormat in WriteBuffer, Assign, and AssignDirect
After migrating format dispatch from colours to imagePixFormat,
several methods that update colours/subpixelorder were not also
updating imagePixFormat, leaving it stale. This caused format
misidentification downstream — e.g. DecodeJpeg falling back to
RGB24 while imagePixFormat still claimed RGBA, producing vertical
lines and washed-out colors in the live stream.

Fix WriteBuffer, Assign(buffer), Assign(Image), AssignDirect(buffer),
and AssignDirect(AVFrame) to keep imagePixFormat in sync.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:18:03 -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
2526b9b85e feat: install SIGPIPE handler in zms for clean browser-disconnect shutdown
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>
2026-04-18 09:53:23 -04:00
Isaac Connor
9aa2f2c306 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-04-16 15:37:16 -04:00
Isaac Connor
aa5e493694 perf: default ffmpeg decoder thread_count to 2
libavcodec's avcodec_alloc_context3 leaves thread_count at 1, so
software 1080p H.264 decoding hits ~60ms/frame and saturates a core
per camera.  Default to 2 frame-threads, which roughly halves per-
frame decode time without oversubscribing systems with many cameras.

User can still override via thread_count in monitor Options (0 = let
ffmpeg auto-detect based on CPU count).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:58:44 -04:00
Isaac Connor
3f8bc81760 fix: HLS fragment tracking, live playback, and fallback handling
Fragment tracking:
- Rewrite detection to use avio_flush()+avio_tell() before each video
  keyframe write, giving exact fragment boundaries. Fixes duplicate
  entries and missing first fragment in the m3u8 manifest.
- Remove unused pending_fragment_dts_ and last_flush_pos_ members.

Live event playback:
- Set DefaultVideo to index.m3u8 at event INSERT time (no DB update
  needed after videoStore opens)
- Rename incomplete file to include codec (incomplete.h264.mp4) so
  canPlayCodec() works during recording
- Rewrite final m3u8 after video file rename to reference final name
- Add live retry handler in event.php: retries all error codes with
  backoff (3s then 5s), max 30 retries, resets on successful playback
- Update progress bar duration from video element for live events

Fallback handling:
- view_video.php: when DefaultVideo is m3u8 and no file param given,
  search event dir for actual mp4 (final name then incomplete)
- view_hls.php: reject m3u8 with no EXTINF entries (event just started)
- event.php: require m3u8 file to exist on disk before offering HLS,
  fall back to direct MP4 otherwise

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:57:46 -04:00
Isaac Connor
0dff9c12b1 Merge remote-tracking branch 'upstream/master' into hls-byte-range-playback 2026-04-12 15:40:45 -04:00
Isaac Connor
e15d2a6427 feat: add HLS byte-range playback for event video
Write a single continuous fragmented MP4 per event and generate an HLS
m3u8 manifest with byte-range references into that file. This enables
seamless browser playback via video.js's built-in http-streaming (VHS)
without needing separate segment files.

C++ changes:
- VideoStore tracks fragment boundaries (moof+mdat byte offsets and
  durations) as packets are written, by monitoring avio_tell() around
  keyframe writes in write_packet()
- Add writeM3U8() method that generates EXT-X-VERSION:7 byte-range
  manifests with EXT-X-MAP for the init segment
- Event writes a live m3u8 (no EXT-X-ENDLIST) on each new fragment
  for in-progress viewing, and a final VOD manifest at event close
- Change movflags to frag_keyframe+empty_moov+default_base_moof
  (default_base_moof required by HLS fMP4 spec, faststart removed
  as it's meaningless with empty_moov)

PHP/web changes:
- New view_hls.php endpoint serves pre-built m3u8 with auth tokens
- event.php detects index.m3u8 and uses HLS as primary source with
  direct MP4 as fallback
- CSRF exemption for view_hls in index.php

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:40:12 -04:00
Isaac Connor
7bcdfae0d8 fix: prevent empty events in ONDEMAND mode and fix VideoStore fd/codec leaks
The ONDEMAND capture mode rapidly cycled between Pause() and Play()
because Pause() resets the write index, making the GetLastWriteIndex()
guard false, which fell through to Play(). This created ~2 empty events
per second. Remove the write index guard so monitors stay paused when
nobody is watching.

In VideoStore, fix three resource management issues:
- Free the codec context opened in the PASSTHROUGH+new_extradata path
  immediately after extracting stream parameters, preventing flush_codecs
  from crashing on an encoder that never received frames.
- Clean up video_out_ctx, opts dict, and hw_device_ctx when
  setup_hwaccel() fails, preventing fd accumulation.
- Track whether frames were actually sent to the encoder and skip
  flush_codecs when none were, avoiding segfaults in avcodec_send_frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:40:26 -04:00
Isaac Connor
83ed47abf4 fix: scan-line polygon fill incorrectly filled non-convex polygon gaps
Image::Fill(Polygon) implements scan-line polygon fill but iterated
through active edges one at a time instead of in pairs. For convex
polygons (always exactly 2 active edges per scan line) this happened
to work, but for non-convex polygons it would fill the gaps between
concave sections.

A banana-shaped zone, for example, would have its inner concave area
incorrectly marked as inside the zone, causing motion detection to
trigger on the area the user explicitly drew the zone to avoid.

Fix by stepping the iterator by 2 to fill between pairs of edges
following the standard parity rule for scan-line polygon fill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:33:52 -04:00
Isaac Connor
1e4ec3d251 fix: treat ENOTTY like EINVAL when querying V4L2 JPEG compression options
Some V4L2 drivers return ENOTTY (rather than EINVAL) when VIDIOC_G_JPEGCOMP
is unsupported. Treat both as "feature unavailable" and log at debug level
instead of warning, and include the errno string for clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:48:57 -04:00
Isaac Connor
0e51fb6788 fix: call disconnect() in MonitorLink::connect() before reopening map_fd
MonitorLink::connect() opened a new map_fd on each invocation without
closing any previously-opened one. Token::score() in
zm_monitorlink_token.h calls connect() on every analysis cycle when the
linked monitor is unavailable, causing rapid file descriptor
accumulation and eventual "Too many open files" errors in zmc.

Call disconnect() first to release any prior map_fd, mmap, and shared
state before re-establishing the link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:48:42 -04:00
Isaac Connor
d22f8450e0 fix: free video_out_ctx after failed avcodec_open2 in new_extradata path
When avcodec_open2 failed in the new_extradata code path,
video_out_ctx was left allocated but half-initialized. Later
flush_codecs() would call avcodec_send_frame on this context, causing
a segfault (null deref at offset 0x28, likely ctx->internal). Free
the context on failure and guard the subsequent
avcodec_parameters_from_context call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:30:18 -04:00