mirror of
				https://github.com/scottlamb/moonfire-nvr.git
				synced 2025-10-30 00:05:03 -04: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 { | ||||
|   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 ( | ||||
|         <Row | ||||
|           key={r.startId} | ||||
|           className="recording" | ||||
|           onClick={() => 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`} | ||||
|  | ||||
| @ -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: { | ||||
|     width: "max-content", | ||||
|     "& .MuiCard-root": { | ||||
|       marginRight: theme.spacing(2), | ||||
|       marginBottom: theme.spacing(2), | ||||
|     width: "max-content", | ||||
|     }, | ||||
|   }, | ||||
|   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<number | undefined>(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} | ||||
|         /> | ||||
|         <DisplaySelector | ||||
|           split90k={split90k} | ||||
|           setSplit90k={setSplit90k} | ||||
|           trimStartAndEnd={trimStartAndEnd} | ||||
|           setTrimStartAndEnd={setTrimStartAndEnd} | ||||
|           timestampTrack={timestampTrack} | ||||
|           setTimestampTrack={setTimestampTrack} | ||||
|         /> | ||||
|       </div> | ||||
|       {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 | ||||
|             )} | ||||
|           /> | ||||
|         </Modal> | ||||
|  | ||||
| @ -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, | ||||
|   }); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user