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
|
|
|
|
|
|
|
|
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 <tt>TableBody</tt> with a list of videos for a given
|
|
|
|
* <tt>stream</tt> and <tt>range90k</tt>.
|
|
|
|
*
|
|
|
|
* The parent is responsible for creating the greater table.
|
|
|
|
*
|
|
|
|
* When one is clicked, calls <tt>setActiveRecording</tt>.
|
|
|
|
*/
|
|
|
|
const VideoList = ({
|
|
|
|
stream,
|
|
|
|
range90k,
|
|
|
|
setActiveRecording,
|
|
|
|
formatTime,
|
|
|
|
}: Props) => {
|
|
|
|
const snackbars = useSnackbars();
|
|
|
|
const [
|
|
|
|
response,
|
|
|
|
setResponse,
|
|
|
|
] = React.useState<api.FetchResult<api.RecordingsResponse> | 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],
|
|
|
|
};
|
2021-03-14 16:27:45 -07:00
|
|
|
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);
|
2021-03-05 16:56:51 -08:00
|
|
|
};
|
2021-03-14 21:44:03 -07:00
|
|
|
if (range90k !== null) {
|
2021-03-05 16:56:51 -08:00
|
|
|
doFetch(abort.signal, range90k);
|
|
|
|
const timeout = setTimeout(() => setShowLoading(true), 1000);
|
|
|
|
return () => {
|
|
|
|
abort.abort();
|
|
|
|
clearTimeout(timeout);
|
|
|
|
};
|
2021-03-14 21:44:03 -07:00
|
|
|
} else {
|
|
|
|
setResponse(null);
|
2021-03-05 16:56:51 -08:00
|
|
|
}
|
|
|
|
}, [range90k, snackbars, stream]);
|
|
|
|
|
|
|
|
let body = null;
|
|
|
|
if (response === null) {
|
|
|
|
if (showLoading) {
|
|
|
|
body = (
|
|
|
|
<TableRow>
|
|
|
|
<TableCell colSpan={6}>
|
|
|
|
<CircularProgress />
|
|
|
|
</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (response.status === "error") {
|
|
|
|
body = (
|
|
|
|
<TableRow>
|
|
|
|
<TableCell colSpan={6}>Error: {response.status}</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
);
|
|
|
|
} 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 (
|
|
|
|
<TableRow
|
|
|
|
key={r.startId}
|
|
|
|
onClick={() => setActiveRecording([stream, r])}
|
|
|
|
>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell align="right">
|
2021-03-13 22:25:05 -08:00
|
|
|
{formatTime(Math.max(r.startTime90k, range90k![0]))}
|
|
|
|
</TableCell>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell align="right">
|
2021-03-13 22:25:05 -08:00
|
|
|
{formatTime(Math.min(r.endTime90k, range90k![1]))}
|
|
|
|
</TableCell>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell className="opt" align="right">
|
2021-03-05 16:56:51 -08:00
|
|
|
{vse.width}x{vse.height}
|
|
|
|
</TableCell>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell className="opt" align="right">
|
2021-03-05 16:56:51 -08:00
|
|
|
{frameRateFmt.format(r.videoSamples / durationSec)}
|
|
|
|
</TableCell>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell className="opt" align="right">
|
2021-03-05 16:56:51 -08:00
|
|
|
{sizeFmt.format(r.sampleFileBytes / 1048576)} MiB
|
|
|
|
</TableCell>
|
2021-03-15 11:08:02 -07:00
|
|
|
<TableCell align="right">
|
2021-03-05 16:56:51 -08:00
|
|
|
{sizeFmt.format((r.sampleFileBytes / durationSec) * 0.000008)} Mbps
|
|
|
|
</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return <TableBody>{body}</TableBody>;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default VideoList;
|