Convert all media endpoints to FastAPI. Added /media prefix (/media/camera && media/events && /media/preview)

This commit is contained in:
Rui Alves 2024-09-08 15:33:56 +01:00
parent b83f9532ab
commit 012c953dfb
17 changed files with 566 additions and 473 deletions

View File

@ -23,7 +23,6 @@ from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.api.event import EventBp from frigate.api.event import EventBp
from frigate.api.export import ExportBp from frigate.api.export import ExportBp
from frigate.api.media import MediaBp
from frigate.api.notification import NotificationBp from frigate.api.notification import NotificationBp
from frigate.api.review import ReviewBp from frigate.api.review import ReviewBp
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@ -49,7 +48,6 @@ logger = logging.getLogger(__name__)
bp = Blueprint("frigate", __name__) bp = Blueprint("frigate", __name__)
bp.register_blueprint(EventBp) bp.register_blueprint(EventBp)
bp.register_blueprint(ExportBp) bp.register_blueprint(ExportBp)
bp.register_blueprint(MediaBp)
bp.register_blueprint(ReviewBp) bp.register_blueprint(ReviewBp)
bp.register_blueprint(AuthBp) bp.register_blueprint(AuthBp)
bp.register_blueprint(NotificationBp) bp.register_blueprint(NotificationBp)

View File

@ -3,7 +3,7 @@ import logging
from fastapi import FastAPI from fastapi import FastAPI
from frigate.api import app as main_app from frigate.api import app as main_app
from frigate.api import preview from frigate.api import media, preview
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
@ -21,9 +21,13 @@ def create_fastapi_app(
stats_emitter: StatsEmitter, stats_emitter: StatsEmitter,
): ):
logger.info("Starting FastAPI app") logger.info("Starting FastAPI app")
app = FastAPI(debug=False) app = FastAPI(
debug=False,
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
)
# Routes # Routes
app.include_router(main_app.router) app.include_router(main_app.router)
app.include_router(media.router)
app.include_router(preview.router) app.include_router(preview.router)
# App Properties # App Properties
app.frigate_config = frigate_config app.frigate_config = frigate_config

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,14 @@ import os
import unittest import unittest
from unittest.mock import Mock from unittest.mock import Mock
from fastapi.testclient import TestClient
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from frigate.api.app import create_app from frigate.api.app import create_app
from frigate.api.fastapi_app import create_fastapi_app
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.plus import PlusApi from frigate.plus import PlusApi
@ -362,24 +364,23 @@ class TestHttp(unittest.TestCase):
assert config["cameras"]["front_door"] assert config["cameras"]["front_door"]
def test_recordings(self): def test_recordings(self):
app = create_app( app_fastapi = create_fastapi_app(
FrigateConfig(**self.minimal_config).runtime_config(), FrigateConfig(**self.minimal_config).runtime_config(),
self.db,
None,
None,
None, None,
None, None,
None, None,
PlusApi(), PlusApi(),
None, None,
) )
client = TestClient(app_fastapi)
id = "123456.random" id = "123456.random"
with app.test_client() as client: _insert_mock_recording(id)
_insert_mock_recording(id) response = client.get("/media/camera/front_door/recordings")
recording = client.get("/front_door/recordings").json assert response.status_code == 200
assert recording recording = response.json()
assert recording[0]["id"] == id assert recording
assert recording[0]["id"] == id
def test_stats(self): def test_stats(self):
stats = Mock(spec=StatsEmitter) stats = Mock(spec=StatsEmitter)

View File

@ -54,7 +54,7 @@ export default function CameraImage({
return; return;
} }
const newSrc = `${apiHost}api/${name}/latest.webp?h=${requestHeight}${ const newSrc = `${apiHost}api/media/camera/${name}/frame/latest?extension=webp&?height=${requestHeight}${
searchParams ? `&${searchParams}` : "" searchParams ? `&${searchParams}` : ""
}`; }`;

View File

@ -89,7 +89,7 @@ export default function CameraImage({
if (!config || scaledHeight === 0 || !canvasRef.current) { if (!config || scaledHeight === 0 || !canvasRef.current) {
return; return;
} }
img.src = `${apiHost}api/${name}/latest.webp?h=${scaledHeight}${ img.src = `${apiHost}api/media/camera/${name}/frame/latest?extension=webp&height=${scaledHeight}${
searchParams ? `&${searchParams}` : "" searchParams ? `&${searchParams}` : ""
}`; }`;
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);

View File

@ -173,7 +173,7 @@ export function AnimatedEventCard({
}} }}
> >
<source <source
src={`${baseUrl}api/review/${event.id}/preview?format=mp4`} src={`${baseUrl}api/media/review/${event.id}/preview?format=mp4`}
type="video/mp4" type="video/mp4"
/> />
</video> </video>

View File

