From ed84517dc0cd93abd9986c3e294363f15fed22b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 12:48:50 +0000 Subject: [PATCH] Generate snapshot.jpg/alarm.jpg from video at end of event when no decoded frames were available When a monitor runs in PASSTHROUGH mode or decodes only keyframes, packet->image is null for most (or all) packets, so snapshot.jpg and alarm.jpg are never written during recording. Fix: in Event::~Event(), after the video file is finalised, if snapshot_file_written is still false and a completed video file exists on disk, open it with FFmpeg_Input, seek to the offset of the highest-scoring frame (tracked via the new max_score_time member), decode one frame, convert it to a ZM Image, and write snapshot.jpg. If alarm frames were recorded but alarm.jpg was also never written, copy snapshot.jpg to alarm.jpg as a fallback. Fixes #issue - snapshot.jpg generation for recorded events. --- src/zm_event.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ src/zm_event.h | 1 + 2 files changed, 70 insertions(+) diff --git a/src/zm_event.cpp b/src/zm_event.cpp index 7838b72af..9ed14766f 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -22,6 +22,7 @@ #include "zm_camera.h" #include "zm_db.h" #include "zm_event_tag.h" +#include "zm_ffmpeg_input.h" #include "zm_frame.h" #include "zm_logger.h" #include "zm_monitor.h" @@ -64,6 +65,7 @@ Event::Event( tot_score(0), max_score(-1), max_score_frame_id(0), + max_score_time(p_start_time), //path(""), //snapshit_file(), snapshot_file_written(false), @@ -244,6 +246,72 @@ Event::~Event() { } } + // If snapshot.jpg was not written during recording (e.g. PASSTHROUGH mode + // with no decoded images), try to generate it now from the completed video. + if (!snapshot_file_written && !video_file.empty()) { + std::string actual_video_path = path + "/" + video_file; + struct stat fs; + if (stat(actual_video_path.c_str(), &fs) == 0) { + double offset_secs = std::max(0.0, + std::chrono::duration_cast(max_score_time - start_time).count()); + FFmpeg_Input ffmpeg_input; + if (ffmpeg_input.Open(actual_video_path.c_str()) > 0) { + int vid_stream = ffmpeg_input.get_video_stream_id(); + if (vid_stream >= 0) { + // av_frame is owned by ffmpeg_input (av_frame_ptr) and freed on scope exit + AVFrame *av_frame = ffmpeg_input.get_frame(vid_stream, offset_secs); + if (av_frame) { + Image snap(av_frame, av_frame->width, av_frame->height); + if (snap.WriteJpeg(snapshot_file)) { + snapshot_file_written = true; + Debug(1, "Generated snapshot.jpg from video for event %" PRIu64, id); + } else { + Warning("Failed to write snapshot.jpg from video for event %" PRIu64, id); + } + } else { + Warning("Failed to get video frame for snapshot, event %" PRIu64, id); + } + } + } else { + Warning("Failed to open video %s to generate snapshot for event %" PRIu64, + actual_video_path.c_str(), id); + } + } + } + + // If alarm.jpg was not written but we have alarm frames and a snapshot exists, + // copy snapshot.jpg to alarm.jpg as a fallback. + if (!alarm_frame_written && alarm_frames > 0 && snapshot_file_written) { + struct stat alarm_stat; + if (stat(alarm_file.c_str(), &alarm_stat) != 0) { + // alarm.jpg doesn't exist, copy snapshot.jpg to alarm.jpg + FILE *src = fopen(snapshot_file.c_str(), "rb"); + if (src) { + FILE *dst = fopen(alarm_file.c_str(), "wb"); + if (dst) { + char buf[65536]; + size_t n; + bool write_ok = true; + while ((n = fread(buf, 1, sizeof(buf), src)) > 0) { + if (fwrite(buf, 1, n, dst) != n) { + write_ok = false; + break; + } + } + fclose(dst); + if (write_ok) { + Debug(1, "Copied snapshot.jpg to alarm.jpg for event %" PRIu64, id); + } else { + Warning("Failed to copy snapshot.jpg to alarm.jpg for event %" PRIu64, id); + } + } else { + Warning("Failed to open alarm.jpg for writing for event %" PRIu64, id); + } + fclose(src); + } + } + } + // endtime is set in AddFrame, so SHOULD be set to the value of the last frame timestamp. if (end_time.time_since_epoch() == Seconds(0)) { Warning("Empty endtime for event. Should not happen. Setting to now."); @@ -604,6 +672,7 @@ void Event::AddFrame(const std::shared_ptr&packet) { if (score > max_score) { max_score = score; max_score_frame_id = frames; + max_score_time = packet->timestamp; } if (db_frame) { diff --git a/src/zm_event.h b/src/zm_event.h index 38cee8a6d..a75295526 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -92,6 +92,7 @@ class Event { int tot_score; int max_score; int max_score_frame_id; + SystemTimePoint max_score_time; std::string path; std::string snapshot_file; bool snapshot_file_written;