mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-26 04:49:17 -05:00
new display options selector
This commit is contained in:
parent
1cb6a19a65
commit
4d6a8c98d9
98
ui/src/List/DisplaySelector.tsx
Normal file
98
ui/src/List/DisplaySelector.tsx
Normal file
@ -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 (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/*<TextField
|
||||||
|
select
|
||||||
|
label="Max video duration"
|
||||||
|
value={split90k}
|
||||||
|
onChange={(e) => setSplit90k(e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<MenuItem value={60 * 60 * 90000}>1 hour</MenuItem>
|
||||||
|
<MenuItem value={4 * 60 * 60 * 90000}>4 hours</MenuItem>
|
||||||
|
<MenuItem value={24 * 60 * 60 * 90000}>24 hours</MenuItem>
|
||||||
|
<MenuItem value={undefined}>infinite</MenuItem>
|
||||||
|
</TextField>*/}
|
||||||
|
<FormControl fullWidth variant="outlined">
|
||||||
|
<InputLabel id="split90k-label" shrink>
|
||||||
|
Max video duration
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="split90k-label"
|
||||||
|
id="split90k"
|
||||||
|
value={props.split90k}
|
||||||
|
onChange={(e) => props.setSplit90k(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem value={60 * 60 * 90000}>1 hour</MenuItem>
|
||||||
|
<MenuItem value={4 * 60 * 60 * 90000}>4 hours</MenuItem>
|
||||||
|
<MenuItem value={24 * 60 * 60 * 90000}>24 hours</MenuItem>
|
||||||
|
<MenuItem value={undefined}>infinite</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
title="Trim each segment of video so that it is fully
|
||||||
|
contained within the select time range. When this is not selected,
|
||||||
|
all segments will overlap with the selected time range but may start
|
||||||
|
and/or end outside it."
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={props.trimStartAndEnd}
|
||||||
|
onChange={(_, checked: boolean) =>
|
||||||
|
props.setTrimStartAndEnd(checked)
|
||||||
|
}
|
||||||
|
name="trim-start-and-end"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Trim start and end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
title="Include a text track in each .mp4 with the
|
||||||
|
timestamp at which the video was recorded."
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={props.timestampTrack}
|
||||||
|
onChange={(_, checked: boolean) => props.setTimestampTrack(checked)}
|
||||||
|
name="timestamp-track"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Timestamp track"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisplaySelector;
|
@ -15,6 +15,8 @@ import Alert from "@material-ui/core/Alert";
|
|||||||
interface Props {
|
interface Props {
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
range90k: [number, number] | null;
|
range90k: [number, number] | null;
|
||||||
|
split90k?: number;
|
||||||
|
trimStartAndEnd: boolean;
|
||||||
setActiveRecording: (recording: [Stream, api.Recording] | null) => void;
|
setActiveRecording: (recording: [Stream, api.Recording] | null) => void;
|
||||||
formatTime: (time90k: number) => string;
|
formatTime: (time90k: number) => string;
|
||||||
}
|
}
|
||||||
@ -85,6 +87,8 @@ const Row = ({
|
|||||||
const VideoList = ({
|
const VideoList = ({
|
||||||
stream,
|
stream,
|
||||||
range90k,
|
range90k,
|
||||||
|
split90k,
|
||||||
|
trimStartAndEnd,
|
||||||
setActiveRecording,
|
setActiveRecording,
|
||||||
formatTime,
|
formatTime,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@ -102,6 +106,7 @@ const VideoList = ({
|
|||||||
stream: stream.streamType,
|
stream: stream.streamType,
|
||||||
startTime90k: range90k[0],
|
startTime90k: range90k[0],
|
||||||
endTime90k: range90k[1],
|
endTime90k: range90k[1],
|
||||||
|
split90k,
|
||||||
};
|
};
|
||||||
let response = await api.recordings(req, { signal });
|
let response = await api.recordings(req, { signal });
|
||||||
if (response.status === "success") {
|
if (response.status === "success") {
|
||||||
@ -122,7 +127,7 @@ const VideoList = ({
|
|||||||
clearTimeout(timerId);
|
clearTimeout(timerId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [range90k, snackbars, stream]);
|
}, [range90k, split90k, snackbars, stream]);
|
||||||
|
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
return null;
|
return null;
|
||||||
@ -154,13 +159,19 @@ const VideoList = ({
|
|||||||
const vse = resp.videoSampleEntries[r.videoSampleEntryId];
|
const vse = resp.videoSampleEntries[r.videoSampleEntryId];
|
||||||
const durationSec = (r.endTime90k - r.startTime90k) / 90000;
|
const durationSec = (r.endTime90k - r.startTime90k) / 90000;
|
||||||
const rate = (r.sampleFileBytes / durationSec) * 0.000008;
|
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 (
|
return (
|
||||||
<Row
|
<Row
|
||||||
key={r.startId}
|
key={r.startId}
|
||||||
className="recording"
|
className="recording"
|
||||||
onClick={() => setActiveRecording([stream, r])}
|
onClick={() => setActiveRecording([stream, r])}
|
||||||
start={formatTime(Math.max(r.startTime90k, state.range90k[0]))}
|
start={formatTime(start)}
|
||||||
end={formatTime(Math.min(r.endTime90k, state.range90k[1]))}
|
end={formatTime(end)}
|
||||||
resolution={`${vse.width}x${vse.height}`}
|
resolution={`${vse.width}x${vse.height}`}
|
||||||
fps={frameRateFmt.format(r.videoSamples / durationSec)}
|
fps={frameRateFmt.format(r.videoSamples / durationSec)}
|
||||||
storage={`${sizeFmt.format(r.sampleFileBytes / 1048576)} MiB`}
|
storage={`${sizeFmt.format(r.sampleFileBytes / 1048576)} MiB`}
|
||||||
|
@ -15,6 +15,7 @@ import TableContainer from "@material-ui/core/TableContainer";
|
|||||||
import Paper from "@material-ui/core/Paper";
|
import Paper from "@material-ui/core/Paper";
|
||||||
import StreamMultiSelector from "./StreamMultiSelector";
|
import StreamMultiSelector from "./StreamMultiSelector";
|
||||||
import TimerangeSelector from "./TimerangeSelector";
|
import TimerangeSelector from "./TimerangeSelector";
|
||||||
|
import DisplaySelector from "./DisplaySelector";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@ -23,9 +24,11 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
margin: theme.spacing(2),
|
margin: theme.spacing(2),
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
width: "max-content",
|
width: "max-content",
|
||||||
|
"& .MuiCard-root": {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
objectFit: "contain",
|
objectFit: "contain",
|
||||||
@ -78,6 +81,11 @@ const Main = ({ cameras, timeZoneName }: Props) => {
|
|||||||
/** Selected time range. */
|
/** Selected time range. */
|
||||||
const [range90k, setRange90k] = useState<[number, number] | null>(null);
|
const [range90k, setRange90k] = useState<[number, number] | null>(null);
|
||||||
|
|
||||||
|
const [split90k, setSplit90k] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const [trimStartAndEnd, setTrimStartAndEnd] = useState(true);
|
||||||
|
const [timestampTrack, setTimestampTrack] = useState(true);
|
||||||
|
|
||||||
const [activeRecording, setActiveRecording] = useState<
|
const [activeRecording, setActiveRecording] = useState<
|
||||||
[Stream, api.Recording] | null
|
[Stream, api.Recording] | null
|
||||||
>(null);
|
>(null);
|
||||||
@ -97,6 +105,8 @@ const Main = ({ cameras, timeZoneName }: Props) => {
|
|||||||
key={`${s.camera.uuid}-${s.streamType}`}
|
key={`${s.camera.uuid}-${s.streamType}`}
|
||||||
stream={s}
|
stream={s}
|
||||||
range90k={range90k}
|
range90k={range90k}
|
||||||
|
split90k={split90k}
|
||||||
|
trimStartAndEnd={trimStartAndEnd}
|
||||||
setActiveRecording={setActiveRecording}
|
setActiveRecording={setActiveRecording}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
/>
|
/>
|
||||||
@ -125,6 +135,14 @@ const Main = ({ cameras, timeZoneName }: Props) => {
|
|||||||
setRange90k={setRange90k}
|
setRange90k={setRange90k}
|
||||||
timeZoneName={timeZoneName}
|
timeZoneName={timeZoneName}
|
||||||
/>
|
/>
|
||||||
|
<DisplaySelector
|
||||||
|
split90k={split90k}
|
||||||
|
setSplit90k={setSplit90k}
|
||||||
|
trimStartAndEnd={trimStartAndEnd}
|
||||||
|
setTrimStartAndEnd={setTrimStartAndEnd}
|
||||||
|
timestampTrack={timestampTrack}
|
||||||
|
setTimestampTrack={setTimestampTrack}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{videoLists.length > 0 && recordingsTable}
|
{videoLists.length > 0 && recordingsTable}
|
||||||
{activeRecording != null && (
|
{activeRecording != null && (
|
||||||
@ -138,7 +156,8 @@ const Main = ({ cameras, timeZoneName }: Props) => {
|
|||||||
activeRecording[0].camera.uuid,
|
activeRecording[0].camera.uuid,
|
||||||
activeRecording[0].streamType,
|
activeRecording[0].streamType,
|
||||||
activeRecording[1],
|
activeRecording[1],
|
||||||
range90k!
|
timestampTrack,
|
||||||
|
trimStartAndEnd ? range90k! : undefined
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -315,6 +315,7 @@ export function recordingUrl(
|
|||||||
cameraUuid: string,
|
cameraUuid: string,
|
||||||
stream: StreamType,
|
stream: StreamType,
|
||||||
r: Recording,
|
r: Recording,
|
||||||
|
timestampTrack: boolean,
|
||||||
trimToRange90k?: [number, number]
|
trimToRange90k?: [number, number]
|
||||||
): string {
|
): string {
|
||||||
let s = `${r.startId}`;
|
let s = `${r.startId}`;
|
||||||
@ -340,6 +341,6 @@ export function recordingUrl(
|
|||||||
}
|
}
|
||||||
return withQuery(`/api/cameras/${cameraUuid}/${stream}/view.mp4`, {
|
return withQuery(`/api/cameras/${cameraUuid}/${stream}/view.mp4`, {
|
||||||
s,
|
s,
|
||||||
ts: true,
|
ts: timestampTrack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user