Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Frigate HTTP API using FastAPI #1

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6abdffb
Semantic Search for Detections (#11899)
hunterjm Jun 21, 2024
c1ffc7e
Semantic Search API (#12105)
hunterjm Jun 23, 2024
acd5754
reindex events in batches to reduce memory and cpu load (#12124)
hunterjm Jun 23, 2024
a87cd9e
Semantic Search Frontend (#12112)
NickM-27 Jun 23, 2024
665e10f
Use 127.0.0.1 for chroma (#12135)
Daniel-dev22 Jun 24, 2024
8a2977d
Chroma logs in Frontend (#12131)
hunterjm Jun 24, 2024
5d1c9c7
Use thumbnails instead of review images for search (#12381)
NickM-27 Jul 10, 2024
b2f0913
Use grid for searches (#12386)
NickM-27 Jul 11, 2024
295ce5a
Initial support for Hailo-8L (#12431)
spanner3003 Jul 14, 2024
d0e2f76
Fix calendar
NickM-27 Jul 18, 2024
430e2bc
Catch hailo initialization error (#12558)
NickM-27 Jul 22, 2024
a45e8ae
Implement support for notifications (#12523)
NickM-27 Jul 22, 2024
14a5983
Disable semantic search by default (#12568)
NickM-27 Aug 4, 2024
78beeef
Notification action (#12742)
NickM-27 Aug 5, 2024
06aaa42
Update version
NickM-27 Aug 6, 2024
75ae7f6
Fix embeddings failing to start
NickM-27 Aug 8, 2024
b3c059f
Chunk timeline deletes (#12900)
NickM-27 Aug 9, 2024
5f80ec2
Hailo amd64 support (#12820)
NickM-27 Aug 9, 2024
442fc05
Use review item thumbnail for export (#12998)
NickM-27 Aug 12, 2024
9bea0ac
Add support for review information side panel (#13063)
NickM-27 Aug 14, 2024
4172e19
Move plus dialog to separate component
NickM-27 Aug 14, 2024
5085965
Add ability to upload to Frigate+ from review side panel (#13071)
NickM-27 Aug 14, 2024
981cfb4
Make review detail scrollable on mobile and ensure F+ is enabled (#13…
NickM-27 Aug 16, 2024
0257698
Add button for downloading full set of logs (#13188)
NickM-27 Aug 19, 2024
c866331
Fix ZMQ race condition with events (#13198)
NickM-27 Aug 19, 2024
47440e0
Fix mobile scroll behavior (#13201)
NickM-27 Aug 19, 2024
6f4f438
POC: started example of fastapi usage
iursevla Aug 20, 2024
1bca8e2
POC: Started example for logs endpoint
iursevla Aug 20, 2024
3312077
POC: Fix logs endpoint
iursevla Aug 21, 2024
efb89d3
POC: Updated preview frames endpoint to use FastAPI
iursevla Aug 21, 2024
30b6f1c
POC: Started adding tags for preview and logs
iursevla Aug 22, 2024
50a79a0
POC: Convert the preview routes to use FastAPI
iursevla Aug 22, 2024
6bdc784
POC: Convert the media latest.webp/latest.jpg route to use FastAPI
iursevla Aug 22, 2024
ae079fc
POC: Logs won't be moved for time being
iursevla Aug 22, 2024
9767d22
POC: Removed test files
iursevla Aug 22, 2024
b4d2c3c
POC: Simplified app setup by moving the new_app.py to frigate/api
iursevla Aug 22, 2024
1104f39
POC: Removed TODO
iursevla Aug 22, 2024
1dea920
POC: Removed TODO
iursevla Aug 22, 2024
07f7422
POC: Disabled nginx rule for images
iursevla Aug 24, 2024
abcfb84
POC: Set uvicorn http config to auto
iursevla Aug 24, 2024
cef4749
POC: tags can be global for the APIRouter
iursevla Aug 24, 2024
2e8b284
POC: tags can be global for the APIRouter
iursevla Aug 24, 2024
f51bf50
POC: Removed duplicated http property
iursevla Aug 24, 2024
a071f12
POC: Minimum properties passed to uvicorn.run
iursevla Aug 24, 2024
e7b6eea
POC: Updated comment
iursevla Aug 24, 2024
694c082
POC: Removed TODO
iursevla Aug 24, 2024
f1f198c
POC: Revert minor change
iursevla Aug 24, 2024
1c97857
Merge branch '0.15' into poc-fast-api
iursevla Aug 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions docker/main/rootfs/usr/local/nginx/conf/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,16 @@ http {
include proxy.conf;
}

location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ {
include auth_request.conf;
rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api;
include proxy.conf;
}
# FIXME: Needed to disabled this rule, otherwise it fails for endpoints that end with one of those file extensions
# 1. with httptools it passes the auth.conf but then throws a 400 error "WARN "Invalid HTTP request received." -> https://github.com/encode/uvicorn/blob/47304d9ae76321f0f5f649ff4f73e09b17085933/uvicorn/protocols/http/httptools_impl.py#L165
# 2. With h11 it goes through the auth.conf but returns a 404 error
# We might need to add extra rules that will allow endpoint that end with an extension OR find a fix without creating other rules
# location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ {
# include auth_request.conf;
# rewrite ^/api/(.*)$ $1 break;
# proxy_pass http://frigate_api;
# include proxy.conf;
# }

location /api/ {
include auth_request.conf;
Expand Down
49 changes: 28 additions & 21 deletions frigate/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from typing import Optional

import requests
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from flask import Blueprint, Flask, current_app, jsonify, make_response, request
from markupsafe import escape
from peewee import operator
Expand All @@ -21,7 +24,6 @@
from frigate.api.export import ExportBp
from frigate.api.media import MediaBp
from frigate.api.notification import NotificationBp
from frigate.api.preview import PreviewBp
from frigate.api.review import ReviewBp
from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR
Expand All @@ -47,11 +49,12 @@
bp.register_blueprint(EventBp)
bp.register_blueprint(ExportBp)
bp.register_blueprint(MediaBp)
bp.register_blueprint(PreviewBp)
bp.register_blueprint(ReviewBp)
bp.register_blueprint(AuthBp)
bp.register_blueprint(NotificationBp)

router = APIRouter()


def create_app(
frigate_config,
Expand Down Expand Up @@ -454,19 +457,26 @@ def vainfo():
)


@bp.route("/logs/<service>", methods=["GET"])
def logs(service: str):
@router.get("/logs/{service}", tags=["Logs"])
def logs(
service: str,
download: Optional[str] = None,
start: Optional[int] = 0,
end: Optional[int] = None,
):
"""Get logs for the requested service (frigate/nginx/go2rtc/chroma)"""

def download_logs(service_location: str):
try:
file = open(service_location, "r")
contents = file.read()
file.close()
return jsonify(contents)
return JSONResponse(jsonable_encoder(contents))
except FileNotFoundError as e:
logger.error(e)
return make_response(
jsonify({"success": False, "message": "Could not find log file"}),
500,
return JSONResponse(
content={"success": False, "message": "Could not find log file"},
status_code=500,
)

log_locations = {
Expand All @@ -478,17 +488,14 @@ def download_logs(service_location: str):
service_location = log_locations.get(service)

if not service_location:
return make_response(
jsonify({"success": False, "message": "Not a valid service"}),
404,
return JSONResponse(
content={"success": False, "message": "Not a valid service"},
status_code=404,
)

if request.args.get("download", type=bool, default=False):
if download:
return download_logs(service_location)

start = request.args.get("start", type=int, default=0)
end = request.args.get("end", type=int)

try:
file = open(service_location, "r")
contents = file.read()
Expand Down Expand Up @@ -529,15 +536,15 @@ def download_logs(service_location: str):

logLines.append(currentLine)

return make_response(
jsonify({"totalLines": len(logLines), "lines": logLines[start:end]}),
200,
return JSONResponse(
content={"totalLines": len(logLines), "lines": logLines[start:end]},
status_code=200,
)
except FileNotFoundError as e:
logger.error(e)
return make_response(
jsonify({"success": False, "message": "Could not find log file"}),
500,
return JSONResponse(
content={"success": False, "message": "Could not find log file"},
status_code=500,
)


Expand Down
102 changes: 60 additions & 42 deletions frigate/api/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

import base64
import glob
import io
import logging
import os
import subprocess as sp
import time
from datetime import datetime, timedelta, timezone
from typing import Optional
from urllib.parse import unquote

import cv2
import numpy as np
import pytz
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse
from flask import Blueprint, Response, current_app, jsonify, make_response, request
from peewee import DoesNotExist, fn
from tzlocal import get_localzone_name
Expand All @@ -32,6 +36,8 @@

MediaBp = Blueprint("media", __name__)

router = APIRouter(tags=["Media"])


@MediaBp.route("/<camera_name>")
def mjpeg_feed(camera_name):
Expand Down Expand Up @@ -92,90 +98,102 @@ def camera_ptz_info(camera_name):
404,
)


@MediaBp.route("/<camera_name>/latest.jpg")
@MediaBp.route("/<camera_name>/latest.webp")
def latest_frame(camera_name):
@router.get("/{camera_name}/latest.{extension}")
def latest_frame(
request: Request,
camera_name: str,
extension: str, # jpg/jpeg/png/webp
bbox: Optional[int] = None,
timestamp: Optional[int] = None,
zones: Optional[int] = None,
mask: Optional[int] = None,
motion: Optional[int] = None,
regions: Optional[int] = None,
quality: Optional[int] = 70,
h: Optional[int] = None,
):
draw_options = {
"bounding_boxes": request.args.get("bbox", type=int),
"timestamp": request.args.get("timestamp", type=int),
"zones": request.args.get("zones", type=int),
"mask": request.args.get("mask", type=int),
"motion_boxes": request.args.get("motion", type=int),
"regions": request.args.get("regions", type=int),
"bounding_boxes": bbox,
"timestamp": timestamp,
"zones": zones,
"mask": mask,
"motion_boxes": motion,
"regions": regions,
}
resize_quality = request.args.get("quality", default=70, type=int)
extension = os.path.splitext(request.path)[1][1:]

if camera_name in current_app.frigate_config.cameras:
frame = current_app.detected_frames_processor.get_current_frame(
if camera_name in request.app.frigate_config.cameras:
frame = request.app.detected_frames_processor.get_current_frame(
camera_name, draw_options
)
retry_interval = float(
current_app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
or 10
)

if frame is None or datetime.now().timestamp() > (
current_app.detected_frames_processor.get_current_frame_time(camera_name)
request.app.detected_frames_processor.get_current_frame_time(camera_name)
+ retry_interval
):
if current_app.camera_error_image is None:
if request.app.camera_error_image is None:
error_image = glob.glob("/opt/frigate/frigate/images/camera-error.jpg")

if len(error_image) > 0:
current_app.camera_error_image = cv2.imread(
request.app.camera_error_image = cv2.imread(
error_image[0], cv2.IMREAD_UNCHANGED
)

frame = current_app.camera_error_image
frame = request.app.camera_error_image

height = int(request.args.get("h", str(frame.shape[0])))
height = int(h or str(frame.shape[0]))
width = int(height * frame.shape[1] / frame.shape[0])

if frame is None:
return make_response(
jsonify({"success": False, "message": "Unable to get valid frame"}),
500,
return JSONResponse(
content={"success": False, "message": "Unable to get valid frame"},
status_code=500,
)

if height < 1 or width < 1:
return (
"Invalid height / width requested :: {} / {}".format(height, width),
400,
return JSONResponse(
content="Invalid height / width requested :: {} / {}".format(
height, width
),
status_code=400,
)

frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)

ret, img = cv2.imencode(
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), resize_quality]
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), quality]
)
response = make_response(img.tobytes())
response.headers["Content-Type"] = f"image/{extension}"
response.headers["Cache-Control"] = "no-store"
return response
elif camera_name == "birdseye" and current_app.frigate_config.birdseye.restream:
return StreamingResponse(
io.BytesIO(img.tobytes()),
media_type=f"image/{extension}",
headers={"Content-Type": f"image/{extension}", "Cache-Control": "no-store"},
)
elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream:
frame = cv2.cvtColor(
current_app.detected_frames_processor.get_current_frame(camera_name),
request.app.detected_frames_processor.get_current_frame(camera_name),
cv2.COLOR_YUV2BGR_I420,
)

height = int(request.args.get("h", str(frame.shape[0])))
height = int(h or str(frame.shape[0]))
width = int(height * frame.shape[1] / frame.shape[0])

frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)

ret, img = cv2.imencode(
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), resize_quality]
f".{extension}", frame, [int(cv2.IMWRITE_WEBP_QUALITY), quality]
)
return StreamingResponse(
io.BytesIO(img.tobytes()),
media_type=f"image/{extension}",
headers={"Content-Type": f"image/{extension}", "Cache-Control": "no-store"},
)
response = make_response(img.tobytes())
response.headers["Content-Type"] = f"image/{extension}"
response.headers["Cache-Control"] = "no-store"
return response
else:
return make_response(
jsonify({"success": False, "message": "Camera not found"}),
404,
return JSONResponse(
content={"success": False, "message": "Camera not found"},
status_code=404,
)


Expand Down
37 changes: 37 additions & 0 deletions frigate/api/new_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging

from fastapi import FastAPI

from frigate.api import app as main_app
from frigate.api import media, preview

logger = logging.getLogger(__name__)

# https://fastapi.tiangolo.com/tutorial/metadata/#use-your-tags
tags_metadata = [
{
"name": "Preview",
"description": "Preview routes",
},
{
"name": "Logs",
"description": "Logs routes",
},
{
"name": "Media",
"description": "Media routes",
},
]


def create_fastapi_app(frigate_config, detected_frames_processor):
logger.info("Starting FastAPI app")
app = FastAPI(debug=False, tags_metadata=tags_metadata)
app.include_router(preview.router)
app.include_router(media.router)
app.include_router(main_app.router)
app.frigate_config = frigate_config
app.detected_frames_processor = detected_frames_processor
app.camera_error_image = None

return app
Loading