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