// 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 CircularProgress from "@material-ui/core/CircularProgress"; import React from "react"; import * as api from "../api"; import { useSnackbars } from "../snackbars"; import { Stream } from "../types"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableRow from "@material-ui/core/TableRow"; interface Props { stream: Stream; range90k: [number, number] | null; setActiveRecording: (recording: [Stream, api.Recording] | null) => void; formatTime: (time90k: number) => string; } const frameRateFmt = new Intl.NumberFormat([], { maximumFractionDigits: 0, }); const sizeFmt = new Intl.NumberFormat([], { maximumFractionDigits: 1, }); /** * Creates a TableBody with a list of videos for a given * stream and range90k. * * The parent is responsible for creating the greater table. * * When one is clicked, calls setActiveRecording. */ const VideoList = ({ stream, range90k, setActiveRecording, formatTime, }: Props) => { const snackbars = useSnackbars(); const [ response, setResponse, ] = React.useState | null>(null); const [showLoading, setShowLoading] = React.useState(false); React.useEffect(() => { const abort = new AbortController(); const doFetch = async (signal: AbortSignal, range90k: [number, number]) => { const req: api.RecordingsRequest = { cameraUuid: stream.camera.uuid, stream: stream.streamType, startTime90k: range90k[0], endTime90k: range90k[1], }; let r = await api.recordings(req, { signal }); if (r.status === "success") { // Sort recordings in descending order by start time. r.response.recordings.sort((a, b) => b.startId - a.startId); } setResponse(r); }; if (range90k !== null) { doFetch(abort.signal, range90k); const timeout = setTimeout(() => setShowLoading(true), 1000); return () => { abort.abort(); clearTimeout(timeout); }; } else { setResponse(null); } }, [range90k, snackbars, stream]); let body = null; if (response === null) { if (showLoading) { body = ( ); } } else if (response.status === "error") { body = ( Error: {response.status} ); } else if (response.status === "success") { const resp = response.response; body = resp.recordings.map((r: api.Recording) => { const vse = resp.videoSampleEntries[r.videoSampleEntryId]; const durationSec = (r.endTime90k - r.startTime90k) / 90000; return ( setActiveRecording([stream, r])} > {formatTime(Math.max(r.startTime90k, range90k![0]))} {formatTime(Math.min(r.endTime90k, range90k![1]))} {vse.width}x{vse.height} {frameRateFmt.format(r.videoSamples / durationSec)} {sizeFmt.format(r.sampleFileBytes / 1048576)} MiB {sizeFmt.format((r.sampleFileBytes / durationSec) * 0.000008)} Mbps ); }); } return {body}; }; export default VideoList;