Skip to content

Commit 841686e

Browse files
authored
Merge pull request #1007 from roflcoopter/feature/live-page
Live page
2 parents d1b81ec + a7bc921 commit 841686e

44 files changed

Lines changed: 2062 additions & 690 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codespellrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[codespell]
22

33
ignore-regex = .*codespell-ignore$
4+
skip = frontend/src/components/player/liveplayer/video-rtc.js

docker/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ RUN if [ "$ARCH" = "amd64-cuda" ]; then PLATFORM="amd64"; \
5555
&& wget https://github.com/AlexxIT/go2rtc/releases/download/v${GO2RTC_VERSION}/go2rtc_linux_$PLATFORM -O go2rtc \
5656
&& chmod +x /usr/local/bin/go2rtc
5757

58+
WORKDIR /go2rtc
59+
RUN wget https://raw.githubusercontent.com/AlexxIT/go2rtc/refs/tags/v${GO2RTC_VERSION}/www/video-rtc.js -O video-rtc.js
60+
5861
FROM scratch AS go2rtc
5962
COPY --from=go2rtc-base /usr/local/bin/go2rtc /usr/local/bin/go2rtc
63+
COPY --from=go2rtc-base /go2rtc/video-rtc.js /go2rtc/
6064

6165

6266
# Build frontend
@@ -70,6 +74,7 @@ COPY frontend/package-lock.json ./
7074
RUN npm ci --legacy-peer-deps
7175

7276
COPY frontend/ ./
77+
COPY --from=go2rtc /go2rtc/video-rtc.js /frontend/src/components/player/liveplayer/video-rtc.js
7378
RUN npm run build
7479

7580

