mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-25 05:28:29 +03:00
use latest preview frame for latest image when camera is offline
This commit is contained in:
parent
31ee62b760
commit
a6c5f4a82b
@ -42,6 +42,7 @@ from frigate.const import (
|
|||||||
PREVIEW_FRAME_TYPE,
|
PREVIEW_FRAME_TYPE,
|
||||||
)
|
)
|
||||||
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
|
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.track.object_processing import TrackedObjectProcessor
|
||||||
from frigate.util.file import get_event_thumbnail_bytes
|
from frigate.util.file import get_event_thumbnail_bytes
|
||||||
from frigate.util.image import get_image_from_recording
|
from frigate.util.image import get_image_from_recording
|
||||||
@ -159,20 +160,38 @@ async def latest_frame(
|
|||||||
or 10
|
or 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_offline = False
|
||||||
if frame is None or datetime.now().timestamp() > (
|
if frame is None or datetime.now().timestamp() > (
|
||||||
frame_processor.get_current_frame_time(camera_name) + retry_interval
|
frame_processor.get_current_frame_time(camera_name) + retry_interval
|
||||||
):
|
):
|
||||||
if request.app.camera_error_image is None:
|
last_frame_time = frame_processor.get_current_frame_time(camera_name)
|
||||||
error_image = glob.glob(
|
config: FrigateConfig = request.app.frigate_config
|
||||||
os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg")
|
preview_path = get_most_recent_preview_frame(
|
||||||
)
|
camera_name, before=last_frame_time, ffmpeg_config=config.ffmpeg
|
||||||
|
)
|
||||||
|
|
||||||
if len(error_image) > 0:
|
if preview_path:
|
||||||
request.app.camera_error_image = cv2.imread(
|
logger.debug(f"Using most recent preview frame for {camera_name}")
|
||||||
error_image[0], cv2.IMREAD_UNCHANGED
|
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]))
|
height = int(params.height or str(frame.shape[0]))
|
||||||
width = int(height * frame.shape[1] / 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)
|
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
_, img = cv2.imencode(f".{extension.value}", frame, quality_params)
|
_, 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(
|
return Response(
|
||||||
content=img.tobytes(),
|
content=img.tobytes(),
|
||||||
media_type=extension.get_mime_type(),
|
media_type=extension.get_mime_type(),
|
||||||
headers={
|
headers=headers,
|
||||||
"Cache-Control": "no-store"
|
|
||||||
if not params.store
|
|
||||||
else "private, max-age=60",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
camera_name == "birdseye"
|
camera_name == "birdseye"
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import subprocess as sp
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
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.const import CACHE_DIR, CLIPS_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE
|
||||||
from frigate.ffmpeg_presets import (
|
from frigate.ffmpeg_presets import (
|
||||||
FPS_VFR_PARAM,
|
FPS_VFR_PARAM,
|
||||||
@ -23,7 +24,12 @@ from frigate.ffmpeg_presets import (
|
|||||||
)
|
)
|
||||||
from frigate.models import Previews
|
from frigate.models import Previews
|
||||||
from frigate.track.object_processing import TrackedObject
|
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__)
|
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):
|
class FFMpegConverter(threading.Thread):
|
||||||
"""Convert a list of still frames into a vfr mp4."""
|
"""Convert a list of still frames into a vfr mp4."""
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user