2023-12-03 15:16:01 +01:00
|
|
|
"""Handle outputting raw frigate frames"""
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import multiprocessing as mp
|
2024-03-05 20:56:38 +01:00
|
|
|
import os
|
|
|
|
import shutil
|
2023-12-03 15:16:01 +01:00
|
|
|
import signal
|
|
|
|
import threading
|
|
|
|
from typing import Optional
|
|
|
|
from wsgiref.simple_server import make_server
|
|
|
|
|
|
|
|
from setproctitle import setproctitle
|
|
|
|
from ws4py.server.wsgirefserver import (
|
|
|
|
WebSocketWSGIHandler,
|
|
|
|
WebSocketWSGIRequestHandler,
|
|
|
|
WSGIServer,
|
|
|
|
)
|
|
|
|
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
|
|
|
|
2024-02-19 14:26:59 +01:00
|
|
|
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
|
2023-12-03 15:16:01 +01:00
|
|
|
from frigate.comms.ws import WebSocket
|
|
|
|
from frigate.config import FrigateConfig
|
2024-03-05 20:56:38 +01:00
|
|
|
from frigate.const import CACHE_DIR, CLIPS_DIR
|
2023-12-03 15:16:01 +01:00
|
|
|
from frigate.output.birdseye import Birdseye
|
|
|
|
from frigate.output.camera import JsmpegCamera
|
|
|
|
from frigate.output.preview import PreviewRecorder
|
|
|
|
from frigate.util.image import SharedMemoryFrameManager
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
def output_frames(
|
|
|
|
config: FrigateConfig,
|
|
|
|
):
|
|
|
|
threading.current_thread().name = "output"
|
|
|
|
setproctitle("frigate.output")
|
|
|
|
|
|
|
|
stop_event = mp.Event()
|
|
|
|
|
|
|
|
def receiveSignal(signalNumber, frame):
|
|
|
|
stop_event.set()
|
|
|
|
|
|
|
|
signal.signal(signal.SIGTERM, receiveSignal)
|
|
|
|
signal.signal(signal.SIGINT, receiveSignal)
|
|
|
|
|
|
|
|
frame_manager = SharedMemoryFrameManager()
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
2024-02-19 14:26:59 +01:00
|
|
|
detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video)
|
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
jsmpeg_cameras: dict[str, JsmpegCamera] = {}
|
|
|
|
birdseye: Optional[Birdseye] = None
|
|
|
|
preview_recorders: dict[str, PreviewRecorder] = {}
|
2024-08-31 18:48:58 +02:00
|
|
|
preview_write_times: dict[str, float] = {}
|
2024-11-06 14:59:33 +01:00
|
|
|
failed_frame_requests: dict[str, int] = {}
|
2023-12-03 15:16:01 +01:00
|
|
|
|
2024-03-05 20:56:38 +01:00
|
|
|
move_preview_frames("cache")
|
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
for camera, cam_config in config.cameras.items():
|
|
|
|
if not cam_config.enabled:
|
|
|
|
continue
|
|
|
|
|
|
|
|
jsmpeg_cameras[camera] = JsmpegCamera(cam_config, stop_event, websocket_server)
|
2024-02-15 01:24:36 +01:00
|
|
|
preview_recorders[camera] = PreviewRecorder(cam_config)
|
2024-08-31 18:48:58 +02:00
|
|
|
preview_write_times[camera] = 0
|
2023-12-03 15:16:01 +01:00
|
|
|
|
|
|
|
if config.birdseye.enabled:
|
2024-09-04 16:25:00 +02:00
|
|
|
birdseye = Birdseye(config, stop_event, websocket_server)
|
2023-12-03 15:16:01 +01:00
|
|
|
|
|
|
|
websocket_thread.start()
|
|
|
|
|
|
|
|
while not stop_event.is_set():
|
2024-06-21 23:30:19 +02:00
|
|
|
(topic, data) = detection_subscriber.check_for_update(timeout=1)
|
2024-02-19 14:26:59 +01:00
|
|
|
|
|
|
|
if not topic:
|
2023-12-03 15:16:01 +01:00
|
|
|
continue
|
|
|
|
|
2024-02-19 14:26:59 +01:00
|
|
|
(
|
|
|
|
camera,
|
|
|
|
frame_time,
|
|
|
|
current_tracked_objects,
|
|
|
|
motion_boxes,
|
|
|
|
regions,
|
|
|
|
) = data
|
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
frame_id = f"{camera}{frame_time}"
|
|
|
|
|
|
|
|
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)
|
|
|
|
|
2024-09-03 18:22:30 +02:00
|
|
|
if frame is None:
|
|
|
|
logger.debug(f"Failed to get frame {frame_id} from SHM")
|
2024-11-06 14:59:33 +01:00
|
|
|
failed_frame_requests[camera] = failed_frame_requests.get(camera, 0) + 1
|
|
|
|
|
|
|
|
if failed_frame_requests[camera] > config.cameras[camera].detect.fps:
|
|
|
|
logger.warning(
|
|
|
|
f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues."
|
|
|
|
)
|
|
|
|
|
2024-09-03 18:22:30 +02:00
|
|
|
continue
|
2024-11-06 14:59:33 +01:00
|
|
|
else:
|
|
|
|
failed_frame_requests[camera] = 0
|
2024-09-03 18:22:30 +02:00
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
# 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
|
|
|
|
)
|
|
|
|
):
|
|
|
|
birdseye.write_data(
|
|
|
|
camera,
|
|
|
|
current_tracked_objects,
|
|
|
|
motion_boxes,
|
|
|
|
frame_time,
|
|
|
|
frame,
|
|
|
|
)
|
|
|
|
|
|
|
|
# send frames for low fps recording
|
2024-08-31 18:48:58 +02:00
|
|
|
generated_preview = preview_recorders[camera].write_data(
|
2023-12-03 15:16:01 +01:00
|
|
|
current_tracked_objects, motion_boxes, frame_time, frame
|
|
|
|
)
|
2024-08-31 18:48:58 +02:00
|
|
|
preview_write_times[camera] = frame_time
|
2024-07-09 18:49:08 +02:00
|
|
|
|
2024-08-31 18:48:58 +02:00
|
|
|
# if another camera generated a preview,
|
|
|
|
# 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
|
|
|
|
|
2024-09-03 18:22:30 +02:00
|
|
|
frame_manager.close(frame_id)
|
2023-12-03 15:16:01 +01:00
|
|
|
|
2024-03-23 20:45:15 +01:00
|
|
|
move_preview_frames("clips")
|
|
|
|
|
2024-02-19 14:26:59 +01:00
|
|
|
while True:
|
2024-06-21 23:30:19 +02:00
|
|
|
(topic, data) = detection_subscriber.check_for_update(timeout=0)
|
2024-02-19 14:26:59 +01:00
|
|
|
|
|
|
|
if not topic:
|
|
|
|
break
|
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
(
|
|
|
|
camera,
|
|
|
|
frame_time,
|
|
|
|
current_tracked_objects,
|
|
|
|
motion_boxes,
|
|
|
|
regions,
|
2024-02-19 14:26:59 +01:00
|
|
|
) = data
|
2023-12-03 15:16:01 +01:00
|
|
|
|
|
|
|
frame_id = f"{camera}{frame_time}"
|
|
|
|
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)
|
2024-09-03 18:22:30 +02:00
|
|
|
frame_manager.close(frame_id)
|
2023-12-03 15:16:01 +01:00
|
|
|
|
2024-02-19 14:26:59 +01:00
|
|
|
detection_subscriber.stop()
|
|
|
|
|
2023-12-03 15:16:01 +01:00
|
|
|
for jsmpeg in jsmpeg_cameras.values():
|
|
|
|
jsmpeg.stop()
|
|
|
|
|
|
|
|
for preview in preview_recorders.values():
|
|
|
|
preview.stop()
|
|
|
|
|
|
|
|
if birdseye is not None:
|
|
|
|
birdseye.stop()
|
|
|
|
|
|
|
|
websocket_server.manager.close_all()
|
|
|
|
websocket_server.manager.stop()
|
|
|
|
websocket_server.manager.join()
|
|
|
|
websocket_server.shutdown()
|
|
|
|
websocket_thread.join()
|
|
|
|
logger.info("exiting output process...")
|
2024-03-05 20:56:38 +01:00
|
|
|
|
|
|
|
|
|
|
|
def move_preview_frames(loc: str):
|
|
|
|
preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache")
|
|
|
|
preview_cache = os.path.join(CACHE_DIR, "preview_frames")
|
|
|
|
|
2024-06-17 15:56:24 +02:00
|
|
|
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:
|
|
|
|
logger.error("Failed to restore preview cache.")
|