diff --git a/ui/src/List/DisplaySelector.tsx b/ui/src/List/DisplaySelector.tsx new file mode 100644 index 0000000..8d47719 --- /dev/null +++ b/ui/src/List/DisplaySelector.tsx @@ -0,0 +1,98 @@ +// 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 + +import Card from "@material-ui/core/Card"; +import Checkbox from "@material-ui/core/Checkbox"; +import InputLabel from "@material-ui/core/InputLabel"; +import FormControl from "@material-ui/core/FormControl"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import React from "react"; +import { useTheme } from "@material-ui/core/styles"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; + +interface Props { + split90k?: number; + setSplit90k: (split90k?: number) => void; + trimStartAndEnd: boolean; + setTrimStartAndEnd: (trimStartAndEnd: boolean) => void; + timestampTrack: boolean; + setTimestampTrack: (timestampTrack: boolean) => void; +} + +/** + * Returns a card for setting options relating to how videos are displayed. + */ +const DisplaySelector = (props: Props) => { + const theme = useTheme(); + return ( + + {/* setSplit90k(e.target.value)} + variant="outlined" + > + 1 hour + 4 hours + 24 hours + infinite + */} + + + Max video duration + + + + + props.setTrimStartAndEnd(checked) + } + name="trim-start-and-end" + /> + } + label="Trim start and end" + /> + props.setTimestampTrack(checked)} + name="timestamp-track" + /> + } + label="Timestamp track" + /> + + ); +}; + +export default DisplaySelector; diff --git a/ui/src/List/VideoList.tsx b/ui/src/List/VideoList.tsx index dc2661f..743485e 100644 --- a/ui/src/List/VideoList.tsx +++ b/ui/src/List/VideoList.tsx @@ -15,6 +15,8 @@ import Alert from "@material-ui/core/Alert"; interface Props { stream: Stream; range90k: [number, number] | null; + split90k?: number; + trimStartAndEnd: boolean; setActiveRecording: (recording: [Stream, api.Recording] | null) => void; formatTime: (time90k: number) => string; } @@ -85,6 +87,8 @@ const Row = ({ const VideoList = ({ stream, range90k, + split90k, + trimStartAndEnd, setActiveRecording, formatTime, }: Props) => { @@ -102,6 +106,7 @@ const VideoList = ({ stream: stream.streamType, startTime90k: range90k[0], endTime90k: range90k[1], + split90k, }; let response = await api.recordings(req, { signal }); if (response.status === "success") { @@ -122,7 +127,7 @@ const VideoList = ({ clearTimeout(timerId); }; } - }, [range90k, snackbars, stream]); + }, [range90k, split90k, snackbars, stream]); if (state === null) { return null; @@ -154,13 +159,19 @@ const VideoList = ({ const vse = resp.videoSampleEntries[r.videoSampleEntryId]; const durationSec = (r.endTime90k - r.startTime90k) / 90000; const rate = (r.sampleFileBytes / durationSec) * 0.000008; + const start = trimStartAndEnd + ? Math.max(r.startTime90k, state.range90k[0]) + : r.startTime90k; + const end = trimStartAndEnd + ? Math.min(r.endTime90k, state.range90k[1]) + : r.endTime90k; return ( setActiveRecording([stream, r])} - start={formatTime(Math.max(r.startTime90k, state.range90k[0]))} - end={formatTime(Math.min(r.endTime90k, state.range90k[1]))} + start={formatTime(start)} + end={formatTime(end)} resolution={`${vse.width}x${vse.height}`} fps={frameRateFmt.format(r.videoSamples / durationSec)} storage={`${sizeFmt.format(r.sampleFileBytes / 1048576)} MiB`} diff --git a/ui/src/List/index.tsx b/ui/src/List/index.tsx index 5bed262..1e6f04f 100644 --- a/ui/src/List/index.tsx +++ b/ui/src/List/index.tsx @@ -15,6 +15,7 @@ import TableContainer from "@material-ui/core/TableContainer"; import Paper from "@material-ui/core/Paper"; import StreamMultiSelector from "./StreamMultiSelector"; import TimerangeSelector from "./TimerangeSelector"; +import DisplaySelector from "./DisplaySelector"; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -23,9 +24,11 @@ const useStyles = makeStyles((theme: Theme) => ({ margin: theme.spacing(2), }, selectors: { - marginRight: theme.spacing(2), - marginBottom: theme.spacing(2), width: "max-content", + "& .MuiCard-root": { + marginRight: theme.spacing(2), + marginBottom: theme.spacing(2), + }, }, video: { objectFit: "contain", @@ -78,6 +81,11 @@ const Main = ({ cameras, timeZoneName }: Props) => { /** Selected time range. */ const [range90k, setRange90k] = useState<[number, number] | null>(null); + const [split90k, setSplit90k] = useState(undefined); + + const [trimStartAndEnd, setTrimStartAndEnd] = useState(true); + const [timestampTrack, setTimestampTrack] = useState(true); + const [activeRecording, setActiveRecording] = useState< [Stream, api.Recording] | null >(null); @@ -97,6 +105,8 @@ const Main = ({ cameras, timeZoneName }: Props) => { key={`${s.camera.uuid}-${s.streamType}`} stream={s} range90k={range90k} + split90k={split90k} + trimStartAndEnd={trimStartAndEnd} setActiveRecording={setActiveRecording} formatTime={formatTime} /> @@ -125,6 +135,14 @@ const Main = ({ cameras, timeZoneName }: Props) => { setRange90k={setRange90k} timeZoneName={timeZoneName} /> + {videoLists.length > 0 && recordingsTable} {activeRecording != null && ( @@ -138,7 +156,8 @@ const Main = ({ cameras, timeZoneName }: Props) => { activeRecording[0].camera.uuid, activeRecording[0].streamType, activeRecording[1], - range90k! + timestampTrack, + trimStartAndEnd ? range90k! : undefined )} /> diff --git a/ui/src/api.ts b/ui/src/api.ts index 99cb2ec..204c4d7 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -315,6 +315,7 @@ export function recordingUrl( cameraUuid: string, stream: StreamType, r: Recording, + timestampTrack: boolean, trimToRange90k?: [number, number] ): string { let s = `${r.startId}`; @@ -340,6 +341,6 @@ export function recordingUrl( } return withQuery(`/api/cameras/${cameraUuid}/${stream}/view.mp4`, { s, - ts: true, + ts: timestampTrack, }); }