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:
Scott Lamb
2018-04-27 06:42:39 -07:00
67 changed files with 8771 additions and 4588 deletions

View File

@@ -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');
}
/**

View File

@@ -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 &amp; Time Range</legend>
@@ -73,4 +74,4 @@
</div>
<table id="videos"></table>
</body>
</html>
</html>

View File

@@ -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,
});

View File

@@ -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_;
}
}

View File

@@ -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
View 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];
})
);
}
}

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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

View 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;
}
}

View File

@@ -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');
}
/**