mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-13 17:13:01 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e41e22b6 | ||
|
|
3fb819c025 | ||
|
|
e5b25d23e5 |
@@ -105,8 +105,6 @@ if [[ -n "$sentinel" ]]; then
|
|||||||
mismatch_expr+=(-a ! -path "$sentinel")
|
mismatch_expr+=(-a ! -path "$sentinel")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec 3>&1
|
|
||||||
|
|
||||||
for path in "$@"; do
|
for path in "$@"; do
|
||||||
# An absent root is an incomplete sweep, not a finished one: /media/frigate
|
# An absent root is an incomplete sweep, not a finished one: /media/frigate
|
||||||
# is not in the image, so a boot before the volume is mounted would
|
# is not in the image, so a boot before the volume is mounted would
|
||||||
@@ -123,18 +121,7 @@ for path in "$@"; do
|
|||||||
# stale mount. Tolerate it rather than aborting under errexit, but never
|
# stale mount. Tolerate it rather than aborting under errexit, but never
|
||||||
# read a failed scan as "nothing to do": that would record the sweep as
|
# read a failed scan as "nothing to do": that would record the sweep as
|
||||||
# complete without having looked.
|
# complete without having looked.
|
||||||
# The walk prints nothing until it ends, so report every 30s through fd 3
|
if ! count=$(find "$path" -name lost+found -prune -o "${mismatch_expr[@]}" -printf '.' 2>/dev/null | wc -c); then
|
||||||
# (stdout) while the count itself comes back through the substitution.
|
|
||||||
if ! count=$(find "$path" -name lost+found -prune -o "(" "${mismatch_expr[@]}" ")" -printf 'm\n' -o -printf '.\n' 2>/dev/null \
|
|
||||||
| awk -v path="$path" '
|
|
||||||
BEGIN { next_report = systime() + 30 }
|
|
||||||
$0 == "m" { mismatched++ }
|
|
||||||
systime() >= next_report {
|
|
||||||
printf "[INFO] fix-ownership: still scanning %s, %d entries checked, %d need changes\n", path, NR, mismatched > "/dev/fd/3"
|
|
||||||
fflush("/dev/fd/3")
|
|
||||||
next_report = systime() + 30
|
|
||||||
}
|
|
||||||
END { print mismatched + 0 }'); then
|
|
||||||
swept_clean=0
|
swept_clean=0
|
||||||
echo "[WARN] fix-ownership: could not scan ${path}; will retry on next boot"
|
echo "[WARN] fix-ownership: could not scan ${path}; will retry on next boot"
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -71,14 +71,13 @@ Both the script and the boot sweep report progress, so you can tell a slow sweep
|
|||||||
|
|
||||||
```
|
```
|
||||||
[INFO] fix-ownership: scanning /media/frigate for ownership mismatches; this may take a while on large filesystems
|
[INFO] fix-ownership: scanning /media/frigate for ownership mismatches; this may take a while on large filesystems
|
||||||
[INFO] fix-ownership: still scanning /media/frigate, 2481000 entries checked, 2390112 need changes
|
|
||||||
[WARN] fix-ownership: adjusting ownership of 4823941 entries under /media/frigate
|
[WARN] fix-ownership: adjusting ownership of 4823941 entries under /media/frigate
|
||||||
[INFO] fix-ownership: /media/frigate 5% (241197/4823941 entries)
|
[INFO] fix-ownership: /media/frigate 5% (241197/4823941 entries)
|
||||||
[INFO] fix-ownership: /media/frigate 10% (482394/4823941 entries)
|
[INFO] fix-ownership: /media/frigate 10% (482394/4823941 entries)
|
||||||
[INFO] fix-ownership: finished /media/frigate in 12m 4s
|
[INFO] fix-ownership: finished /media/frigate in 12m 4s
|
||||||
```
|
```
|
||||||
|
|
||||||
The scan has no percentage because the total isn't known until it finishes, so it reports a running count every 30 seconds instead. Watch the boot sweep with `docker logs -f frigate`.
|
The scan has no percentage because the total isn't known until it finishes. Watch the boot sweep with `docker logs -f frigate`.
|
||||||
|
|
||||||
Once the volumes are aligned, start Frigate normally. A file at `/config/.permissions_version` records what was done, so later boots skip the sweep unless you change `PUID`/`PGID`.
|
Once the volumes are aligned, start Frigate normally. A file at `/config/.permissions_version` records what was done, so later boots skip the sweep unless you change `PUID`/`PGID`.
|
||||||
|
|
||||||
|
|||||||
+1
-17
@@ -45,7 +45,7 @@ from frigate.config.camera.updater import (
|
|||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
CameraConfigUpdateTopic,
|
CameraConfigUpdateTopic,
|
||||||
)
|
)
|
||||||
from frigate.const import REDACTED_CREDENTIAL_SENTINEL, REPLAY_CAMERA_PREFIX
|
from frigate.const import REDACTED_CREDENTIAL_SENTINEL
|
||||||
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
|
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
|
||||||
from frigate.genai import PROVIDERS, load_providers
|
from frigate.genai import PROVIDERS, load_providers
|
||||||
from frigate.jobs.media_sync import (
|
from frigate.jobs.media_sync import (
|
||||||
@@ -979,17 +979,6 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
# an unresolvable scene would crash the threads that look up the model
|
|
||||||
detect_update = sections.get("detect")
|
|
||||||
if isinstance(detect_update, dict) and "scene" in detect_update:
|
|
||||||
try:
|
|
||||||
config._resolve_camera_model(cam_name, detect_update["scene"])
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "Invalid detect scene"},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
for section_name, update in sections.items():
|
for section_name, update in sections.items():
|
||||||
err = apply_section_update(camera_config, section_name, update)
|
err = apply_section_update(camera_config, section_name, update)
|
||||||
if err is not None:
|
if err is not None:
|
||||||
@@ -998,11 +987,6 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
# the model is resolved again from the scene when the replay
|
|
||||||
# camera is recycled by the refresh below
|
|
||||||
if "detect" in sections and cam_name.startswith(REPLAY_CAMERA_PREFIX):
|
|
||||||
config.drop_camera_model(cam_name)
|
|
||||||
|
|
||||||
# Publish ZMQ updates so processing threads pick up changes
|
# Publish ZMQ updates so processing threads pick up changes
|
||||||
if body.update_topic and body.update_topic.startswith("config/cameras/"):
|
if body.update_topic and body.update_topic.startswith("config/cameras/"):
|
||||||
_, _, camera, field = body.update_topic.split("/")
|
_, _, camera, field = body.update_topic.split("/")
|
||||||
|
|||||||
+9
-2
@@ -76,7 +76,7 @@ from frigate.models import (
|
|||||||
from frigate.notices import install_registry
|
from frigate.notices import install_registry
|
||||||
from frigate.notices.registry import NoticeRegistry
|
from frigate.notices.registry import NoticeRegistry
|
||||||
from frigate.object_detection.base import ObjectDetectProcess
|
from frigate.object_detection.base import ObjectDetectProcess
|
||||||
from frigate.object_detection.util import create_detection_input_shm
|
from frigate.object_detection.util import detection_frame_size
|
||||||
from frigate.output.output import OutputProcess
|
from frigate.output.output import OutputProcess
|
||||||
from frigate.ptz.autotrack import PtzAutoTracker
|
from frigate.ptz.autotrack import PtzAutoTracker
|
||||||
from frigate.ptz.onvif import OnvifController
|
from frigate.ptz.onvif import OnvifController
|
||||||
@@ -399,7 +399,14 @@ class FrigateApp:
|
|||||||
model = self.config.model_for_camera(name)
|
model = self.config.model_for_camera(name)
|
||||||
model_cameras[model.scene].append(name)
|
model_cameras[model.scene].append(name)
|
||||||
|
|
||||||
shm_in = create_detection_input_shm(name, self.config.models)
|
try:
|
||||||
|
shm_in = UntrackedSharedMemory(
|
||||||
|
name=name,
|
||||||
|
create=True,
|
||||||
|
size=detection_frame_size(model),
|
||||||
|
)
|
||||||
|
except FileExistsError:
|
||||||
|
shm_in = UntrackedSharedMemory(name=name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shm_out = UntrackedSharedMemory(
|
shm_out = UntrackedSharedMemory(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from frigate.config.camera.updater import (
|
|||||||
)
|
)
|
||||||
from frigate.const import REPLAY_CAMERA_PREFIX
|
from frigate.const import REPLAY_CAMERA_PREFIX
|
||||||
from frigate.models import Regions
|
from frigate.models import Regions
|
||||||
from frigate.object_detection.util import create_detection_input_shm
|
from frigate.object_detection.util import detection_frame_size
|
||||||
from frigate.util.builtin import empty_and_close_queue
|
from frigate.util.builtin import empty_and_close_queue
|
||||||
from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory
|
from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory
|
||||||
from frigate.util.object import get_camera_regions_grid
|
from frigate.util.object import get_camera_regions_grid
|
||||||
@@ -127,10 +127,13 @@ class CameraMaintainer(threading.Thread):
|
|||||||
max(model.width, model.height),
|
max(model.width, model.height),
|
||||||
)
|
)
|
||||||
|
|
||||||
create_detection_input_shm(name, self.config.models)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4)
|
UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4)
|
||||||
|
UntrackedSharedMemory(
|
||||||
|
name=name,
|
||||||
|
create=True,
|
||||||
|
size=detection_frame_size(model),
|
||||||
|
)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -296,8 +296,7 @@ class CameraState:
|
|||||||
return frame_copy
|
return frame_copy
|
||||||
|
|
||||||
def finished(self, obj_id: str) -> None:
|
def finished(self, obj_id: str) -> None:
|
||||||
# state rebuilt after a resolution or scene change no longer has the object
|
del self.tracked_objects[obj_id]
|
||||||
self.tracked_objects.pop(obj_id, None)
|
|
||||||
|
|
||||||
def on(self, event_type: str, callback: Callable[..., Any]) -> None:
|
def on(self, event_type: str, callback: Callable[..., Any]) -> None:
|
||||||
self.callbacks[event_type].append(callback)
|
self.callbacks[event_type].append(callback)
|
||||||
|
|||||||
@@ -113,23 +113,6 @@ class AsyncLocalObjectDetector(BaseLocalDetector):
|
|||||||
return self.detect_api.receive_output()
|
return self.detect_api.receive_output()
|
||||||
|
|
||||||
|
|
||||||
def map_input_frame(
|
|
||||||
frame_manager: SharedMemoryFrameManager, camera: str, model: ModelConfig
|
|
||||||
) -> np.ndarray | None:
|
|
||||||
"""Map a camera's detection input segment as this model's input tensor.
|
|
||||||
|
|
||||||
The segment is sized for the largest configured model, so it can be larger
|
|
||||||
than this model's input.
|
|
||||||
"""
|
|
||||||
buf = frame_manager.write(camera)
|
|
||||||
shape = (1, model.height, model.width, 3)
|
|
||||||
|
|
||||||
if buf is None or len(buf) < model.height * model.width * 3:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return np.ndarray(shape, dtype=np.uint8, buffer=buf)
|
|
||||||
|
|
||||||
|
|
||||||
class DetectorRunner(FrigateProcess):
|
class DetectorRunner(FrigateProcess):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -171,10 +154,14 @@ class DetectorRunner(FrigateProcess):
|
|||||||
connection_id = self.detection_queue.get(timeout=1)
|
connection_id = self.detection_queue.get(timeout=1)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
input_frame = map_input_frame(
|
input_frame = frame_manager.get(
|
||||||
frame_manager,
|
|
||||||
connection_id,
|
connection_id,
|
||||||
self.detector_config.model, # type: ignore[arg-type]
|
(
|
||||||
|
1,
|
||||||
|
self.detector_config.model.height, # type: ignore[union-attr]
|
||||||
|
self.detector_config.model.width, # type: ignore[union-attr]
|
||||||
|
3,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_frame is None:
|
if input_frame is None:
|
||||||
@@ -240,10 +227,14 @@ class AsyncDetectorRunner(FrigateProcess):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
assert self._frame_manager is not None
|
assert self._frame_manager is not None
|
||||||
input_frame = map_input_frame(
|
input_frame = self._frame_manager.get(
|
||||||
self._frame_manager,
|
|
||||||
connection_id,
|
connection_id,
|
||||||
self.detector_config.model, # type: ignore[arg-type]
|
(
|
||||||
|
1,
|
||||||
|
self.detector_config.model.height, # type: ignore[union-attr]
|
||||||
|
self.detector_config.model.width, # type: ignore[union-attr]
|
||||||
|
3,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_frame is None:
|
if input_frame is None:
|
||||||
|
|||||||
@@ -6,39 +6,18 @@ import threading
|
|||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
|
|
||||||
from frigate.detectors.detector_config import InputTensorEnum, ModelConfig
|
from frigate.detectors.detector_config import InputTensorEnum, ModelConfig
|
||||||
from frigate.util.image import UntrackedSharedMemory
|
|
||||||
|
|
||||||
|
|
||||||
def create_detection_input_shm(
|
def detection_frame_size(model: ModelConfig) -> int:
|
||||||
camera: str, models: list[ModelConfig]
|
"""Get the shared memory size a camera needs to hand frames to a model.
|
||||||
) -> UntrackedSharedMemory:
|
|
||||||
"""Create the shared memory a camera hands detection frames through.
|
|
||||||
|
|
||||||
It is sized for the largest model so a camera re-added under the same name
|
|
||||||
with another scene can reuse it. Detectors keep mappings of it, so it is
|
|
||||||
never unlinked while they run.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
camera: Name of the camera
|
model: The model the camera runs on
|
||||||
models: Every configured model
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The camera's detection input segment
|
Size in bytes of one model input frame
|
||||||
"""
|
"""
|
||||||
size = max(model.height * model.width * 3 for model in models)
|
return model.height * model.width * 3
|
||||||
|
|
||||||
try:
|
|
||||||
return UntrackedSharedMemory(name=camera, create=True, size=size)
|
|
||||||
except FileExistsError:
|
|
||||||
shm = UntrackedSharedMemory(name=camera)
|
|
||||||
|
|
||||||
if shm.size >= size:
|
|
||||||
return shm
|
|
||||||
|
|
||||||
# left by an earlier run with smaller models, so no detector maps it yet
|
|
||||||
shm.close()
|
|
||||||
shm.unlink()
|
|
||||||
return UntrackedSharedMemory(name=camera, create=True, size=size)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestStore:
|
class RequestStore:
|
||||||
|
|||||||
@@ -98,46 +98,6 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert app.frigate_config.cameras["front_door"].objects.track == ["person"]
|
assert app.frigate_config.cameras["front_door"].objects.track == ["person"]
|
||||||
|
|
||||||
@patch("frigate.detectors.detector_config.load_labels")
|
|
||||||
def test_config_set_in_memory_rebinds_replay_model_on_scene_change(
|
|
||||||
self, mock_labels
|
|
||||||
):
|
|
||||||
mock_labels.return_value = {}
|
|
||||||
replay = "_replay_front_door"
|
|
||||||
self.minimal_config["models"] = [
|
|
||||||
{"devices": ["cpu"]},
|
|
||||||
{"scene": "thermal", "devices": ["openvino:CPU"], "width": 300},
|
|
||||||
]
|
|
||||||
self.minimal_config["cameras"][replay] = self.minimal_config["cameras"].pop(
|
|
||||||
"front_door"
|
|
||||||
)
|
|
||||||
app = super().create_app()
|
|
||||||
app.config_publisher = Mock()
|
|
||||||
assert app.frigate_config.model_for_camera(replay).scene == "default"
|
|
||||||
|
|
||||||
with AuthTestClient(app) as client:
|
|
||||||
response = client.put(
|
|
||||||
"/config/set",
|
|
||||||
json={
|
|
||||||
"requires_restart": 0,
|
|
||||||
"skip_save": True,
|
|
||||||
"update_topic": f"config/cameras/{replay}/detect",
|
|
||||||
"config_data": {
|
|
||||||
"cameras": {replay: {"detect": {"scene": "thermal"}}}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert app.frigate_config.cameras[replay].detect.scene == "thermal"
|
|
||||||
assert app.frigate_config.model_for_camera(replay).scene == "thermal"
|
|
||||||
topics = [
|
|
||||||
call.args[0].update_type.name
|
|
||||||
for call in app.config_publisher.publish_update.call_args_list
|
|
||||||
]
|
|
||||||
assert topics[0] == "detect"
|
|
||||||
assert topics[-1] == "refresh"
|
|
||||||
|
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
################################### Credential redaction sentinel ################################################
|
################################### Credential redaction sentinel ################################################
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
CameraConfigUpdateEnum.enabled,
|
CameraConfigUpdateEnum.enabled,
|
||||||
CameraConfigUpdateEnum.motion,
|
CameraConfigUpdateEnum.motion,
|
||||||
CameraConfigUpdateEnum.objects,
|
CameraConfigUpdateEnum.objects,
|
||||||
CameraConfigUpdateEnum.refresh,
|
|
||||||
CameraConfigUpdateEnum.remove,
|
CameraConfigUpdateEnum.remove,
|
||||||
CameraConfigUpdateEnum.timestamp_style,
|
CameraConfigUpdateEnum.timestamp_style,
|
||||||
CameraConfigUpdateEnum.zones,
|
CameraConfigUpdateEnum.zones,
|
||||||
@@ -785,21 +784,6 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
|
|
||||||
self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity)
|
self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity)
|
||||||
|
|
||||||
# a replay camera recycled onto another scene's model needs new state
|
|
||||||
for camera in updated_topics.get("refresh", []):
|
|
||||||
camera_state = self.camera_states.get(camera)
|
|
||||||
|
|
||||||
if (
|
|
||||||
camera_state is None
|
|
||||||
or camera_state.model is self.config.model_for_camera(camera)
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.force_end_all_events(camera, camera_state)
|
|
||||||
camera_state.shutdown()
|
|
||||||
self.frame_manager.close_camera(camera)
|
|
||||||
self.create_camera_state(camera)
|
|
||||||
|
|
||||||
# manage camera disabled state
|
# manage camera disabled state
|
||||||
for camera, config in self.config.cameras.items():
|
for camera, config in self.config.cameras.items():
|
||||||
if not config.enabled_in_config:
|
if not config.enabled_in_config:
|
||||||
|
|||||||
@@ -240,14 +240,14 @@ const detect: SectionConfigOverrides = {
|
|||||||
},
|
},
|
||||||
replay: {
|
replay: {
|
||||||
restartRequired: [],
|
restartRequired: [],
|
||||||
fieldOrder: ["scene", "width", "height", "fps"],
|
fieldOrder: ["width", "height", "fps"],
|
||||||
fieldGroups: {
|
fieldGroups: {
|
||||||
resolution: ["width", "height", "fps"],
|
resolution: ["width", "height", "fps"],
|
||||||
},
|
},
|
||||||
hiddenFields: [
|
hiddenFields: [
|
||||||
"enabled",
|
"enabled",
|
||||||
"enabled_in_config",
|
"enabled_in_config",
|
||||||
hideSceneWithoutChoice,
|
"scene",
|
||||||
"min_initialized",
|
"min_initialized",
|
||||||
"max_disappeared",
|
"max_disappeared",
|
||||||
"annotation_offset",
|
"annotation_offset",
|
||||||
|
|||||||
@@ -150,18 +150,17 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
// Fetch the VOD manifest JSON to get the actual clipFrom after keyframe
|
// Fetch the VOD manifest JSON to get the actual clipFrom after keyframe
|
||||||
// snapping. The backend may snap clipFrom backwards to a keyframe, making
|
// snapping. The backend may snap clipFrom backwards to a keyframe, making
|
||||||
// the video start earlier than the requested time. Uses sourceOffsetRef so
|
// the video start earlier than the requested time.
|
||||||
// the manifest describes the same clip as videoSource.
|
|
||||||
const vodManifestUrl = useMemo(() => {
|
const vodManifestUrl = useMemo(() => {
|
||||||
if (!event.camera) return null;
|
if (!event.camera) return null;
|
||||||
const sourceOffset = sourceOffsetRef.current;
|
const startTime =
|
||||||
const startTime = event.start_time + sourceOffset / 1000 - REVIEW_PADDING;
|
event.start_time + annotationOffset / 1000 - REVIEW_PADDING;
|
||||||
const endTime =
|
const endTime =
|
||||||
(event.end_time ?? Date.now() / 1000) +
|
(event.end_time ?? Date.now() / 1000) +
|
||||||
sourceOffset / 1000 +
|
annotationOffset / 1000 +
|
||||||
REVIEW_PADDING;
|
REVIEW_PADDING;
|
||||||
return `vod/clip/${event.camera}/start/${startTime}/end/${endTime}`;
|
return `vod/clip/${event.camera}/start/${startTime}/end/${endTime}`;
|
||||||
}, [event]);
|
}, [event, annotationOffset]);
|
||||||
|
|
||||||
const { data: vodManifest } = useSWR<VodManifest>(vodManifestUrl, null, {
|
const { data: vodManifest } = useSWR<VodManifest>(vodManifestUrl, null, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user