diff --git a/frigate/api/media.py b/frigate/api/media.py index be4ea08d8..ba4bba608 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -7,6 +7,7 @@ import os import subprocess as sp import time from datetime import datetime, timedelta, timezone +from io import BytesIO from urllib.parse import unquote import cv2 @@ -14,6 +15,7 @@ import numpy as np import pytz from flask import Blueprint, Response, current_app, jsonify, make_response, request from peewee import DoesNotExist, fn +from PIL import Image from tzlocal import get_localzone_name from werkzeug.utils import secure_filename @@ -26,6 +28,7 @@ from frigate.const import ( ) from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment from frigate.util.builtin import get_tz_modifiers +from frigate.util.image import get_image_from_recording logger = logging.getLogger(__name__) @@ -205,30 +208,20 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str): try: recording: Recordings = recording_query.get() time_in_segment = frame_time - recording.start_time + image_data = get_image_from_recording(recording.path, time_in_segment) - ffmpeg_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "warning", - "-ss", - f"00:00:{time_in_segment}", - "-i", - recording.path, - "-frames:v", - "1", - "-c:v", - "png", - "-f", - "image2pipe", - "-", - ] + if not image_data: + return make_response( + jsonify( + { + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + } + ), + 404, + ) - process = sp.run( - ffmpeg_cmd, - capture_output=True, - ) - response = make_response(process.stdout) + response = make_response(image_data) response.headers["Content-Type"] = "image/png" return response except DoesNotExist: @@ -243,6 +236,71 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str): ) +@MediaBp.route("//plus/") +def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str): + if camera_name not in current_app.frigate_config.cameras: + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) + + frame_time = float(frame_time) + recording_query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + ( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + ) + + try: + recording: Recordings = recording_query.get() + time_in_segment = frame_time - recording.start_time + image_data = get_image_from_recording(recording.path, time_in_segment) + + if not image_data: + return make_response( + jsonify( + { + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + } + ), + 404, + ) + + nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) + current_app.plus_api.upload_image(nd, camera_name) + + return make_response( + jsonify( + { + "success": True, + "message": "Successfully submitted image.", + } + ), + 200, + ) + except DoesNotExist: + return make_response( + jsonify( + { + "success": False, + "message": "Recording not found at {}".format(frame_time), + } + ), + 404, + ) + + @MediaBp.route("/recordings/storage", methods=["GET"]) def get_recordings_storage_usage(): recording_stats = current_app.stats_emitter.get_latest_stats()["service"][ diff --git a/frigate/util/image.py b/frigate/util/image.py index 67f8b5c22..ac32f095e 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -2,6 +2,7 @@ import datetime import logging +import subprocess as sp from abc import ABC, abstractmethod from multiprocessing import shared_memory from string import printable @@ -360,14 +361,17 @@ def yuv_crop_and_resize(frame, region, height=None): # copy u2 yuv_cropped_frame[ size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, - size // 2 + uv_channel_x_offset : size // 2 + size // 2 + + uv_channel_x_offset : size // 2 + uv_channel_x_offset + uv_crop_width, ] = frame[u2[1] : u2[3], u2[0] : u2[2]] # copy v1 yuv_cropped_frame[ - size + size // 4 + uv_channel_y_offset : size + size + + size // 4 + + uv_channel_y_offset : size + size // 4 + uv_channel_y_offset + uv_crop_height, @@ -376,11 +380,14 @@ def yuv_crop_and_resize(frame, region, height=None): # copy v2 yuv_cropped_frame[ - size + size // 4 + uv_channel_y_offset : size + size + + size // 4 + + uv_channel_y_offset : size + size // 4 + uv_channel_y_offset + uv_crop_height, - size // 2 + uv_channel_x_offset : size // 2 + size // 2 + + uv_channel_x_offset : size // 2 + uv_channel_x_offset + uv_crop_width, ] = frame[v2[1] : v2[3], v2[0] : v2[2]] @@ -746,3 +753,37 @@ def add_mask(mask: str, mask_img: np.ndarray): ] ) cv2.fillPoly(mask_img, pts=[contour], color=(0)) + + +def get_image_from_recording( + file_path: str, relative_frame_time: float +) -> Optional[any]: + """retrieve a frame from given time in recording file.""" + + ffmpeg_cmd = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "warning", + "-ss", + f"00:00:{relative_frame_time}", + "-i", + file_path, + "-frames:v", + "1", + "-c:v", + "png", + "-f", + "image2pipe", + "-", + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode == 0: + return process.stdout + else: + return None