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>
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>
disconnect() was guarded by `if (connected)`, but connected is only
set to true as the last step of a successful connect(). Every error
path in connect() called disconnect() before connected was true, so
map_fd was never closed and mmap was never unmapped. Each failed
connect attempt leaked one fd, eventually causing "Too many open
files" errors when opening new events.
Fix by cleaning up based on actual resource state (map_fd >= 0,
mem_ptr != nullptr) instead of the connected flag. Also fix
MAP_FAILED check, null out derived pointers, and fix shm_id being
zeroed before its IPC_RMID call.
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>
The existing PTS backward jump check in FfmpegCamera::Capture() does not
catch cases where DTS jumps back significantly (e.g. stream restarts,
encoder resets) while PTS may remain unaffected. This causes the
VideoStore to spend minutes forcing DTS monotonicity on every packet via
the write_packet fixup path, flooding logs with warnings.
Add per-stream DTS tracking (mLastVideoDTS/mLastAudioDTS) and a backward
jump check mirroring the existing PTS check: if DTS jumps back more than
10 seconds, increment error_count and after 6 occurrences return -1 to
trigger capture reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move MJPEG/JPEG ahead of YUYV/UYVY in the preferred format arrays
for all color spaces. MJPEG uses far less USB bandwidth than raw YUV
while the decode cost is minimal (sw MJPEG decoder).
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>
- Add user= parameter to get_auth_relay() so zms can use the indexed
Username column instead of iterating all users to validate the hash
- Apply the same fix to Event.php getStreamSrc() and getThumbnailSrc()
- Tighten Monitor.php from isset() to !empty() for consistency
- In MonitorStream.js start(), check if the auth hash in the img src
matches the current auth_hash before resuming via CMD_PLAY. If stale,
fall through to rebuild the URL with fresh auth_relay. This prevents
long-running montage pages from spawning zms with expired credentials.
- Downgrade zms auth failure from Error to Warning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Linked monitor alarm detection was purely point-in-time: it only
returned a score when the linked monitor's state was exactly ALARM at
the instant of the check. If the linked monitor's entire alarm cycle
(ALARM -> ALERT -> IDLE) occurred between two analysis cycles of the
checking monitor, the alarm was missed entirely.
Add a latch using last_event_id: if the linked monitor's event id has
changed since the last check, a new event started and we return a
score even if the ALARM state has already passed. Also remove dead
code in hasAlarmed() where last_event_id was updated after an early
return.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 60-second reconnect throttle meant that any brief disconnection
(e.g., linked monitor restart, momentary shared memory issue) created
a full 60-second window where linked monitor alarms were silently
missed. Reducing to 1 second minimizes this blind spot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zone threshold fields (MinAlarmPixels, MaxAlarmPixels, MinFilterPixels,
MaxFilterPixels, MinBlobPixels, MaxBlobPixels) were being corrupted on
every save because the PHP conversion used monitor pixel area instead of
zone pixel area. This caused values to inflate progressively, breaking
motion detection.
The fix changes the storage model: thresholds are now stored as
percentages of zone area (DECIMAL(7,2) columns) matching the percentage
coordinate system from zm_update-1.39.2. The C++ Zone::Load() converts
percentages to pixel counts at runtime using polygon.Area(). Legacy
pixel-coordinate zones pass through unchanged.
JS changes:
- submitForm() converts to percentages when in Pixels display mode
- Init skips applyZoneUnits() since DB values are already percentages
- limitRange() only updates field.value when constrained value differs,
preventing oninput from stripping decimal points mid-keystroke
fixes#4690
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The C++ User class (used by zms for streaming) had no awareness of
roles. It only checked user-direct permissions from Monitors_Permissions
and Groups_Permissions tables, completely ignoring Role_Monitors_Permissions,
Role_Groups_Permissions, and User_Roles base permissions. This caused
users who received camera permissions via Roles to be denied live stream
access, even though the PHP web interface (which has its own role-aware
checks in visibleMonitor()) showed the monitors correctly.
Changes:
- Add role_id to C++ User class, loaded via COALESCE(RoleId, 0) in all
SQL queries (find, zmLoadTokenUser, zmLoadAuthUser)
- Add loadRoleBasePermissions() to merge role's Stream/Events/Monitors/
etc. as fallback when user's own permission is PERM_NONE
- Add findByRole() to Group_Permission and Monitor_Permission classes
to query Role_Groups_Permissions and Role_Monitors_Permissions tables
- Extend User::canAccess() to check role monitor and group permissions
after user-direct permissions, matching the PHP visibleMonitor() logic
- Fix Monitor::canView() in PHP to also check role permissions when
called for a user other than the global $user
- Fix off-by-one in zmLoadTokenUser where dbrow[10] read TokenMinExpiry
out of bounds (was at index 9); adding RoleId shifts it to index 10
Fixes#4692
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a boolean analysis_image field to the CMD_QUERY status response that
reports whether zms is actually sending analysis images (with motion zone
overlays) or regular capture images. This lets MonitorStream.js detect
when the stream state is out of sync with what the client requested and
re-send the CMD_ANALYZE_ON/OFF command to correct it.
The field is true only when frame_type is FRAME_ANALYSIS, shared memory
is valid, and the monitor has analysis enabled — matching the same
condition used to select the image in the streaming loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The zone loader now ignores the Units DB field and detects the coordinate
format by checking for decimal points: decimal values are percentages,
integer-only values are legacy pixels. This fixes motion detection being
broken when zones had Units=Pixels but percentage coordinates (or vice
versa), which resulted in a ~99x99 pixel zone on a 2560x1440 monitor.
The PHP zone view now always forces Units=Percent when saving, since it
always works in percentage space. convertPixelPointsToPercent() now
returns bool to indicate whether conversion occurred.
Tests added for: truncation bug via atoi, correct percentage-to-pixel
conversion, auto-detect heuristic, and resolution independence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When two packets have the same DTS (common with B-frames near
keyframes), bumping by last_duration overshoots and causes a cascading
misalignment where every subsequent packet triggers a "non increasing
dts" warning with a growing gap. The comment already said "add 1" but
the code used last_duration.
Co-Authored-By: Claude Opus 4.6 <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>
Instead of wrapping entire class declarations and usage sites in
#if HAVE_LIBCURL, each inner class (AmcrestAPI, RTSP2WebManager,
Go2RTCManager, JanusManager) now has the guard inside the class body
with #else stubs. This makes zm_monitor.cpp completely free of
HAVE_LIBCURL guards.
Stub behaviour when curl is absent:
- isHealthy() returns true (avoids recreation loop in Poll)
- isAlarmed() returns false (no spurious triggers)
- check_*() returns 1 (no-op, avoids re-add loop in Poll)
- all other methods are no-ops / return 0
refs #4967
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
getpid() requires explicit inclusion of <unistd.h> on FreeBSD where
transitive includes from other headers are not guaranteed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The chrono duration rep type is long long on some platforms but long on
others. Using %ld causes -Wformat warnings on platforms where it is
long long. Use %jd with static_cast<intmax_t>() for portability,
matching the convention used elsewhere in the codebase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Integrate EventStream.js into montagereview.js for persistent MJPEG
streaming in replay mode (speeds >= 1x), falling back to per-frame
mode=single for speeds < 1x where zms lacks slow-motion support
- Add EventStream recovery logic: detect zms death via AJAX failures,
img.onerror, and error responses; auto-restart with new connkey and
exponential backoff (max 5 retries)
- Fix zms crash on bulk/interpolated frames: add stat() check in
sendFrame() for SaveJPEGs & 1 path, fall through to ffmpeg_input
when JPEG file doesn't exist on disk; send "No frame available"
text frame instead of terminating when no source available
- Center monitor canvases: text-align in CSS for scale mode,
horizontal offset calculation in maxfit2() for fit mode
- Reduce console noise: comment out high-frequency debug logs,
convert error-path logs to console.warn/error
- Fix ESLint sourceType to "script" for traditional non-module JS
files, resolving false-positive no-unused-vars on global functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix FFmpeg initial seek overshooting to a future keyframe when the
requested timestamp falls between keyframes. After the initial
AVSEEK_FLAG_FRAME seek, detect if the returned frame is past the
target and re-seek backward to get the correct keyframe before it.
On the JS side, preserve fractional seconds throughout the playback
pipeline: remove Math.floor() truncation in mmove() and setSpeed(),
and use parseFloat instead of parseInt for currentTimeSecs
initialization. Reduce initial display interval from 1000ms to 100ms
for ~10fps refresh rate during review playback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GCC < 9 (CentOS/RHEL 8) requires explicitly linking stdc++fs for
std::filesystem support. Add a CMake compile check that detects this
and conditionally links the library for the zma target.
Also add the zma binary to the Red Hat spec file's %files section
so it gets packaged in the RPM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Some cameras produce packets where both pts and dts are AV_NOPTS_VALUE.
Previously, video passthrough skipped timestamp processing for these,
write_packet set dts to -1 and never updated tracking state,
audio_first_dts could be set to AV_NOPTS_VALUE permanently, and the
reorder queue compared AV_NOPTS_VALUE values meaninglessly.
- Video passthrough: synthesize dts as last_dts+1 when both pts/dts
are undefined, keeping monotonic ordering without colliding with the
next valid packet's timestamp
- Audio: guard audio_first_dts from being set to AV_NOPTS_VALUE, and
guard passthrough subtraction to avoid NOPTS arithmetic overflow
- write_packet: synthesize dts as last_dts+1 instead of reusing stale
last_dts, fall back to 0 when no history exists, and always update
tracking state (last_dts/next_dts/last_duration) regardless of
which branch was taken
- Reorder queue: skip reordering when incoming dts is undefined and
treat queued NOPTS packets as in-order
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>
Add www-data to video and dialout groups in Debian/Ubuntu postinst
scripts so zmc can access /dev/video* devices on fresh installs.
RedHat packaging already handled this via gpasswd in %post.
Add compile-time ZM_FONTDIR to zm_config_data.h.in and use it as a
fallback in zm_image.cpp when the configured font_file_location is not
found, fixing font loading failures caused by stale DB config values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
send_twice was set to true by zoom/pan/scale/seek commands when paused
but never reset to false. Once set, every subsequent frame was sent
twice forever, even after unpausing. This doubled bandwidth usage and
increased exposure to the processCommand race condition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>