@ -160,13 +160,13 @@ export default function ObjectLifecycle({
// image // image
const [src, setSrc] = useState( const [src, setSrc] = useState(
`${apiHost}api/${event.camera}/recordings/${event.start_time + annotationOffset / 1000}/snapshot.jpg?height=500`, `${apiHost}api/media/camera/${event.camera}/recordings/${event.start_time + annotationOffset / 1000}/snapshot.jpg?height=500`,
); );
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
useEffect(() => { useEffect(() => {
if (timeIndex) { if (timeIndex) {
const newSrc = `${apiHost}api/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`; const newSrc = `${apiHost}api/media/camera/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`;
setSrc(newSrc); setSrc(newSrc);
} }
setImgLoaded(false); setImgLoaded(false);

View File

@ -248,8 +248,8 @@ function EventItem({
draggable={false} draggable={false}
src={ src={
event.has_snapshot event.has_snapshot
? `${apiHost}api/events/${event.id}/snapshot.jpg` ? `${apiHost}api/media/events/${event.id}/snapshot.jpg`
: `${apiHost}api/events/${event.id}/thumbnail.jpg` : `${apiHost}api/media/events/${event.id}/thumbnail.jpg`
} }
/> />
{hovered && ( {hovered && (
@ -263,8 +263,8 @@ function EventItem({
download download
href={ href={
event.has_snapshot event.has_snapshot
? `${apiHost}api/events/${event.id}/snapshot.jpg` ? `${apiHost}api/media/events/${event.id}/snapshot.jpg`
: `${apiHost}api/events/${event.id}/thumbnail.jpg` : `${apiHost}api/media/events/${event.id}/thumbnail.jpg`
} }
> >
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">

View File

@ -95,7 +95,7 @@ export function FrigatePlusDialog({
{upload?.id && ( {upload?.id && (
<img <img
className={`w-full ${grow} bg-black`} className={`w-full ${grow} bg-black`}
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`} src={`${baseUrl}api/media/events/${upload?.id}/snapshot.jpg`}
alt={`${upload?.label}`} alt={`${upload?.label}`}
/> />
)} )}

View File

@ -586,7 +586,7 @@ class PreviewFramesController extends PreviewController {
if (this.seeking) { if (this.seeking) {
this.timeToSeek = frame; this.timeToSeek = frame;
} else { } else {
const newSrc = `${baseUrl}api/preview/preview_${this.camera}-${frame}.webp/thumbnail.webp`; const newSrc = `${baseUrl}api/media/preview/preview_${this.camera}-${frame}.webp/thumbnail.webp`;
if (this.imgController.current.src != newSrc) { if (this.imgController.current.src != newSrc) {
this.imgController.current.src = newSrc; this.imgController.current.src = newSrc;
@ -603,7 +603,7 @@ class PreviewFramesController extends PreviewController {
} }
if (this.timeToSeek) { if (this.timeToSeek) {
const newSrc = `${baseUrl}api/preview/preview_${this.camera}-${this.timeToSeek}.webp/thumbnail.webp`; const newSrc = `${baseUrl}api/media/preview/preview_${this.camera}-${this.timeToSeek}.webp/thumbnail.webp`;
if (this.imgController.current.src != newSrc) { if (this.imgController.current.src != newSrc) {
this.imgController.current.src = newSrc; this.imgController.current.src = newSrc;

View File

@ -149,7 +149,7 @@ export default function DynamicVideoPlayer({
} }
const time = controller.getProgress(playTime); const time = controller.getProgress(playTime);
return axios.post(`/${camera}/plus/${time}`); return axios.post(`/media/camera/${camera}/plus/${time}`);
}, },
[camera, controller], [camera, controller],
); );
@ -164,7 +164,7 @@ export default function DynamicVideoPlayer({
[timeRange], [timeRange],
); );
const { data: recordings } = useSWR<Recording[]>( const { data: recordings } = useSWR<Recording[]>(
[`${camera}/recordings`, recordingParams], [`media/camera/${camera}/recordings`, recordingParams],
{ revalidateOnFocus: false }, { revalidateOnFocus: false },
); );

View File

@ -433,7 +433,7 @@ export function InProgressPreview({
<div className="relative flex size-full items-center bg-black"> <div className="relative flex size-full items-center bg-black">
<img <img
className="pointer-events-none size-full object-contain" className="pointer-events-none size-full object-contain"
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`} src={`${apiHost}api/media/preview/${previewFrames[key]}/thumbnail.webp`}
onLoad={handleLoad} onLoad={handleLoad}
/> />
{showProgress && ( {showProgress && (

View File

@ -42,7 +42,7 @@ export function PolygonCanvas({
const element = new window.Image(); const element = new window.Image();
element.width = width; element.width = width;
element.height = height; element.height = height;
element.src = `${apiHost}api/${camera}/latest.webp?cache=${Date.now()}`; element.src = `${apiHost}api/media/camera/${camera}/frame/latest?extension=webp&?cache=${Date.now()}`;
return element; return element;
} }
// we know that these deps are correct // we know that these deps are correct

View File

@ -259,7 +259,7 @@ export default function SubmitPlus() {
</div> </div>
<img <img
className="aspect-video h-full rounded-lg object-contain md:rounded-2xl" className="aspect-video h-full rounded-lg object-contain md:rounded-2xl"
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`} src={`${baseUrl}api/media/events/${event.id}/snapshot.jpg`}
loading="lazy" loading="lazy"
/> />
</div> </div>

View File

@ -499,7 +499,9 @@ function PtzControlPanel({
clickOverlay: boolean; clickOverlay: boolean;
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>; setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
}) { }) {
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`); const { data: ptz } = useSWR<CameraPtzInfo>(
`/media/camera/${camera}/ptz/info`,
);
const { send: sendPtz } = usePtzCommand(camera); const { send: sendPtz } = usePtzCommand(camera);

View File

@ -18,7 +18,7 @@ type StorageMetricsProps = {
export default function StorageMetrics({ export default function StorageMetrics({
setLastUpdated, setLastUpdated,
}: StorageMetricsProps) { }: StorageMetricsProps) {
const { data: cameraStorage } = useSWR<CameraStorage>("recordings/storage"); const { data: cameraStorage } = useSWR<CameraStorage>("media/recordings/storage");
const { data: stats } = useSWR<FrigateStats>("stats"); const { data: stats } = useSWR<FrigateStats>("stats");
const totalStorage = useMemo(() => { const totalStorage = useMemo(() => {