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