From e0d05743832e3017a30f28ea82651025edbed6c9 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Fri, 5 Apr 2024 21:27:24 +0000 Subject: [PATCH 01/10] react-lazy forceCheck on scroll in EventTable --- frontend/src/components/events/EventTable.tsx | 25 ++++++++++++++++++- .../src/components/events/EventTableItem.tsx | 1 + frontend/src/components/events/Layouts.tsx | 1 + frontend/src/lib/helpers.tsx | 11 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/events/EventTable.tsx b/frontend/src/components/events/EventTable.tsx index b86c7e423..cf199a704 100644 --- a/frontend/src/components/events/EventTable.tsx +++ b/frontend/src/components/events/EventTable.tsx @@ -3,16 +3,36 @@ import Divider from "@mui/material/Divider"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import dayjs, { Dayjs } from "dayjs"; -import { memo } from "react"; +import { memo, useEffect } from "react"; +import { forceCheck } from "react-lazyload"; import ServerDown from "svg/undraw/server_down.svg?react"; import { ErrorMessage } from "components/error/ErrorMessage"; import { EventTableItem } from "components/events/EventTableItem"; import { Loading } from "components/loading/Loading"; import { useRecordings } from "lib/api/recordings"; +import { throttle } from "lib/helpers"; import * as types from "lib/types"; +const useOnScroll = (parentRef: React.RefObject) => { + useEffect(() => { + const container = parentRef.current; + if (!container) return () => {}; + + const throttleForceCheck = throttle(() => { + console.log("scrolling"); + forceCheck(); + }, 100); + container.addEventListener("scroll", throttleForceCheck); + + return () => { + container.removeEventListener("scroll", throttleForceCheck); + }; + }); +}; + type EventTableProps = { + parentRef: React.RefObject; camera: types.Camera | types.FailedCamera; date: Dayjs | null; selectedRecording: types.Recording | null; @@ -21,6 +41,7 @@ type EventTableProps = { export const EventTable = memo( ({ + parentRef, camera, date, selectedRecording, @@ -34,6 +55,8 @@ export const EventTable = memo( configOptions: { enabled: !!date }, }); + useOnScroll(parentRef); + if (recordingsQuery.isError) { return ( {selectedCamera ? ( void, timeFrame: number) { + let lastTime = 0; + return () => { + const now = new Date().getTime(); + if (now - lastTime >= timeFrame) { + func(); + lastTime = now; + } + }; +} From 363e5e7716c58bb19765eddba987f83f355b233c Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Fri, 5 Apr 2024 21:28:47 +0000 Subject: [PATCH 02/10] refresh Hls-Client-Id on source change --- .../events/timeline/TimelinePlayer.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/events/timeline/TimelinePlayer.tsx b/frontend/src/components/events/timeline/TimelinePlayer.tsx index 6e67b43dc..ff2dd7323 100644 --- a/frontend/src/components/events/timeline/TimelinePlayer.tsx +++ b/frontend/src/components/events/timeline/TimelinePlayer.tsx @@ -17,6 +17,7 @@ dayjs.extend(utc); const loadSource = ( hlsRef: React.MutableRefObject, + hlsClientIdRef: React.MutableRefObject, requestedTimestamp: number, camera: types.Camera | types.FailedCamera, ) => { @@ -24,6 +25,7 @@ const loadSource = ( return; } const source = `/api/v1/hls/${camera.identifier}/index.m3u8?start_timestamp=${requestedTimestamp}&daily=true`; + hlsClientIdRef.current = uuidv4(); hlsRef.current.loadSource(source); }; @@ -98,6 +100,7 @@ const onMediaAttached = ( const initializePlayer = ( hlsRef: React.MutableRefObject, + hlsClientIdRef: React.MutableRefObject, videoRef: React.RefObject, initialProgramDateTime: React.MutableRefObject, auth: types.AuthEnabledResponse, @@ -111,7 +114,6 @@ const initializePlayer = ( } // Create a new hls instance - const hlsClientId = uuidv4(); hlsRef.current = new Hls({ maxBufferLength: 30, // 30 seconds of forward buffer backBufferLength: 15, // 15 seconds of back buffer @@ -125,7 +127,7 @@ const initializePlayer = ( xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("Authorization", `Bearer ${token}`); } - xhr.setRequestHeader("Hls-Client-Id", hlsClientId); + xhr.setRequestHeader("Hls-Client-Id", hlsClientIdRef.current); } }, }); @@ -135,7 +137,7 @@ const initializePlayer = ( } // Load the source and start the hls instance - loadSource(hlsRef, requestedTimestampRef.current, camera); + loadSource(hlsRef, hlsClientIdRef, requestedTimestampRef.current, camera); // Handle MEDIA_ATTACHED event hlsRef.current.on(Hls.Events.MEDIA_ATTACHED, () => { @@ -160,6 +162,7 @@ const initializePlayer = ( default: initializePlayer( hlsRef, + hlsClientIdRef, videoRef, initialProgramDateTime, auth, @@ -174,6 +177,7 @@ const initializePlayer = ( const useInitializePlayer = ( hlsRef: React.MutableRefObject, + hlsClientIdRef: React.MutableRefObject, videoRef: React.RefObject, initialProgramDateTime: React.MutableRefObject, requestedTimestampRef: React.MutableRefObject, @@ -185,6 +189,7 @@ const useInitializePlayer = ( if (Hls.isSupported()) { initializePlayer( hlsRef, + hlsClientIdRef, videoRef, initialProgramDateTime, auth, @@ -207,6 +212,7 @@ const useInitializePlayer = ( // Seek to the requestedTimestamp if it is within the seekable range const useSeekToTimestamp = ( hlsRef: React.MutableRefObject, + hlsClientIdRef: React.MutableRefObject, videoRef: React.RefObject, initialProgramDateTime: React.MutableRefObject, requestedTimestamp: number, @@ -251,10 +257,17 @@ const useSeekToTimestamp = ( } if (!seeked) { - loadSource(hlsRef, requestedTimestamp, camera); + loadSource(hlsRef, hlsClientIdRef, requestedTimestamp, camera); } videoRef.current.play(); - }, [camera, hlsRef, initialProgramDateTime, requestedTimestamp, videoRef]); + }, [ + camera, + hlsClientIdRef, + hlsRef, + initialProgramDateTime, + requestedTimestamp, + videoRef, + ]); }; const useResizeObserver = ( @@ -295,11 +308,13 @@ export const TimelinePlayer: React.FC = ({ requestedTimestamp, }) => { const videoRef = useRef(null); + const hlsClientIdRef = useRef(uuidv4()); const initialProgramDateTime = useRef(null); const requestedTimestampRef = useRef(requestedTimestamp); requestedTimestampRef.current = requestedTimestamp; useInitializePlayer( hlsRef, + hlsClientIdRef, videoRef, initialProgramDateTime, requestedTimestampRef, @@ -307,6 +322,7 @@ export const TimelinePlayer: React.FC = ({ ); useSeekToTimestamp( hlsRef, + hlsClientIdRef, videoRef, initialProgramDateTime, requestedTimestamp, From 12905b83f287341c986b3208df8d8a8ef3374b13 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 21:27:09 +0000 Subject: [PATCH 03/10] make CameraGrid scrollable and set consistent margins --- .../components/events/CameraPickerDialog.tsx | 1 + .../src/components/events/EventPlayerCard.tsx | 63 +++++----- frontend/src/components/events/EventTable.tsx | 1 - .../components/events/EventsCameraGrid.tsx | 110 +++++++++++++++++- frontend/src/components/events/Layouts.tsx | 15 ++- .../events/timeline/TimelinePlayer.tsx | 9 +- .../src/components/events/timeline/utils.tsx | 6 +- frontend/src/context/ColorModeContext.tsx | 3 + 8 files changed, 163 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/events/CameraPickerDialog.tsx b/frontend/src/components/events/CameraPickerDialog.tsx index fc84a5146..51aeee007 100644 --- a/frontend/src/components/events/CameraPickerDialog.tsx +++ b/frontend/src/components/events/CameraPickerDialog.tsx @@ -33,6 +33,7 @@ export const CameraPickerDialog = ({ Cameras ; + playerCardRef: React.RefObject; }; export const PlayerCard = ({ @@ -126,35 +127,33 @@ export const PlayerCard = ({ requestedTimestamp, selectedTab, hlsRef, -}: PlayerCardProps) => { - const ref = useRef(null); - return ( - - {camera && } - - {eventSource && selectedTab === "events" ? ( - - ) : camera && requestedTimestamp && selectedTab === "timeline" ? ( - - ) : ( - - )} - - - ); -}; + playerCardRef, +}: PlayerCardProps) => ( + ({ + marginBottom: theme.margin, + position: "relative", + })} + > + {camera && } + + {eventSource && selectedTab === "events" ? ( + + ) : camera && requestedTimestamp && selectedTab === "timeline" ? ( + + ) : ( + + )} + + +); diff --git a/frontend/src/components/events/EventTable.tsx b/frontend/src/components/events/EventTable.tsx index cf199a704..dddb949b4 100644 --- a/frontend/src/components/events/EventTable.tsx +++ b/frontend/src/components/events/EventTable.tsx @@ -20,7 +20,6 @@ const useOnScroll = (parentRef: React.RefObject) => { if (!container) return () => {}; const throttleForceCheck = throttle(() => { - console.log("scrolling"); forceCheck(); }, 100); container.addEventListener("scroll", throttleForceCheck); diff --git a/frontend/src/components/events/EventsCameraGrid.tsx b/frontend/src/components/events/EventsCameraGrid.tsx index 95247459e..5dded5fce 100644 --- a/frontend/src/components/events/EventsCameraGrid.tsx +++ b/frontend/src/components/events/EventsCameraGrid.tsx @@ -1,11 +1,45 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; import Grid from "@mui/material/Grid"; import Grow from "@mui/material/Grow"; import { useTheme } from "@mui/material/styles"; +import { useEffect, useRef } from "react"; import { CameraCard } from "components/camera/CameraCard"; +import { COLUMN_HEIGHT } from "components/events/timeline/utils"; import * as types from "lib/types"; -type EventsCameraGridProps = { +const useResizeObserver = ( + divRef: React.RefObject, + playerCardRef: React.RefObject | undefined, +) => { + const theme = useTheme(); + const resizeObserver = useRef(); + + useEffect(() => { + if (!divRef.current || !playerCardRef || !playerCardRef.current) { + return () => {}; + } + + resizeObserver.current = new ResizeObserver(() => { + if (!divRef.current || !playerCardRef.current) { + return; + } + + divRef.current.style.maxHeight = `calc(${COLUMN_HEIGHT} - ${theme.headerHeight}px - ${theme.margin} - ${playerCardRef.current.offsetHeight}px)`; + }); + + resizeObserver.current.observe(playerCardRef.current); + + return () => { + if (resizeObserver.current) { + resizeObserver.current.disconnect(); + } + }; + }, [divRef, playerCardRef, theme.headerHeight, theme.margin]); +}; + +type CameraGridProps = { cameras: types.CamerasOrFailedCameras; changeSelectedCamera: ( ev: React.MouseEvent, @@ -13,14 +47,15 @@ type EventsCameraGridProps = { ) => void; selectedCamera: types.Camera | types.FailedCamera | null; }; -export function EventsCameraGrid({ +function CameraGrid({ cameras, changeSelectedCamera, selectedCamera, -}: EventsCameraGridProps) { +}: CameraGridProps) { const theme = useTheme(); + return ( - + {cameras ? Object.keys(cameras) .sort() @@ -54,3 +89,70 @@ export function EventsCameraGrid({ ); } + +type EventsCameraGridPropsCard = { + variant: "card"; + playerCardRef: React.RefObject; + cameras: types.CamerasOrFailedCameras; + changeSelectedCamera: ( + ev: React.MouseEvent, + camera: types.Camera | types.FailedCamera, + ) => void; + selectedCamera: types.Camera | types.FailedCamera | null; +}; +type EventsCameraGridPropsGrid = { + variant?: "grid"; + cameras: types.CamerasOrFailedCameras; + changeSelectedCamera: ( + ev: React.MouseEvent, + camera: types.Camera | types.FailedCamera, + ) => void; + selectedCamera: types.Camera | types.FailedCamera | null; +}; +type EventsCameraGridProps = + | EventsCameraGridPropsCard + | EventsCameraGridPropsGrid; +export function EventsCameraGrid(props: EventsCameraGridProps) { + const { + variant = "card", + cameras, + changeSelectedCamera, + selectedCamera, + } = props; + + const playerCardRef = + variant === "card" + ? (props as EventsCameraGridPropsCard).playerCardRef + : undefined; + + const ref = useRef(null); + useResizeObserver(ref, playerCardRef); + + if (variant === "grid") { + return ( + + ); + } + return ( + + + + + + ); +} diff --git a/frontend/src/components/events/Layouts.tsx b/frontend/src/components/events/Layouts.tsx index a9ef4470d..58785e1bc 100644 --- a/frontend/src/components/events/Layouts.tsx +++ b/frontend/src/components/events/Layouts.tsx @@ -23,6 +23,7 @@ import { PlayerCard } from "components/events/EventPlayerCard"; import { EventTable } from "components/events/EventTable"; import { EventsCameraGrid } from "components/events/EventsCameraGrid"; import { TimelineTable } from "components/events/timeline/TimelineTable"; +import { COLUMN_HEIGHT } from "components/events/timeline/utils"; import { insertURLParameter } from "lib/helpers"; import * as types from "lib/types"; @@ -226,8 +227,10 @@ export const Layout = memo( selectedTab, setSelectedTab, }: LayoutProps) => { + const theme = useTheme(); const parentRef = useRef(null); const hlsRef = useRef(null); + const playerCardRef = useRef(null); return (
({ + sx={{ width: "650px", - height: `calc(98dvh - ${theme.headerHeight}px)`, + height: `calc(${COLUMN_HEIGHT} - ${theme.headerHeight}px)`, overflow: "auto", overflowX: "hidden", - })} + }} > (null); const hlsRef = useRef(null); + const playerCardRef = useRef(null); // Observe div height to calculate the height of the EventTable const [height, setHeight] = useState(); @@ -332,6 +338,7 @@ export const LayoutSmall = memo( requestedTimestamp={requestedTimestamp} selectedTab={selectedTab} hlsRef={hlsRef} + playerCardRef={playerCardRef} />
{ videoRef.current!.style.height = `${calculateHeight( - camera, + camera.width, + camera.height, containerRef.current!.offsetWidth, )}px`; }); @@ -337,7 +338,11 @@ export const TimelinePlayer: React.FC = ({ width: "100%", verticalAlign: "top", height: containerRef.current - ? calculateHeight(camera, containerRef.current.offsetWidth) + ? calculateHeight( + camera.width, + camera.height, + containerRef.current.offsetWidth, + ) : undefined, }} controls diff --git a/frontend/src/components/events/timeline/utils.tsx b/frontend/src/components/events/timeline/utils.tsx index 1a2d69b6e..fd50ae71b 100644 --- a/frontend/src/components/events/timeline/utils.tsx +++ b/frontend/src/components/events/timeline/utils.tsx @@ -7,6 +7,7 @@ import * as types from "lib/types"; export const TICK_HEIGHT = 8; export const SCALE = 60; export const EXTRA_TICKS = 10; +export const COLUMN_HEIGHT = "99dvh"; export const DEFAULT_ITEM: TimelineItem = { time: 0, timedEvent: null, @@ -261,6 +262,7 @@ export const findFragmentByTimestamp = ( // Calculate the height of the camera while maintaining aspect ratio export const calculateHeight = ( - camera: types.Camera | types.FailedCamera, + cameraWidth: number, + cameraHeight: number, width: number, -): number => (width * camera.height) / camera.width; +): number => (width * cameraHeight) / cameraWidth; diff --git a/frontend/src/context/ColorModeContext.tsx b/frontend/src/context/ColorModeContext.tsx index a73c22c30..89db1a7ae 100644 --- a/frontend/src/context/ColorModeContext.tsx +++ b/frontend/src/context/ColorModeContext.tsx @@ -42,9 +42,11 @@ declare module "@mui/material/styles/createPalette" { declare module "@mui/material/styles" { interface Theme { headerHeight: number; + margin: string; } interface ThemeOptions { headerHeight?: number; + margin?: string; } } @@ -141,6 +143,7 @@ export function ColorModeProvider({ children }: ColorModeProviderProps) { }), grey, headerHeight: 56, + margin: "0.5dvh", palette: { mode, motion: "#f9b4f6", From 5455a70defbee65e18cdcbd0fbc4daa0abb79f09 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 21:27:43 +0000 Subject: [PATCH 04/10] reduce spacing between cards --- frontend/src/pages/Cameras.tsx | 2 +- frontend/src/pages/recordings/CameraRecordings.tsx | 2 +- frontend/src/pages/recordings/CameraRecordingsDaily.tsx | 2 +- frontend/src/pages/recordings/Recordings.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Cameras.tsx b/frontend/src/pages/Cameras.tsx index a61a09a52..f2a503076 100644 --- a/frontend/src/pages/Cameras.tsx +++ b/frontend/src/pages/Cameras.tsx @@ -33,7 +33,7 @@ const Cameras = () => { Cameras - + {failedCameras.data ? Object.keys(failedCameras.data) .sort() diff --git a/frontend/src/pages/recordings/CameraRecordings.tsx b/frontend/src/pages/recordings/CameraRecordings.tsx index ab01b3e82..bc9885fcb 100644 --- a/frontend/src/pages/recordings/CameraRecordings.tsx +++ b/frontend/src/pages/recordings/CameraRecordings.tsx @@ -72,7 +72,7 @@ const CameraRecordings = () => { {cameraQuery.data.name} - + {Object.keys(recordingsQuery.data) .sort() .reverse() diff --git a/frontend/src/pages/recordings/CameraRecordingsDaily.tsx b/frontend/src/pages/recordings/CameraRecordingsDaily.tsx index c60c8fb9a..00d368dea 100644 --- a/frontend/src/pages/recordings/CameraRecordingsDaily.tsx +++ b/frontend/src/pages/recordings/CameraRecordingsDaily.tsx @@ -71,7 +71,7 @@ const CameraRecordingsDaily = () => { {`${cameraQuery.data.name} - ${date}`} - + {Object.keys(recordingsQuery.data[date]) .reverse() .map((recording) => ( diff --git a/frontend/src/pages/recordings/Recordings.tsx b/frontend/src/pages/recordings/Recordings.tsx index 726c1a885..d7134a2c9 100644 --- a/frontend/src/pages/recordings/Recordings.tsx +++ b/frontend/src/pages/recordings/Recordings.tsx @@ -52,7 +52,7 @@ const Recordings = () => { Recordings - + {failedCameras.data ? Object.keys(failedCameras.data) .sort() From 4acc74b640659ce8545e60256d03d15740f30c77 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 21:29:37 +0000 Subject: [PATCH 05/10] show HoverLine at correct position when window is scrolled --- frontend/src/components/events/timeline/HoverLine.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/events/timeline/HoverLine.tsx b/frontend/src/components/events/timeline/HoverLine.tsx index a2162493c..120f8dabb 100644 --- a/frontend/src/components/events/timeline/HoverLine.tsx +++ b/frontend/src/components/events/timeline/HoverLine.tsx @@ -24,7 +24,7 @@ const useSetPosition = ( if (y === 0) { return; } - const top = e.clientY; + const top = e.clientY + window.scrollY; const dateAtCursor = getDateAtPosition( y, From ed367a41f61c8efce1f4c137eab67d177b5537ff Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 21:36:00 +0000 Subject: [PATCH 06/10] reduce margin in Header --- frontend/src/components/header/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index d742f362b..9bbe49b51 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -48,7 +48,7 @@ const Header = styled("header", { theme.palette.mode === "dark" ? alpha(theme.palette.background.default, 0.72) : "rgba(255,255,255,0.72)", - marginBottom: "10px", + marginBottom: theme.margin, transform: showHeader ? "translateY(0)" : "translateY(-100%)", transition: "transform 300ms ease-in", })); From c935d821aed0c1e5746655b79bd487836aef6c6a Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 21:36:26 +0000 Subject: [PATCH 07/10] reduce Container gutter --- frontend/src/context/ColorModeContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/context/ColorModeContext.tsx b/frontend/src/context/ColorModeContext.tsx index 89db1a7ae..e2b91b5aa 100644 --- a/frontend/src/context/ColorModeContext.tsx +++ b/frontend/src/context/ColorModeContext.tsx @@ -204,8 +204,8 @@ export function ColorModeProvider({ children }: ColorModeProviderProps) { MuiContainer: { styleOverrides: { root: { - paddingLeft: 10, - paddingRight: 10, + paddingLeft: 5, + paddingRight: 5, }, }, defaultProps: { From e7b633760b313170139d7f6daa06de4d569fce34 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sat, 6 Apr 2024 22:26:23 +0000 Subject: [PATCH 08/10] add page name in Header for small layouts --- .../src/components/header/Breadcrumbs.tsx | 14 +++- frontend/src/components/header/Header.tsx | 67 ++++++++++--------- frontend/src/pages/Cameras.tsx | 4 -- frontend/src/pages/recordings/Recordings.tsx | 4 -- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/header/Breadcrumbs.tsx b/frontend/src/components/header/Breadcrumbs.tsx index 12847842b..0c88b07b3 100644 --- a/frontend/src/components/header/Breadcrumbs.tsx +++ b/frontend/src/components/header/Breadcrumbs.tsx @@ -2,6 +2,8 @@ import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; import Link from "@mui/material/Link"; import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { Link as RouterLink, useLocation } from "react-router-dom"; import queryClient from "lib/api/client"; @@ -9,10 +11,20 @@ import { toTitleCase } from "lib/helpers"; import * as types from "lib/types"; export default function Breadcrumbs() { + const theme = useTheme(); + const mediaQueryMedium = useMediaQuery(theme.breakpoints.up("sm")); const location = useLocation(); const pathnames = location.pathname.split("/").filter((x) => x); if (pathnames.length === 0) { - return null; + pathnames.push("cameras"); + } + + if (!mediaQueryMedium) { + return ( + + {toTitleCase(pathnames[0])} + + ); } return ( diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index 9bbe49b51..31fec64df 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -1,6 +1,5 @@ import Brightness4Icon from "@mui/icons-material/Brightness4"; import Brightness7Icon from "@mui/icons-material/Brightness7"; -import GitHubIcon from "@mui/icons-material/GitHub"; import LogoutIcon from "@mui/icons-material/Logout"; import MenuIcon from "@mui/icons-material/Menu"; import SettingsIcon from "@mui/icons-material/Settings"; @@ -11,7 +10,6 @@ import Stack from "@mui/material/Stack"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { alpha, styled, useTheme } from "@mui/material/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; import { useContext, useRef, useState } from "react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import ViseronLogo from "svg/viseron-logo.svg?react"; @@ -56,7 +54,6 @@ const Header = styled("header", { export default function AppHeader({ setDrawerOpen }: AppHeaderProps) { const colorMode = useContext(ColorModeContext); const theme = useTheme(); - const mediaQueryMedium = useMediaQuery(theme.breakpoints.up("md")); const [showHeader, setShowHeader] = useState(true); const lastTogglePos = useRef(0); const { auth } = useAuthContext(); @@ -101,39 +98,47 @@ export default function AppHeader({ setDrawerOpen }: AppHeaderProps) { minHeight: theme.headerHeight, }} > - - { - setDrawerOpen(true); - }} - > - - - - - - - - - {mediaQueryMedium && } - - - + + { + setDrawerOpen(true); + }} > - + + + + + + + + + + + { return ( - - Cameras - {failedCameras.data ? Object.keys(failedCameras.data) diff --git a/frontend/src/pages/recordings/Recordings.tsx b/frontend/src/pages/recordings/Recordings.tsx index d7134a2c9..b0b171cd8 100644 --- a/frontend/src/pages/recordings/Recordings.tsx +++ b/frontend/src/pages/recordings/Recordings.tsx @@ -1,7 +1,6 @@ import Container from "@mui/material/Container"; import Grid from "@mui/material/Grid"; import Grow from "@mui/material/Grow"; -import Typography from "@mui/material/Typography"; import { ScrollToTopOnMount } from "components/ScrollToTop"; import { Loading } from "components/loading/Loading"; @@ -49,9 +48,6 @@ const Recordings = () => { return ( - - Recordings - {failedCameras.data ? Object.keys(failedCameras.data) From cd4111fa6464da55abc58449ae76803132cf8ed0 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Sun, 7 Apr 2024 21:40:02 +0000 Subject: [PATCH 09/10] connect to db using postgres user --- docker/Dockerfile | 3 ++- rootfs/etc/cont-finish.d/10-postgres | 23 +++++++++++++++++++++++ rootfs/etc/cont-init.d/80-postgres | 15 +++++++-------- rootfs/etc/services.d/postgres/run | 2 +- rootfs/etc/services.d/viseron/run | 2 +- viseron/components/storage/const.py | 2 +- 6 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 rootfs/etc/cont-finish.d/10-postgres diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d1425678..ba46acf5a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -63,7 +63,8 @@ ENV \ DEBIAN_FRONTEND=noninteractive \ S6_KEEP_ENV=1 \ S6_SERVICES_GRACETIME=30000 \ - S6_KILL_GRACETIME=1000 \ + S6_KILL_GRACETIME=30000 \ + S6_KILL_FINISH_MAXTIME=30000 \ PATH=$PATH:/home/abc/bin \ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib \ PYTHONPATH=$PYTHONPATH:/usr/local/lib/python3.8/site-packages \ diff --git a/rootfs/etc/cont-finish.d/10-postgres b/rootfs/etc/cont-finish.d/10-postgres new file mode 100644 index 000000000..a4b9429a7 --- /dev/null +++ b/rootfs/etc/cont-finish.d/10-postgres @@ -0,0 +1,23 @@ +#!/usr/bin/with-contenv bash +# +source /helpers/logger.sh + +log_info "Wait for Viseron to stop..." +PID=$(pgrep -f ^viseron) +while ps -p $PID > /dev/null; do + sleep 1 +done +log_info "Viseron has stopped!" + +log_info "Stopping PostgreSQL..." +s6-setuidgid postgres /usr/lib/postgresql/12/bin/pg_ctl -D /config/postgresql -l /config/postgresql/logfile stop + +# Wait until PostgreSQL has stopped +log_info "Waiting for PostgreSQL Server to stop..." +while s6-setuidgid abc pg_isready -d viseron; do + sleep 1 +done +log_info "PostgreSQL Server has stopped!" + +# Restore permissions +chown -R abc:abc /config/postgresql diff --git a/rootfs/etc/cont-init.d/80-postgres b/rootfs/etc/cont-init.d/80-postgres index 470c8565b..846c18d67 100644 --- a/rootfs/etc/cont-init.d/80-postgres +++ b/rootfs/etc/cont-init.d/80-postgres @@ -5,26 +5,25 @@ source /helpers/logger.sh log_info "***************** Preparing PostgreSQL *******************" mkdir -p /config/postgresql mkdir -p /var/run/postgresql -chown abc:abc /var/run/postgresql -chown -R abc:abc /config/postgresql +chown postgres:abc /var/run/postgresql +chown -R postgres:abc /config/postgresql chmod 0700 /config/postgresql if [ -e /config/postgresql/postgresql.conf ]; then log_info "Database has already been initialized." else log_info "Database has not been initialized. Initializing..." - s6-setuidgid abc /usr/lib/postgresql/12/bin/initdb + s6-setuidgid postgres /usr/lib/postgresql/12/bin/initdb log_info "Starting PostgreSQL..." - s6-setuidgid abc /usr/lib/postgresql/12/bin/pg_ctl -D /config/postgresql -l /config/postgresql/logfile start - if s6-setuidgid abc psql -q -d viseron -tc "SELECT 1 FROM pg_database WHERE datname = 'viseron';" ; then + s6-setuidgid postgres /usr/lib/postgresql/12/bin/pg_ctl -D /config/postgresql -l /config/postgresql/logfile start + if s6-setuidgid postgres psql -q -d viseron -tc "SELECT 1 FROM pg_database WHERE datname = 'viseron';" ; then log_info "Database has already been created." else log_info "Database has not been created. Creating..." - s6-setuidgid abc createuser viseron - s6-setuidgid abc createdb -U abc -O viseron viseron + s6-setuidgid postgres createdb -U postgres -O postgres viseron fi log_info "Stopping PostgreSQL..." - s6-setuidgid abc /usr/lib/postgresql/12/bin/pg_ctl -D /config/postgresql -l /config/postgresql/logfile stop + s6-setuidgid postgres /usr/lib/postgresql/12/bin/pg_ctl -D /config/postgresql -l /config/postgresql/logfile stop fi log_info "*********************** Done *****************************" diff --git a/rootfs/etc/services.d/postgres/run b/rootfs/etc/services.d/postgres/run index 3bec09543..258dd0bcf 100644 --- a/rootfs/etc/services.d/postgres/run +++ b/rootfs/etc/services.d/postgres/run @@ -1,4 +1,4 @@ #!/usr/bin/with-contenv bash echo "Starting PostgreSQL Server..." -s6-setuidgid abc /usr/lib/postgresql/12/bin/postgres -D /config/postgresql +s6-setuidgid postgres /usr/lib/postgresql/12/bin/postgres -D /config/postgresql diff --git a/rootfs/etc/services.d/viseron/run b/rootfs/etc/services.d/viseron/run index 19197bffe..1c9b36c60 100644 --- a/rootfs/etc/services.d/viseron/run +++ b/rootfs/etc/services.d/viseron/run @@ -7,7 +7,7 @@ pkill -f ffmpeg_ pkill -f gstreamer_ # Wait until PostgreSQL is ready -until s6-setuidgid abc pg_isready -d viseron; do +until s6-setuidgid postgres pg_isready -d viseron; do log_info "Waiting for PostgreSQL Server to start..." sleep 1 done diff --git a/viseron/components/storage/const.py b/viseron/components/storage/const.py index d446eb851..f91e89b67 100644 --- a/viseron/components/storage/const.py +++ b/viseron/components/storage/const.py @@ -5,7 +5,7 @@ COMPONENT = "storage" -DATABASE_URL = "postgresql://localhost/viseron" +DATABASE_URL = "postgresql://postgres@localhost/viseron" MOVE_FILES_THROTTLE_SECONDS = 10 From d31f52766b883a80c2a886375713033b03d8a184 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Mon, 8 Apr 2024 08:18:42 +0000 Subject: [PATCH 10/10] set default value for EXTINF if missing --- viseron/domains/camera/fragmenter.py | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/viseron/domains/camera/fragmenter.py b/viseron/domains/camera/fragmenter.py index e285e702f..ea292f8c3 100644 --- a/viseron/domains/camera/fragmenter.py +++ b/viseron/domains/camera/fragmenter.py @@ -19,7 +19,7 @@ from viseron.components.storage.const import COMPONENT as STORAGE_COMPONENT from viseron.components.storage.models import FilesMeta -from viseron.const import TEMP_DIR, VISERON_SIGNAL_SHUTDOWN +from viseron.const import CAMERA_SEGMENT_DURATION, TEMP_DIR, VISERON_SIGNAL_SHUTDOWN from viseron.helpers.logs import LogPipe if TYPE_CHECKING: @@ -145,18 +145,8 @@ def _move_to_segments_folder(self, file: str): except FileNotFoundError: self._logger.debug(f"{file} not found") - def _write_files_metadata(self, file: str): + def _write_files_metadata(self, file: str, extinf: float): """Write metadata about the fragmented mp4 to the database.""" - extinf = _extract_extinf_number( - open( - os.path.join( - self._camera.temp_segments_folder, - file.split(".")[0], - "master_1.m3u8", - ), - encoding="utf-8", - ).read() - ) with self._storage.get_session() as session: orig_ctime = datetime.datetime.fromtimestamp( int(file.split(".")[0]), tz=None @@ -172,6 +162,16 @@ def _write_files_metadata(self, file: str): session.execute(stmt) session.commit() + def _read_m3u8(self, file: str) -> str: + return open( + os.path.join( + self._camera.temp_segments_folder, + file.split(".")[0], + "master_1.m3u8", + ), + encoding="utf-8", + ).read() + def _create_fragmented_mp4(self): """Create fragmented mp4 from mp4 using MP4Box.""" self._logger.debug("Checking for new segments to fragment") @@ -180,7 +180,10 @@ def _create_fragmented_mp4(self): self._logger.debug(f"Processing {mp4}") try: if self._mp4box_command(mp4): - self._write_files_metadata(mp4) + extinf = _extract_extinf_number(self._read_m3u8(mp4)) + self._write_files_metadata( + mp4, extinf if extinf else float(CAMERA_SEGMENT_DURATION) + ) self._move_to_segments_folder(mp4) except Exception as err: # pylint: disable=broad-except self._logger.error(f"Failed to fragment {mp4}", exc_info=err)