use latest preview frame for latest image when camera is offline

This commit is contained in:
Josh Hawkins 2026-01-22 10:42:23 -06:00
parent 31ee62b760
commit a6c5f4a82b
2 changed files with 162 additions and 16 deletions

View File

@ -42,6 +42,7 @@ from frigate.const import (
PREVIEW_FRAME_TYPE,
)
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
from frigate.output.preview import get_most_recent_preview_frame
from frigate.track.object_processing import TrackedObjectProcessor
from frigate.util.file import get_event_thumbnail_bytes
from frigate.util.image import get_image_from_recording
@ -159,20 +160,38 @@ async def latest_frame(
or 10
)
is_offline = False
if frame is None or datetime.now().timestamp() > (
frame_processor.get_current_frame_time(camera_name) + retry_interval
):
if request.app.camera_error_image is None:
error_image = glob.glob(
os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg")
)
last_frame_time = frame_processor.get_current_frame_time(camera_name)
config: FrigateConfig = request.app.frigate_config
preview_path = get_most_recent_preview_frame(
camera_name, before=last_frame_time, ffmpeg_config=config.ffmpeg
)
if len(error_image) > 0:
request.app.camera_error_image = cv2.imread(
error_image[0], cv2.IMREAD_UNCHANGED
if preview_path:
logger.debug(f"Using most recent preview frame for {camera_name}")
frame = cv2.imread(preview_path, cv2.IMREAD_UNCHANGED)
if frame is not None:
is_offline = True
if frame is None or not is_offline:
logger.debug(
f"No live or preview frame available for {camera_name}. Using error image."
)
if request.app.camera_error_image is None:
error_image = glob.glob(
os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg")
)
frame = request.app.camera_error_image
if len(error_image) > 0:
request.app.camera_error_image = cv2.imread(
error_image[0], cv2.IMREAD_UNCHANGED
)
frame = request.app.camera_error_image
height = int(params.height or str(frame.shape[0]))
width = int(height * frame.shape[1] / frame.shape[0])
@ -194,14 +213,18 @@ async def latest_frame(
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
_, img = cv2.imencode(f".{extension.value}", frame, quality_params)
headers = {
"Cache-Control": "no-store" if not params.store else "private, max-age=60",
}
if is_offline:
headers["X-Frigate-Offline"] = "true"
return Response(
content=img.tobytes(),
media_type=extension.get_mime_type(),
headers={
"Cache-Control": "no-store"
if not params.store
else "private, max-age=60",
},
headers=headers,
)
elif (
camera_name == "birdseye"

View File

@ -8,13 +8,14 @@ import subprocess as sp
import threading
import time
from pathlib import Path
from typing import Any
from typing import Any, Optional
import cv2
import numpy as np
from peewee import DoesNotExist
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import CameraConfig, RecordQualityEnum
from frigate.config import CameraConfig, FfmpegConfig, RecordQualityEnum
from frigate.const import CACHE_DIR, CLIPS_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE
from frigate.ffmpeg_presets import (
FPS_VFR_PARAM,
@ -23,7 +24,12 @@ from frigate.ffmpeg_presets import (
)
from frigate.models import Previews
from frigate.track.object_processing import TrackedObject
from frigate.util.image import copy_yuv_to_position, get_blank_yuv_frame, get_yuv_crop
from frigate.util.image import (
copy_yuv_to_position,
get_blank_yuv_frame,
get_yuv_crop,
run_ffmpeg_snapshot,
)
logger = logging.getLogger(__name__)
@ -57,6 +63,123 @@ def get_cache_image_name(camera: str, frame_time: float) -> str:
)
def get_most_recent_preview_frame(
camera: str, before: float = None, ffmpeg_config: Optional[FfmpegConfig] = None
) -> str | None:
"""Get the most recent preview frame for a camera.
First tries to find a cached preview webp frame. If none exists (e.g., they've been
cleaned up after preview video creation), attempts to extract the last frame from
the most recent preview video.
Args:
camera: Camera name
before: Optional timestamp to find frames before this time
ffmpeg_config: Optional ffmpeg configuration for extracting from video fallback
Returns:
Path to a preview frame image file, or None if not available
"""
if not os.path.exists(PREVIEW_CACHE_DIR):
logger.debug(f"Preview cache directory does not exist: {PREVIEW_CACHE_DIR}")
else:
try:
# files are named preview_{camera}-{timestamp}.webp
# we want the largest timestamp that is less than or equal to before
preview_files = [
f
for f in os.listdir(PREVIEW_CACHE_DIR)
if f.startswith(f"preview_{camera}-")
and f.endswith(f".{PREVIEW_FRAME_TYPE}")
]
if preview_files:
# sort by timestamp in descending order
# filenames are like preview_front-1712345678.901234.webp
preview_files.sort(reverse=True)
if before is None:
return os.path.join(PREVIEW_CACHE_DIR, preview_files[0])
for file_name in preview_files:
try:
# Extract timestamp: preview_front-1712345678.901234.webp
# Split by dash and extension
timestamp_part = file_name.split("-")[-1].split(
f".{PREVIEW_FRAME_TYPE}"
)[0]
timestamp = float(timestamp_part)
if timestamp <= before:
return os.path.join(PREVIEW_CACHE_DIR, file_name)
except (ValueError, IndexError):
continue
except Exception as e:
logger.error(f"Error searching for cached preview frames: {e}")
# If no cached frames exist and ffmpeg_config is provided, try extracting from preview video
if ffmpeg_config is not None:
try:
logger.debug(
f"No cached preview frames found for {camera}, attempting to extract from preview video"
)
query = Previews.select().where(Previews.camera == camera)
if before is not None:
query = query.where(Previews.end_time <= before)
preview = query.order_by(Previews.end_time.desc()).limit(1).get()
if preview and os.path.exists(preview.path):
logger.debug(
f"Found preview video for {camera}: {preview.path} (duration: {preview.duration}s)"
)
# Extract the last frame from the video
# Use webp for consistency with cached frames
image_data, error_msg = run_ffmpeg_snapshot(
ffmpeg_config,
preview.path,
codec="webp",
seek_time=max(0, preview.duration - 0.1), # Seek to near the end
height=PREVIEW_HEIGHT,
timeout=10,
)
if image_data:
# Write to a temporary file in the cache directory
# Use timestamp from the preview end time
temp_frame_path = os.path.join(
PREVIEW_CACHE_DIR,
f"preview_{camera}-{preview.end_time}_from_video.{PREVIEW_FRAME_TYPE}",
)
with open(temp_frame_path, "wb") as f:
f.write(image_data)
logger.debug(
f"Successfully extracted last frame from preview video to {temp_frame_path}"
)
return temp_frame_path
else:
logger.warning(
f"Failed to extract frame from preview video {preview.path}: {error_msg}"
)
else:
if preview:
logger.warning(f"Preview video path does not exist: {preview.path}")
else:
logger.debug(f"No preview video found in database for {camera}")
except DoesNotExist:
logger.debug(f"No preview video found in database for {camera}")
except Exception as e:
logger.error(f"Error extracting frame from preview video: {e}")
return None
class FFMpegConverter(threading.Thread):
"""Convert a list of still frames into a vfr mp4."""