Merge pull request #4941 from ZoneMinder/perf-eventstream-reuse-image-modern-cpp

perf: reuse Image buffer in EventStream::sendFrame (modern C++ alternative to #4609)
This commit is contained in:
Steve Gilvarry
2026-06-19 21:53:22 +10:00
committed by GitHub
2 changed files with 26 additions and 9 deletions

View File

@@ -27,6 +27,7 @@
#include "zm_storage.h"
#include <algorithm>
#include <arpa/inet.h>
#include <memory>
#include <sys/stat.h>
#include <filesystem>
@@ -861,23 +862,25 @@ bool EventStream::sendFrame(Microseconds delta_us) {
// Reusable string member avoids per-frame heap allocations.
// After the first frame, the string's buffer is reused (unless path exceeds capacity).
reuse_filepath_.clear();
struct stat filestat = {};
// exists() with an error_code overload mirrors stat()'s behaviour (treat any
// failure as "not present") without throwing or filling an unused struct stat.
std::error_code exists_ec;
// This needs to be abstracted. If we are saving jpgs, then load the capture file.
// If we are only saving analysis frames, then send that.
if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
reuse_filepath_ = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
if (!std::filesystem::exists(reuse_filepath_, exists_ec)) {
Debug(1, "analyze file %s not found will try to stream from other", reuse_filepath_.c_str());
reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
if (!std::filesystem::exists(reuse_filepath_, exists_ec)) {
Debug(1, "capture file %s not found either", reuse_filepath_.c_str());
reuse_filepath_.clear();
}
}
} else if (event_data->SaveJPEGs & 1) {
reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
if (!std::filesystem::exists(reuse_filepath_, exists_ec)) {
Debug(1, "Capture file %s not found (bulk/interpolated frame %d), trying ffmpeg_input",
reuse_filepath_.c_str(), curr_frame_id);
reuse_filepath_.clear();
@@ -889,9 +892,12 @@ bool EventStream::sendFrame(Microseconds delta_us) {
}
if ( type == STREAM_MPEG ) {
Image image(reuse_filepath_.c_str());
// Decode into the reused member rather than a fresh per-frame stack Image;
// ReadJpeg reuses its buffer when dimensions match, avoiding a ~2MB
// malloc/free each frame.
reuse_image_.ReadJpeg(reuse_filepath_, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB);
Image *send_image = prepareImage(&image);
Image *send_image = prepareImage(&reuse_image_);
if ( !vid_stream ) {
vid_stream = new VideoStream("pipe:", format, bitrate, effective_fps,
@@ -919,9 +925,18 @@ bool EventStream::sendFrame(Microseconds delta_us) {
}
} else {
Image *image = nullptr;
// Owns the image only on the ffmpeg (mp4) path; the JPEG path decodes into
// the reused member, so nothing is freed there. unique_ptr replaces the
// old new/delete pair and frees automatically at scope exit.
std::unique_ptr<Image> owned_image;
if (!reuse_filepath_.empty()) {
image = new Image(reuse_filepath_.c_str());
// Decode into the reused member instead of allocating a fresh Image each
// frame. ReadJpeg's WriteBuffer reuses the pixel allocation when the
// dimensions match (every frame of a given event), eliminating a ~2MB
// malloc/free per streamed frame.
reuse_image_.ReadJpeg(reuse_filepath_, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB);
image = &reuse_image_;
} else if (ffmpeg_input) {
// Get the frame from the mp4 input
const FrameData *frame_data = &event_data->frames[curr_frame_id-1];
@@ -929,7 +944,8 @@ bool EventStream::sendFrame(Microseconds delta_us) {
ffmpeg_input->get_video_stream_id(),
FPSeconds(frame_data->offset).count());
if (frame) {
image = new Image(frame, monitor->Width(), monitor->Height());
owned_image = std::make_unique<Image>(frame, monitor->Width(), monitor->Height());
image = owned_image.get();
} else {
Error("Failed getting a frame.");
sendTextFrame("Failed getting frame");
@@ -998,7 +1014,7 @@ bool EventStream::sendFrame(Microseconds delta_us) {
break;
}
int rc = send_buffer(img_buffer, img_buffer_size);
delete image;
// owned_image (ffmpeg path) frees here; reuse_image_ persists for next frame
image = nullptr;
if (!rc) return false;
} // end if send_raw or not

View File

@@ -128,6 +128,7 @@ class EventStream : public StreamBase {
Storage *storage;
FFmpeg_Input *ffmpeg_input;
std::string reuse_filepath_; // reused across sendFrame calls to avoid per-frame heap alloc
Image reuse_image_; // reused JPEG decode target; ReadJpeg reuses its buffer when dimensions match
};
#endif // ZM_EVENTSTREAM_H