- 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