mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-20 09:56:07 -05:00
Merge branch 'new-schema'
The Rust portions of the merge are straightforward, but the Javascript is not. The new-schema branch is based on my hacky prototype UI; the master branch is based on Dolf's rewrite. I attempted to match the new-schema changes in Dolf's new structure.
This commit is contained in:
@@ -52,18 +52,18 @@ import './assets/index.css';
|
||||
import 'jquery-ui/ui/widgets/tooltip';
|
||||
|
||||
import Camera from './lib/models/Camera';
|
||||
import CameraView from './lib/views/CameraView';
|
||||
import CalendarView from './lib/views/CalendarView';
|
||||
import VideoDialogView from './lib/views/VideoDialogView';
|
||||
import NVRSettingsView from './lib/views/NVRSettingsView';
|
||||
import CheckboxGroupView from './lib/views/CheckboxGroupView';
|
||||
import RecordingFormatter from './lib/support/RecordingFormatter';
|
||||
import StreamSelectorView from './lib/views/StreamSelectorView';
|
||||
import StreamView from './lib/views/StreamView';
|
||||
import TimeFormatter from './lib/support/TimeFormatter';
|
||||
import TimeStamp90kFormatter from './lib/support/TimeStamp90kFormatter';
|
||||
import MoonfireAPI from './lib/MoonfireAPI';
|
||||
|
||||
const api = new MoonfireAPI();
|
||||
let cameraViews = null; // CameraView objects
|
||||
let streamViews = null; // StreamView objects
|
||||
let calendarView = null; // CalendarView object
|
||||
|
||||
/**
|
||||
@@ -118,15 +118,17 @@ function newTimeFormat(format) {
|
||||
*
|
||||
* @param {NVRSettings} nvrSettingsView NVRSettingsView in effect
|
||||
* @param {object} camera Object for the camera
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @param {object} range Range Object
|
||||
* @param {object} recording Recording object
|
||||
* @return {void}
|
||||
*/
|
||||
function onSelectVideo(nvrSettingsView, camera, range, recording) {
|
||||
function onSelectVideo(nvrSettingsView, camera, streamType, range, recording) {
|
||||
console.log('Recording clicked: ', recording);
|
||||
const trimmedRange = recording.range90k(nvrSettingsView.trim ? range : null);
|
||||
const url = api.videoPlayUrl(
|
||||
camera.uuid,
|
||||
streamType,
|
||||
recording,
|
||||
trimmedRange,
|
||||
nvrSettingsView.timeStampTrack
|
||||
@@ -146,7 +148,7 @@ function onSelectVideo(nvrSettingsView, camera, range, recording) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch camera view data for a given date/time range.
|
||||
* Fetch stream view data for a given date/time range.
|
||||
*
|
||||
* @param {Range90k} selectedRange Desired time range
|
||||
* @param {Number} videoLength Desired length of video segments, or Infinity
|
||||
@@ -161,28 +163,29 @@ function fetch(selectedRange, videoLength) {
|
||||
' to ' +
|
||||
selectedRange.formatTimeStamp90k(selectedRange.endTime90k)
|
||||
);
|
||||
for (let cameraView of cameraViews) {
|
||||
for (let streamView of streamViews) {
|
||||
let url = api.recordingsUrl(
|
||||
cameraView.camera.uuid,
|
||||
streamView.camera.uuid,
|
||||
streamView.streamType,
|
||||
selectedRange.startTime90k,
|
||||
selectedRange.endTime90k,
|
||||
videoLength
|
||||
);
|
||||
if (cameraView.recordingsReq !== null) {
|
||||
if (streamView.recordingsReq !== null) {
|
||||
/*
|
||||
* If there is another request, it would be because settings changed
|
||||
* and so an abort is to make room for this new request, now necessary
|
||||
* for the changed situation.
|
||||
*/
|
||||
cameraView.recordingsReq.abort();
|
||||
streamView.recordingsReq.abort();
|
||||
}
|
||||
cameraView.delayedShowLoading(500);
|
||||
streamView.delayedShowLoading(500);
|
||||
let r = api.request(url);
|
||||
cameraView.recordingsUrl = url;
|
||||
cameraView.recordingsReq = r;
|
||||
cameraView.recordingsRange = selectedRange.range90k();
|
||||
streamView.recordingsUrl = url;
|
||||
streamView.recordingsReq = r;
|
||||
streamView.recordingsRange = selectedRange.range90k();
|
||||
r.always(function() {
|
||||
cameraView.recordingsReq = null;
|
||||
streamView.recordingsReq = null;
|
||||
});
|
||||
r
|
||||
.then(function(data /* , status, req */) {
|
||||
@@ -191,10 +194,10 @@ function fetch(selectedRange, videoLength) {
|
||||
return b.startId - a.startId;
|
||||
});
|
||||
console.log(
|
||||
'Fetched results for "%s" > updating recordings',
|
||||
cameraView.camera.shortName
|
||||
'Fetched results for "%s-%s" > updating recordings',
|
||||
streamView.camera.shortName, streamView.streamType
|
||||
);
|
||||
cameraView.recordingsJSON = data.recordings;
|
||||
streamView.recordingsJSON = data.recordings;
|
||||
})
|
||||
.catch(function(data, status, err) {
|
||||
console.error(url, ' load failed: ', status, ': ', err);
|
||||
@@ -207,7 +210,7 @@ function fetch(selectedRange, videoLength) {
|
||||
*
|
||||
* Sets the following globals:
|
||||
* zone - timezone from data received
|
||||
* cameraViews - array of views, one per camera
|
||||
* streamViews - array of views, one per stream
|
||||
*
|
||||
* Builds the dom for the left side controllers
|
||||
*
|
||||
@@ -221,56 +224,56 @@ function onReceivedCameras(data) {
|
||||
nvrSettingsView.onVideoLengthChange = (vl) =>
|
||||
fetch(calendarView.selectedRange, vl);
|
||||
nvrSettingsView.onTimeFormatChange = (format) =>
|
||||
cameraViews.forEach((view) => (view.timeFormat = format));
|
||||
streamViews.forEach((view) => (view.timeFormat = format));
|
||||
nvrSettingsView.onTrimChange = (t) =>
|
||||
cameraViews.forEach((view) => (view.trimmed = t));
|
||||
streamViews.forEach((view) => (view.trimmed = t));
|
||||
newTimeFormat(nvrSettingsView.timeFormatString);
|
||||
|
||||
calendarView = new CalendarView({timeZone: timeFormatter.tz});
|
||||
calendarView.onRangeChange = (selectedRange) =>
|
||||
fetch(selectedRange, nvrSettingsView.videoLength);
|
||||
|
||||
const camerasParent = $('#cameras');
|
||||
const streamsParent = $('#streams');
|
||||
const videos = $('#videos');
|
||||
|
||||
cameraViews = data.cameras.map((cameraJson) => {
|
||||
streamViews = [];
|
||||
let streamSelectorCameras = [];
|
||||
for (const cameraJson of data.cameras) {
|
||||
const camera = new Camera(cameraJson);
|
||||
const cv = new CameraView(
|
||||
camera,
|
||||
new RecordingFormatter(timeFormatter.formatStr, timeFormatter.tz),
|
||||
nvrSettingsView.trim,
|
||||
videos
|
||||
);
|
||||
cv.onRecordingClicked = (recordingModel) => {
|
||||
console.log('Recording clicked', recordingModel);
|
||||
onSelectVideo(
|
||||
nvrSettingsView,
|
||||
camera,
|
||||
calendarView.selectedRange,
|
||||
recordingModel
|
||||
);
|
||||
};
|
||||
return cv;
|
||||
});
|
||||
|
||||
// Create camera enable checkboxes
|
||||
const cameraCheckBoxes = new CheckboxGroupView(
|
||||
cameraViews.map((cv) => ({
|
||||
id: cv.camera.uuid,
|
||||
checked: true,
|
||||
text: cv.camera.shortName,
|
||||
camView: cv,
|
||||
})),
|
||||
camerasParent
|
||||
);
|
||||
cameraCheckBoxes.onCheckChange = (groupEl) => {
|
||||
groupEl.camView.enabled = groupEl.checked;
|
||||
calendarView.initializeWith(cameraViews);
|
||||
let cameraStreams = {};
|
||||
Object.keys(camera.streams).forEach((streamType) => {
|
||||
const sv = new StreamView(
|
||||
camera,
|
||||
streamType,
|
||||
new RecordingFormatter(timeFormatter.formatStr, timeFormatter.tz),
|
||||
nvrSettingsView.trim,
|
||||
videos);
|
||||
sv.onRecordingClicked = (recordingModel) => {
|
||||
console.log('Recording clicked', recordingModel);
|
||||
onSelectVideo(
|
||||
nvrSettingsView,
|
||||
camera,
|
||||
streamType,
|
||||
calendarView.selectedRange,
|
||||
recordingModel
|
||||
);
|
||||
};
|
||||
streamViews.push(sv);
|
||||
cameraStreams[streamType] = sv;
|
||||
});
|
||||
streamSelectorCameras.push({
|
||||
camera: camera,
|
||||
streamViews: cameraStreams,
|
||||
});
|
||||
};
|
||||
|
||||
calendarView.initializeWith(cameraViews);
|
||||
// Create stream enable checkboxes
|
||||
const streamSelector =
|
||||
new StreamSelectorView(streamSelectorCameras, streamsParent);
|
||||
streamSelector.onChange = () => calendarView.initializeWith(streamViews);
|
||||
calendarView.initializeWith(streamViews);
|
||||
|
||||
console.log('Loaded: ' + cameraViews.length + ' camera views');
|
||||
console.log('Loaded: ' + streamViews.length + ' stream views');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
<body>
|
||||
<div id="nav">
|
||||
<form action="#">
|
||||
<fieldset id="cameras">
|
||||
<legend>Cameras</legend>
|
||||
<fieldset>
|
||||
<legend>Streams</legend>
|
||||
<table id="streams"></table>
|
||||
</fieldset>
|
||||
<fieldset id="datetime">
|
||||
<legend>Date & Time Range</legend>
|
||||
@@ -73,4 +74,4 @@
|
||||
</div>
|
||||
<table id="videos"></table>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -77,13 +77,14 @@ export default class MoonfireAPI {
|
||||
* URL that will cause the state of a specific recording to be returned.
|
||||
*
|
||||
* @param {String} cameraUUID UUID for the camera
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @param {String} start90k Timestamp for beginning of range of interest
|
||||
* @param {String} end90k Timestamp for end of range of interest
|
||||
* @param {String} split90k Desired maximum size of segments returned, or
|
||||
* Infinity for infinite range
|
||||
* @return {String} Constructed url
|
||||
*/
|
||||
recordingsUrl(cameraUUID, start90k, end90k, split90k = Infinity) {
|
||||
recordingsUrl(cameraUUID, streamType, start90k, end90k, split90k = Infinity) {
|
||||
const query = {
|
||||
startTime90k: start90k,
|
||||
endTime90k: end90k,
|
||||
@@ -92,7 +93,7 @@ export default class MoonfireAPI {
|
||||
query.split90k = split90k;
|
||||
}
|
||||
return this._builder.makeUrl(
|
||||
'cameras/' + cameraUUID + '/recordings',
|
||||
'cameras/' + cameraUUID + '/' + streamType + '/recordings',
|
||||
query
|
||||
);
|
||||
}
|
||||
@@ -101,16 +102,21 @@ export default class MoonfireAPI {
|
||||
* URL that will playback a video segment.
|
||||
*
|
||||
* @param {String} cameraUUID UUID for the camera from whence comes the video
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @param {Recording} recording Recording model object
|
||||
* @param {Range90k} trimmedRange Range restricting segments
|
||||
* @param {Boolean} timestampTrack True if track should be timestamped
|
||||
* @param {Range90k} trimmedRange Range restricting segments
|
||||
* @param {Boolean} timestampTrack True if track should be timestamped
|
||||
* @return {String} Constructed url
|
||||
*/
|
||||
videoPlayUrl(cameraUUID, recording, trimmedRange, timestampTrack = true) {
|
||||
videoPlayUrl(cameraUUID, streamType, recording, trimmedRange,
|
||||
timestampTrack = true) {
|
||||
let sParam = recording.startId;
|
||||
if (recording.endId !== undefined) {
|
||||
sParam += '-' + recording.endId;
|
||||
}
|
||||
if (recording.firstUncommitted !== undefined) {
|
||||
sParam += '@' + recording.openId; // disambiguate.
|
||||
}
|
||||
let rel = '';
|
||||
if (recording.startTime90k < trimmedRange.startTime90k) {
|
||||
rel += trimmedRange.startTime90k - recording.startTime90k;
|
||||
@@ -118,6 +124,9 @@ export default class MoonfireAPI {
|
||||
rel += '-';
|
||||
if (recording.endTime90k > trimmedRange.endTime90k) {
|
||||
rel += trimmedRange.endTime90k - recording.startTime90k;
|
||||
} else if (recording.growing !== undefined) {
|
||||
// View just the portion described by recording.
|
||||
rel += recording.endTime90k - recording.startTime90k;
|
||||
}
|
||||
if (rel !== '-') {
|
||||
sParam += '.' + rel;
|
||||
@@ -126,7 +135,8 @@ export default class MoonfireAPI {
|
||||
s: sParam,
|
||||
ts: timestampTrack,
|
||||
});
|
||||
return this._builder.makeUrl('cameras/' + cameraUUID + '/view.mp4', {
|
||||
return this._builder.makeUrl('cameras/' + cameraUUID + '/' + streamType +
|
||||
'/view.mp4', {
|
||||
s: sParam,
|
||||
ts: timestampTrack,
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import JsonWrapper from './JsonWrapper';
|
||||
import Range90k from './Range90k';
|
||||
import Stream from './Stream';
|
||||
|
||||
/**
|
||||
* Camera JSON wrapper.
|
||||
@@ -44,89 +44,29 @@ export default class Camera extends JsonWrapper {
|
||||
*/
|
||||
constructor(cameraJson) {
|
||||
super(cameraJson);
|
||||
this.streams_ = {};
|
||||
Object.keys(cameraJson.streams).forEach((streamType) => {
|
||||
this.streams_[streamType] = new Stream(cameraJson.streams[streamType]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera uuid.
|
||||
*
|
||||
* @return {String} Camera's uuid
|
||||
*/
|
||||
/** @return {String} */
|
||||
get uuid() {
|
||||
return this.json.uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera's short name.
|
||||
*
|
||||
* @return {String} Name of the camera
|
||||
*/
|
||||
/** @return {String} */
|
||||
get shortName() {
|
||||
return this.json.shortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera's description.
|
||||
*
|
||||
* @return {String} Camera's description
|
||||
*/
|
||||
/** @return {String} */
|
||||
get description() {
|
||||
return this.json.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximimum amount of storage allowed to be used for camera's video
|
||||
* samples.
|
||||
*
|
||||
* @return {Number} Amount in bytes
|
||||
*/
|
||||
get retainBytes() {
|
||||
return this.json.retainBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Range90K object representing the range encompassing all available
|
||||
* video samples for the camera.
|
||||
*
|
||||
* This range does not mean every second of the range has video!
|
||||
*
|
||||
* @return {Range90k} The camera's available recordings range
|
||||
*/
|
||||
get range90k() {
|
||||
return new Range90k(
|
||||
this.json.minStartTime90k,
|
||||
this.json.maxEndTime90k,
|
||||
this.json.totalDuration90k
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount of storage currently taken up by the camera's video
|
||||
* samples.
|
||||
*
|
||||
* @return {Number} Amount in bytes
|
||||
*/
|
||||
get totalSampleFileBytes() {
|
||||
return this.json.totalSampleFileBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of the camera's days for which there are video samples.
|
||||
*
|
||||
* The result is a Map with dates as keys (in YYYY-MM-DD format) and each
|
||||
* value is a Range90k object for that day. Here too, the range does not
|
||||
* mean every second in the range has video, but presence of an entry for
|
||||
* a day does mean there is at least one (however short) video segment
|
||||
* available.
|
||||
*
|
||||
* @return {Map} Dates are keys, values are Range90K objects.
|
||||
*/
|
||||
get days() {
|
||||
return new Map(
|
||||
Object.entries(this.json.days).map(function(t) {
|
||||
let [k, v] = t;
|
||||
v = new Range90k(v.startTime90k, v.endTime90k, v.totalDuration90k);
|
||||
return [k, v];
|
||||
})
|
||||
);
|
||||
/** @return {Object.<string, Stream>} */
|
||||
get streams() {
|
||||
return this.streams_;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,24 +46,31 @@ export default class Recording extends JsonWrapper {
|
||||
super(recordingJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recording's startId.
|
||||
*
|
||||
* @return {String} startId for recording
|
||||
*/
|
||||
/** @return {Number} */
|
||||
get startId() {
|
||||
return this.json.startId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recording's endId.
|
||||
*
|
||||
* @return {String} endId for recording
|
||||
*/
|
||||
/** @return {Number} */
|
||||
get endId() {
|
||||
return this.json.endId;
|
||||
}
|
||||
|
||||
/** @return {Number} */
|
||||
get openId() {
|
||||
return this.json.openId;
|
||||
}
|
||||
|
||||
/** @return {Number} or undefined */
|
||||
get firstUncommitted() {
|
||||
return this.json.firstUncommitted;
|
||||
}
|
||||
|
||||
/** @return {Boolean} or undefined */
|
||||
get growing() {
|
||||
return this.json.growing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return start time of recording in 90k units.
|
||||
* @return {Number} Time in units of 90k parts of a second
|
||||
|
||||
105
ui-src/lib/models/Stream.js
Normal file
105
ui-src/lib/models/Stream.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// vim: set et sw=2 ts=2:
|
||||
//
|
||||
// This file is part of Moonfire NVR, a security stream digital video recorder.
|
||||
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import JsonWrapper from './JsonWrapper';
|
||||
import Range90k from './Range90k';
|
||||
|
||||
/**
|
||||
* Stream JSON wrapper.
|
||||
*/
|
||||
export default class Stream extends JsonWrapper {
|
||||
/**
|
||||
* Construct from JSON.
|
||||
*
|
||||
* @param {JSON} streamJson JSON for single stream.
|
||||
*/
|
||||
constructor(streamJson) {
|
||||
super(streamJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximimum amount of storage allowed to be used for stream's video
|
||||
* samples.
|
||||
*
|
||||
* @return {Number} Amount in bytes
|
||||
*/
|
||||
get retainBytes() {
|
||||
return this.json.retainBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Range90K object representing the range encompassing all available
|
||||
* video samples for the stream.
|
||||
*
|
||||
* This range does not mean every second of the range has video!
|
||||
*
|
||||
* @return {Range90k} The stream's available recordings range
|
||||
*/
|
||||
get range90k() {
|
||||
return new Range90k(
|
||||
this.json.minStartTime90k,
|
||||
this.json.maxEndTime90k,
|
||||
this.json.totalDuration90k
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount of storage currently taken up by the stream's video
|
||||
* samples.
|
||||
*
|
||||
* @return {Number} Amount in bytes
|
||||
*/
|
||||
get totalSampleFileBytes() {
|
||||
return this.json.totalSampleFileBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of the stream's days for which there are video samples.
|
||||
*
|
||||
* The result is a Map with dates as keys (in YYYY-MM-DD format) and each
|
||||
* value is a Range90k object for that day. Here too, the range does not
|
||||
* mean every second in the range has video, but presence of an entry for
|
||||
* a day does mean there is at least one (however short) video segment
|
||||
* available.
|
||||
*
|
||||
* @return {Map} Dates are keys, values are Range90K objects.
|
||||
*/
|
||||
get days() {
|
||||
return new Map(
|
||||
Object.entries(this.json.days).map(function(t) {
|
||||
let [k, v] = t;
|
||||
v = new Range90k(v.startTime90k, v.endTime90k, v.totalDuration90k);
|
||||
return [k, v];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
|
||||
import Time90kParser from '../support/Time90kParser';
|
||||
|
||||
/**
|
||||
* Find the earliest and latest dates from an array of CameraView
|
||||
* Find the earliest and latest dates from an array of StreamView
|
||||
* objects.
|
||||
*
|
||||
* Each camera view has a "days" property, whose keys identify days with
|
||||
@@ -47,20 +47,20 @@ import Time90kParser from '../support/Time90kParser';
|
||||
*
|
||||
* "days" for camera views that are not enabled are ignored.
|
||||
*
|
||||
* @param {[Iterable]} cameraViews Camera views to look into
|
||||
* @param {[Iterable]} streamViews Camera views to look into
|
||||
* @return {[Set, String, String]} Array with set of all dates, and
|
||||
* earliest and latest dates
|
||||
*/
|
||||
function minMaxDates(cameraViews) {
|
||||
function minMaxDates(streamViews) {
|
||||
/*
|
||||
* Produce a set with all dates, across all enabled cameras, that
|
||||
* have at least one recording available (allDates).
|
||||
*/
|
||||
const allDates = new Set(
|
||||
[].concat(
|
||||
...cameraViews
|
||||
...streamViews
|
||||
.filter((v) => v.enabled)
|
||||
.map((v) => Array.from(v.camera.days.keys()))
|
||||
.map((v) => Array.from(v.stream.days.keys()))
|
||||
)
|
||||
);
|
||||
return [
|
||||
@@ -137,7 +137,7 @@ export default class CalendarView {
|
||||
this._minDateStr = null;
|
||||
this._maxDateStr = null;
|
||||
this._singleDateStr = null;
|
||||
this._cameraViews = null;
|
||||
this._streamViews = null;
|
||||
this._rangeChangedHandler = null;
|
||||
}
|
||||
|
||||
@@ -329,12 +329,12 @@ export default class CalendarView {
|
||||
* This is necessary as the camera views ultimately define the limits on
|
||||
* the date pickers.
|
||||
*
|
||||
* @param {Iterable} cameraViews Collection of camera views
|
||||
* @param {Iterable} streamViews Collection of camera views
|
||||
*/
|
||||
initializeWith(cameraViews) {
|
||||
this._cameraViews = cameraViews;
|
||||
initializeWith(streamViews) {
|
||||
this._streamViews = streamViews;
|
||||
[this._availableDates, this._minDateStr, this._maxDateStr] = minMaxDates(
|
||||
cameraViews
|
||||
streamViews
|
||||
);
|
||||
this._configureDatePickers();
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// vim: set et sw=2 ts=2:
|
||||
//
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* Class to handle a group of (related) checkboxes.
|
||||
*
|
||||
* Each checkbox is managed through a simple object containing properties:
|
||||
* - id: {String} Id (some unique value within the group)
|
||||
* - selector: {String} jQuery compatible selector to find the dom element
|
||||
* - checked: {Boolean} Value for checkbox
|
||||
* - jq: {jQuery} jQuery element for the checkbox, or null if not found
|
||||
*
|
||||
* A handler can be called if a checbox changes value.
|
||||
*/
|
||||
export default class CheckboxGroupView {
|
||||
/**
|
||||
* Construct the seteup for the checkboxes.
|
||||
*
|
||||
* The passed group array should contain individual maps describing each
|
||||
* checkbox. THe maps should contain:
|
||||
* - id
|
||||
* - selector: optional. If not provided #id will be used
|
||||
* - checked: Initial value for checkbox, default true
|
||||
* - text: Text for the checkbox label (not generated if empty)
|
||||
*
|
||||
* @param {Array} group Array of maps, one for each checkbox
|
||||
* @param {jQuery} parent jQuery parent element to append to
|
||||
*/
|
||||
constructor(group = [], parent = null) {
|
||||
this._group = group.slice(); // Copy
|
||||
this._group.forEach((element) => {
|
||||
// If parent specified, create and append
|
||||
if (parent) {
|
||||
let cb = `<input type="checkbox" id="${element.id}" name="${
|
||||
element.id
|
||||
}">`;
|
||||
if (element.text) {
|
||||
cb += `<label for="${element.id}">${element.text}</label>`;
|
||||
}
|
||||
parent.append($(cb + '<br/>'));
|
||||
}
|
||||
const jq = $(element.selector || `#${element.id}`);
|
||||
element.jq = jq;
|
||||
if (jq !== null) {
|
||||
jq.prop('checked', element.checked || true);
|
||||
jq.change((e) => {
|
||||
if (this._checkChangeHandler) {
|
||||
element.checked = e.target.checked;
|
||||
this._checkChangeHandler(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._checkChangeHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox object for the specified checkbox.
|
||||
*
|
||||
* The checkbox is looked up by the specified id or selector, which must
|
||||
* match what was specified during construction.
|
||||
*
|
||||
* @param {String} idOrSelector Identifying string
|
||||
* @return {Object} Object for checkbox, or null if not found
|
||||
*/
|
||||
checkBox(idOrSelector) {
|
||||
return this._group.find(
|
||||
(el) => el.id === idOrSelector || el.selector === idOrSelector
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a handler for checkbox changes.
|
||||
*
|
||||
* Handler will be called with same result as would be found by checkBox().
|
||||
*
|
||||
* @param {Function} handler function (checbox)
|
||||
*/
|
||||
set onCheckChange(handler) {
|
||||
this._checkChangeHandler = handler;
|
||||
}
|
||||
}
|
||||
@@ -73,18 +73,19 @@ export default class RecordingsView {
|
||||
* Construct display from camera data and use supplied formatter.
|
||||
*
|
||||
* @param {Camera} camera camera object (immutable)
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @param {RecordingFormatter} recordingFormatter Desired formatter
|
||||
* @param {Boolean} trimmed True if the display should include trimmed ranges
|
||||
* @param {jQuery} parent Parent to which new DOM is attached, or null
|
||||
*/
|
||||
constructor(camera, recordingFormatter, trimmed = false, parent = null) {
|
||||
constructor(camera, streamType, recordingFormatter, trimmed = false,
|
||||
parent = null) {
|
||||
this._cameraName = camera.shortName;
|
||||
this._cameraRange = camera.range90k;
|
||||
this._formatter = recordingFormatter;
|
||||
this._element = $(`tab-${camera.uuid}`); // Might not be there initially
|
||||
if (this._element.length == 0) {
|
||||
this._element = this._createElement(camera.uuid, camera.shortName);
|
||||
}
|
||||
|
||||
const id = `tab-${camera.uuid}-${streamType}`;
|
||||
this._element = this._createElement(id, camera.shortName, streamType);
|
||||
this._trimmed = trimmed;
|
||||
this._recordings = null;
|
||||
this._recordingsRange = null;
|
||||
@@ -100,12 +101,14 @@ export default class RecordingsView {
|
||||
*
|
||||
* @param {String} id DOM id for the main element
|
||||
* @param {String} cameraName Name of the corresponding camera
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @return {jQuery} Partial DOM as jQuery object
|
||||
*/
|
||||
_createElement(id, cameraName) {
|
||||
_createElement(id, cameraName, streamType) {
|
||||
const tab = $('<tbody>').attr('id', id);
|
||||
tab.append(
|
||||
$('<tr class="name">').append($('<th colspan=6/>').text(cameraName)),
|
||||
$('<tr class="name">').append($('<th colspan=6/>')
|
||||
.text(cameraName + ' ' + streamType)),
|
||||
$('<tr class="hdr">').append(
|
||||
$(
|
||||
_columnOrder
|
||||
|
||||
99
ui-src/lib/views/StreamSelectorView.js
Normal file
99
ui-src/lib/views/StreamSelectorView.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// vim: set et sw=2 ts=2:
|
||||
//
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
const allStreamTypes = ['main', 'sub'];
|
||||
|
||||
/**
|
||||
* View for selecting the enabled streams.
|
||||
*
|
||||
* This displays a table with a camera per row and stream type per column.
|
||||
* It propagates the enabled status on to the stream view. It also calls
|
||||
* the optional onChange handler on any change.
|
||||
*/
|
||||
export default class StreamSelectorView {
|
||||
/**
|
||||
* @param {Array} cameras An element for each camera with
|
||||
* - camera: a {Camera}
|
||||
* - streamViews: a map of stream type to {StreamView}
|
||||
* @param {jQuery} parent jQuery parent element to append to
|
||||
*/
|
||||
constructor(cameras, parent) {
|
||||
this._cameras = cameras;
|
||||
|
||||
if (cameras.length !== 0) {
|
||||
// Add a header row.
|
||||
let hdr = $('<tr/>').append($('<th/>'));
|
||||
for (const streamType of allStreamTypes) {
|
||||
hdr.append($('<th/>').text(streamType));
|
||||
}
|
||||
parent.append(hdr);
|
||||
}
|
||||
|
||||
this._cameras.forEach((c) => {
|
||||
let row = $('<tr/>').append($('<td>').text(c.camera.shortName));
|
||||
let firstStreamType = true;
|
||||
for (const streamType of allStreamTypes) {
|
||||
const streamView = c.streamViews[streamType];
|
||||
if (streamView === undefined) {
|
||||
row.append('<td/>');
|
||||
} else {
|
||||
const id = 'cam-' + c.camera.uuid + '-' + streamType;
|
||||
let cb = $('<input type="checkbox">').attr('name', id).attr('id', id);
|
||||
|
||||
// Only the first stream type for each camera should be checked
|
||||
// initially.
|
||||
cb.prop('checked', firstStreamType);
|
||||
streamView.enabled = firstStreamType;
|
||||
firstStreamType = false;
|
||||
|
||||
cb.change((e) => {
|
||||
streamView.enabled = e.target.checked;
|
||||
if (this._onChangeHandler) {
|
||||
this._onChangeHandler();
|
||||
}
|
||||
});
|
||||
row.append($('<td/>').append(cb));
|
||||
}
|
||||
}
|
||||
parent.append(row);
|
||||
});
|
||||
|
||||
this._onChangeHandler = null;
|
||||
}
|
||||
|
||||
/** @param {function()} handler a handler to run after toggling a stream */
|
||||
set onChange(handler) {
|
||||
this._onChangeHandler = handler;
|
||||
}
|
||||
}
|
||||
@@ -33,29 +33,29 @@
|
||||
import RecordingsView from './RecordingsView';
|
||||
|
||||
/**
|
||||
* Class handling a camer view.
|
||||
*
|
||||
* A camera view consists of a list of available recording segments for
|
||||
* playback.
|
||||
* Stream view: a list of available recording segments for playback.
|
||||
*/
|
||||
export default class CameraView {
|
||||
export default class StreamView {
|
||||
/**
|
||||
* Construct the view.
|
||||
*
|
||||
* @param {Camera} cameraModel Model object for camera
|
||||
* @param {String} streamType "main" or "sub"
|
||||
* @param {[type]} recordingFormatter Formatter to be used by recordings
|
||||
* @param {[type]} trimmed True if rec. ranges should be trimmed
|
||||
* @param {[type]} recordingsParent Parent element to attach to or null)
|
||||
*/
|
||||
constructor(
|
||||
cameraModel,
|
||||
streamType,
|
||||
recordingFormatter,
|
||||
trimmed,
|
||||
recordingsParent = null
|
||||
) {
|
||||
this.camera = cameraModel;
|
||||
this.streamType = streamType;
|
||||
this.stream = cameraModel.streams[streamType];
|
||||
this.recordingsView = new RecordingsView(
|
||||
this.camera,
|
||||
this.streamType,
|
||||
recordingFormatter,
|
||||
trimmed,
|
||||
recordingsParent
|
||||
@@ -82,11 +82,8 @@ export default class CameraView {
|
||||
set enabled(enabled) {
|
||||
this._enabled = enabled;
|
||||
this.recordingsView.show = enabled;
|
||||
console.log(
|
||||
'Camera %s %s',
|
||||
this.camera.shortName,
|
||||
this.enabled ? 'enabled' : 'disabled'
|
||||
);
|
||||
console.log('Stream %s-%s %s', this.camera.shortName, this.streamType,
|
||||
this.enabled ? 'enabled' : 'disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
Reference in New Issue
Block a user