From fef1fb36ccdee2cad183b59fb82ea0e71abc4707 Mon Sep 17 00:00:00 2001 From: FL42 <46161216+fl42@users.noreply.github.com> Date: Sun, 8 Feb 2026 15:47:06 +0100 Subject: [PATCH] feat: add X-Frame-Time when returning snapshot (#21932) Co-authored-by: Florent MORICONI <170678386+fmcloudconsulting@users.noreply.github.com> --- frigate/api/media.py | 3 ++- frigate/track/object_processing.py | 2 +- frigate/track/tracked_object.py | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 7d0d02a45..01776f903 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -761,7 +761,7 @@ async def event_snapshot( if event_id in camera_state.tracked_objects: tracked_obj = camera_state.tracked_objects.get(event_id) if tracked_obj is not None: - jpg_bytes = tracked_obj.get_img_bytes( + jpg_bytes, frame_time = tracked_obj.get_img_bytes( ext="jpg", timestamp=params.timestamp, bounding_box=params.bbox, @@ -790,6 +790,7 @@ async def event_snapshot( headers = { "Content-Type": "image/jpeg", "Cache-Control": "private, max-age=31536000" if event_complete else "no-store", + "X-Frame-Time": frame_time, } if params.download: diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index e0ee74228..f44f21be3 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -185,7 +185,7 @@ class TrackedObjectProcessor(threading.Thread): def snapshot(camera: str, obj: TrackedObject) -> bool: mqtt_config: CameraMqttConfig = self.config.cameras[camera].mqtt if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj): - jpg_bytes = obj.get_img_bytes( + jpg_bytes, _ = obj.get_img_bytes( ext="jpg", timestamp=mqtt_config.timestamp, bounding_box=mqtt_config.bounding_box, diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index a95221bbd..f435de7b6 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -434,7 +434,7 @@ class TrackedObject: return count > (self.camera_config.detect.stationary.threshold or 50) def get_thumbnail(self, ext: str) -> bytes | None: - img_bytes = self.get_img_bytes( + img_bytes, _ = self.get_img_bytes( ext, timestamp=False, bounding_box=False, crop=True, height=175 ) @@ -475,20 +475,21 @@ class TrackedObject: crop: bool = False, height: int | None = None, quality: int | None = None, - ) -> bytes | None: + ) -> tuple[bytes | None, float | None]: if self.thumbnail_data is None: - return None + return None, None try: + frame_time = self.thumbnail_data["frame_time"] best_frame = cv2.cvtColor( - self.frame_cache[self.thumbnail_data["frame_time"]]["frame"], + self.frame_cache[frame_time]["frame"], cv2.COLOR_YUV2BGR_I420, ) except KeyError: logger.warning( - f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache" + f"Unable to create jpg because frame {frame_time} is not in the cache" ) - return None + return None, None if bounding_box: thickness = 2 @@ -570,13 +571,13 @@ class TrackedObject: ret, jpg = cv2.imencode(f".{ext}", best_frame, quality_params) if ret: - return jpg.tobytes() + return jpg.tobytes(), frame_time else: - return None + return None, None def write_snapshot_to_disk(self) -> None: snapshot_config: SnapshotsConfig = self.camera_config.snapshots - jpg_bytes = self.get_img_bytes( + jpg_bytes, _ = self.get_img_bytes( ext="jpg", timestamp=snapshot_config.timestamp, bounding_box=snapshot_config.bounding_box,