frontend/.eslintrc.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ module.exports = defineConfig({
1919
project: "tsconfig.json",
2020
tsconfigRootDir: __dirname,
2121
},
22+
env: {
23+
browser: true,
24+
},
2225
ignorePatterns: [
2326
".eslintrc.cjs",
2427
"vite.config.ts",
2528
"vitest.config.ts",
2629
"lint-staged.config.js",
30+
"video-rtc.js",
2731
],
2832
rules: {
2933
"@typescript-eslint/no-non-null-assertion": "off",

frontend/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"react-router-dom": "^6.26.2",
4444
"react-toastify": "^10.0.5",
4545
"react-transition-group": "^4.4.5",
46+
"screenfull": "^6.0.2",
4647
"typescript": "^5.6.2",
4748
"uuid": "^10.0.0",
4849
"video.js": "^8.17.3",

frontend/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const Configuration = lazy(() => import("pages/Configuration"));
1717
const Entities = lazy(() => import("pages/Entities"));
1818
const Events = lazy(() => import("pages/Events"));
1919
const Login = lazy(() => import("pages/Login"));
20+
const Live = lazy(() => import("pages/Live"));
2021
const NotFound = lazy(() => import("pages/NotFound"));
2122
const Onboarding = lazy(() => import("pages/Onboarding"));
2223
const PublicLayout = lazy(() => import("layouts/PublicLayout"));
@@ -55,6 +56,10 @@ function App() {
5556
path: "/events",
5657
element: <Events />,
5758
},
59+
{
60+
path: "/live",
61+
element: <Live />,
62+
},
5863
{
5964
path: "/entities",
6065
element: <Entities />,

frontend/src/components/camera/CameraNameOverlay.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import * as types from "lib/types";
99

1010
type CameraNameOverlayProps = {
1111
camera_identifier: string;
12+
extraStatusText?: string;
1213
};
1314

1415
const overlayStyles: SxProps<Theme> = {
1516
position: "absolute",
16-
zIndex: 999,
17+
zIndex: 1,
1718
right: "0px",
1819
top: "0px",
1920
margin: "5px",
@@ -42,6 +43,7 @@ const StatusIcon = ({ camera }: { camera: types.Camera }) =>
4243

4344
export const CameraNameOverlay = ({
4445
camera_identifier,
46+
extraStatusText,
4547
}: CameraNameOverlayProps) => {
4648
const cameraQuery = useCamera(camera_identifier);
4749
if (!cameraQuery.data) {
@@ -70,11 +72,19 @@ export const CameraNameOverlay = ({
7072
{showStatusText && (
7173
<Typography
7274
variant="body2"
73-
sx={{ ...cameraNameStyles, fontSize: "0.7rem" }}
75+
sx={{ ...cameraNameStyles, fontSize: "0.7rem", textAlign: "right" }}
7476
>
7577
{statusText}
7678
</Typography>
7779
)}
80+
{extraStatusText && (
81+
<Typography
82+
variant="body2"
83+
sx={{ ...cameraNameStyles, fontSize: "0.7rem", textAlign: "right" }}
84+
>
85+
{extraStatusText}
86+
</Typography>
87+
)}
7888
</Box>
7989
);
8090
};

frontend/src/components/events/CameraPickerDialog.tsx renamed to frontend/src/components/camera/CameraPickerDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import DialogActions from "@mui/material/DialogActions";
44
import DialogContent from "@mui/material/DialogContent";
55
import DialogTitle from "@mui/material/DialogTitle";
66

7-
import { EventsCameraGrid } from "components/events/EventsCameraGrid";
7+
import { CameraPickerGrid } from "components/camera/CameraPickerGrid";
88

99
type CameraPickerDialogProps = {
1010
open: boolean;
@@ -22,7 +22,7 @@ export const CameraPickerDialog = ({
2222
<Dialog fullWidth maxWidth={false} open={open} onClose={handleClose}>
2323
<DialogTitle>Cameras</DialogTitle>
2424
<DialogContent onClick={handleClose}>
25-
<EventsCameraGrid />
25+
<CameraPickerGrid />
2626
</DialogContent>
2727
<DialogActions>
2828
<Button onClick={handleClose}>Close</Button>

frontend/src/components/events/EventsCameraGrid.tsx renamed to frontend/src/components/camera/CameraPickerGrid.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import Grow from "@mui/material/Grow";
33
import { useTheme } from "@mui/material/styles";
44

55
import { CameraCard } from "components/camera/CameraCard";
6-
import { useCameraStore, useFilteredCameras } from "components/events/utils";
6+
import {
7+
useCameraStore,
8+
useFilteredCameras,
9+
} from "components/camera/useCameraStore";
710
import { useCamerasAll } from "lib/api/cameras";
811
import * as types from "lib/types";
912

10-
export function EventsCameraGrid() {
13+
export function CameraPickerGrid() {
1114
const theme = useTheme();
1215
const { toggleCamera } = useCameraStore();
1316
const camerasAll = useCamerasAll();
@@ -31,10 +34,10 @@ export function EventsCameraGrid() {
3134
<Grid
3235
key={camera_identifier}
3336
size={{
34-
xs: 12,
35-
sm: 12,
36-
md: 6,
37-
lg: 6,
37+
xs: 6,
38+
sm: 6,
39+
md: 4,
40+
lg: 4,
3841
xl: 4,
3942
}}
4043
>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useMemo } from "react";
2+
import { create } from "zustand";
3+
import { persist } from "zustand/middleware";
4+
5+
import { useCameras, useCamerasFailed } from "lib/api/cameras";
6+
import * as types from "lib/types";
7+
8+
type Cameras = {
9+
[key: string]: boolean;
10+
};
11+
interface CameraState {
12+
cameras: Cameras;
13+
selectedCameras: string[];
14+
toggleCamera: (cameraIdentifier: string) => void;
15+
selectSingleCamera: (cameraIdentifier: string) => void;
16+
selectionOrder: string[];
17+
}
18+
19+
export const useCameraStore = create<CameraState>()(
20+
persist(
21+
(set) => ({
22+
cameras: {},
23+
selectedCameras: [],
24+
toggleCamera: (cameraIdentifier) => {
25+
set((state) => {
26+
const newCameras = { ...state.cameras };
27+
newCameras[cameraIdentifier] = !newCameras[cameraIdentifier];
28+
let newSelectionOrder = [...state.selectionOrder];
29+
if (newCameras[cameraIdentifier]) {
30+
newSelectionOrder.push(cameraIdentifier);
31+
} else {
32+
newSelectionOrder = newSelectionOrder.filter(
33+
(id) => id !== cameraIdentifier,
34+
);
35+
}
36+
return {
37+
cameras: newCameras,
38+
selectedCameras: Object.entries(newCameras)
39+
.filter(([_key, value]) => value)
40+
.map(([key]) => key),
41+
selectionOrder: newSelectionOrder,
42+
};
43+
});
44+
},
45+
selectSingleCamera: (cameraIdentifier) => {
46+
set((state) => {
47+
const newCameras = { ...state.cameras };
48+
Object.keys(newCameras).forEach((key) => {
49+
newCameras[key] = key === cameraIdentifier;
50+
});
51+
return {
52+
cameras: newCameras,
53+
selectedCameras: [cameraIdentifier],
54+
selectionOrder: [cameraIdentifier],
55+
};
56+
});
57+
},
58+
selectionOrder: [],
59+
}),
60+
{ name: "camera-store" },
61+
),
62+
);
63+
64+
export const useFilteredCameras = () => {
65+
const camerasQuery = useCameras({});
66+
const failedCamerasQuery = useCamerasFailed({});
67+
68+
// Combine the two queries into one object
69+
const cameraData: types.CamerasOrFailedCameras = useMemo(() => {
70+
if (!camerasQuery.data && !failedCamerasQuery.data) {
71+
return {};
72+
}
73+
return {
74+
...camerasQuery.data,
75+
...failedCamerasQuery.data,
76+
};
77+
}, [camerasQuery.data, failedCamerasQuery.data]);
78+
79+
const { selectedCameras } = useCameraStore();
80+
return useMemo(
81+
() =>
82+
Object.keys(cameraData)
83+
.filter((key) => selectedCameras.includes(key))
84+
.reduce((obj: types.CamerasOrFailedCameras, key) => {
85+
obj[key] = cameraData[key];
86+
return obj;
87+
}, {}),
88+
[cameraData, selectedCameras],
89+
);
90+
};

0 commit comments

Comments
 (0)