mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-20 05:32:49 +03:00
* add last time to active notices * Stop retrying the review lookup in the explore detail menu when it 404s * don't let a slow websocket client block publishers A websocket client that stopped reading its socket (for example a suspended workstation with a Frigate tab open) blocked ws4py's sendall on whatever thread was publishing, including the single inter-process responder thread, so the recording maintainer and stats hung until nginx dropped the client after 5 minutes. Each client now gets its own bounded send queue and writer thread, and a client that overflows the queue is shut down. ws4py writes close and pong frames directly on its shared manager thread, bypassing the queue, so a 10s socket send timeout keeps a stalled client from pinning that thread. The jsmpeg and birdseye outputs share the class, so their unreachable send error handling is removed. * don't let a lost object enter a zone When the detector lost an object, the tracker kept it at its last box and every held frame still counted toward the zone's inertia. A car detected in a zone once or twice as it drove out of view could reach an inertia of 8 and be marked as entering, which sent it to GenAI and any other required zones check even though Review never showed the zone. Inertia and loitering still count on frames without a detection, but an object can only newly enter a zone on a frame where it was actually detected. * add test * limit retry stop to 404 only * add docstrings
180 lines
4.8 KiB
Python
180 lines
4.8 KiB
Python
"""Handle outputting individual cameras via jsmpeg."""
|
|
|
|
import logging
|
|
import queue
|
|
import subprocess as sp
|
|
import threading
|
|
from multiprocessing.synchronize import Event as MpEvent
|
|
from typing import Any
|
|
|
|
from frigate.config import CameraConfig, FfmpegConfig, FrigateConfig
|
|
from frigate.output.ws_auth import ws_has_camera_access
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FFMpegConverter(threading.Thread):
|
|
def __init__(
|
|
self,
|
|
camera: str,
|
|
ffmpeg: FfmpegConfig,
|
|
input_queue: queue.Queue,
|
|
stop_event: MpEvent,
|
|
in_width: int,
|
|
in_height: int,
|
|
out_width: int,
|
|
out_height: int,
|
|
quality: int,
|
|
):
|
|
super().__init__(name=f"{camera}_output_converter")
|
|
self.camera = camera
|
|
self.input_queue = input_queue
|
|
self.stop_event = stop_event
|
|
|
|
ffmpeg_cmd = [
|
|
ffmpeg.ffmpeg_path,
|
|
"-threads",
|
|
"1",
|
|
"-f",
|
|
"rawvideo",
|
|
"-pix_fmt",
|
|
"yuv420p",
|
|
"-video_size",
|
|
f"{in_width}x{in_height}",
|
|
"-i",
|
|
"pipe:",
|
|
"-threads",
|
|
"1",
|
|
"-f",
|
|
"mpegts",
|
|
"-s",
|
|
f"{out_width}x{out_height}",
|
|
"-codec:v",
|
|
"mpeg1video",
|
|
"-q",
|
|
f"{quality}",
|
|
"-bf",
|
|
"0",
|
|
"pipe:",
|
|
]
|
|
|
|
self.process = sp.Popen(
|
|
ffmpeg_cmd,
|
|
stdout=sp.PIPE,
|
|
stderr=sp.DEVNULL,
|
|
stdin=sp.PIPE,
|
|
start_new_session=True,
|
|
)
|
|
|
|
def __write(self, b: bytes) -> None:
|
|
assert self.process.stdin is not None
|
|
self.process.stdin.write(b)
|
|
|
|
def read(self, length: int) -> Any:
|
|
try:
|
|
return self.process.stdout.read1(length) # type: ignore[union-attr]
|
|
except ValueError:
|
|
return False
|
|
|
|
def exit(self) -> None:
|
|
self.process.terminate()
|
|
|
|
try:
|
|
self.process.communicate(timeout=30)
|
|
except sp.TimeoutExpired:
|
|
self.process.kill()
|
|
self.process.communicate()
|
|
|
|
def run(self) -> None:
|
|
while not self.stop_event.is_set():
|
|
try:
|
|
frame = self.input_queue.get(True, timeout=1)
|
|
self.__write(frame)
|
|
except queue.Empty:
|
|
pass
|
|
|
|
self.exit()
|
|
|
|
|
|
class BroadcastThread(threading.Thread):
|
|
def __init__(
|
|
self,
|
|
camera: str,
|
|
converter: FFMpegConverter,
|
|
websocket_server: Any,
|
|
stop_event: MpEvent,
|
|
config: FrigateConfig,
|
|
):
|
|
super().__init__()
|
|
self.camera = camera
|
|
self.converter = converter
|
|
self.websocket_server = websocket_server
|
|
self.stop_event = stop_event
|
|
self.config = config
|
|
|
|
def run(self) -> None:
|
|
while not self.stop_event.is_set():
|
|
buf = self.converter.read(65536)
|
|
if buf:
|
|
manager = self.websocket_server.manager
|
|
with manager.lock:
|
|
websockets = manager.websockets.copy()
|
|
ws_iter = iter(websockets.values())
|
|
|
|
for ws in ws_iter:
|
|
if (
|
|
not ws.terminated
|
|
and ws.environ["PATH_INFO"] == f"/{self.camera}"
|
|
and ws_has_camera_access(ws, self.camera, self.config)
|
|
):
|
|
ws.send(buf, binary=True)
|
|
elif self.converter.process.poll() is not None:
|
|
break
|
|
|
|
|
|
class JsmpegCamera:
|
|
def __init__(
|
|
self,
|
|
config: CameraConfig,
|
|
frigate_config: FrigateConfig,
|
|
stop_event: MpEvent,
|
|
websocket_server: Any,
|
|
) -> None:
|
|
self.config = config
|
|
self.input: queue.Queue[bytes] = queue.Queue(maxsize=config.detect.fps)
|
|
width = int(
|
|
config.live.height * (config.frame_shape[1] / config.frame_shape[0])
|
|
)
|
|
self.converter = FFMpegConverter(
|
|
config.name or "",
|
|
config.ffmpeg,
|
|
self.input,
|
|
stop_event,
|
|
config.frame_shape[1],
|
|
config.frame_shape[0],
|
|
width,
|
|
config.live.height,
|
|
config.live.quality,
|
|
)
|
|
self.broadcaster = BroadcastThread(
|
|
config.name or "",
|
|
self.converter,
|
|
websocket_server,
|
|
stop_event,
|
|
frigate_config,
|
|
)
|
|
|
|
self.converter.start()
|
|
self.broadcaster.start()
|
|
|
|
def write_frame(self, frame_bytes: bytes) -> None:
|
|
try:
|
|
self.input.put_nowait(frame_bytes)
|
|
except queue.Full:
|
|
# drop frames if queue is full
|
|
pass
|
|
|
|
def stop(self) -> None:
|
|
self.converter.join()
|
|
self.broadcaster.join()
|