diff --git a/frigate/app.py b/frigate/app.py index 97d59b115..a48634eea 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -14,7 +14,6 @@ from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase -from frigate.birdseye import BirdsEyeFrameOutputter from frigate.config import FrigateConfig from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR from frigate.edgetpu import EdgeTPUProcess diff --git a/frigate/birdseye.py b/frigate/birdseye.py deleted file mode 100644 index 364cf11ae..000000000 --- a/frigate/birdseye.py +++ /dev/null @@ -1,145 +0,0 @@ -import logging -import multiprocessing as mp -import subprocess as sp -import threading - -import gevent -import numpy as np -from flask import ( - Blueprint, - Flask, - Response, - current_app, - jsonify, - make_response, - request, -) -from flask_sockets import Sockets -from gevent import pywsgi -from geventwebsocket.handler import WebSocketHandler - -from frigate.util import SharedMemoryFrameManager - -logger = logging.getLogger(__name__) - -# methods for maintaining the birdseyeframe in the object processing thread -# avoids work when no clients are listening -class BirdsEyeFrameManager: - def __init__(self): - # self.config = config - self.frame_manager = SharedMemoryFrameManager() - self._frame_shape = (1080, 1920) - self.frame_shape_yuv = (self._frame_shape[0] * 3 // 2, self._frame_shape[1]) - self.frame_shm = mp.shared_memory.SharedMemory( - name=f"birdseye-frame", - create=True, - size=self.frame_shape_yuv[0] * self.frame_shape_yuv[1], - ) - self.frame = np.ndarray( - self.frame_shape_yuv, dtype=np.uint8, buffer=self.frame_shm.buf - ) - - # initialize the frame as black and with the frigate logo - self.blank_frame = np.zeros((1080 * 3 // 2, 1920), np.uint8) - self.blank_frame[:] = 128 - self.blank_frame[0:1080, 0:1920] = 16 - - self.frame[:] = self.blank_frame - - def update_frame(self, camera, object_count, motion_count, frame_time, frame): - # determine how many cameras are tracking objects (or recently were) - # decide on a layout for the birdseye view (try to avoid too much churn) - # calculate position of each camera - # calculate resolution of each position in the layout - # if layout is changing, wipe the frame black again - # For each camera currently tracking objects (alphabetical): - # - resize the current frame and copy into the birdseye view - # signal to birdseye process that the frame is ready to send - - self.frame[:] = frame - - -# separate process for managing the external ffmpeg process and sending frame -# bytes to ffmpeg -class BirdsEyeFrameOutputter(threading.Thread): - def __init__(self, stop_event): - threading.Thread.__init__(self) - self.stop_event = stop_event - self.frame_shm = mp.shared_memory.SharedMemory( - name=f"birdseye-frame", - create=False, - ) - - def start_ffmpeg(self): - ffmpeg_cmd = "ffmpeg -f rawvideo -pix_fmt yuv420p -video_size 1920x1080 -i pipe: -f mpegts -codec:v mpeg1video -b:v 1000k -bf 0 pipe:".split( - " " - ) - self.process = sp.Popen( - ffmpeg_cmd, - stdout=sp.PIPE, - # TODO: logging - stderr=sp.DEVNULL, - stdin=sp.PIPE, - start_new_session=True, - ) - - def run(self): - self.start_ffmpeg() - - while not self.stop_event.wait(1): - if self.process.poll() != None: - logger.info(f"ffmpeg process is not running. restarting ...") - self.start_ffmpeg() - - self.process.stdin.write(self.frame_shm.buf.tobytes()) - - -# separate process for passing jsmpeg packets over websockets -# signals to the frame manager when a client is listening -def run_jsmpeg_server(): - app = Flask(__name__) - sockets = Sockets(app) - - http = Blueprint("http", __name__) - ws = Blueprint("ws", __name__) - - # TODO: add something for notification of subscribers - # self.app.frigate_config = frigate_config - - app.register_blueprint(http) - sockets.register_blueprint(ws) - - clients = list() - - @http.route("/birdseye") - def receive_mpegts(): - chunk_size = 4096 - while True: - chunk = request.stream.read(chunk_size) - if len(chunk) == 0: - break - for client in clients: - try: - client.send(chunk) - except: - logger.debug( - "Removing websocket client due to a closed connection." - ) - clients.remove(client) - - @ws.route("/birdseye") - def echo_socket(socket): - # TODO: get reference to - # current_app.mqtt_backend.register(socket) - clients.append(socket) - - while not socket.closed: - # Sleep to prevent *constant* context-switches. - gevent.sleep(0.1) - - server = pywsgi.WSGIServer(("127.0.0.1", 5050), app, handler_class=WebSocketHandler) - - try: - server.serve_forever() - except KeyboardInterrupt: - pass diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 6359cfd85..348f049ca 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -17,7 +17,6 @@ import cv2 import matplotlib.pyplot as plt import numpy as np -from frigate.birdseye import BirdsEyeFrameManager from frigate.config import FrigateConfig, CameraConfig from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR from frigate.edgetpu import load_labels diff --git a/frigate/output.py b/frigate/output.py index 4672e459c..e68e591bb 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -3,6 +3,7 @@ import queue import signal import subprocess as sp import threading +import numpy as np from multiprocessing import shared_memory from wsgiref.simple_server import make_server @@ -65,6 +66,32 @@ class BroadcastThread(threading.Thread): break +class BirdsEyeFrameManager: + def __init__(self, height, width): + frame_shape = (height, width) + yuv_shape = (height * 3 // 2, width) + self.frame = np.ndarray(yuv_shape, dtype=np.uint8) + + # initialize the frame as black and with the frigate logo + self.blank_frame = np.zeros(yuv_shape, np.uint8) + self.blank_frame[:] = 128 + self.blank_frame[0 : frame_shape[0], 0 : frame_shape[1]] = 16 + + self.frame[:] = self.blank_frame + + def update(self, camera, object_count, motion_count, frame_time, frame): + # determine how many cameras are tracking objects (or recently were) + # decide on a layout for the birdseye view (try to avoid too much churn) + # calculate position of each camera + # calculate resolution of each position in the layout + # if layout is changing, wipe the frame black again + # For each camera currently tracking objects (alphabetical): + # - resize the current frame and copy into the birdseye view + # signal to birdseye process that the frame is ready to send + + self.frame[:] = frame + + def output_frames(config, video_output_queue): threading.current_thread().name = f"output" setproctitle(f"frigate.output") @@ -103,11 +130,18 @@ def output_frames(config, video_output_queue): camera, converters[camera], websocket_server ) + converters["birdseye"] = FFMpegConverter(1920, 1080, 640, 320, "1000k") + broadcasters["birdseye"] = BroadcastThread( + "birdseye", converters["birdseye"], websocket_server + ) + websocket_thread.start() for t in broadcasters.values(): t.start() + birdseye_manager = BirdsEyeFrameManager(1080, 1920) + while not stop_event.is_set(): try: ( @@ -131,6 +165,20 @@ def output_frames(config, video_output_queue): # write to the converter for the camera if clients are listening to the specific camera converters[camera].write(frame.tobytes()) + # update birdseye if websockets are connected + if any( + ws.environ["PATH_INFO"].endswith("birdseye") + for ws in websocket_server.manager + ): + birdseye_manager.update( + camera, + len(current_tracked_objects), + len(motion_boxes), + frame_time, + frame, + ) + converters["birdseye"].write(birdseye_manager.frame.tobytes()) + if camera in previous_frames: frame_manager.delete(previous_frames[camera])