Commit Graph
6573 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 4.8 326e1b0ab2 feat: synthesize in_frame from the captured image when not decoded
Capture paths that deliver a raw Image without an ffmpeg decode (e.g.
LocalCamera/V4L2) left packet->in_frame null even though the pixels were
already present, so anything expecting a decoded frame failed. In
particular YChannel analysis called get_y_image(), which needs
in_frame->data[0], and logged "Can't get y_image without frame".

At the end of Monitor::Decode(), when a packet has an image but no
in_frame, wrap the image's planes in an AVFrame via Image::PopulateFrame
(av_image_fill_arrays over a dont_free buffer ref: pointers, no copy).
Any format is populated; consumers that need a specific layout check for
themselves (get_y_image now reports RGB has no Y plane rather than "no
frame").

Done after PHASE 5 so the frame reflects the oriented/masked image and we
don't re-orient a shared Y plane, and after the codec phases so
transfer_hwframe is never called with the null codec context a
non-decoding camera has. videostore is unaffected: it prefers
packet->image for frame data and derives pts from packet->timestamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:59:18 -04:00
Isaac ConnorandClaude Opus 4.8 92ab811720 feat: publish analysis images through a shared-memory ring
Replace the single alarm_image slot with an analysis_image_buffer ring of
image_buffer_count Images living in the already-reserved alarm_images SHM
region. Successive WriteAlarmImage calls rotate through the ring and
publish last_analysis_index last (after the bytes and per-slot format),
so a reader sampling last_analysis_index always sees a fully written
slot. GetAlarmImage returns that slot, syncing its AVPixelFormat from the
per-slot analysis_image_pixelformats array.

SharedData gains last_analysis_index and analysis_image_count (plus 8
bytes of padding to keep the 16-byte-multiple layout), making it 888
bytes. The Perl (Memory.pm) and PHP (Monitor.php) SHM readers are updated
in lockstep, and a static_assert(sizeof(SharedData)==888) in zm_monitor.h
guards the layout against silent drift.

This lets multiple in-flight analysis/annotated frames be buffered and
streamed in sync rather than always overwriting one slot, and gives the
AI object-detection work a place to publish annotated frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:32:51 -04:00
Isaac Connor f6f2e0fd7a Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-07-01 11:52:54 -04:00
Isaac Connor 346fa0e8a0 Improve error message when we fail to write capture jpg 2026-07-01 11:52:25 -04:00
Isaac ConnorandClaude Opus 4.8 53afbafb84 feat: add realtime pacing option for ffmpeg file sources
Add a "realtime=1" (alias "re=1") ffmpeg input option, modeled on the
existing "loop" option and equivalent to ffmpeg's -re flag. When set on a
file source, FfmpegCamera throttles packet delivery to the rate implied by
the stream timestamps instead of reading the file as fast as possible.

The option is parsed out of the ffmpeg Options string in OpenFfmpeg and
consumed so it is not passed to the demuxer or decoder. Capture() anchors
wall-clock time to the first delivered packet and sleeps before each
subsequent packet so it is not delivered ahead of schedule. Pacing uses dts
(monotonic in read order) with a pts fallback, runs after the existing
drop/jump filters, and re-anchors on backward jumps or gaps beyond a 10s cap
to avoid stalling on a discontinuity. Works alongside loop, whose
offset-adjusted timestamps stay monotonic across restarts.

The pacing decision is factored into a pure ComputeRealtimePace() function
and unit-tested in tests/zm_ffmpeg_camera.cpp (8 cases covering full/partial
interval waits, behind-schedule, on-schedule, backward jump, over-cap, and
the cap boundary).

Ported from the ai_server branch onto master's OpenFfmpeg structure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:08:58 -04:00
Isaac ConnorandClaude Opus 4.8 c9be205846 feat: loop file-based ffmpeg input on EOF via loop=1 option
When an ffmpeg monitor reads a seekable file source (e.g. file:///x.mp4)
and reaches the end, it previously failed and reconnected. Add a loop
mode, enabled with loop=1 in the monitor Options, that seeks back to the
start and continues instead.

Packet pts/dts are shifted by a per-stream absolute offset recomputed on
each loop (last emitted dts + frame duration - stream start_time) so
timestamps stay monotonically increasing for analysis and recording. The
loop option is consumed before passing options to ffmpeg (both the demuxer
in OpenFfmpeg and the decoder), and non-seekable inputs fall through to the
normal reconnect path.

Ported from the ai_server branch onto master's OpenFfmpeg structure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:08:58 -04:00
Isaac Connor 356e3226cc Merge branch '4946-videostore-null-stream-guard' 2026-06-27 12:59:15 -04:00
Isaac Connor ad6afadbb6 Merge branch '4943-conf-line-continuation' 2026-06-27 12:59:15 -04:00
Isaac Connor 5fadb760e5 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-27 12:41:37 -04:00
Isaac ConnorandClaude Opus 4.8 ac5fe49d57 fix: guard against null video out stream in encode path fixes #4946
avformat_new_stream() can return NULL, and the encode/transcode branch
dereferenced it immediately via ->codecpar when calling
avcodec_parameters_from_context, segfaulting zmc on the Event recording
thread (SIGSEGV, fault address 0xc). The encoder opens fine; it is the
output stream allocation that comes back NULL.

Check the return value and fail open() gracefully, matching the guard
already present on the PASSTHROUGH path a few lines above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:13:49 -04:00
Isaac ConnorandClaude Opus 4.7 e792020bb4 feat: support backslash line continuation in conf.d parsers fixes #4943
The C++ and PHP parsers for zm.conf and /etc/zm/conf.d/*.conf used fgets
with a 512-byte buffer, silently truncating any line longer than that.
There was also no way to split a value across multiple lines, so editors
hit the cap with no workaround.

Accept a trailing backslash (with optional whitespace before the newline)
as a line-continuation marker. Leading whitespace on continuation lines
is stripped so users can indent for readability without it leaking into
the value. Three parsers all read the same files and must agree:

- src/zm_config.cpp: switch to std::ifstream + std::getline so a single
  physical line is no longer capped at 512 bytes, then join continuation
  lines before running the existing pointer-based parser
- scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in: accumulate a logical
  line across trailing-backslash physical lines
- web/includes/config.php.in: drop the 512-byte fgets cap and accumulate
  in the same way

tests/zm_config.cpp covers three cases against the C++ parser: a
three-segment continuation joins to "firstsecondthird" with leading
whitespace stripped, a bare backslash inside a value is preserved
(C:\Users\zm), and a 1500-byte single line survives intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 12:13:49 -04:00
Isaac ConnorandClaude Opus 4.8 5e499347f6 fix: link stdc++fs into libzm for std::filesystem on GCC 8
zm_eventstream.cpp uses std::filesystem, which compiles into libzm.a.
On Rocky 8 (GCC 8) std::filesystem lives in a separate libstdc++fs that
must be linked explicitly. FILESYSTEM_LIBRARY was only attached to zma,
so zmc, zms, zmu, zmbenchmark and zm_rtsp_server failed to link with
undefined references to std::filesystem symbols.

Attach FILESYSTEM_LIBRARY to the zm library as PUBLIC so all consumers
inherit it transitively, and drop the redundant entry on zma.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:27:47 -04:00
Isaac ConnorandClaude Opus 4.8 cf92249820 fix: build zone alarm highlights in YUV420P to stop spurious Colourise warning
The "Target image is already colourised, colours: 1" warning fired from
Image::Colourise() during zone alarm overlay on YUV420P monitors, even with
ZM_COLOUR_JPEG_FILES off.

monitor->Colours() returns 1 for both GRAY8 and planar YUV420P (the GRAY8
alias), so zm_zone.cpp misclassified YUV420P monitors as grayscale and built
an RGB24 alarm highlight. Overlaying that RGB24 highlight onto the YUV420P
analysis_image hit the RGB-on-mono branch, which calls Colourise() - valid
only for GRAY8. Colourise() warned and bailed, after which the per-pixel loop
walked the 1.5x planar buffer as 3x packed RGB (out-of-bounds write).

Resolve the real capture format via zm_pixformat_from_colours() so true GRAY8
still upgrades to an RGB24 highlight while YUV420P keeps its format. Add a
YUV420P output branch to HighlightEdges (single alarm colour written to luma
and the shared chroma sample, transparent elsewhere) and a YUV420P-on-YUV420P
branch to Overlay (copies luma where non-zero, copies each chroma sample when
any covered luma pixel is marked). Colourise() is now only reached for GRAY8.

Add Catch2 coverage for the YUV420P Overlay masking and HighlightEdges output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:10:07 -04:00
Isaac Connor 356f22c9cc Warning=>Debug 2026-06-25 17:30:35 -04:00
Isaac Connor fb8c1ae0ee Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-19 19:01:58 -04:00
Isaac Connor 60060a5d9f fix: close DB/rename race that exposed incomplete.* to event consumers
On event close ~Event() renamed the video file on disk synchronously but
queued the DefaultVideo=final DB update on the async dbQueue. The monitor's
close_event_thread forks EventEndCommand as soon as ~Event returns, so an
external consumer (e.g. AI analysis via the CakePHP API) could read the
stale DefaultVideo='incomplete.<codec>.<container>' after the file had
already been renamed, producing "File does not exist" errors.

Hard-link the file to its final <Id>-video.* name instead of renaming, so
it resolves under both names for the whole window the DB may still advertise
the old name. Write the finalized Events row synchronously (close runs in
its own thread, so this no longer blocks the analysis thread) and only then
unlink the incomplete.* name, once DefaultVideo=final is committed.

Dedupe by inode in the DiskSpace sum so the doubly-named file is not counted
twice, and guard the m3u8 URL rewrite against the link-failure fallback
where video_file == video_incomplete_file, which would loop forever.
2026-06-19 10:42:18 -04:00
Isaac Connor a380f4a83e fix: keep VideoStore path in sync when incomplete file is renamed
During recording the incomplete video file is renamed to embed the codec
in its name (incomplete.<codec>.<container>) so canPlayCodec() works. The
VideoStore had opened the file under the old name, so FFmpeg's faststart
trailer re-open (oc->url) and the mfra read in finalize() could fail with
ENOENT after the rename.

Add VideoStore::set_filename() to update filename and oc->url, and call it
after the rename. Also warn when movflags contains faststart, which is
incompatible with the fragmented MP4 / HLS output this class produces.
2026-06-19 09:52:36 -04:00
Steve Gilvarry f45286794b Merge pull request #4941 from ZoneMinder/perf-eventstream-reuse-image-modern-cpp
perf: reuse Image buffer in EventStream::sendFrame (modern C++ alternative to #4609)
2026-06-19 21:53:22 +10:00
Isaac ConnorandClaude Opus 4.8 8342effd49 perf: reuse Image buffer in EventStream::sendFrame to cut per-frame heap allocations
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>
2026-06-18 17:45:12 -04:00
Isaac Connor 01de3fd7d5 Merge pull request #4938 from SteveGilvarry/4936-zms-sigfpe-buffer-level
fix: guard zms buffer_level against zero buffer count
2026-06-17 19:19:33 -04:00
SteveGilvarryandClaude Opus 4.8 629842c938 fix: start zms command processor after buffer state is initialised refs #4936
The command_processor thread was started in runStream() before
temp_image_buffer_count, the read/write indices and temp_image_buffer were
assigned, so a command arriving in that window observed a half-initialised
stream (the root cause behind the divide-by-zero guarded in the previous
commit). Keep the thread object declared up front for the join(), but defer
its launch until after the playback_buffer setup so processCommand can only
run against fully initialised state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 08:56:11 +10:00
Steve GilvarryandClaude Opus 4.8 dc4cf32a63 fix: guard zms buffer_level against zero buffer count refs #4936
MonitorStream::processCommand computed buffer_level by dividing by
temp_image_buffer_count (and via MOD_ADD modulo) while only guarding on
playback_buffer. processCommand runs on the command_processor thread,
started in runStream before temp_image_buffer_count is assigned from
playback_buffer. In that window temp_image_buffer_count is still 0 while
playback_buffer is already > 0, so a command arriving then divided by
zero and raised SIGFPE, crashing nph-zms.

Extract the percentage math into MonitorStreamBufferLevel which returns 0
when the buffer count is not positive, and add Catch2 coverage for the
zero-count and wrap-around cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:21:29 +10:00
SteveGilvarryandClaude Opus 4.8 45099e53a7 fix: address review feedback on camera buffer overflow fix
- zm_libvnc_camera.cpp resize(): return FALSE and log if av_malloc fails,
  rather than returning TRUE with a null frameBuffer (libVNC would then
  write into it and crash). Matters more now that size_t sizing can
  request large allocations for server-advertised dimensions.
- zm_libvnc_camera.cpp: compute the buffer sizes in the scale Debug() in
  size_t with %zu, so the logged sizes can't overflow int and match the
  values passed to SWScale::Convert.
- zm_libvlc_camera: widen LibvlcPrivateData::bufferSize to size_t and
  compute it in size_t in PrimeCapture, so the allocation itself can't
  overflow. Image::Assign now passes the same stored bufferSize used for
  the allocation, so the read size can't exceed the buffer. Widen the
  compare loop index to size_t to match.

Verified: both translation units compile with -Werror -fsyntax-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:54:15 +10:00
SteveGilvarryandClaude Opus 4.8 286cb90883 fix: compute camera frame buffer sizes in size_t to avoid int overflow
The frame buffer size calculations in the libVNC and libVLC camera
backends multiplied 16/32-bit operands and only widened the result to
size_t afterwards, so the multiplication itself could overflow before
the conversion. Flagged by CodeQL cpp/integer-multiplication-cast-to-long
(alerts 282, 145, 85). The VNC framebuffer dimensions are advertised by
the remote server (uint16_t), making the inputs externally influenced.

Cast the first operand to size_t so each product is computed in size_t:
- zm_libvnc_camera.cpp: SwScale::Convert src size (framebufferWidth *
  framebufferHeight * 4) and dest size (width * height * colours)
- zm_libvnc_camera.cpp: resize() bufferSize, changed from int to size_t
  (not converted to a larger type, so CodeQL did not flag it, but the
  same overflow applied); Debug format updated to %zu
- zm_libvlc_camera.cpp: Image::Assign size (width * height * mBpp)

Verified: both translation units compile with -Werror -fsyntax-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:55:06 +10:00
Isaac ConnorandClaude Opus 4.8 bc15782474 fix: skip final stream sleep when zm_terminate is already set
Avoid sleeping up to MAX_SLEEP (500ms) at the end of runStream when
termination has been requested, so zms shuts down more promptly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:11:51 -04:00
Isaac ConnorandClaude Opus 4.8 a36375585c refactor: make zm_terminate a std::atomic<bool>
zm_terminate is written from signal handlers and read from daemon loops.
A plain bool is undefined to access from a signal handler and may be hoisted
out of a loop by the optimizer. std::atomic<bool> is lock-free on supported
platforms and matches the already-atomic per-object terminate_ flags. Update
the three Debug() vararg sites to call .load() since atomics can't pass
through ...

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:11:51 -04:00
Isaac Connor 1e76424db1 Merge branch 'onvif-wsse-created-race' 2026-06-14 09:15:01 -05:00
Isaac ConnorandClaude Opus 4.8 a04cd7a432 fix: use utf8mb4 for C++ DB connection so Unicode monitor names log intact refs #4785
The connection charset was "utf8", MySQL's alias for 3-byte utf8mb3, while
Monitors.Name and the DB default are utf8mb4. On a utf8mb3 connection any
4-byte UTF-8 character is read back as '?' and, when embedded in a log message
written to the Logs table, raises ERROR 1366 and truncates the value. Names
with such characters lost their multibyte portion in logs, keeping only the
ASCII tail.

Connect with utf8mb4, falling back to utf8 with a warning if the server
rejects it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:06:37 -04:00
Isaac Connor 55bbec4c55 Merge pull request #4924 from SteveGilvarry/3816-db-ssl-verify-server-cert
feat: add ZM_DB_SSL_VERIFY_SERVER_CERT option (portable across MySQL/MariaDB)
2026-06-14 09:39:36 -04:00
SteveGilvarry 8801c42064 fix: address review feedback on DB SSL verify option
- API (database.php.default): only set the PDO verify flag when SSL is
  actually configured (ZM_DB_SSL_CA_CERT set), matching the web/Perl/C++
  layers. Previously a fresh install's default (1) would set the flag on a
  non-SSL connection, since the CakePHP datasource merges 'flags' uncondi-
  tionally.
- Both PHP layers: cast to string and trim before parsing the value, and use
  strict in_array, to avoid type-juggling and stray-whitespace edge cases.
- zm_db.cpp: use my_bool (not char) for the MYSQL_OPT_SSL_VERIFY_SERVER_CERT
  fallback argument, the type libmysqlclient expects. That branch only
  compiles on older clients without MYSQL_OPT_SSL_MODE, where my_bool exists.

refs #3816
2026-06-14 15:57:09 +10:00
SteveGilvarry e60bdc67b2 feat: add ZM_DB_SSL_VERIFY_SERVER_CERT option (portable across MySQL/MariaDB)
Add a ZM_DB_SSL_VERIFY_SERVER_CERT setting so a database connection that uses
ZM_DB_SSL_CA_CERT can talk to a server with a self-signed or otherwise
non-matching certificate. When enabled, verification is by identity (the cert
must chain to the CA and its CN/SAN must match ZM_DB_HOST), consistent across
the C++ daemons, the PHP web interface, the CakePHP API and the Perl scripts.

This re-does the reverted #3817. That PR broke the build because it called
mysql_options(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, ...), and that enum was removed
from the MySQL 8.0 C client in favour of MYSQL_OPT_SSL_MODE; it also passed a
c_str() where a my_bool* was expected, and referenced the PHP constant
unconditionally (fatal on PHP 8 for an upgraded install whose zm.conf predates
the option).

The option that controls server-cert verification differs by client library and
the symbols are enum values, not macros, so CMake feature-detects them by
compiling:
  - HAVE_MYSQL_OPT_SSL_MODE  (MySQL 5.7.11+/8.0, MariaDB Connector/C 3.1+)
  - HAVE_MYSQL_OPT_SSL_VERIFY_SERVER_CERT  (older MariaDB/MySQL)
zm_db.cpp uses SSL_MODE_VERIFY_IDENTITY / SSL_MODE_REQUIRED when the former is
available, else falls back to the latter with a proper my_bool.

Value handling is three-way in every layer: a truthy value verifies, a false-y
value (0/false/no/off) skips verification, and an empty/unset value leaves the
client default in place so existing installs are unchanged on upgrade. PHP, the
API datasource (via PDO flags) and the Perl DSN are all guarded with defined()
checks. Fresh installs default to 1.

Documents the full ZM_DB_* connection and SSL settings, including the hostname
verification gotcha when connecting by IP, in docs/userguide/configfiles.rst.

refs #3816
2026-06-14 13:20:00 +10:00
Isaac ConnorandClaude Opus 4.8 312ce1d676 fix: pin ONVIF WS-Security Timestamp and UsernameToken Created together
ONVIF PullMessages intermittently failed with ter:NotAuthorized (logged as
the misleading clock-drift error) every few thousand requests, then ZM tore
down a healthy subscription and re-subscribed. SOAP logs showed the trigger:
the failing request always had wsu:Timestamp/Created one second behind
UsernameToken/Created, while every successful request had them identical.

The cause is in set_credentials(): soap_wsse_add_Timestamp() and
soap_wsse_add_UsernameTokenDigest() each call time(NULL) on their own, so when
the two calls straddle a one-second boundary the two Created values diverge by
a second and Hikvision rejects the request. This is probabilistic, which is
why it hit roughly hourly per camera and constantly across a fleet.

Capture time(NULL) once, re-stamp the Timestamp Created/Expires from it, and
use soap_wsse_add_UsernameTokenDigest_at() so the token Created and its
password digest are pinned to the same instant. Both Created values are then
always identical.

Add tests/zm_onvif_wsse.cpp asserting the two Created values match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:12:49 -05:00
Isaac Connor 2a61595335 Merge pull request #4915 from SteveGilvarry/3399-passthrough-flip-displaymatrix
fix: handle flips in passthrough orientation, fix uninitialized display matrix (fixes #3399)
2026-06-13 10:21:16 -04:00
Isaac Connor 5e0a0eeaee Merge pull request #4909 from SteveGilvarry/fix/rotate-plane-segfault
fix: corrupted scaled/converted output for rotated monitors (SWScale buffer alignment)
2026-06-13 10:20:56 -04:00
Isaac ConnorandClaude Opus 4.8 3b633a281b fix: Rotate/Flip honor borrowed source linesize; size Flip dest for 32-aligned planes
get_y_image() wraps the decoder Y plane zero-copy but recorded
FFALIGN(width,32) as the Image stride instead of the frame's real
linesize[0]. For widths that are not a multiple of 32 the decoder packs
the plane tighter than FFALIGN, so:

- Image::Rotate/Flip re-derived the source stride via
  av_image_fill_arrays(...,32) and read past the end of the borrowed
  plane (and skewed Y-channel motion analysis, which reads the same
  buffer).
- Image::Flip sized its destination from this->size, which is tight for a
  borrowed plane and smaller than the 32-aligned layout the planes are
  written at, overrunning the destination.

Record the frame's real linesize in get_y_image(); use the Image's own
linesize for the source plane-0 stride in Rotate/Flip; size Flip's
destination from av_image_get_buffer_size(...,32). All are no-ops for
self-consistent ZM-allocated (32-aligned) images.

tests/zm_image_linesize.cpp: Rotate 90/180/270 and Flip H/V over a tight,
non-32-aligned GRAY8 source verifying correct output.

refs #4788

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:35:40 -04:00
SteveGilvarryandClaude Opus 4.8 10add95dad fix: handle flips in passthrough orientation and fix uninitialized matrix
In passthrough mode the orientation was written as a display-matrix side
data plus the legacy rotate tag, but only rotations were handled; flips
(FLIP_HORI/FLIP_VERT) fell to an "Unsupported Orientation" warning. The
displaymatrix was av_malloc'd and only written in the rotation branches,
so a flipped passthrough monitor attached an uninitialized matrix as side
data — garbage rotation metadata in the MP4.

Initialise the matrix to identity (av_display_rotation_set(.,0)) so it is
always fully written, and express flips with av_display_matrix_flip so
rotation and flips are both carried losslessly in the display matrix with
no decode/re-encode. The legacy rotate tag only expresses rotation, so it
no longer warns for flips.

Note: the rotation component of the display matrix is honored by HTML5
<video>, but the reflection component may not be in all browsers; ZM's web
event player may need a CSS transform companion to render flips mirrored.
Desktop players (VLC/QuickTime) and the file metadata are correct.

refs #3399

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:51:31 +10:00
SteveGilvarryandClaude Fable 5 7e15324385 fix: corrupted scaled/converted output for rotated monitors — SWScale guessed buffer alignment from width
SWScale::Convert chose av_image_fill_arrays alignment by heuristic
(width % 32 ? 1 : 32) for both buffers. Image buffers are always laid
out align-32, so any width not divisible by 32 made Convert read luma
rows 16 bytes short and chroma planes from packed offsets: diagonal
shear plus garbage chroma. Rotating a monitor is what produces such
widths (1280x720 ROTATE_270 -> 720 wide, 3840x2160 ROTATE_90 -> 2160
wide, both % 32 == 16), so every scaled view and every re-encode of a
rotated monitor was corrupted while unrotated monitors (1280/2688/3840
all % 32 == 0) were untouched. The rotate/flip segfault fix exposed
this: before it, rotated planar frames crashed zmc before reaching
Scale.

Alignment is a fact about how a buffer was laid out, not something
derivable from dimensions. Convert now takes explicit in/out alignment:
Image::Scale passes 32/32, the videostore encode path mirrors
get_out_frame's allocation choice, libvnc passes 1 (packed VNC
framebuffer) and 32 (Image WriteBuffer). Remove the unused
SetDefaults/ConvertDefaults API rather than threading alignments
through dead code.

Tests: new Scale regression case on a 720x1280 YUV420P image
(column-banded luma, uniform chroma) fails before the fix exactly as
observed live (sheared rows, V plane reading 0) and passes after.
Full suite 84/84.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:19:09 +10:00
Isaac Connor 3eb0792b57 Merge pull request #4900 from SteveGilvarry/fix/rotate-plane-segfault
fix: rotate/flip segfault from AV_CEIL_RSHIFT on unsigned dimensions
2026-06-12 13:42:57 -04:00
Isaac Connor c103c40337 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-11 08:47:17 -04:00
Isaac ConnorandClaude Opus 4.7 0d93742b97 fix: delete FifoSources before RemoveSession on reconnect failure
When monitor->ShmValid() returns false and the reconnect attempt fails,
the cleanup path erased the video_source/audio_source entries from their
maps without deleting the FifoSource objects, then called RemoveSession
on the MediaSession. Two problems:

1. ZoneMinderFifoSource is only stopped/joined in its destructor. erase()
   on a raw pointer leaks the object and leaves its read_thread_ and
   write_thread_ running.

2. The xop::H264Source / H265Source / AV1Source is owned by the
   MediaSession and gets destroyed inside RemoveSession. The orphaned
   FifoSource still holds a raw m_h264Source pointer set via
   setH264Source(); the next SPS NAL parsed by the still-running
   ReadRun thread calls H264Source::SetSPS on freed memory and crashes
   in std::vector::assign.

Stack of the observed crash:
  H264Source::SetSPS                          H264Source.h:34
  H264_ZoneMinderFifoSource::splitFrames      zm_rtsp_server_fifo_h264_source.cpp:53
  ZoneMinderFifoSource::getNextFrame          zm_rtsp_server_fifo_source.cpp:265
  ZoneMinderFifoSource::ReadRun               zm_rtsp_server_fifo_source.cpp:59

Mirror the correct order already used by the "monitor went away" path
above (lines 188-197) and by the shutdown path (lines 362-375): delete
the FifoSources first so their dtors set stop_ and join the threads,
then RemoveSession can safely destroy the H264Source.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 08:47:13 -04:00
Isaac ConnorandClaude Opus 4.8 dc7d89bea8 fix: bound ONVIF SOAP timeouts below zmdc 30s kill window
The ONVIF event poller runs in its own thread (Run -> WaitForMessage ->
PullMessages). On shutdown ~ONVIF() joins that thread, but the join could
block past zmdc's 30s SIGTERM->SIGKILL window because two timeouts were
effectively unbounded:

- pull_timeout_seconds was only clamped to < 60s, so the camera could hold
  the long-poll open that long.
- gSOAP connect/recv/send timeouts were 0 (infinite), so a hung camera
  blocked recv() forever.

Either case let thread_.join() exceed 30s, causing zmdc to SIGKILL zmc.

Set the SOAP socket timeouts to 25s and clamp pull_timeout to 20s, keeping
the invariant pull_timeout (<=20) < socket_timeout (25) < zmdc window (30):
a quiet long-poll still returns normally, a hung connection aborts within
25s, and the worst-case join finishes with margin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:11:23 -04:00
SteveGilvarryandClaude Fable 5 ee366cdf78 fix: rotate/flip segfault from AV_CEIL_RSHIFT on unsigned dimensions
Image::Rotate and Image::Flip computed chroma plane dimensions with
AV_CEIL_RSHIFT(width, log2_chroma_w). The macro's runtime form is
-((-(a)) >> (b)), which relies on arithmetic right shift of a negative
value and is only valid for signed operands - FFmpeg always passes int.
Image::width/height are unsigned, so the negation wraps, the shift is
logical, and the result is 2^31 + ceil(w/2^b) instead of ceil(w/2^b):
for 1280x720 the chroma rotate received src_w=2147484288 and
src_h=2147484008 (captured in gdb), writing gigabytes out of bounds.

Effect: zmc's decoder thread segfaulted on the first decoded frame of
any monitor with a rotated or flipped orientation and a planar pixel
format - a monitor with decoding Always crash-loops.

Replace the macro at both sites with an explicit unsigned ceiling
shift helper and a comment documenting the trap.

Tests: new tests/zm_image.cpp covers Rotate 90/180/270 and Flip on
YUV420P with per-plane marker pixels, plus odd dimensions exercising
the chroma ceiling. The Rotate cases segfault before this fix (verified,
exit 139) and pass after. Full suite 85/85 via ctest. Live-verified on
a 1280x720 ROTATE_270 monitor with decoding Always: pre-fix crash
within seconds, post-fix 90s clean run under gdb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:42:52 +10:00
Isaac Connor 37cfb64922 Merge pull request #4788 from ZoneMinder/fix/avpixelformat-respin
fix: deprecate ZM_COLOUR for AVPixelFormat (resubmit with planar/SHM fixes)
2026-06-06 12:45:03 -04:00
Isaac ConnorandClaude Opus 4.7 9687faac67 refactor: remove dead FfmpegCamera::imagePixFormat field
The field was assigned in three branches of the constructor (always to
the same value as Camera::pixelFormat) and never read anywhere. Unlike
LocalCamera, FfmpegCamera doesn't run a sws_scale step at the Camera
layer — libavcodec hands frames to the pipeline directly — so there's
no separate capture-vs-image format distinction to track.

Drop the redundant assignments and the member declaration. The
canonical camera-side format is now exclusively Camera::pixelFormat
(via Camera::PixelFormat()).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 12:29:35 -04:00
Isaac ConnorandClaude Opus 4.7 b486c0cd6b fix: include <libavutil/pixdesc.h> in zm_pixformat.h (PR #4788)
zm_get_pix_fmt_name() calls av_get_pix_fmt_name() which is declared in
<libavutil/pixdesc.h>. The header previously got away with relying on
transitive includes via zm_ffmpeg.h, but tests/zm_pixformat.cpp (and
any other consumer that includes only zm_pixformat.h) would fail to
compile. Add the explicit include so the header is self-contained.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 11:49:30 -04:00
Isaac ConnorandClaude Opus 4.7 2b55ed45d1 fix: nullptr-safe pix fmt name in Colourise/Deinterlace_4Field error paths (PR #4788)
- Image::Colourise sizing-failure Error: av_image_get_buffer_size /
  av_image_get_linesize return negative for AV_PIX_FMT_NONE and
  unrecognised formats, so this branch fires exactly when
  av_get_pix_fmt_name(p_req_pixfmt) can return nullptr. Switched to
  zm_get_pix_fmt_name.
- Image::Deinterlace_4Field fallback Panic: same issue — fires when
  imagePixFormat is outside our dispatch set, where av_get_pix_fmt_name
  may return nullptr. Switched to zm_get_pix_fmt_name.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 10:01:13 -04:00
Isaac ConnorandClaude Opus 4.7 965936eaf8 fix: pass delta8_* fn ptrs directly; zm_get_pix_fmt_name in Delta panic (PR #4788)
- Image::Delta: pass delta8_bgr/rgb/argb/abgr/bgra/rgba/gray8 directly to
  delta_row() instead of dereferencing with *. They're already function
  pointers; the deref is redundant and would be undefined behaviour if
  any pointer were null (Initialise() sets them, but the explicit *
  obscures the intent and adds nothing).
- Switched the Delta fallback Panic from av_get_pix_fmt_name() to the
  nullptr-safe zm_get_pix_fmt_name() wrapper. The Panic fires exactly
  when imagePixFormat is unrecognised, where the raw function would
  return nullptr.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 21:26:55 -04:00
Isaac ConnorandClaude Opus 4.7 12d864e772 fix: Deinterlace_Discard OOB write on odd heights (PR #4788)
The y += 2 loop ran while `y < height`, but copied even row y into odd
row y+1. For odd image heights, the final iteration had y == height-1
and wrote to row height — past the end of the image buffer in all three
colour branches (GRAY8/YUV-Y plane, RGB24, RGB32).

Stop one short of the last row (y < height - 1) so y+1 stays in
bounds; the orphan last row in odd-height images is left untouched
(consistent with the (even, odd) pairing this discard pass is doing).
Also added a guard for height < 2 to keep the unsigned `height - 1`
calculation from wrapping.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:10:39 -04:00
Isaac Connor 20f95ccde9 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-04 09:46:11 -04:00
Isaac ConnorandClaude Opus 4.7 6ecf51061a fix: more actionable error messages in GetImage/getSnapshot/WriteBuffer (PR #4788)
- Monitor::GetImage and Monitor::getSnapshot bounds-check branches were
  logging "Image Buffer has not been allocated", but by the time control
  reaches them the empty-vector check has already passed — the buffer IS
  allocated, the index is just out of range. Replaced with explicit
  "index N out of range (image_buffer.size() = M)" so debugging starts
  from the right place.
- Image::WriteBuffer unsupported-format error only printed `colours`,
  but the mapping in zm_pixformat_from_colours depends on both `colours`
  and `subpixelorder`, and planar formats legitimately have colours=1.
  Print both fields so a bad (colours, subpixelorder) pair is
  immediately identifiable.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:12:26 -04:00