// vim: set et sw=2: // TODO: test abort. // TODO: add error bar on fetch failure. // TODO: style: no globals? string literals? line length? fn comments? // TODO: live updating. import 'jquery-ui/themes/base/button.css'; import 'jquery-ui/themes/base/core.css'; import 'jquery-ui/themes/base/datepicker.css'; import 'jquery-ui/themes/base/dialog.css'; import 'jquery-ui/themes/base/resizable.css'; import 'jquery-ui/themes/base/theme.css'; import 'jquery-ui/themes/base/tooltip.css'; import $ from 'jquery'; import 'jquery-ui/ui/widgets/datepicker'; import 'jquery-ui/ui/widgets/dialog'; import 'jquery-ui/ui/widgets/tooltip'; import moment from 'moment-timezone'; const apiUrl = '/api/'; const allStreamTypes = ['main', 'sub']; // IANA timezone name. let zone = null; // A dict describing the currently selected range. let selectedRange = { startDateStr: null, // null or YYYY-MM-DD startTimeStr: '', // empty or HH:mm[:ss[:FFFFF]][+-HHmm] startTime90k: null, // null or 90k units since epoch endDateStr: null, // null or YYYY-MM-DD endTimeStr: '', // empty or HH:mm[:ss[:FFFFF]][+-HHmm] endTime90k: null, // null or 90k units since epoch singleDateStr: null, // if startDateStr===endDateStr, that value, otherwise null }; // Cameras is a dictionary as retrieved from apiUrl + some extra props within // the streams dicts: // * "enabled" is a boolean indicating if the camera should be displayed and // if it should be used to constrain the datepickers. // * "recordingsUrl" is null or the currently fetched/fetching .../recordings url. // * "recordingsRange" is a null or a dict (in the same format as // selectedRange) describing what is fetching/fetched. // * "recordingsData" is null or the data fetched from "recordingsUrl". // * "recordingsReq" is null or a jQuery ajax object of an active .../recordings // request if currently fetching. let cameras = null; function req(url) { return $.ajax(url, { dataType: 'json', headers: {'Accept': 'application/json'}, }); } // Produces a human-readable but unambiguous format of the given timestamp: // ISO-8601 plus an extra colon component after the seconds indicating the // fractional time in 90,000ths of a second. function formatTime(ts90k) { const m = moment.tz(ts90k / 90, zone); const frac = ts90k % 90000; return m.format('YYYY-MM-DDTHH:mm:ss:' + String(100000 + frac).substr(1) + 'Z'); } function onSelectVideo(camera, streamType, range, recording) { let url = apiUrl + 'cameras/' + camera.uuid + '/' + streamType + '/view.mp4?s=' + recording.startId; if (recording.endId !== undefined) { url += '-' + recording.endId; } if (recording.firstUncommitted !== undefined) { url += '@' + recording.openId; // disambiguate. } const trim = $("#trim").prop("checked"); let rel = ''; let startTime90k = recording.startTime90k; if (trim && recording.startTime90k < range.startTime90k) { rel += range.startTime90k - recording.startTime90k; startTime90k = range.startTime90k; } rel += '-'; let endTime90k = recording.endTime90k; if (trim && recording.endTime90k > range.endTime90k) { rel += range.endTime90k - recording.startTime90k; endTime90k = range.endTime90k; } else if (recording.growing !== undefined) { // View just the portion described here. rel += recording.endTime90k - recording.startTime90k; } if (rel !== '-') { url += '.' + rel; } if ($("#ts").prop("checked")) { url += '&ts=true'; } console.log('Displaying video: ', url); let video = $('