// 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/'; // 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: // * "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'}, }); } /** * Format timestamp using a format string. * * The timestamp to be formatted is expected to be in units of 90,000 to a * second (90k format). * * The format string should comply with what is accepted by moment.format, * with one addition. A format pattern of FFFFF (5 Fs) can be used. This * format pattern will be replaced with the fractional second part of the * timestamp, still in 90k units. Thus if the timestamp was 89900 (which is * almost a full second; 0.99888 seconds decimal), the output would be * 89900, and NOT 0.99888. Only a pattern of five Fs is recognized and it * will produce exactly a five position output! You cannot vary the number * of Fs to produce less. * * The default format string was chosen to produce results identical to * a previous version of this code that was hard-coded to produce that output. * * @param {Number} ts90k Timestamp in 90k units * @param {String} format moment.format plus FFFFF pattern supported * @return {String} Formatted timestamp */ function formatTime(ts90k, format = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ') { const ms = ts90k / 90.0; const fracFmt = 'FFFFF'; let fracLoc = format.indexOf(fracFmt); if (fracLoc != -1) { const frac = ts90k % 90000; format = format.substr(0, fracLoc) + String(100000 + frac).substr(1) + format.substr(fracLoc + fracFmt.length); } return moment.tz(ms, zone).format(format); } function onSelectVideo(camera, range, recording) { let url = apiUrl + 'cameras/' + camera.uuid + '/view.mp4?s=' + recording.startId; if (recording.endId !== undefined) { url += '-' + recording.endId; } 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; } if (rel !== '-') { url += '.' + rel; } if ($("#ts").prop("checked")) { url += '&ts=true'; } console.log('Displaying video: ', url); let video = $('