Process subclass for output_frames

This commit is contained in:
George Tsiamasiotis 2024-10-02 12:14:14 +03:00
parent 09256a4cc8
commit 3badc757cc
2 changed files with 139 additions and 153 deletions

View File

@ -13,7 +13,6 @@ from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from frigate import util
from frigate.api.auth import hash_password from frigate.api.auth import hash_password
from frigate.api.fastapi_app import create_fastapi_app from frigate.api.fastapi_app import create_fastapi_app
from frigate.camera.camera import Camera from frigate.camera.camera import Camera
@ -55,7 +54,7 @@ from frigate.models import (
) )
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output.output import output_frames from frigate.output.output import OutputProcessor
from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.record.cleanup import RecordingCleanup from frigate.record.cleanup import RecordingCleanup
@ -334,15 +333,9 @@ class FrigateApp:
self.detected_frames_processor.start() self.detected_frames_processor.start()
def start_video_output_processor(self) -> None: def start_video_output_processor(self) -> None:
output_processor = util.Process( self.output_processor = OutputProcessor(self.config)
target=output_frames, self.output_processor.start()
name="output_processor", logger.info(f"Output process started: {self.output_processor.pid}")
args=(self.config,),
daemon=True,
)
self.output_processor = output_processor
output_processor.start()
logger.info(f"Output process started: {output_processor.pid}")
def init_cameras(self) -> None: def init_cameras(self) -> None:
for name in self.config.cameras.keys(): for name in self.config.cameras.keys():

View File

