mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Disabled camera output (#16920)
* Fix live cameras not showing on refresh * Fix live dashboard when birdseye is added * Handle cameras that are offline / disabled * Use black instead of green frame * Fix missing mqtt topics
This commit is contained in:
		
							parent
							
								
									180b0af3c9
								
							
						
					
					
						commit
						2946c935ee
					
				@ -43,6 +43,11 @@ class MqttClient(Communicator):  # type: ignore[misc]
 | 
				
			|||||||
    def _set_initial_topics(self) -> None:
 | 
					    def _set_initial_topics(self) -> None:
 | 
				
			||||||
        """Set initial state topics."""
 | 
					        """Set initial state topics."""
 | 
				
			||||||
        for camera_name, camera in self.config.cameras.items():
 | 
					        for camera_name, camera in self.config.cameras.items():
 | 
				
			||||||
 | 
					            self.publish(
 | 
				
			||||||
 | 
					                f"{camera_name}/enabled/state",
 | 
				
			||||||
 | 
					                "ON" if camera.enabled_in_config else "OFF",
 | 
				
			||||||
 | 
					                retain=True,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            self.publish(
 | 
					            self.publish(
 | 
				
			||||||
                f"{camera_name}/recordings/state",
 | 
					                f"{camera_name}/recordings/state",
 | 
				
			||||||
                "ON" if camera.record.enabled_in_config else "OFF",
 | 
					                "ON" if camera.record.enabled_in_config else "OFF",
 | 
				
			||||||
@ -196,6 +201,7 @@ class MqttClient(Communicator):  # type: ignore[misc]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # register callbacks
 | 
					        # register callbacks
 | 
				
			||||||
        callback_types = [
 | 
					        callback_types = [
 | 
				
			||||||
 | 
					            "enabled",
 | 
				
			||||||
            "recordings",
 | 
					            "recordings",
 | 
				
			||||||
            "snapshots",
 | 
					            "snapshots",
 | 
				
			||||||
            "detect",
 | 
					            "detect",
 | 
				
			||||||
 | 
				
			|||||||
@ -390,8 +390,11 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
    def _get_enabled_state(self, camera: str) -> bool:
 | 
					    def _get_enabled_state(self, camera: str) -> bool:
 | 
				
			||||||
        """Fetch the latest enabled state for a camera from ZMQ."""
 | 
					        """Fetch the latest enabled state for a camera from ZMQ."""
 | 
				
			||||||
        _, config_data = self.enabled_subscribers[camera].check_for_update()
 | 
					        _, config_data = self.enabled_subscribers[camera].check_for_update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if config_data:
 | 
					        if config_data:
 | 
				
			||||||
 | 
					            self.config.cameras[camera].enabled = config_data.enabled
 | 
				
			||||||
            return config_data.enabled
 | 
					            return config_data.enabled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.config.cameras[camera].enabled
 | 
					        return self.config.cameras[camera].enabled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_frame(self, frame: Optional[np.ndarray] = None) -> bool:
 | 
					    def update_frame(self, frame: Optional[np.ndarray] = None) -> bool:
 | 
				
			||||||
@ -704,14 +707,16 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
    ) -> bool:
 | 
					    ) -> bool:
 | 
				
			||||||
        # don't process if birdseye is disabled for this camera
 | 
					        # don't process if birdseye is disabled for this camera
 | 
				
			||||||
        camera_config = self.config.cameras[camera].birdseye
 | 
					        camera_config = self.config.cameras[camera].birdseye
 | 
				
			||||||
 | 
					        force_update = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # disabling birdseye is a little tricky
 | 
					        # disabling birdseye is a little tricky
 | 
				
			||||||
        if not camera_config.enabled or not self._get_enabled_state(camera):
 | 
					        if not self._get_enabled_state(camera):
 | 
				
			||||||
            # if we've rendered a frame (we have a value for last_active_frame)
 | 
					            # if we've rendered a frame (we have a value for last_active_frame)
 | 
				
			||||||
            # then we need to set it to zero
 | 
					            # then we need to set it to zero
 | 
				
			||||||
            if self.cameras[camera]["last_active_frame"] > 0:
 | 
					            if self.cameras[camera]["last_active_frame"] > 0:
 | 
				
			||||||
                self.cameras[camera]["last_active_frame"] = 0
 | 
					                self.cameras[camera]["last_active_frame"] = 0
 | 
				
			||||||
 | 
					                force_update = True
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # update the last active frame for the camera
 | 
					        # update the last active frame for the camera
 | 
				
			||||||
@ -723,7 +728,7 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
        now = datetime.datetime.now().timestamp()
 | 
					        now = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # limit output to 10 fps
 | 
					        # limit output to 10 fps
 | 
				
			||||||
        if (now - self.last_output_time) < 1 / 10:
 | 
					        if not force_update and (now - self.last_output_time) < 1 / 10:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@ -735,7 +740,7 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
            print(traceback.format_exc())
 | 
					            print(traceback.format_exc())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # if the frame was updated or the fps is too low, send frame
 | 
					        # if the frame was updated or the fps is too low, send frame
 | 
				
			||||||
        if updated_frame or (now - self.last_output_time) > 1:
 | 
					        if force_update or updated_frame or (now - self.last_output_time) > 1:
 | 
				
			||||||
            self.last_output_time = now
 | 
					            self.last_output_time = now
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
@ -783,6 +788,22 @@ class Birdseye:
 | 
				
			|||||||
        self.converter.start()
 | 
					        self.converter.start()
 | 
				
			||||||
        self.broadcaster.start()
 | 
					        self.broadcaster.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __send_new_frame(self) -> None:
 | 
				
			||||||
 | 
					        frame_bytes = self.birdseye_manager.frame.tobytes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.config.birdseye.restream:
 | 
				
			||||||
 | 
					            self.birdseye_buffer[:] = frame_bytes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.input.put_nowait(frame_bytes)
 | 
				
			||||||
 | 
					        except queue.Full:
 | 
				
			||||||
 | 
					            # drop frames if queue is full
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def all_cameras_disabled(self) -> None:
 | 
				
			||||||
 | 
					        self.birdseye_manager.clear_frame()
 | 
				
			||||||
 | 
					        self.__send_new_frame()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def write_data(
 | 
					    def write_data(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        camera: str,
 | 
					        camera: str,
 | 
				
			||||||
@ -811,16 +832,7 @@ class Birdseye:
 | 
				
			|||||||
            frame_time,
 | 
					            frame_time,
 | 
				
			||||||
            frame,
 | 
					            frame,
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            frame_bytes = self.birdseye_manager.frame.tobytes()
 | 
					            self.__send_new_frame()
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if self.config.birdseye.restream:
 | 
					 | 
				
			||||||
                self.birdseye_buffer[:] = frame_bytes
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                self.input.put_nowait(frame_bytes)
 | 
					 | 
				
			||||||
            except queue.Full:
 | 
					 | 
				
			||||||
                # drop frames if queue is full
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def stop(self) -> None:
 | 
					    def stop(self) -> None:
 | 
				
			||||||
        self.config_subscriber.stop()
 | 
					        self.config_subscriber.stop()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,12 @@
 | 
				
			|||||||
"""Handle outputting raw frigate frames"""
 | 
					"""Handle outputting raw frigate frames"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import multiprocessing as mp
 | 
					import multiprocessing as mp
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
import signal
 | 
					import signal
 | 
				
			||||||
import threading
 | 
					import threading
 | 
				
			||||||
from typing import Optional
 | 
					 | 
				
			||||||
from wsgiref.simple_server import make_server
 | 
					from wsgiref.simple_server import make_server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from setproctitle import setproctitle
 | 
					from setproctitle import setproctitle
 | 
				
			||||||
@ -25,11 +25,43 @@ from frigate.const import CACHE_DIR, CLIPS_DIR
 | 
				
			|||||||
from frigate.output.birdseye import Birdseye
 | 
					from frigate.output.birdseye import Birdseye
 | 
				
			||||||
from frigate.output.camera import JsmpegCamera
 | 
					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, get_blank_yuv_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_disabled_camera_update(
 | 
				
			||||||
 | 
					    config: FrigateConfig,
 | 
				
			||||||
 | 
					    birdseye: Birdseye | None,
 | 
				
			||||||
 | 
					    previews: dict[str, PreviewRecorder],
 | 
				
			||||||
 | 
					    write_times: dict[str, float],
 | 
				
			||||||
 | 
					) -> None:
 | 
				
			||||||
 | 
					    """Check if camera is disabled / offline and needs an update."""
 | 
				
			||||||
 | 
					    now = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					    has_enabled_camera = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for camera, last_update in write_times.items():
 | 
				
			||||||
 | 
					        if config.cameras[camera].enabled:
 | 
				
			||||||
 | 
					            has_enabled_camera = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if now - last_update > 1:
 | 
				
			||||||
 | 
					            # last camera update was more than one second ago
 | 
				
			||||||
 | 
					            # need to send empty data to updaters because current
 | 
				
			||||||
 | 
					            # frame is now out of date
 | 
				
			||||||
 | 
					            frame = get_blank_yuv_frame(
 | 
				
			||||||
 | 
					                config.cameras[camera].detect.width,
 | 
				
			||||||
 | 
					                config.cameras[camera].detect.height,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if birdseye:
 | 
				
			||||||
 | 
					                birdseye.write_data(camera, [], [], now, frame)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            previews[camera].write_data([], [], now, frame)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not has_enabled_camera and birdseye:
 | 
				
			||||||
 | 
					        birdseye.all_cameras_disabled()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def output_frames(
 | 
					def output_frames(
 | 
				
			||||||
    config: FrigateConfig,
 | 
					    config: FrigateConfig,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
@ -67,10 +99,11 @@ def output_frames(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    jsmpeg_cameras: dict[str, JsmpegCamera] = {}
 | 
					    jsmpeg_cameras: dict[str, JsmpegCamera] = {}
 | 
				
			||||||
    birdseye: Optional[Birdseye] = None
 | 
					    birdseye: Birdseye | None = None
 | 
				
			||||||
    preview_recorders: dict[str, PreviewRecorder] = {}
 | 
					    preview_recorders: dict[str, PreviewRecorder] = {}
 | 
				
			||||||
    preview_write_times: dict[str, float] = {}
 | 
					    preview_write_times: dict[str, float] = {}
 | 
				
			||||||
    failed_frame_requests: dict[str, int] = {}
 | 
					    failed_frame_requests: dict[str, int] = {}
 | 
				
			||||||
 | 
					    last_disabled_cam_check = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    move_preview_frames("cache")
 | 
					    move_preview_frames("cache")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -89,13 +122,23 @@ def output_frames(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_enabled_state(camera: str) -> bool:
 | 
					    def get_enabled_state(camera: str) -> bool:
 | 
				
			||||||
        _, config_data = enabled_subscribers[camera].check_for_update()
 | 
					        _, config_data = enabled_subscribers[camera].check_for_update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if config_data:
 | 
					        if config_data:
 | 
				
			||||||
 | 
					            config.cameras[camera].enabled = config_data.enabled
 | 
				
			||||||
            return config_data.enabled
 | 
					            return config_data.enabled
 | 
				
			||||||
        # default
 | 
					
 | 
				
			||||||
        return config.cameras[camera].enabled
 | 
					        return config.cameras[camera].enabled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while not stop_event.is_set():
 | 
					    while not stop_event.is_set():
 | 
				
			||||||
        (topic, data) = detection_subscriber.check_for_update(timeout=1)
 | 
					        (topic, data) = detection_subscriber.check_for_update(timeout=1)
 | 
				
			||||||
 | 
					        now = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if now - last_disabled_cam_check > 5:
 | 
				
			||||||
 | 
					            # check disabled cameras every 5 seconds
 | 
				
			||||||
 | 
					            last_disabled_cam_check = now
 | 
				
			||||||
 | 
					            check_disabled_camera_update(
 | 
				
			||||||
 | 
					                config, birdseye, preview_recorders, preview_write_times
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not topic:
 | 
					        if not topic:
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
@ -151,23 +194,10 @@ def output_frames(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # send frames for low fps recording
 | 
					        # send frames for low fps recording
 | 
				
			||||||
        generated_preview = preview_recorders[camera].write_data(
 | 
					        preview_recorders[camera].write_data(
 | 
				
			||||||
            current_tracked_objects, motion_boxes, frame_time, frame
 | 
					            current_tracked_objects, motion_boxes, frame_time, frame
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        preview_write_times[camera] = frame_time
 | 
					        preview_write_times[camera] = frame_time
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # if another camera generated a preview,
 | 
					 | 
				
			||||||
        # check for any cameras that are currently offline
 | 
					 | 
				
			||||||
        # and need to generate a preview
 | 
					 | 
				
			||||||
        if generated_preview:
 | 
					 | 
				
			||||||
            logger.debug(
 | 
					 | 
				
			||||||
                "Checking for offline cameras because another camera generated a 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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        frame_manager.close(frame_name)
 | 
					        frame_manager.close(frame_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    move_preview_frames("clips")
 | 
					    move_preview_frames("clips")
 | 
				
			||||||
 | 
				
			|||||||
@ -632,6 +632,22 @@ def copy_yuv_to_position(
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_blank_yuv_frame(width: int, height: int) -> np.ndarray:
 | 
				
			||||||
 | 
					    """Creates a black YUV 4:2:0 frame."""
 | 
				
			||||||
 | 
					    yuv_height = height * 3 // 2
 | 
				
			||||||
 | 
					    yuv_frame = np.zeros((yuv_height, width), dtype=np.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_height = height // 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # The U and V planes are stored after the Y plane.
 | 
				
			||||||
 | 
					    u_start = height  # U plane starts right after Y plane
 | 
				
			||||||
 | 
					    v_start = u_start + uv_height // 2  # V plane starts after U plane
 | 
				
			||||||
 | 
					    yuv_frame[u_start : u_start + uv_height, :width] = 128
 | 
				
			||||||
 | 
					    yuv_frame[v_start : v_start + uv_height, :width] = 128
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return yuv_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def yuv_region_2_yuv(frame, region):
 | 
					def yuv_region_2_yuv(frame, region):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        # TODO: does this copy the numpy array?
 | 
					        # TODO: does this copy the numpy array?
 | 
				
			||||||
 | 
				
			|||||||
@ -200,7 +200,7 @@ export default function LivePlayer({
 | 
				
			|||||||
  // enabled states
 | 
					  // enabled states
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [isReEnabling, setIsReEnabling] = useState(false);
 | 
					  const [isReEnabling, setIsReEnabling] = useState(false);
 | 
				
			||||||
  const prevCameraEnabledRef = useRef(cameraEnabled);
 | 
					  const prevCameraEnabledRef = useRef(cameraEnabled ?? true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (!prevCameraEnabledRef.current && cameraEnabled) {
 | 
					    if (!prevCameraEnabledRef.current && cameraEnabled) {
 | 
				
			||||||
 | 
				
			|||||||
@ -396,10 +396,12 @@ export default function DraggableGridLayout({
 | 
				
			|||||||
    const initialVolumeStates: VolumeState = {};
 | 
					    const initialVolumeStates: VolumeState = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
 | 
					    Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
 | 
				
			||||||
 | 
					      if (groupSettings) {
 | 
				
			||||||
        Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
 | 
					        Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
 | 
				
			||||||
          initialAudioStates[camera] = cameraSettings.playAudio ?? false;
 | 
					          initialAudioStates[camera] = cameraSettings.playAudio ?? false;
 | 
				
			||||||
          initialVolumeStates[camera] = cameraSettings.volume ?? 1;
 | 
					          initialVolumeStates[camera] = cameraSettings.volume ?? 1;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setAudioStates(initialAudioStates);
 | 
					    setAudioStates(initialAudioStates);
 | 
				
			||||||
 | 
				
			|||||||
@ -268,10 +268,12 @@ export default function LiveDashboardView({
 | 
				
			|||||||
    const initialVolumeStates: VolumeState = {};
 | 
					    const initialVolumeStates: VolumeState = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
 | 
					    Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
 | 
				
			||||||
 | 
					      if (groupSettings) {
 | 
				
			||||||
        Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
 | 
					        Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
 | 
				
			||||||
          initialAudioStates[camera] = cameraSettings.playAudio ?? false;
 | 
					          initialAudioStates[camera] = cameraSettings.playAudio ?? false;
 | 
				
			||||||
          initialVolumeStates[camera] = cameraSettings.volume ?? 1;
 | 
					          initialVolumeStates[camera] = cameraSettings.volume ?? 1;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setAudioStates(initialAudioStates);
 | 
					    setAudioStates(initialAudioStates);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user