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: | ||||
|         """Set initial state topics.""" | ||||
|         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( | ||||
|                 f"{camera_name}/recordings/state", | ||||
|                 "ON" if camera.record.enabled_in_config else "OFF", | ||||
| @ -196,6 +201,7 @@ class MqttClient(Communicator):  # type: ignore[misc] | ||||
| 
 | ||||
|         # register callbacks | ||||
|         callback_types = [ | ||||
|             "enabled", | ||||
|             "recordings", | ||||
|             "snapshots", | ||||
|             "detect", | ||||
|  | ||||
| @ -390,8 +390,11 @@ class BirdsEyeFrameManager: | ||||
|     def _get_enabled_state(self, camera: str) -> bool: | ||||
|         """Fetch the latest enabled state for a camera from ZMQ.""" | ||||
|         _, config_data = self.enabled_subscribers[camera].check_for_update() | ||||
| 
 | ||||
|         if config_data: | ||||
|             self.config.cameras[camera].enabled = config_data.enabled | ||||
|             return config_data.enabled | ||||
| 
 | ||||
|         return self.config.cameras[camera].enabled | ||||
| 
 | ||||
|     def update_frame(self, frame: Optional[np.ndarray] = None) -> bool: | ||||
| @ -704,15 +707,17 @@ class BirdsEyeFrameManager: | ||||
|     ) -> bool: | ||||
|         # don't process if birdseye is disabled for this camera | ||||
|         camera_config = self.config.cameras[camera].birdseye | ||||
|         force_update = False | ||||
| 
 | ||||
|         # 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) | ||||
|             # then we need to set it to zero | ||||
|             if self.cameras[camera]["last_active_frame"] > 0: | ||||
|                 self.cameras[camera]["last_active_frame"] = 0 | ||||
| 
 | ||||
|             return False | ||||
|                 force_update = True | ||||
|             else: | ||||
|                 return False | ||||
| 
 | ||||
|         # update the last active frame for the camera | ||||
|         self.cameras[camera]["current_frame"] = frame.copy() | ||||
| @ -723,7 +728,7 @@ class BirdsEyeFrameManager: | ||||
|         now = datetime.datetime.now().timestamp() | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|         try: | ||||
| @ -735,7 +740,7 @@ class BirdsEyeFrameManager: | ||||
|             print(traceback.format_exc()) | ||||
| 
 | ||||
|         # 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 | ||||
|             return True | ||||
|         return False | ||||
| @ -783,6 +788,22 @@ class Birdseye: | ||||
|         self.converter.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( | ||||
|         self, | ||||
|         camera: str, | ||||
| @ -811,16 +832,7 @@ class Birdseye: | ||||
|             frame_time, | ||||
|             frame, | ||||
|         ): | ||||
|             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 | ||||
|             self.__send_new_frame() | ||||
| 
 | ||||
|     def stop(self) -> None: | ||||
|         self.config_subscriber.stop() | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| """Handle outputting raw frigate frames""" | ||||
| 
 | ||||
| import datetime | ||||
| import logging | ||||
| import multiprocessing as mp | ||||
| import os | ||||
| import shutil | ||||
| import signal | ||||
| import threading | ||||
| from typing import Optional | ||||
| from wsgiref.simple_server import make_server | ||||
| 
 | ||||
| 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.camera import JsmpegCamera | ||||
| 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__) | ||||
| 
 | ||||
| 
 | ||||
| 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( | ||||
|     config: FrigateConfig, | ||||
| ): | ||||
| @ -67,10 +99,11 @@ def output_frames( | ||||
|     } | ||||
| 
 | ||||
|     jsmpeg_cameras: dict[str, JsmpegCamera] = {} | ||||
|     birdseye: Optional[Birdseye] = None | ||||
|     birdseye: Birdseye | None = None | ||||
|     preview_recorders: dict[str, PreviewRecorder] = {} | ||||
|     preview_write_times: dict[str, float] = {} | ||||
|     failed_frame_requests: dict[str, int] = {} | ||||
|     last_disabled_cam_check = datetime.datetime.now().timestamp() | ||||
| 
 | ||||
|     move_preview_frames("cache") | ||||
| 
 | ||||
| @ -89,13 +122,23 @@ def output_frames( | ||||
| 
 | ||||
|     def get_enabled_state(camera: str) -> bool: | ||||
|         _, config_data = enabled_subscribers[camera].check_for_update() | ||||
| 
 | ||||
|         if config_data: | ||||
|             config.cameras[camera].enabled = config_data.enabled | ||||
|             return config_data.enabled | ||||
|         # default | ||||
| 
 | ||||
|         return config.cameras[camera].enabled | ||||
| 
 | ||||
|     while not stop_event.is_set(): | ||||
|         (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: | ||||
|             continue | ||||
| @ -151,23 +194,10 @@ def output_frames( | ||||
|             ) | ||||
| 
 | ||||
|         # 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 | ||||
|         ) | ||||
|         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) | ||||
| 
 | ||||
|     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): | ||||
|     try: | ||||
|         # TODO: does this copy the numpy array? | ||||
|  | ||||
| @ -200,7 +200,7 @@ export default function LivePlayer({ | ||||
|   // enabled states
 | ||||
| 
 | ||||
|   const [isReEnabling, setIsReEnabling] = useState(false); | ||||
|   const prevCameraEnabledRef = useRef(cameraEnabled); | ||||
|   const prevCameraEnabledRef = useRef(cameraEnabled ?? true); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!prevCameraEnabledRef.current && cameraEnabled) { | ||||
|  | ||||
| @ -396,10 +396,12 @@ export default function DraggableGridLayout({ | ||||
|     const initialVolumeStates: VolumeState = {}; | ||||
| 
 | ||||
|     Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => { | ||||
|       Object.entries(groupSettings).forEach(([camera, cameraSettings]) => { | ||||
|         initialAudioStates[camera] = cameraSettings.playAudio ?? false; | ||||
|         initialVolumeStates[camera] = cameraSettings.volume ?? 1; | ||||
|       }); | ||||
|       if (groupSettings) { | ||||
|         Object.entries(groupSettings).forEach(([camera, cameraSettings]) => { | ||||
|           initialAudioStates[camera] = cameraSettings.playAudio ?? false; | ||||
|           initialVolumeStates[camera] = cameraSettings.volume ?? 1; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     setAudioStates(initialAudioStates); | ||||
|  | ||||
| @ -268,10 +268,12 @@ export default function LiveDashboardView({ | ||||
|     const initialVolumeStates: VolumeState = {}; | ||||
| 
 | ||||
|     Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => { | ||||
|       Object.entries(groupSettings).forEach(([camera, cameraSettings]) => { | ||||
|         initialAudioStates[camera] = cameraSettings.playAudio ?? false; | ||||
|         initialVolumeStates[camera] = cameraSettings.volume ?? 1; | ||||
|       }); | ||||
|       if (groupSettings) { | ||||
|         Object.entries(groupSettings).forEach(([camera, cameraSettings]) => { | ||||
|           initialAudioStates[camera] = cameraSettings.playAudio ?? false; | ||||
|           initialVolumeStates[camera] = cameraSettings.volume ?? 1; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     setAudioStates(initialAudioStates); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user