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: FastAPI #13615

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
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
60 changes: 34 additions & 26 deletions frigate/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@
from typing import Optional

import requests
from fastapi import APIRouter, Path, Request, Response
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
from playhouse.sqliteq import SqliteQueueDatabase
from werkzeug.middleware.proxy_fix import ProxyFix

from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.defs.tags import Tags
from frigate.api.event import EventBp
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 +50,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 @@ -105,15 +109,15 @@ def _db_close(exc):
return app


@bp.route("/")
@router.get("/", tags=[Tags.app])
def is_healthy():
return "Frigate is running. Alive and healthy!"


@bp.route("/config/schema.json")
def config_schema():
return current_app.response_class(
current_app.frigate_config.schema_json(), mimetype="application/json"
@router.get("/config/schema.json", tags=[Tags.app])
def config_schema(request: Request):
return Response(
content=request.app.frigate_config.schema_json(), media_type="application/json"
)


Expand Down Expand Up @@ -454,19 +458,26 @@ def vainfo():
)


@bp.route("/logs/<service>", methods=["GET"])
def logs(service: str):
@router.get("/logs/{service}", tags=[Tags.logs])
def logs(
service: str = Path(enum=["frigate", "nginx", "go2rtc", "chroma"]),
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 +489,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 +537,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
8 changes: 8 additions & 0 deletions frigate/api/defs/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class Tags(Enum):
app = "App"
preview = "Preview"
logs = "Logs"
media = "Media"
37 changes: 37 additions & 0 deletions frigate/api/fastapi_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 preview
from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer

logger = logging.getLogger(__name__)


def create_fastapi_app(
frigate_config,
detected_frames_processor,
storage_maintainer: StorageMaintainer,
onvif: OnvifController,
plus_api: PlusApi,
stats_emitter: StatsEmitter,
):
logger.info("Starting FastAPI app")
app = FastAPI(debug=False)
# Routes
app.include_router(main_app.router)
app.include_router(preview.router)
# App Properties
app.frigate_config = frigate_config
app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer
app.camera_error_image = None
app.onvif = onvif
app.plus_api = plus_api
app.stats_emitter = stats_emitter

return app
52 changes: 23 additions & 29 deletions frigate/api/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@
from datetime import datetime, timedelta, timezone

import pytz
from flask import (
Blueprint,
jsonify,
make_response,
)
from fastapi import APIRouter
from fastapi.responses import JSONResponse

from frigate.api.defs.tags import Tags
from frigate.const import CACHE_DIR, PREVIEW_FRAME_TYPE
from frigate.models import Previews

logger = logging.getLogger(__name__)

PreviewBp = Blueprint("previews", __name__)

router = APIRouter(tags=[Tags.preview])

@PreviewBp.route("/preview/<camera_name>/start/<int:start_ts>/end/<int:end_ts>")
@PreviewBp.route("/preview/<camera_name>/start/<float:start_ts>/end/<float:end_ts>")
def preview_ts(camera_name, start_ts, end_ts):

@router.get("/preview/{camera_name}/start/{start_ts}/end/{end_ts}")
def preview_ts(camera_name: str, start_ts: float, end_ts: float):
"""Get all mp4 previews relevant for time period."""
if camera_name != "all":
camera_clause = Previews.camera == camera_name
Expand Down Expand Up @@ -62,24 +60,20 @@ def preview_ts(camera_name, start_ts, end_ts):
)

if not clips:
return make_response(
jsonify(
{
"success": False,
"message": "No previews found.",
}
),
404,
return JSONResponse(
content={
"success": False,
"message": "No previews found.",
},
status_code=404,
)

return make_response(jsonify(clips), 200)
return JSONResponse(content=clips, status_code=200)


@PreviewBp.route("/preview/<year_month>/<int:day>/<int:hour>/<camera_name>/<tz_name>")
@PreviewBp.route(
"/preview/<year_month>/<float:day>/<float:hour>/<camera_name>/<tz_name>"
)
def preview_hour(year_month, day, hour, camera_name, tz_name):
@router.get("/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}")
def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: str):
"""Get all mp4 previews relevant for time period given the timezone"""
parts = year_month.split("-")
start_date = (
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc)
Expand All @@ -92,11 +86,8 @@ def preview_hour(year_month, day, hour, camera_name, tz_name):
return preview_ts(camera_name, start_ts, end_ts)


@PreviewBp.route("/preview/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/frames")
@PreviewBp.route(
"/preview/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/frames"
)
def get_preview_frames_from_cache(camera_name: str, start_ts, end_ts):
@router.get("/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames")
def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: float):
"""Get list of cached preview frames"""
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
file_start = f"preview_{camera_name}"
Expand All @@ -116,4 +107,7 @@ def get_preview_frames_from_cache(camera_name: str, start_ts, end_ts):

selected_previews.append(file)

return jsonify(selected_previews)
return JSONResponse(
content=selected_previews,
status_code=200,
)
22 changes: 20 additions & 2 deletions frigate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
from typing import Optional

import psutil
import uvicorn
from fastapi.middleware.wsgi import WSGIMiddleware
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError

from frigate.api.app import create_app
from frigate.api.auth import hash_password
from frigate.api.fastapi_app import create_fastapi_app
from frigate.comms.config_updater import ConfigPublisher
from frigate.comms.dispatcher import Communicator, Dispatcher
from frigate.comms.inter_process import InterProcessCommunicator
Expand Down Expand Up @@ -397,6 +400,15 @@ def init_web_server(self) -> None:
self.stats_emitter,
)

self.fastapi_app = create_fastapi_app(
self.config,
self.detected_frames_processor,
self.storage_maintainer,
self.onvif_controller,
self.plus_api,
self.stats_emitter,
)
Comment on lines +403 to +410
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickM-27 @hawkeye217 @blakeblackshear When you have some time please take a look. Please let me know what you think about this PR.

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do, we are currently really busy with ffmpeg 7

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, @NickM-27, totally understandable. I hope you guys get it working 💯 .

I'll continue working on other endpoints in my free time. I already covered all media endpoints, and today I will convert notification and preview (at least).

So, in short:

  • POC -> this PR (let's call it B1)
  • media endpoints -> This branch (B2)
  • notification and preview -> on it (B3)

Basically dev <- B1 <- B2 <- B3 <- B.....


def init_onvif(self) -> None:
self.onvif_controller = OnvifController(self.config, self.ptz_metrics)

Expand Down Expand Up @@ -754,11 +766,17 @@ def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
signal.signal(signal.SIGTERM, receiveSignal)

try:
self.flask_app.run(host="127.0.0.1", port=5001, debug=False, threaded=True)
# Run the flask app inside fastapi: https://fastapi.tiangolo.com/advanced/sub-applications/
self.fastapi_app.mount("", WSGIMiddleware(self.flask_app))
uvicorn.run(
self.fastapi_app,
host="127.0.0.1",
port=5001,
)
except KeyboardInterrupt:
pass

logger.info("Flask has exited...")
logger.info("FastAPI/Flask has exited...")

self.stop()

Expand Down