loadInitialEventData() selected on unix_timestamp(`EndDateTime`) > t. An event
still being written has no EndDateTime, unix_timestamp(NULL) is NULL, and
NULL > t is never true, so the event currently recording could never be found.
zms.cpp:374 prefers the monitor_id form of setStreamStart whenever both
monitor and time are given, which is what montage review always sends, so its
`event` parameter never got a chance to help. Reviewing the present moment
returned the no-image placeholder, and every such request logged at ERR and
inserted a row into Logs.
Falls back to StartDateTime + Length, which zmc flushes every few seconds. An
event with neither ends where it starts, so a crash-orphaned event cannot match
every query and be picked ahead of the real one by the ORDER BY. Same
expression the API's Event model uses for EndTimeSecs.
Finding no event for a time is also no longer ERR. A client may ask for a time
the monitor was not recording, and it does so once per request.
Verified against a live instance: for a montage review request into the
recording event, the installed zms returned 2935 bytes (the placeholder) and
logged the failure, the rebuilt one returned a 134949 byte frame and logged
nothing.
Add instrumentation to trace which stream type and workload precede the
occasional runaway memory growth in nph-zms.
- zm_utils: add zm_get_rss_kb() reading resident set size from
/proc/self/statm
- zms: log a one-shot Debug summary of the decoded request (source, mode,
monitor/event, connkey, scale, buffer, ttl, rss) so a runaway process
can be tied to its stream parameters
- MonitorStream/EventStream: emit a periodic Debug RSS trace every 30s.
The EventStream trace includes frames-vector size and frame_count,
which scale with reloaded event length and will show whether the
in-process event reload path drives the growth
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Streaming an event allocated and freed a ~2MB Image on every frame. Add a
reuse_image_ member and decode JPEGs into it via ReadJpeg, which reuses its
pixel buffer when dimensions match (every frame of a given event), on both
the JPEG and MPEG paths. The uncommon ffmpeg (mp4) path uses a unique_ptr so
it frees automatically without a manual delete or pointer-identity guard.
Replace the existence-check stat() calls, which filled an unused struct stat,
with std::filesystem::exists() using the error_code overload to keep stat's
non-throwing 'any failure means not present' behaviour.
This achieves the heap-allocation reduction of #4609 using modern C++ rather
than fixed char buffers and string_view. The per-frame path-string allocation
was already eliminated on master via the reuse_filepath_ member.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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>
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>
processCommand runs on a separate thread but several command handlers
(CMD_PREV, CMD_NEXT, CMD_FASTREV, CMD_PAUSE, CMD_STOP, and zoom/pan/scale
commands) modified shared state (curr_frame_id, paused, replay_rate, etc.)
without holding the mutex. This allowed the main runStream loop to read a
corrupted curr_frame_id between its bounds check and the vector access in
sendFrame, causing a vector out-of-bounds assertion failure.
Move the mutex acquisition to the top of processCommand and remove the
redundant per-case scoped_locks that would otherwise deadlock.
maybe fixes#4644
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When SaveJPEGs is enabled, sendFrame() reads JPEG files directly from
disk and never touches the FFmpeg input. However, loadEventData()
unconditionally called FFmpeg_Input::Open() on the event video file,
which runs avformat_find_stream_info() — a 2-5 second probe that was
pure overhead for JPEG-based streaming.
Gate the Open() call on !(SaveJPEGs & 1) so the expensive probe is
only performed when frame extraction from the video container is
actually needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace two linear scans in event playback seeking:
- seek(): O(n) walk from frame 0 replaced with std::lower_bound on
timestamp. Falls back to last frame if target is past end.
- processCommand(CMD_SEEK): O(n) estimate-then-walk replaced with
std::upper_bound on offset, then step back one. Handles edge cases
where offset lands before first frame or past last frame.
For a 10-minute event at 30fps (~18,000 frames), worst case drops
from 18,000 comparisons to ~14 (log2).
When checkEventLoaded pauses at the end of an event in MODE_SINGLE/
MODE_NONE, the main loop relies on last_frame_sent to decide when to
send keepalive frames. If last_frame_sent is stale, the loop waits up
to 5 seconds (MAX_STREAM_DELAY) before sending anything. During that
gap the HTTP connection can time out, causing SIGPIPE on the next write.
Reset last_frame_sent to epoch so the next iteration sends a keepalive
immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sendFrame() reads frames[curr_frame_id-1] but was called outside the
mutex scope. The command processor thread (running processCommand via
checkCommandQueue) can modify curr_frame_id or reload the event
(clearing/rebuilding frames) between the mutex unlock and the
sendFrame call, causing out-of-bounds access.
Move sendFrame into the first mutex scope so frames[] access is
protected from concurrent modification by the command thread.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three related safety fixes for event_data->frames vector usage:
- Replace frame_count/last_frame_id with frames.size() for bounds checks,
since synthetic frame interpolation inflates the vector beyond the DB
row count, making the old fields unreliable for vector indexing.
- Replace dangling last_frame pointer with last_frame_idx index, since
emplace_back can reallocate the vector and invalidate all pointers.
- Fix seek backward off-by-one in checkEventLoaded() that set
curr_frame_id = event_data->frames.size() then accessed
frames[curr_frame_id - 1] before the vector was populated.
Remove the FramesDuration subquery from the event metadata query in
EventStream::loadEventData(). Previously every playback initiation
ran:
SELECT max(Delta)-min(Delta) FROM Frames WHERE EventId=Events.Id
as an embedded subquery. Without a covering index on (EventId, Delta),
MySQL walks the EventId_idx to find matching rows then fetches each
table row to read the Delta column. For a 10-minute event in
Record/Mocord mode at 30fps with bulk_frame_interval=100, that is
~180 index lookups + row fetches. For alarm-heavy events where every
frame gets a DB row, it can reach thousands of row fetches. This
blocks playback start on every event view.
The FramesDuration value was only consumed by a Debug level 2 log
comparing it against Event Length. It was never used in any playback
computation, frame timing, seek logic, or client-facing output.
The frames_duration field has been removed from the EventData struct
entirely. The diagnostic query and its log are now colocated inside
a logLevel() >= DEBUG2 guard using a local variable.
Production impact: none. Default ZoneMinder log level is INFO (0).
DEBUG2 is only reached via explicit operator configuration
(Options > System > LOG_LEVEL_FILE or ZM_DBG_LEVEL env variable).
No production deployment runs at DEBUG2 as it generates thousands
of log lines per second per daemon. The subquery code path is
unreachable under default configuration. Operators who enable DEBUG2
get the same diagnostic output as before.
Theoretical gains on playback initiation per event:
- Eliminates 1 SQL subquery performing N row fetches from the
critical path (N = Frames rows for the event)
- Record mode, 10min event: ~180 fewer row fetches
- Alarm-heavy 10min event: ~3,000-18,000 fewer row fetches
- Reduces MySQL buffer pool pressure from Frames page reads
- Reduces InnoDB row lock contention with concurrent frame INSERTs
from active recording daemons hitting the same EventId range
Co-Authored-By: Claude Opus 4.6 <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