@ -1,15 +1,11 @@
"""Handle outputting raw frigate frames""" """Handle outputting raw frigate frames"""
import logging
import multiprocessing as mp
import os import os
import shutil import shutil
import signal
import threading import threading
from typing import Optional from typing import Optional
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
from setproctitle import setproctitle
from ws4py.server.wsgirefserver import ( from ws4py.server.wsgirefserver import (
WebSocketWSGIHandler, WebSocketWSGIHandler,
WebSocketWSGIRequestHandler, WebSocketWSGIRequestHandler,
@ -17,6 +13,7 @@ from ws4py.server.wsgirefserver import (
) )
from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.server.wsgiutils import WebSocketWSGIApplication
from frigate import util
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
from frigate.comms.ws import WebSocket from frigate.comms.ws import WebSocket
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@ -26,171 +23,167 @@ from frigate.output.camera import JsmpegCamera
from frigate.output.preview import PreviewRecorder from frigate.output.preview import PreviewRecorder
from frigate.util.image import SharedMemoryFrameManager from frigate.util.image import SharedMemoryFrameManager
logger = logging.getLogger(__name__)
class OutputProcessor(util.Process):
def __init__(self, config: FrigateConfig):
super().__init__(name="frigate.output", daemon=True)
self.config = config
def output_frames( def run(self):
config: FrigateConfig, frame_manager = SharedMemoryFrameManager()
):
threading.current_thread().name = "output"
setproctitle("frigate.output")
stop_event = mp.Event() # start a websocket server on 8082
WebSocketWSGIHandler.http_version = "1.1"
websocket_server = make_server(
"127.0.0.1",
8082,
server_class=WSGIServer,
handler_class=WebSocketWSGIRequestHandler,
app=WebSocketWSGIApplication(handler_cls=WebSocket),
)
websocket_server.initialize_websockets_manager()
websocket_thread = threading.Thread(target=websocket_server.serve_forever)
def receiveSignal(signalNumber, frame): detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video)
stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) jsmpeg_cameras: dict[str, JsmpegCamera] = {}
signal.signal(signal.SIGINT, receiveSignal) birdseye: Optional[Birdseye] = None
preview_recorders: dict[str, PreviewRecorder] = {}
preview_write_times: dict[str, float] = {}
frame_manager = SharedMemoryFrameManager() self.move_preview_frames("cache")
# start a websocket server on 8082 for camera, cam_config in self.config.cameras.items():
WebSocketWSGIHandler.http_version = "1.1" if not cam_config.enabled:
websocket_server = make_server( continue
"127.0.0.1",
8082,
server_class=WSGIServer,
handler_class=WebSocketWSGIRequestHandler,
app=WebSocketWSGIApplication(handler_cls=WebSocket),
)
websocket_server.initialize_websockets_manager()
websocket_thread = threading.Thread(target=websocket_server.serve_forever)
detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video) jsmpeg_cameras[camera] = JsmpegCamera(
cam_config, self.stop_event, websocket_server
jsmpeg_cameras: dict[str, JsmpegCamera] = {}
birdseye: Optional[Birdseye] = None
preview_recorders: dict[str, PreviewRecorder] = {}
preview_write_times: dict[str, float] = {}
move_preview_frames("cache")
for camera, cam_config in config.cameras.items():
if not cam_config.enabled:
continue
jsmpeg_cameras[camera] = JsmpegCamera(cam_config, stop_event, websocket_server)
preview_recorders[camera] = PreviewRecorder(cam_config)
preview_write_times[camera] = 0
if config.birdseye.enabled:
birdseye = Birdseye(config, stop_event, websocket_server)
websocket_thread.start()
while not stop_event.is_set():
(topic, data) = detection_subscriber.check_for_update(timeout=1)
if not topic:
continue
(
camera,
frame_time,
current_tracked_objects,
motion_boxes,
regions,
) = data
frame_id = f"{camera}{frame_time}"
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)
if frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")
continue
# send camera frame to ffmpeg process if websockets are connected
if any(
ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager
):
# write to the converter for the camera if clients are listening to the specific camera
jsmpeg_cameras[camera].write_frame(frame.tobytes())
# send output data to birdseye if websocket is connected or restreaming
if config.birdseye.enabled and (
config.birdseye.restream
or any(
ws.environ["PATH_INFO"].endswith("birdseye")
for ws in websocket_server.manager
) )
): preview_recorders[camera] = PreviewRecorder(cam_config)
birdseye.write_data( preview_write_times[camera] = 0
if self.config.birdseye.enabled:
birdseye = Birdseye(self.config, self.stop_event, websocket_server)
websocket_thread.start()
while not self.stop_event.is_set():
(topic, data) = detection_subscriber.check_for_update(timeout=1)
if not topic:
continue
(
camera, camera,
frame_time,
current_tracked_objects, current_tracked_objects,
motion_boxes, motion_boxes,
frame_time, regions,
frame, ) = data
frame_id = f"{camera}{frame_time}"
frame = frame_manager.get(
frame_id, self.config.cameras[camera].frame_shape_yuv
) )
# send frames for low fps recording if frame is None:
generated_preview = preview_recorders[camera].write_data( self.logger.debug(f"Failed to get frame {frame_id} from SHM")
current_tracked_objects, motion_boxes, frame_time, frame continue
)
preview_write_times[camera] = frame_time
# if another camera generated a preview, # send camera frame to ffmpeg process if websockets are connected
# check for any cameras that are currently offline if any(
# and need to generate a preview ws.environ["PATH_INFO"].endswith(camera)
if generated_preview: for ws in websocket_server.manager
for camera, time in preview_write_times.copy().items(): ):
if time != 0 and frame_time - time > 10: # write to the converter for the camera if clients are listening to the specific camera
preview_recorders[camera].flag_offline(frame_time) jsmpeg_cameras[camera].write_frame(frame.tobytes())
preview_write_times[camera] = frame_time
frame_manager.close(frame_id) # send output data to birdseye if websocket is connected or restreaming
if self.config.birdseye.enabled and (
self.config.birdseye.restream
or any(
ws.environ["PATH_INFO"].endswith("birdseye")
for ws in websocket_server.manager
)
):
birdseye.write_data(
camera,
current_tracked_objects,
motion_boxes,
frame_time,
frame,
)
move_preview_frames("clips") # send frames for low fps recording
generated_preview = preview_recorders[camera].write_data(
current_tracked_objects, motion_boxes, frame_time, frame
)
preview_write_times[camera] = frame_time
while True: # if another camera generated a preview,
(topic, data) = detection_subscriber.check_for_update(timeout=0) # check for any cameras that are currently offline
# and need to generate a preview
if generated_preview:
for camera, time in preview_write_times.copy().items():
if time != 0 and frame_time - time > 10:
preview_recorders[camera].flag_offline(frame_time)
preview_write_times[camera] = frame_time
if not topic: frame_manager.close(frame_id)
break
( self.move_preview_frames("clips")
camera,
frame_time,
current_tracked_objects,
motion_boxes,
regions,
) = data
frame_id = f"{camera}{frame_time}" while True:
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv) (topic, data) = detection_subscriber.check_for_update(timeout=0)
frame_manager.close(frame_id)
detection_subscriber.stop() if not topic:
break
for jsmpeg in jsmpeg_cameras.values(): (
jsmpeg.stop() camera,
frame_time,
current_tracked_objects,
motion_boxes,
regions,
) = data
for preview in preview_recorders.values(): frame_id = f"{camera}{frame_time}"
preview.stop() frame = frame_manager.get(
frame_id, self.config.cameras[camera].frame_shape_yuv
)
frame_manager.close(frame_id)
if birdseye is not None: detection_subscriber.stop()
birdseye.stop()
websocket_server.manager.close_all() for jsmpeg in jsmpeg_cameras.values():
websocket_server.manager.stop() jsmpeg.stop()
websocket_server.manager.join()
websocket_server.shutdown()
websocket_thread.join()
logger.info("exiting output process...")
for preview in preview_recorders.values():
preview.stop()
def move_preview_frames(loc: str): if birdseye is not None:
preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache") birdseye.stop()
preview_cache = os.path.join(CACHE_DIR, "preview_frames")
try: websocket_server.manager.close_all()
if loc == "clips": websocket_server.manager.stop()
shutil.move(preview_cache, preview_holdover) websocket_server.manager.join()
elif loc == "cache": websocket_server.shutdown()
if not os.path.exists(preview_holdover): websocket_thread.join()
return self.logger.info("Exiting output process...")
shutil.move(preview_holdover, preview_cache) def move_preview_frames(self, loc: str):
except shutil.Error: preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache")
logger.error("Failed to restore preview cache.") preview_cache = os.path.join(CACHE_DIR, "preview_frames")
try:
if loc == "clips":
shutil.move(preview_cache, preview_holdover)
elif loc == "cache":
if not os.path.exists(preview_holdover):
return
shutil.move(preview_holdover, preview_cache)
except shutil.Error:
self.logger.error("Failed to restore preview cache.")