address review feedback

- Discard the export row when an export fails before or during ffmpeg, so it doesn't stay in progress until a restart
- Skip the empty thumbnail path when discarding a failed export
- Measure preview clip duration from the clamped start
- Default live player volume to 1 when the group has no saved setting
- Clear the unsaved registration message when notification registration fails
- Keep a highlight timer per trigger
- Remove the severity order_by in /review, which the start_time order_by replaced
This commit is contained in:
Josh Hawkins
2026-11-01 07:29:16 -06:00
parent f68f360386
commit 9fca3f276d
7 changed files with 27 additions and 16 deletions
+6 -4
View File
@@ -1530,7 +1530,8 @@ async def preview_gif(
status_code=404,
)
diff = max(start_ts - preview.start_time, 0)
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
@@ -1542,7 +1543,7 @@ async def preview_gif(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - start_ts}",
f"{end_ts - clip_start}",
"-i",
preview.path,
"-r",
@@ -1721,7 +1722,8 @@ async def preview_mp4(
status_code=404,
)
diff = max(start_ts - preview.start_time, 0)
clip_start = max(start_ts, preview.start_time)
diff = clip_start - preview.start_time
minutes = int(diff / 60)
seconds = int(diff % 60)
config: FrigateConfig = request.app.frigate_config
@@ -1734,7 +1736,7 @@ async def preview_mp4(
"-ss",
f"00:{minutes}:{seconds}",
"-t",
f"{end_ts - start_ts}",
f"{end_ts - clip_start}",
"-i",
preview.path,
"-r",
+1 -2
View File
@@ -163,8 +163,7 @@ async def review(
# Apply ordering and limit
review_query = (
review_query.order_by(ReviewSegment.severity.asc())
.order_by(ReviewSegment.start_time.desc())
review_query.order_by(ReviewSegment.start_time.desc())
.limit(limit)
.dicts()
.iterator()
+6 -1
View File
@@ -1470,6 +1470,9 @@ class RecordingExporter(threading.Thread):
try:
self._run_export(video_path, thumb_path)
except Exception:
self._discard_failed_export(video_path, thumb_path)
raise
finally:
# staged runs hold a full copy of the export, so they must not
# survive a failure the way the small chapter file could
@@ -1479,7 +1482,8 @@ class RecordingExporter(threading.Thread):
"""Drop the partial output and the row that promised it."""
Path(video_path).unlink(missing_ok=True)
Export.delete().where(Export.id == self.export_id).execute()
Path(thumb_path).unlink(missing_ok=True)
if thumb_path:
Path(thumb_path).unlink(missing_ok=True)
def _run_export(self, video_path: str, thumb_path: str) -> None:
try:
@@ -1498,6 +1502,7 @@ class RecordingExporter(threading.Thread):
else:
ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path)
except DoesNotExist:
self._discard_failed_export(video_path, thumb_path)
return
# When neither custom ffmpeg arg is set the default path uses
@@ -415,6 +415,7 @@ export default function NotificationsSettingsExtras({
pushSubscription.unsubscribe();
workerRegistration.unregister();
setRegistration(null);
removeMessage("notification_settings", "registration");
});
})
.catch(() => {
@@ -423,9 +424,10 @@ export default function NotificationsSettingsExtras({
});
workerRegistration.unregister();
setRegistration(null);
removeMessage("notification_settings", "registration");
});
},
[addMessage, publicKey, t],
[addMessage, removeMessage, publicKey, t],
);
useEffect(() => {
+2 -2
View File
@@ -794,7 +794,7 @@ export default function DraggableGridLayout({
toggleAudio={() => toggleAudio(camera.name)}
statsState={statsStates[camera.name]}
toggleStats={() => toggleStats(camera.name)}
volumeState={volumeStates[camera.name]}
volumeState={volumeStates[camera.name] ?? 1}
setVolumeState={(value) =>
setVolumeStates((prev) => ({
...prev,
@@ -839,7 +839,7 @@ export default function DraggableGridLayout({
onError={(e) => handleError(camera.name, e)}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
playAudio={audioStates[camera.name]}
volume={volumeStates[camera.name]}
volume={volumeStates[camera.name] ?? 1}
onLiveAspectChange={liveAspectHandlers[camera.name]}
/>
</GridLiveContextMenu>
+1 -1
View File
@@ -629,7 +629,7 @@ export default function LiveDashboardView({
resetPreferredLiveMode(camera.name)
}
playAudio={audioStates[camera.name] ?? false}
volume={volumeStates[camera.name]}
volume={volumeStates[camera.name] ?? 1}
/>
</LiveContextMenu>
);
+8 -5
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import useSWR from "swr";
@@ -133,6 +133,9 @@ export default function TriggerView({
// watch websocket for updates
const { payload: triggers_status_ws } = useTriggers();
const highlightTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>(
{},
);
useEffect(() => {
if (!triggers_status_ws) return;
@@ -148,11 +151,11 @@ export default function TriggerView({
return current;
});
// not cleared on the next payload, or a second trigger firing within
// 3s would leave this one highlighted
setTimeout(() => {
const name = triggers_status_ws.name;
clearTimeout(highlightTimers.current[name]);
highlightTimers.current[name] = setTimeout(() => {
setTriggeredTrigger((prev) =>
(prev || []).filter((name) => name !== triggers_status_ws.name),
(prev || []).filter((triggered) => triggered !== name),
);
}, 3000);
}, [triggers_status_ws, selectedCamera, mutate]);