2021-03-05 16:56:51 -08:00
|
|
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
|
|
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
|
|
|
|
2021-03-16 16:15:03 -07:00
|
|
|
import Box from "@material-ui/core/Box";
|
2021-03-05 16:56:51 -08:00
|
|
|
import Modal from "@material-ui/core/Modal";
|
2021-03-16 16:15:03 -07:00
|
|
|
import Paper from "@material-ui/core/Paper";
|
2021-08-10 11:57:16 -07:00
|
|
|
import { Theme } from "@material-ui/core/styles";
|
|
|
|
import { makeStyles } from "@material-ui/styles";
|
2021-03-05 16:56:51 -08:00
|
|
|
import Table from "@material-ui/core/Table";
|
|
|
|
import TableContainer from "@material-ui/core/TableContainer";
|
2021-03-16 16:15:03 -07:00
|
|
|
import utcToZonedTime from "date-fns-tz/utcToZonedTime";
|
|
|
|
import format from "date-fns/format";
|
|
|
|
import React, { useMemo, useState } from "react";
|
|
|
|
import * as api from "../api";
|
|
|
|
import { Camera, Stream } from "../types";
|
2021-03-16 22:28:00 -07:00
|
|
|
import DisplaySelector, { DEFAULT_DURATION } from "./DisplaySelector";
|
2021-03-05 16:56:51 -08:00
|
|
|
import StreamMultiSelector from "./StreamMultiSelector";
|
|
|
|
import TimerangeSelector from "./TimerangeSelector";
|
2021-03-16 16:15:03 -07:00
|
|
|
import VideoList from "./VideoList";
|
2021-03-05 16:56:51 -08:00
|
|
|
|
|
|
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
|
|
root: {
|
|
|
|
display: "flex",
|
2021-03-15 11:08:02 -07:00
|
|
|
flexWrap: "wrap",
|
2021-03-05 16:56:51 -08:00
|
|
|
margin: theme.spacing(2),
|
|
|
|
},
|
|
|
|
selectors: {
|
2021-03-15 11:08:02 -07:00
|
|
|
width: "max-content",
|
2021-03-16 14:58:47 -07:00
|
|
|
"& .MuiCard-root": {
|
|
|
|
marginRight: theme.spacing(2),
|
|
|
|
marginBottom: theme.spacing(2),
|
|
|
|
},
|
2021-03-05 16:56:51 -08:00
|
|
|
},
|
|
|
|
videoTable: {
|
2021-03-15 22:45:21 -07:00
|
|
|
flexGrow: 1,
|
2021-03-15 11:08:02 -07:00
|
|
|
width: "max-content",
|
|
|
|
height: "max-content",
|
2021-03-15 22:45:21 -07:00
|
|
|
"& .streamHeader": {
|
|
|
|
background: theme.palette.primary.light,
|
|
|
|
color: theme.palette.primary.contrastText,
|
|
|
|
},
|
2021-03-05 16:56:51 -08:00
|
|
|
"& .MuiTableBody-root:not(:last-child):after": {
|
|
|
|
content: "''",
|
|
|
|
display: "block",
|
|
|
|
height: theme.spacing(2),
|
|
|
|
},
|
2021-03-15 22:45:21 -07:00
|
|
|
"& tbody .recording": {
|
2021-03-15 11:08:02 -07:00
|
|
|
cursor: "pointer",
|
|
|
|
},
|
|
|
|
"& .opt": {
|
|
|
|
[theme.breakpoints.down("lg")]: {
|
|
|
|
display: "none",
|
|
|
|
},
|
|
|
|
},
|
2021-03-05 16:56:51 -08:00
|
|
|
},
|
2021-03-16 16:43:24 -07:00
|
|
|
|
|
|
|
// When there's a video modal up, make the content as large as possible
|
|
|
|
// without distorting it. Center it in the screen and ensure that the video
|
|
|
|
// element only takes up the space actually used by the content, so that
|
|
|
|
// clicking outside it will dismiss the modal.
|
|
|
|
videoModal: {
|
|
|
|
display: "flex",
|
|
|
|
alignItems: "center",
|
|
|
|
justifyContent: "center",
|
|
|
|
"& video": {
|
|
|
|
objectFit: "contain",
|
|
|
|
maxWidth: "100%",
|
|
|
|
maxHeight: "100%",
|
|
|
|
},
|
|
|
|
},
|
2021-03-05 16:56:51 -08:00
|
|
|
}));
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
timeZoneName: string;
|
|
|
|
cameras: Camera[];
|
2021-03-26 13:43:04 -07:00
|
|
|
showSelectors: boolean;
|
2021-03-05 16:56:51 -08:00
|
|
|
}
|
|
|
|
|
2021-03-26 13:43:04 -07:00
|
|
|
const Main = ({ cameras, timeZoneName, showSelectors }: Props) => {
|
2021-03-05 16:56:51 -08:00
|
|
|
const classes = useStyles();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Selected streams to display and use for selecting time ranges.
|
|
|
|
* This currently uses the <tt>Stream</tt> object, which means it will
|
|
|
|
* not be stable across top-level API fetches. Maybe an id would be better.
|
|
|
|
*/
|
|
|
|
const [selectedStreams, setSelectedStreams] = useState<Set<Stream>>(
|
|
|
|
new Set()
|
|
|
|
);
|
|
|
|
|
|
|
|
/** Selected time range. */
|
|
|
|
const [range90k, setRange90k] = useState<[number, number] | null>(null);
|
|
|
|
|
2021-03-16 22:28:00 -07:00
|
|
|
const [split90k, setSplit90k] = useState(DEFAULT_DURATION);
|
2021-03-16 14:58:47 -07:00
|
|
|
|
|
|
|
const [trimStartAndEnd, setTrimStartAndEnd] = useState(true);
|
2021-03-16 22:28:00 -07:00
|
|
|
const [timestampTrack, setTimestampTrack] = useState(false);
|
2021-03-16 14:58:47 -07:00
|
|
|
|
2021-03-05 16:56:51 -08:00
|
|
|
const [activeRecording, setActiveRecording] = useState<
|
|
|
|
[Stream, api.Recording] | null
|
|
|
|
>(null);
|
|
|
|
const formatTime = useMemo(() => {
|
|
|
|
return (time90k: number) => {
|
|
|
|
return format(
|
|
|
|
utcToZonedTime(new Date(time90k / 90), timeZoneName),
|
|
|
|
"d MMM yyyy HH:mm:ss"
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}, [timeZoneName]);
|
|
|
|
|
|
|
|
let videoLists = [];
|
|
|
|
for (const s of selectedStreams) {
|
|
|
|
videoLists.push(
|
2021-03-15 22:45:21 -07:00
|
|
|
<VideoList
|
|
|
|
key={`${s.camera.uuid}-${s.streamType}`}
|
|
|
|
stream={s}
|
|
|
|
range90k={range90k}
|
2021-03-16 14:58:47 -07:00
|
|
|
split90k={split90k}
|
|
|
|
trimStartAndEnd={trimStartAndEnd}
|
2021-03-15 22:45:21 -07:00
|
|
|
setActiveRecording={setActiveRecording}
|
|
|
|
formatTime={formatTime}
|
|
|
|
/>
|
2021-03-05 16:56:51 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
const closeModal = (event: {}, reason: string) => {
|
|
|
|
setActiveRecording(null);
|
|
|
|
};
|
|
|
|
const recordingsTable = (
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableContainer component={Paper} className={classes.videoTable}>
|
|
|
|
<Table size="small">{videoLists}</Table>
|
2021-03-05 16:56:51 -08:00
|
|
|
</TableContainer>
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
<div className={classes.root}>
|
2021-03-16 16:15:03 -07:00
|
|
|
<Box
|
|
|
|
className={classes.selectors}
|
2021-03-26 13:43:04 -07:00
|
|
|
sx={{ display: showSelectors ? "block" : "none" }}
|
2021-03-16 16:15:03 -07:00
|
|
|
>
|
2021-03-05 16:56:51 -08:00
|
|
|
<StreamMultiSelector
|
|
|
|
cameras={cameras}
|
|
|
|
selected={selectedStreams}
|
|
|
|
setSelected={setSelectedStreams}
|
|
|
|
/>
|
|
|
|
<TimerangeSelector
|
|
|
|
selectedStreams={selectedStreams}
|
|
|
|
range90k={range90k}
|
|
|
|
setRange90k={setRange90k}
|
|
|
|
timeZoneName={timeZoneName}
|
|
|
|
/>
|
2021-03-16 14:58:47 -07:00
|
|
|
<DisplaySelector
|
|
|
|
split90k={split90k}
|
|
|
|
setSplit90k={setSplit90k}
|
|
|
|
trimStartAndEnd={trimStartAndEnd}
|
|
|
|
setTrimStartAndEnd={setTrimStartAndEnd}
|
|
|
|
timestampTrack={timestampTrack}
|
|
|
|
setTimestampTrack={setTimestampTrack}
|
|
|
|
/>
|
2021-03-16 16:15:03 -07:00
|
|
|
</Box>
|
2021-03-05 16:56:51 -08:00
|
|
|
{videoLists.length > 0 && recordingsTable}
|
|
|
|
{activeRecording != null && (
|
2021-03-16 16:43:24 -07:00
|
|
|
<Modal open onClose={closeModal} className={classes.videoModal}>
|
2021-03-05 16:56:51 -08:00
|
|
|
<video
|
|
|
|
controls
|
|
|
|
preload="auto"
|
|
|
|
autoPlay
|
|
|
|
src={api.recordingUrl(
|
|
|
|
activeRecording[0].camera.uuid,
|
|
|
|
activeRecording[0].streamType,
|
2021-03-13 22:25:05 -08:00
|
|
|
activeRecording[1],
|
2021-03-16 14:58:47 -07:00
|
|
|
timestampTrack,
|
|
|
|
trimStartAndEnd ? range90k! : undefined
|
2021-03-05 16:56:51 -08:00
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Main;
|