diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index ea7d5ace0..60cba061c 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -27,6 +27,7 @@ #include "zm_storage.h" #include #include +#include #include #include @@ -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 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(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 diff --git a/src/zm_eventstream.h b/src/zm_eventstream.h index a58afbb34..84a79f754 100644 --- a/src/zm_eventstream.h +++ b/src/zm_eventstream.h @@ -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