mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-03-04 00:17:22 +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,15 +707,17 @@ 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
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
# update the last active frame for the camera
|
# update the last active frame for the camera
|
||||||
self.cameras[camera]["current_frame"] = frame.copy()
|
self.cameras[camera]["current_frame"] = frame.copy()
|
||||||
@ -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]) => {
|
||||||
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
if (groupSettings) {
|
||||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
||||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||||
});
|
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]) => {
|
||||||
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
if (groupSettings) {
|
||||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
||||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||||
});
|
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setAudioStates(initialAudioStates);
|
setAudioStates(initialAudioStates);
|
||||||
|
Loading…
Reference in New Issue
Block a user