Fix camera feed freezing and person detection alerts

- Add WebRTC auto-reconnect with exponential backoff when streams
  disconnect or fail, preventing permanent freezes in grid view
- Replace hard-coded Front Porch person alert with generic system
  that monitors all configured personDetectionEntities
- Map Frigate person_occupancy entities to cameras dynamically
- Show correct camera name and feed in alert overlay
- Bump config version to refresh detection entity defaults
This commit is contained in:
root
2026-02-26 15:33:25 -06:00
parent 97a7912eae
commit 58ebd3e239
4 changed files with 151 additions and 64 deletions

View File

@@ -19,17 +19,26 @@ import { useUIStore, useCameraOverlay } from '@/stores/uiStore';
import { useSettingsStore } from '@/stores/settingsStore';
import { env } from '@/config/environment';
// Front porch alert overlay - shows for 30 seconds when person detected
function FrontPorchAlert({ onClose }: { onClose: () => void }) {
// Map a Frigate person_occupancy entity to its camera name
// e.g. 'binary_sensor.fpe_person_occupancy' -> 'fpe' -> match camera by frigateCamera
function entityToCameraName(entityId: string): string | null {
const match = entityId.match(/^binary_sensor\.(.+)_person_occupancy$/);
return match ? match[1] : null;
}
// Person detection alert overlay - shows for 30 seconds when person detected on any configured camera
function PersonAlert({ cameraName, onClose }: { cameraName: string; onClose: () => void }) {
const cameras = useSettingsStore((state) => state.config.cameras);
const frontPorchCamera = cameras.find((c) => c.name === 'Front_Porch');
const camera = cameras.find(
(c) => c.frigateCamera?.toLowerCase() === cameraName || c.name.toLowerCase() === cameraName,
);
useEffect(() => {
const timer = setTimeout(onClose, 30000); // 30 seconds
const timer = setTimeout(onClose, 30000);
return () => clearTimeout(timer);
}, [onClose]);
if (!frontPorchCamera) return null;
if (!camera) return null;
return (
<div className="fixed inset-0 z-50 bg-black flex flex-col">
@@ -37,7 +46,7 @@ function FrontPorchAlert({ onClose }: { onClose: () => void }) {
<div className="flex items-center gap-3">
<div className="w-3 h-3 rounded-full bg-status-warning animate-pulse" />
<h2 className="text-lg font-semibold text-status-warning">
Person Detected - Front Porch
Person Detected - {camera.displayName}
</h2>
</div>
<button
@@ -51,7 +60,7 @@ function FrontPorchAlert({ onClose }: { onClose: () => void }) {
</div>
<div className="flex-1 p-2">
<CameraFeed
camera={frontPorchCamera}
camera={camera}
className="w-full h-full"
showLabel={false}
/>
@@ -174,9 +183,10 @@ export default function App() {
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
const { isOpen: cameraOverlayOpen } = useCameraOverlay();
// Front porch alert state
const [showFrontPorchAlert, setShowFrontPorchAlert] = useState(false);
const frontPorchAlertShownRef = useRef(false);
// Person detection alert state
const personDetectionEntities = useSettingsStore((state) => state.config.personDetectionEntities);
const [alertCamera, setAlertCamera] = useState<string | null>(null);
const alertShownForRef = useRef<Set<string>>(new Set());
// Motion detection now runs in the Electron main process (MotionDetector.ts)
// This prevents browser throttling when the screensaver is active
@@ -230,29 +240,32 @@ export default function App() {
initConfig();
}, [accessToken, connect]);
// Listen for Front Porch person detection - show full screen overlay for 30 seconds
// Listen for person detection on all configured entities
useEffect(() => {
if (!isConnected) return;
const frontPorchEntity = entities['binary_sensor.front_porch_person_occupancy'];
const isPersonDetected = frontPorchEntity?.state === 'on';
for (const entityId of personDetectionEntities) {
const entity = entities[entityId];
const isDetected = entity?.state === 'on';
const cameraName = entityToCameraName(entityId);
if (isPersonDetected && !frontPorchAlertShownRef.current) {
// Person just detected - show alert
frontPorchAlertShownRef.current = true;
setShowFrontPorchAlert(true);
// Also wake the screen
if (window.electronAPI?.screen?.wake) {
window.electronAPI.screen.wake();
if (isDetected && cameraName && !alertShownForRef.current.has(entityId)) {
// Person just detected on this camera - show alert
alertShownForRef.current.add(entityId);
setAlertCamera(cameraName);
if (window.electronAPI?.screen?.wake) {
window.electronAPI.screen.wake();
}
break; // Show one alert at a time
} else if (!isDetected) {
// Reset so next detection on this entity triggers again
alertShownForRef.current.delete(entityId);
}
} else if (!isPersonDetected) {
// Reset flag when person clears so next detection triggers alert
frontPorchAlertShownRef.current = false;
}
}, [isConnected, entities]);
}, [isConnected, entities, personDetectionEntities]);
const closeFrontPorchAlert = useCallback(() => {
setShowFrontPorchAlert(false);
const closePersonAlert = useCallback(() => {
setAlertCamera(null);
}, []);
// Set up screen idle timeout
@@ -321,7 +334,7 @@ export default function App() {
{mediaOverlayOpen && <JellyfinOverlay />}
{cameraOverlayOpen && <CameraOverlay />}
{settingsOpen && <SettingsPanel />}
{showFrontPorchAlert && <FrontPorchAlert onClose={closeFrontPorchAlert} />}
{alertCamera && <PersonAlert cameraName={alertCamera} onClose={closePersonAlert} />}
<GlobalKeyboard />
</>
);