mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-07 05:55:27 +03:00
Compare commits
9 Commits
ed9b098469
...
cf672badb5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf672badb5 | ||
|
|
dfaa178c0e | ||
|
|
51624f755f | ||
|
|
e86c94f294 | ||
|
|
5ddd824b49 | ||
|
|
ccbe2fda9a | ||
|
|
2ec94bea13 | ||
|
|
513cc18715 | ||
|
|
14da821956 |
@ -84,20 +84,20 @@ async def start_debug_replay(request: Request, body: DebugReplayStartBody):
|
|||||||
config_publisher=request.app.config_publisher,
|
config_publisher=request.app.config_publisher,
|
||||||
replay_manager=replay_manager,
|
replay_manager=replay_manager,
|
||||||
)
|
)
|
||||||
except RuntimeError as exc:
|
except RuntimeError:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": str(exc),
|
"message": "A replay session is already active",
|
||||||
},
|
},
|
||||||
status_code=409,
|
status_code=409,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError:
|
||||||
logger.info("Rejected debug replay start request: %s", exc)
|
logger.exception("Rejected debug replay start request")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": str(exc),
|
"message": "Invalid debug replay parameters",
|
||||||
},
|
},
|
||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
@ -137,7 +137,7 @@ def get_debug_replay_status(request: Request):
|
|||||||
if frame is not None:
|
if frame is not None:
|
||||||
frame_time = frame_processor.get_current_frame_time(replay_camera)
|
frame_time = frame_processor.get_current_frame_time(replay_camera)
|
||||||
camera_config = request.app.frigate_config.cameras.get(replay_camera)
|
camera_config = request.app.frigate_config.cameras.get(replay_camera)
|
||||||
retry_interval = 10
|
retry_interval = 10.0
|
||||||
|
|
||||||
if camera_config is not None:
|
if camera_config is not None:
|
||||||
retry_interval = float(camera_config.ffmpeg.retry_interval or 10)
|
retry_interval = float(camera_config.ffmpeg.retry_interval or 10)
|
||||||
|
|||||||
@ -174,12 +174,10 @@ async def latest_frame(
|
|||||||
}
|
}
|
||||||
quality_params = get_image_quality_params(extension.value, params.quality)
|
quality_params = get_image_quality_params(extension.value, params.quality)
|
||||||
|
|
||||||
if camera_name in request.app.frigate_config.cameras:
|
camera_config = request.app.frigate_config.cameras.get(camera_name)
|
||||||
|
if camera_config is not None:
|
||||||
frame = frame_processor.get_current_frame(camera_name, draw_options)
|
frame = frame_processor.get_current_frame(camera_name, draw_options)
|
||||||
retry_interval = float(
|
retry_interval = float(camera_config.ffmpeg.retry_interval or 10)
|
||||||
request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
|
|
||||||
or 10
|
|
||||||
)
|
|
||||||
|
|
||||||
is_offline = False
|
is_offline = False
|
||||||
if frame is None or datetime.now().timestamp() > (
|
if frame is None or datetime.now().timestamp() > (
|
||||||
|
|||||||
@ -13,7 +13,9 @@ import subprocess as sp
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||||
|
|
||||||
|
from peewee import ModelSelect
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.config.camera.updater import CameraConfigUpdatePublisher
|
from frigate.config.camera.updater import CameraConfigUpdatePublisher
|
||||||
@ -90,12 +92,12 @@ class DebugReplayJob(Job):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def query_recordings(source_camera: str, start_ts: float, end_ts: float):
|
def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> ModelSelect:
|
||||||
"""Return the Recordings query for the time range.
|
"""Return the Recordings query for the time range.
|
||||||
|
|
||||||
Module-level so tests can patch it without instantiating a runner.
|
Module-level so tests can patch it without instantiating a runner.
|
||||||
"""
|
"""
|
||||||
return (
|
query = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
@ -109,6 +111,7 @@ def query_recordings(source_camera: str, start_ts: float, end_ts: float):
|
|||||||
.where(Recordings.camera == source_camera)
|
.where(Recordings.camera == source_camera)
|
||||||
.order_by(Recordings.start_time.asc())
|
.order_by(Recordings.start_time.asc())
|
||||||
)
|
)
|
||||||
|
return cast(ModelSelect, query)
|
||||||
|
|
||||||
|
|
||||||
class DebugReplayJobRunner(threading.Thread):
|
class DebugReplayJobRunner(threading.Thread):
|
||||||
|
|||||||
@ -61,7 +61,9 @@ class TestDebugReplayAPI(BaseTestHttp):
|
|||||||
self.assertEqual(resp.status_code, 400)
|
self.assertEqual(resp.status_code, 400)
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
self.assertFalse(body["success"])
|
self.assertFalse(body["success"])
|
||||||
self.assertIn("missing", body["message"])
|
# Message is hard-coded so we don't echo exception text back to clients
|
||||||
|
# (CodeQL: information exposure through an exception).
|
||||||
|
self.assertEqual(body["message"], "Invalid debug replay parameters")
|
||||||
|
|
||||||
def test_start_returns_409_when_session_already_active(self):
|
def test_start_returns_409_when_session_already_active(self):
|
||||||
with patch(
|
with patch(
|
||||||
|
|||||||
@ -53,10 +53,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def inject_progress_flags(cmd: list[str]) -> list[str]:
|
def inject_progress_flags(cmd: list[str]) -> list[str]:
|
||||||
"""Insert ``-progress pipe:2 -nostats`` immediately before the output path.
|
"""Insert `-progress pipe:2 -nostats` immediately before the output path.
|
||||||
|
|
||||||
``-progress pipe:2`` writes structured key=value lines to stderr;
|
`-progress pipe:2` writes structured key=value lines to stderr;
|
||||||
``-nostats`` suppresses the noisy default stats output. The output path
|
`-nostats` suppresses the noisy default stats output. The output path
|
||||||
is conventionally the last token in an FFmpeg argv.
|
is conventionally the last token in an FFmpeg argv.
|
||||||
"""
|
"""
|
||||||
if not cmd:
|
if not cmd:
|
||||||
@ -73,24 +73,24 @@ def run_ffmpeg_with_progress(
|
|||||||
process_started: Optional[Callable[[sp.Popen], None]] = None,
|
process_started: Optional[Callable[[sp.Popen], None]] = None,
|
||||||
use_low_priority: bool = True,
|
use_low_priority: bool = True,
|
||||||
) -> tuple[int, str]:
|
) -> tuple[int, str]:
|
||||||
"""Run an ffmpeg command, streaming progress via ``-progress pipe:2``.
|
"""Run an ffmpeg command, streaming progress via `-progress pipe:2`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd: ffmpeg argv. Output path must be the last token.
|
cmd: ffmpeg argv. Output path must be the last token.
|
||||||
expected_duration_seconds: Duration of the expected output clip in
|
expected_duration_seconds: Duration of the expected output clip in
|
||||||
seconds. Used to convert ffmpeg's ``out_time_us`` into a percent.
|
seconds. Used to convert ffmpeg's `out_time_us` into a percent.
|
||||||
on_progress: Optional callback invoked with a percent in [0, 100].
|
on_progress: Optional callback invoked with a percent in [0, 100].
|
||||||
Called once with 0.0 at start, again on each ``out_time_us=``
|
Called once with 0.0 at start, again on each `out_time_us=`
|
||||||
stderr line, and once with 100.0 on ``progress=end``.
|
stderr line, and once with 100.0 on `progress=end`.
|
||||||
stdin_payload: Optional string written to ffmpeg stdin (used by
|
stdin_payload: Optional string written to ffmpeg stdin (used by
|
||||||
export for concat playlists).
|
export for concat playlists).
|
||||||
process_started: Optional callback invoked with the live ``Popen``
|
process_started: Optional callback invoked with the live `Popen`
|
||||||
once spawned — lets callers store the ref for cancellation.
|
once spawned — lets callers store the ref for cancellation.
|
||||||
use_low_priority: When True, prepend ``nice -n PROCESS_PRIORITY_LOW``
|
use_low_priority: When True, prepend `nice -n PROCESS_PRIORITY_LOW`
|
||||||
so concat doesn't starve detection.
|
so concat doesn't starve detection.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of ``(returncode, captured_stderr)``. Stdout is left attached
|
Tuple of `(returncode, captured_stderr)`. Stdout is left attached
|
||||||
to the parent process to avoid buffer-full deadlocks.
|
to the parent process to avoid buffer-full deadlocks.
|
||||||
"""
|
"""
|
||||||
full_cmd = inject_progress_flags(cmd)
|
full_cmd = inject_progress_flags(cmd)
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"error": "Failed to start debug replay: {{error}}",
|
"error": "Failed to start debug replay: {{error}}",
|
||||||
"alreadyActive": "A replay session is already active",
|
"alreadyActive": "A replay session is already active",
|
||||||
"stopped": "Debug replay stopped",
|
|
||||||
"stopError": "Failed to stop debug replay: {{error}}",
|
"stopError": "Failed to stop debug replay: {{error}}",
|
||||||
"goToReplay": "Go to Replay"
|
"goToReplay": "Go to Replay"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,8 +117,6 @@ const DEBUG_OPTION_I18N_KEY: Record<keyof DebugOptions, string> = {
|
|||||||
paths: "paths",
|
paths: "paths",
|
||||||
};
|
};
|
||||||
|
|
||||||
const REPLAY_INIT_SKELETON_TIMEOUT_MS = 8000;
|
|
||||||
|
|
||||||
export default function Replay() {
|
export default function Replay() {
|
||||||
const { t } = useTranslation(["views/replay", "views/settings", "common"]);
|
const { t } = useTranslation(["views/replay", "views/settings", "common"]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -145,12 +143,6 @@ export default function Replay() {
|
|||||||
initializeStatus();
|
initializeStatus();
|
||||||
}, [refreshStatus]);
|
}, [refreshStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status?.live_ready) {
|
|
||||||
setShowReplayInitSkeleton(false);
|
|
||||||
}
|
|
||||||
}, [status?.live_ready]);
|
|
||||||
|
|
||||||
const [options, setOptions] = useState<DebugOptions>(DEFAULT_OPTIONS);
|
const [options, setOptions] = useState<DebugOptions>(DEFAULT_OPTIONS);
|
||||||
const [isStopping, setIsStopping] = useState(false);
|
const [isStopping, setIsStopping] = useState(false);
|
||||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||||
@ -175,9 +167,6 @@ export default function Replay() {
|
|||||||
axios
|
axios
|
||||||
.post("debug_replay/stop")
|
.post("debug_replay/stop")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(t("dialog.toast.stopped"), {
|
|
||||||
position: "top-center",
|
|
||||||
});
|
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -205,35 +194,10 @@ export default function Replay() {
|
|||||||
|
|
||||||
const { objects } = useCameraActivity(replayCameraConfig);
|
const { objects } = useCameraActivity(replayCameraConfig);
|
||||||
|
|
||||||
const [showReplayInitSkeleton, setShowReplayInitSkeleton] = useState(false);
|
|
||||||
|
|
||||||
// debug draw
|
// debug draw
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [debugDraw, setDebugDraw] = useState(false);
|
const [debugDraw, setDebugDraw] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!status?.active || !status.replay_camera) {
|
|
||||||
setShowReplayInitSkeleton(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowReplayInitSkeleton(true);
|
|
||||||
|
|
||||||
const timeout = window.setTimeout(() => {
|
|
||||||
setShowReplayInitSkeleton(false);
|
|
||||||
}, REPLAY_INIT_SKELETON_TIMEOUT_MS);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [status?.active, status?.replay_camera]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status?.live_ready) {
|
|
||||||
setShowReplayInitSkeleton(false);
|
|
||||||
}
|
|
||||||
}, [status?.live_ready]);
|
|
||||||
|
|
||||||
// Format time range for display
|
// Format time range for display
|
||||||
const timeRangeDisplay = useMemo(() => {
|
const timeRangeDisplay = useMemo(() => {
|
||||||
if (!status?.start_time || !status?.end_time) return "";
|
if (!status?.start_time || !status?.end_time) return "";
|
||||||
@ -436,27 +400,30 @@ export default function Replay() {
|
|||||||
) : (
|
) : (
|
||||||
status.replay_camera && (
|
status.replay_camera && (
|
||||||
<div className="relative size-full min-h-10" ref={containerRef}>
|
<div className="relative size-full min-h-10" ref={containerRef}>
|
||||||
<AutoUpdatingCameraImage
|
{status.live_ready ? (
|
||||||
className="size-full"
|
<>
|
||||||
cameraClasses="relative w-full h-full flex flex-col justify-start"
|
<AutoUpdatingCameraImage
|
||||||
searchParams={searchParams}
|
className="size-full"
|
||||||
camera={status.replay_camera}
|
cameraClasses="relative w-full h-full flex flex-col justify-start"
|
||||||
showFps={false}
|
searchParams={searchParams}
|
||||||
/>
|
camera={status.replay_camera}
|
||||||
{debugDraw && (
|
showFps={false}
|
||||||
<DebugDrawingLayer
|
/>
|
||||||
containerRef={containerRef}
|
{debugDraw && (
|
||||||
cameraWidth={
|
<DebugDrawingLayer
|
||||||
config?.cameras?.[status.source_camera ?? ""]?.detect
|
containerRef={containerRef}
|
||||||
.width ?? 1280
|
cameraWidth={
|
||||||
}
|
config?.cameras?.[status.source_camera ?? ""]?.detect
|
||||||
cameraHeight={
|
.width ?? 1280
|
||||||
config?.cameras?.[status.source_camera ?? ""]?.detect
|
}
|
||||||
.height ?? 720
|
cameraHeight={
|
||||||
}
|
config?.cameras?.[status.source_camera ?? ""]?.detect
|
||||||
/>
|
.height ?? 720
|
||||||
)}
|
}
|
||||||
{showReplayInitSkeleton && (
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<div className="pointer-events-none absolute inset-0 z-10 size-full rounded-lg bg-background">
|
<div className="pointer-events-none absolute inset-0 z-10 size-full rounded-lg bg-background">
|
||||||
<Skeleton className="size-full rounded-lg" />
|
<Skeleton className="size-full rounded-lg" />
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center gap-2">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center gap-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user