├── .eslintrc.json
├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── Logo.afphoto
├── NVRJS.js
├── README.md
├── core
└── MP4Frag.js
├── demo.png
├── nvrjs.config.example.js
├── package.json
├── readme.png
└── web
├── dash.html
├── index.html
└── static
├── css
├── default.css
├── font-awesome.min.css
├── fonts
│ ├── FontAwesome.otf
│ ├── Roboto-Light.ttf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ └── fontawesome-webfont.woff2
├── images
│ ├── ui-icons_444444_256x240.png
│ ├── ui-icons_555555_256x240.png
│ ├── ui-icons_777620_256x240.png
│ ├── ui-icons_777777_256x240.png
│ ├── ui-icons_cc0000_256x240.png
│ └── ui-icons_ffffff_256x240.png
└── jquery-ui.min.css
├── images
├── CPU.png
├── HDD.png
├── LogoSmall.png
├── RAM.png
└── logo.png
└── js
├── canvas2image.js
├── customParseFormat.js
├── dayjs.min.js
├── jquery-3.6.0.min.js
├── jquery-ui.min.js
├── scripts.js
├── socket.io.min.js
├── vis-timeline-graph2d.min.css
└── vis-timeline-graph2d.min.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 12,
4 | "sourceType": "module"
5 | },
6 | "env": {
7 | "node": true,
8 | "es2021": true
9 | },
10 | "rules": {
11 | "prefer-const":"error",
12 | "constructor-super": 2,
13 | "for-direction": 2,
14 | "getter-return": 2,
15 | "no-async-promise-executor": 2,
16 | "no-class-assign": 2,
17 | "no-compare-neg-zero": 2,
18 | "no-cond-assign": 2,
19 | "no-const-assign": 2,
20 | "no-constant-condition": 2,
21 | "no-control-regex": 2,
22 | "no-debugger": 2,
23 | "no-delete-var": 2,
24 | "no-dupe-args": 2,
25 | "no-dupe-class-members": 2,
26 | "no-dupe-else-if": 2,
27 | "no-dupe-keys": 2,
28 | "no-duplicate-case": 2,
29 | "no-empty": 2,
30 | "no-empty-character-class": 2,
31 | "no-empty-pattern": 2,
32 | "no-ex-assign": 2,
33 | "no-extra-boolean-cast": 2,
34 | "no-extra-semi": 2,
35 | "no-fallthrough": 2,
36 | "no-func-assign": 2,
37 | "no-global-assign": 2,
38 | "no-import-assign": 2,
39 | "no-inner-declarations": 2,
40 | "no-invalid-regexp": 2,
41 | "no-irregular-whitespace": 2,
42 | "no-misleading-character-class": 2,
43 | "no-mixed-spaces-and-tabs": 2,
44 | "no-new-symbol": 2,
45 | "no-obj-calls": 2,
46 | "no-octal": 2,
47 | "no-prototype-builtins": 1,
48 | "no-redeclare": 2,
49 | "no-regex-spaces": 2,
50 | "no-self-assign": 2,
51 | "no-setter-return": 2,
52 | "no-shadow-restricted-names": 2,
53 | "no-sparse-arrays": 2,
54 | "no-this-before-super": 2,
55 | "no-undef": 2,
56 | "no-unexpected-multiline": 2,
57 | "no-unreachable": 2,
58 | "no-unsafe-finally": 2,
59 | "no-unsafe-negation": 2,
60 | "no-unused-labels": 2,
61 | "no-unused-vars": 2,
62 | "no-useless-catch": 2,
63 | "no-useless-escape": 2,
64 | "no-with": 2,
65 | "require-yield": 2,
66 | "use-isnan": 2,
67 | "valid-typeof": 2,
68 | "no-case-declarations": 1
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '35 14 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v2
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v2
73 | with:
74 | category: "/language:${{matrix.language}}"
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | node_modules
4 | package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | node_modules
4 | .github
5 | package-lock.json
6 | .eslintrc.json
7 | .prettierrc.json
8 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "printWidth": 80,
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma":"none",
8 | "useTabs": true,
9 | "endOfLine": "lf",
10 | "bracketSpacing": true
11 | }
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # NVR-JS Change Log
2 |
3 | - 2.0.0
4 |
5 | **Breaking Changes**
6 | - NVR system folders have been renamed. rename them to continue with your current data.
7 | - system -> NVRJS_SYSTEM
8 | - cameras -> NVRJS_CAMERA_RECORDINGS
9 | - API Access no longer uses the UI password, it uses its own API key, as configured in the config file.
10 | Add a new value named **apiKey** in the **system** section - this should be a bcript value of your chosen API key
11 | - Username is now requied in the login page.
12 | Add a new value named **username** in the **system** section to set it - this should be plain text
13 | - API is now accessed via the /api/ URI
14 |
15 | **Changes**
16 | - Dependency updates.
17 | - Clean/polish up the UI.
18 | - Re-worked ffmpeg stream pipes.
19 | - SQL data writes are now queued.
20 | - Rate Limiting is now applied to the the HTTP application.
21 |
22 | **New Features**
23 | - New API functions (URI's)
24 | - **/systeminfo**
25 | - **/cameras**
26 | - **/snapshot/:CameraID/:Width**
27 | - **/geteventdata/:CameraID/:Start/:End**
28 |
29 | - 1.0.2
30 |
31 | **Fixes**
32 | - Fix directory creation
33 |
34 | - 1.0.1
35 |
36 | **Fixes**
37 | - Correct drive space usage query.
38 |
39 | - 1.0.0
40 |
41 | **Initial Release**
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Marcus Davies
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Logo.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/Logo.afphoto
--------------------------------------------------------------------------------
/NVRJS.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cookieparser = require('cookie-parser');
3 | const cookie = require('cookie');
4 | const bcrypt = require('bcrypt');
5 | const http = require('http');
6 | const io = require('socket.io');
7 | const handlebars = require('handlebars');
8 | const childprocess = require('child_process');
9 | const MP4Frag = require('./core/MP4Frag');
10 | const fs = require('fs');
11 | const os = require('os');
12 | const path = require('path');
13 | const sql = require('sqlite3');
14 | const osu = require('node-os-utils');
15 | const dayjs = require('dayjs');
16 | const queue = require('queue-fifo');
17 | const customParseFormat = require('dayjs/plugin/customParseFormat');
18 | dayjs.extend(customParseFormat);
19 | const RateLimiter = require('express-rate-limit');
20 |
21 | console.log(' - Checking config.');
22 | if (!fs.existsSync(path.join(os.homedir(), 'nvrjs.config.js'))) {
23 | fs.copyFileSync(
24 | path.join(__dirname, 'nvrjs.config.example.js'),
25 | path.join(os.homedir(), 'nvrjs.config.js')
26 | );
27 | console.log(
28 | ' - New config created: ' + path.join(os.homedir(), 'nvrjs.config.js')
29 | );
30 | console.log(' - Edit config to suite and restart!');
31 | process.exit(0);
32 | }
33 | const config = require(path.join(os.homedir(), 'nvrjs.config.js'));
34 | console.log(' - Config loaded: ' + path.join(os.homedir(), 'nvrjs.config.js'));
35 |
36 | let SQL;
37 | const SensorTimestamps = {};
38 |
39 | console.log(' - Checking volumes and ffmpeg.');
40 |
41 | if (!fs.existsSync(config.system.storageVolume)) {
42 | console.log(' - Storage volume does not exist');
43 | process.exit();
44 | } else {
45 | try {
46 | if (
47 | !fs.existsSync(path.join(config.system.storageVolume, 'NVRJS_SYSTEM'))
48 | ) {
49 | fs.mkdirSync(path.join(config.system.storageVolume, 'NVRJS_SYSTEM'));
50 | }
51 | if (
52 | !fs.existsSync(
53 | path.join(config.system.storageVolume, 'NVRJS_CAMERA_RECORDINGS')
54 | )
55 | ) {
56 | fs.mkdirSync(
57 | path.join(config.system.storageVolume, 'NVRJS_CAMERA_RECORDINGS')
58 | );
59 | }
60 | } catch (e) {
61 | console.log('Error creating system directories.');
62 | console.log(e.message);
63 | process.exit(0);
64 | }
65 | }
66 |
67 | if (!fs.existsSync(config.system.ffmpegLocation)) {
68 | console.log(
69 | 'ffmpeg not found in specifed location: ' + config.system.ffmpegLocation
70 | );
71 | process.exit(0);
72 | }
73 |
74 | CreateOrConnectSQL(() => {
75 | console.log(' - Starting purge interval.');
76 | setInterval(
77 | purgeContinuous,
78 | 1000 * 3600 * config.system.continuousPurgeIntervalHours
79 | );
80 | purgeContinuous();
81 | });
82 |
83 | console.log(' - Starting data write queue.');
84 | const FIFO = new queue();
85 | function Commit() {
86 | if (!FIFO.isEmpty()) {
87 | const Query = FIFO.dequeue();
88 | const STMT = SQL.prepare(Query.statement, () => {
89 | STMT.run(Query.params, () => {
90 | STMT.finalize();
91 | Commit();
92 | });
93 | });
94 | } else {
95 | setTimeout(Commit, 10000);
96 | }
97 | }
98 | setTimeout(Commit, 10000);
99 |
100 | const IOLimiter = RateLimiter({
101 | windowMs: 2000,
102 | max: 100
103 | });
104 |
105 | console.log(' - Creating express application.');
106 | const App = new express();
107 | App.use(IOLimiter);
108 | App.use(express.json());
109 | App.use(cookieparser(config.system.cookieKey));
110 | const HTTP = new http.Server(App);
111 |
112 | console.log(' - Compiling pages.');
113 | const CompiledPages = {};
114 | const Pages = {
115 | Dash: path.join(__dirname, 'web', 'dash.html'),
116 | Index: path.join(__dirname, 'web', 'index.html')
117 | };
118 | Object.keys(Pages).forEach((PS) => {
119 | CompiledPages[PS] = handlebars.compile(fs.readFileSync(Pages[PS], 'utf8'));
120 | });
121 |
122 | // Static
123 | App.use('/static', express.static(path.join(__dirname, 'web', 'static')));
124 |
125 | // UI
126 | App.get('/', (req, res) => {
127 | res.type('text/html');
128 | res.status(200);
129 | res.end(CompiledPages.Index());
130 | });
131 | App.post('/login', (req, res) => {
132 | const Data = req.body;
133 | const Password = Data.password;
134 | const Username = Data.username;
135 |
136 | if (
137 | bcrypt.compareSync(Password, config.system.password) &&
138 | config.system.username === Username
139 | ) {
140 | res.cookie('Authentication', 'Success', {
141 | signed: true
142 | });
143 | res.status(204);
144 | res.end();
145 | } else {
146 | res.status(401);
147 | res.end();
148 | }
149 | });
150 |
151 | App.get('/dashboard', CheckAuthMW, (req, res) => {
152 | res.type('text/html');
153 | res.status(200);
154 | res.end(CompiledPages.Dash(config));
155 | });
156 |
157 | // System Info
158 | App.get('/api/:APIKey/systeminfo', (req, res) => {
159 | if (bcrypt.compareSync(req.params.APIKey, config.system.apiKey)) {
160 | getSystemInfo(req, res);
161 | } else {
162 | res.status(401);
163 | res.end();
164 | }
165 | });
166 | App.get('/systeminfo', CheckAuthMW, (req, res) => {
167 | getSystemInfo(req, res);
168 | });
169 |
170 | function getSystemInfo(req, res) {
171 | osu.cpu.usage().then((CPU) => {
172 | osu.drive.info(config.system.storageVolume).then((DISK) => {
173 | osu.mem.info().then((MEM) => {
174 | const Info = {
175 | CPU: CPU,
176 | DISK: DISK,
177 | MEM: MEM
178 | };
179 | res.type('application/json');
180 | res.status(200);
181 | res.end(JSON.stringify(Info));
182 | });
183 | });
184 | });
185 | }
186 |
187 | // get Cameras
188 | App.get('/api/:APIKey/cameras', (req, res) => {
189 | if (bcrypt.compareSync(req.params.APIKey, config.system.apiKey)) {
190 | const Cams = [];
191 |
192 | Object.keys(config.cameras).forEach((ID) => {
193 | const Cam = config.cameras[ID];
194 | Cams.push({ id: ID, name: Cam.name, continuous: Cam.continuous });
195 | });
196 |
197 | res.type('application/json');
198 | res.status(200);
199 | res.end(JSON.stringify(Cams));
200 | } else {
201 | res.status(401);
202 | res.end();
203 | }
204 | });
205 |
206 | // Event Creation
207 | App.post('/api/:APIKey/event/:CameraID', (req, res) => {
208 | if (bcrypt.compareSync(req.params.APIKey, config.system.apiKey)) {
209 | if (config.cameras[req.params.CameraID].continuous) {
210 | if (!SensorTimestamps.hasOwnProperty(req.body.sensorId)) {
211 | FIFO.enqueue({
212 | statement:
213 | 'INSERT INTO Events(EventID,CameraID,Name,SensorID,Date) VALUES(?,?,?,?,?)',
214 | params: [
215 | generateUUID(),
216 | req.params.CameraID,
217 | req.body.name,
218 | req.body.sensorId,
219 | req.body.date
220 | ]
221 | });
222 | res.status(204);
223 | res.end();
224 |
225 | SensorTimestamps[req.body.sensorId] = dayjs().unix();
226 |
227 | setTimeout(() => {
228 | delete SensorTimestamps[req.body.sensorId];
229 | }, 1000 * config.system.eventSensorIdCoolOffSeconds);
230 |
231 | return;
232 | } else {
233 | res.status(429);
234 | res.end();
235 | }
236 | } else {
237 | res.status(501);
238 | res.end();
239 | }
240 | } else {
241 | res.status(401);
242 | res.end();
243 | }
244 | });
245 |
246 | // Snapshot
247 | App.get('/snapshot/:CameraID/:Width', CheckAuthMW, (req, res) => {
248 | getSnapShot(res, req.params.CameraID, req.params.Width);
249 | });
250 |
251 | App.get('/api/:APIKey/snapshot/:CameraID/:Width', (req, res) => {
252 | if (bcrypt.compareSync(req.params.APIKey, config.system.apiKey)) {
253 | getSnapShot(res, req.params.CameraID, req.params.Width);
254 | } else {
255 | res.status(401);
256 | res.end();
257 | }
258 | });
259 |
260 | function getSnapShot(Res, CameraID, Width) {
261 | const CommandArgs = [];
262 | const Cam = config.cameras[CameraID];
263 |
264 | Object.keys(Cam.inputConfig).forEach((inputConfigKey) => {
265 | CommandArgs.push('-' + inputConfigKey);
266 | if (Cam.inputConfig[inputConfigKey].length > 0) {
267 | CommandArgs.push(Cam.inputConfig[inputConfigKey]);
268 | }
269 | });
270 |
271 | CommandArgs.push('-i');
272 | CommandArgs.push(Cam.input);
273 | CommandArgs.push('-vf');
274 | CommandArgs.push('scale=' + Width + ':-1');
275 | CommandArgs.push('-vframes');
276 | CommandArgs.push('1');
277 | CommandArgs.push('-f');
278 | CommandArgs.push('image2');
279 | CommandArgs.push('-');
280 |
281 | const Process = childprocess.spawn(
282 | config.system.ffmpegLocation,
283 | CommandArgs,
284 | { env: process.env, stderr: 'ignore' }
285 | );
286 |
287 | let imageBuffer = Buffer.alloc(0);
288 |
289 | Process.stdout.on('data', function (data) {
290 | imageBuffer = Buffer.concat([imageBuffer, data]);
291 | });
292 |
293 | Process.on('exit', (Code, Signal) => {
294 | const _Error = FFMPEGExitDueToError(Code, Signal);
295 | if (!_Error) {
296 | Res.type('image/jpeg');
297 | Res.status(200);
298 | Res.end(Buffer.from(imageBuffer, 'binary'));
299 | } else {
300 | Res.status(500);
301 | Res.end();
302 | }
303 | });
304 | }
305 |
306 | // Get Event Data
307 | App.get('/api/:APIKey/geteventdata/:CameraID/:Start/:End', (req, res) => {
308 | if (bcrypt.compareSync(req.params.APIKey, config.system.apiKey)) {
309 | GetEventData(res, req.params.CameraID, req.params.Start, req.params.End);
310 | } else {
311 | res.status(401);
312 | res.end();
313 | }
314 | });
315 |
316 | App.get('/geteventdata/:CameraID/:Start/:End', CheckAuthMW, (req, res) => {
317 | GetEventData(res, req.params.CameraID, req.params.Start, req.params.End);
318 | });
319 |
320 | function GetEventData(res, CameraID, Start, End) {
321 | const Data = {};
322 |
323 | let STMT = SQL.prepare(
324 | 'SELECT * FROM Segments WHERE CameraID = ? AND Start >= ? AND End <= ?'
325 | );
326 | STMT.all([CameraID, parseInt(Start), parseInt(End)], (err, rows) => {
327 | Data.segments = rows;
328 | STMT.finalize();
329 | STMT = SQL.prepare(
330 | 'SELECT * FROM Events WHERE CameraID = ? AND Date >= ? AND Date <= ?'
331 | );
332 | STMT.all([CameraID, parseInt(Start), parseInt(End)], (err, rows) => {
333 | Data.events = rows;
334 | STMT.finalize();
335 | res.type('application/json');
336 | res.status(200);
337 | res.end(JSON.stringify(Data));
338 | });
339 | });
340 | }
341 |
342 | const Processors = {};
343 | const Cameras = Object.keys(config.cameras);
344 | Cameras.forEach((cameraID) => {
345 | const Cam = config.cameras[cameraID];
346 | InitCamera(Cam, cameraID);
347 | });
348 |
349 | function CreateOrConnectSQL(CB) {
350 | const Path = path.join(
351 | config.system.storageVolume,
352 | 'NVRJS_SYSTEM',
353 | 'data.db'
354 | );
355 |
356 | if (!fs.existsSync(Path)) {
357 | console.log(' - Creating db structure.');
358 | SQL = new sql.Database(Path, () => {
359 | SQL.run(
360 | 'CREATE TABLE Segments(SegmentID TEXT, CameraID TEXT, FileName TEXT, Start NUMERIC, End NUMERIC)',
361 | () => {
362 | SQL.run(
363 | 'CREATE TABLE Events(EventID TEXT,CameraID TEXT, Name TEXT, SensorID TEXT, Date NUMERIC)',
364 | () => {
365 | SQL.close();
366 | console.log(' - Connecting to db.');
367 | SQL = new sql.Database(Path, CB);
368 | }
369 | );
370 | }
371 | );
372 | });
373 | } else {
374 | console.log(' - Connecting to db.');
375 | SQL = new sql.Database(Path, CB);
376 | }
377 | }
378 |
379 | function FFMPEGExitDueToError(Code, Signal) {
380 | if (Code == null && Signal === 'SIGKILL') {
381 | return false;
382 | }
383 | if (Code === 255 && Signal == null) {
384 | return false;
385 | }
386 | if (Code > 0 && Code < 255 && Signal == null) {
387 | return true;
388 | }
389 | }
390 |
391 | function InitCamera(Cam, cameraID) {
392 | console.log(' - Configuring camera: ' + Cam.name);
393 |
394 | const CommandArgs = [];
395 |
396 | Object.keys(Cam.inputConfig).forEach((inputConfigKey) => {
397 | if (inputConfigKey !== 'i') {
398 | CommandArgs.push('-' + inputConfigKey);
399 | if (Cam.inputConfig[inputConfigKey].length > 0) {
400 | CommandArgs.push(Cam.inputConfig[inputConfigKey]);
401 | }
402 | }
403 | });
404 |
405 | CommandArgs.push('-i');
406 | CommandArgs.push(Cam.input);
407 |
408 | App.use(
409 | '/segments/' + cameraID,
410 | CheckAuthMW,
411 | express.static(
412 | path.join(
413 | config.system.storageVolume,
414 | 'NVRJS_CAMERA_RECORDINGS',
415 | cameraID
416 | ),
417 | { acceptRanges: true }
418 | )
419 | );
420 |
421 | const Path = path.join(
422 | config.system.storageVolume,
423 | 'NVRJS_CAMERA_RECORDINGS',
424 | cameraID
425 | );
426 | if (!fs.existsSync(Path)) {
427 | fs.mkdirSync(Path);
428 | }
429 |
430 | if (Cam.continuous !== undefined && Cam.continuous) {
431 | CommandArgs.push('-c:v');
432 | CommandArgs.push('copy');
433 | CommandArgs.push('-c:a');
434 | CommandArgs.push('copy');
435 | CommandArgs.push('-f');
436 | CommandArgs.push('segment');
437 | CommandArgs.push('-movflags');
438 | CommandArgs.push('+faststart');
439 | CommandArgs.push('-segment_atclocktime');
440 | CommandArgs.push('1');
441 | CommandArgs.push('-reset_timestamps');
442 | CommandArgs.push('1');
443 | CommandArgs.push('-strftime');
444 | CommandArgs.push('1');
445 | CommandArgs.push('-segment_list');
446 | CommandArgs.push('pipe:4');
447 | CommandArgs.push('-segment_time');
448 | CommandArgs.push(60 * config.system.continuousSegTimeMinutes);
449 | CommandArgs.push(path.join(Path, '%Y-%m-%dT%H-%M-%S.mp4'));
450 | }
451 |
452 | Object.keys(Cam.liveConfig.streamConfig).forEach((streamingConfigKey) => {
453 | CommandArgs.push('-' + streamingConfigKey);
454 | if (Cam.liveConfig.streamConfig[streamingConfigKey].length > 0) {
455 | CommandArgs.push(Cam.liveConfig.streamConfig[streamingConfigKey]);
456 | }
457 | });
458 |
459 | CommandArgs.push('-metadata');
460 | CommandArgs.push('title="NVR JS Stream"');
461 | CommandArgs.push('pipe:3');
462 |
463 | const Options = {
464 | detached: true,
465 | stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe']
466 | };
467 | const respawn = (Spawned) => {
468 | const MP4F = new MP4Frag();
469 |
470 | const IOptions = {
471 | path: '/streams/' + cameraID
472 | };
473 | const Socket = io(HTTP, IOptions);
474 | Socket.on('connection', (ClientSocket) => {
475 | if (CheckAuthMW(ClientSocket)) {
476 | ClientSocket.emit('segment', MP4F.initialization);
477 | }
478 | });
479 |
480 | MP4F.on('segment', (data) => {
481 | Socket.sockets.sockets.forEach((ClientSocket) => {
482 | ClientSocket.emit('segment', data);
483 | });
484 | });
485 |
486 | Spawned.on('close', () => {
487 | console.log(
488 | ' - Camera: ' +
489 | Cam.name +
490 | ' was terminated, respawning after 10 seconds...'
491 | );
492 | Spawned.kill();
493 | MP4F.destroy();
494 | setTimeout(() => {
495 | respawn(
496 | childprocess.spawn(config.system.ffmpegLocation, CommandArgs, Options)
497 | );
498 | }, 10000);
499 | });
500 |
501 | Spawned.stdio[3].on('data', (data) => {
502 | MP4F.write(data, 'binary');
503 | });
504 | Spawned.stdio[4].on('data', (FN) => {
505 | if (Processors[cameraID] !== undefined) {
506 | const FileName = FN.toString().trim().replace(/\n/g, '');
507 | const Start = dayjs(
508 | FileName.replace(/.mp4/g, ''),
509 | 'YYYY-MM-DDTHH-mm-ss'
510 | ).unix();
511 | const End = dayjs().unix();
512 | FIFO.enqueue({
513 | statement:
514 | 'INSERT INTO Segments(SegmentID,CameraID,FileName,Start,End) VALUES(?,?,?,?,?)',
515 | params: [generateUUID(), cameraID, FileName, Start, End]
516 | });
517 | }
518 | });
519 | };
520 |
521 | respawn(
522 | childprocess.spawn(config.system.ffmpegLocation, CommandArgs, Options)
523 | );
524 |
525 | Processors[cameraID] = {
526 | CameraInfo: Cam
527 | };
528 | }
529 | function generateUUID() {
530 | var d = new Date().getTime();
531 | var d2 =
532 | (typeof performance !== 'undefined' &&
533 | performance.now &&
534 | performance.now() * 1000) ||
535 | 0;
536 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
537 | var r = Math.random() * 16;
538 | if (d > 0) {
539 | r = (d + r) % 16 | 0;
540 | d = Math.floor(d / 16);
541 | } else {
542 | r = (d2 + r) % 16 | 0;
543 | d2 = Math.floor(d2 / 16);
544 | }
545 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
546 | });
547 | }
548 |
549 | function CheckAuthMW(req, res, next) {
550 | if (res === undefined && next === undefined) {
551 | if (req.handshake.headers.cookie !== undefined) {
552 | const CS = cookie.parse(req.handshake.headers.cookie);
553 | const Signed = cookieparser.signedCookies(CS, config.system.cookieKey);
554 | if (
555 | Signed.Authentication === undefined ||
556 | Signed.Authentication !== 'Success'
557 | ) {
558 | req.disconnect();
559 | return false;
560 | } else {
561 | return true;
562 | }
563 | } else {
564 | req.disconnect();
565 | return false;
566 | }
567 | } else {
568 | if (
569 | req.signedCookies.Authentication === undefined ||
570 | req.signedCookies.Authentication !== 'Success'
571 | ) {
572 | res.status(401);
573 | res.end();
574 | } else {
575 | next();
576 | }
577 | }
578 | }
579 |
580 | async function purgeContinuous() {
581 | console.log(' - Purging data.');
582 | const Date = dayjs().subtract(config.system.continuousDays, 'day').unix();
583 | const STMT = SQL.prepare('SELECT * FROM Segments WHERE Start <= ?');
584 | STMT.all([Date], (err, rows) => {
585 | rows.forEach((S) => {
586 | fs.unlinkSync(
587 | path.join(
588 | config.system.storageVolume,
589 | 'NVRJS_CAMERA_RECORDINGS',
590 | S.CameraID,
591 | S.FileName
592 | )
593 | );
594 | });
595 | FIFO.enqueue({
596 | statement: `DELETE FROM Segments WHERE Start <= ${Date}`,
597 | params: []
598 | });
599 | FIFO.enqueue({
600 | statement: `DELETE FROM Events WHERE Date <= ${Date}`,
601 | params: []
602 | });
603 | });
604 | }
605 |
606 | HTTP.listen(config.system.interfacePort);
607 | console.log(' - NVR JS is Ready!');
608 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # nvr-js
4 | 
5 | 
6 | [](https://lgtm.com/projects/g/marcus-j-davies/nvr-js/context:javascript)
7 | 
8 | 
9 |
10 | NVR JS is a simple, very lightweight and efficient CCTV NVR based on Node JS.
11 | it's primarily aimed for 24/7 recording and live viewing.
12 |
13 | Under the hood it uses ffmpeg, node js, websockets and sqlite, all wrapped in a web based user interface.
14 | The NVR has an API that allows to create events and timestamp them on the 24/7 recordings.
15 |
16 | The 24/7 recordings can be reviewed using a timeline UI where the events are also time aligned on that same timeline.
17 |
18 | 
19 |
20 | ### Inspired by shinobi.video
21 | [Shinobi](https://shinobi.video) is a fully featured, jam packed NVR, also built using Node JS.
22 | I was using Shinobi and thought it was amazing! - however, it had sooo much to it, and was too overkill for my needs.
23 |
24 | You can think of NVR-JS as a very slimed down version of shinobi video, built from the ground up.
25 | the table below, shows how slimmed down it is.
26 |
27 | | Feature | Shinobi | NVR JS |
28 | |-------------------|---------|---------------------|
29 | | Motion Dectection | ✓ | |
30 | | Object Detection | ✓ | |
31 | | 24/7 Recording | ✓ | ✓ |
32 | | Event Creation | ✓ | ✓ (API Only) |
33 | | Notifications | ✓ | |
34 | | Live Streaming | ✓ | ✓ (Websocket) |
35 | | Configuration UI | ✓ | Manual Editing |
36 | | Mobile Support | ✓ | |
37 |
38 | As you can see, NVR JS does not pack the same features as Shinobi Video, but that's the intention.
39 | NVR JS is designed for 24/7 recording with access to live footage, and the 24/7 recordings.
40 |
41 | ### Configuration.
42 | The first time you start NVR JS, it will create a config file at **/%home%/nvrjs.config.js**,
43 | it will then terminate allowing you to start adjusting your system, before starting it up again.
44 |
45 | ```javascript
46 | module.exports = {
47 | /* System Settings */
48 | system: {
49 | /* Username */
50 | username: "admin",
51 | /* bcrypt password (default: admin) */
52 | password: '$2a$10$CnOx/6vFY2ehRDf68yqd..aLlv0UM.zeBLKnRjuU8YykCsC2Ap3iG',
53 | /* bcrypt API Key (default: x7Te9m38JHQq6ddv) */
54 | apiKey: '$2a$10$N53ci.EIQ7JCu6u1HlOjoO//W0Bmp3GrRruyK1Jysr01CQ1rDrVQK',
55 | /* Any random string */
56 | cookieKey: 'f3gi6FLhIPVV31d1TBQUPEAngrI3wAoP',
57 | interfacePort: 7878,
58 | /* location used for 24/7 recording and database generation */
59 | /* This should be the root of a mount point i.e a dedicated HDD for 24/7 recordings */
60 | storageVolume: '/Volumes/CCTV',
61 | /* Continuous recording settings */
62 | ffmpegLocation: 'ffmpeg',
63 | continuousSegTimeMinutes: 15,
64 | continuousDays: 14,
65 | continuousPurgeIntervalHours: 24,
66 | /* event throttle per sensorId */
67 | eventSensorIdCoolOffSeconds: 60
68 | },
69 | /* Cameras */
70 | cameras: {
71 | '66e39d21-72c4-405c-a838-05a8e8fe0742': {
72 | name: 'Garage',
73 | /* Input Source Config */
74 | /* The keys and values represent the ffmpeg options */
75 | inputConfig: {
76 | use_wallclock_as_timestamps: '1',
77 | fflags: '+igndts',
78 | analyzeduration: '1000000',
79 | probesize: '1000000',
80 | rtsp_transport: 'tcp',
81 | stimeout: '30000000'
82 | },
83 | /* Input Address */
84 | input: 'rtsp://user:password@ip:port/live0',
85 | /* Recording 24/7 */
86 | /* Disabling continuous recording, will disable the ability to create events */
87 | continuous: true,
88 | /* Live streaming config */
89 | /* These settings should be good enough for a low delay live stream, providing your camera produces h264 frames */
90 | /* streaming is achieved with websockets and MP4 fragments */
91 | liveConfig: {
92 | codecString: 'video/mp4; codecs="avc1.64001f"',
93 | streamConfig: {
94 | an: '',
95 | vcodec: 'copy',
96 | f: 'mp4',
97 | movflags: '+frag_keyframe+empty_moov+default_base_moof',
98 | reset_timestamps: '1'
99 | }
100 | }
101 | }
102 | }
103 | };
104 | ```
105 |
106 |
107 | ### The Event API.
108 | To create events one only needs to send the following JSON payload.
109 |
110 | The view here, is that you create events from various sensors in your setup, this effectively acts as your motion detector
111 | or some other key event - It's really up to you.
112 |
113 | ```javascript
114 | {
115 | "name": "Motion Detected" | "Door Opened" | "Some Other Event" | "Of Your Choice",
116 | "sensorId": "HUEN849",
117 | "date": 1636194611
118 | }
119 | ```
120 |
121 | You **POST** this payload to the API as follows:
122 | http://IP:7878/api/{APIKey}/event/{camera-id}
123 | Example: http://IP:7878/api/x7Te9m38JHQq6ddv/event/66e39d21-72c4-405c-a838-05a8e8fe0742
124 |
125 | ### Anyway
126 | I built this for my needs, it's very DIY and will likely have some faults in some places.
127 | But if you want to use it, change it, build on it, feel free - I welcome PR's.
128 |
129 | ```
130 | npm install nvr-js
131 | cd ./node_modules/nvr-js
132 | pm2 start NVRJS.js
133 | pm2 save
134 | ```
135 |
136 | [Change Log](./CHANGELOG.md)
137 |
--------------------------------------------------------------------------------
/core/MP4Frag.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Transform } = require('stream');
4 |
5 | const _FTYP = Buffer.from([0x66, 0x74, 0x79, 0x70]); // ftyp
6 | const _MOOV = Buffer.from([0x6d, 0x6f, 0x6f, 0x76]); // moov
7 | const _MOOF = Buffer.from([0x6d, 0x6f, 0x6f, 0x66]); // moof
8 | const _MFRA = Buffer.from([0x6d, 0x66, 0x72, 0x61]); // mfra
9 | const _MDAT = Buffer.from([0x6d, 0x64, 0x61, 0x74]); // mdat
10 | const _MP4A = Buffer.from([0x6d, 0x70, 0x34, 0x61]); // mp4a
11 | const _AVCC = Buffer.from([0x61, 0x76, 0x63, 0x43]); // avcC
12 |
13 | /**
14 | * @fileOverview Creates a stream transform for piping a fmp4 (fragmented mp4) from ffmpeg.
15 | * Can be used to generate a fmp4 m3u8 HLS playlist and compatible file fragments.
16 | * Can also be used for storing past segments of the mp4 video in a buffer for later access.
17 | * Must use the following ffmpeg flags -movflags +frag_keyframe+empty_moov to generate a fmp4
18 | * with a compatible file structure : ftyp+moov -> moof+mdat -> moof+mdat -> moof+mdat ...
19 | * @requires stream.Transform
20 | */
21 | class Mp4Frag extends Transform {
22 | /**
23 | * @constructor
24 | * @param {Object} [options] - Configuration options.
25 | * @param {String} [options.hlsBase] - Base name of files in fmp4 m3u8 playlist. Affects the generated m3u8 playlist by naming file fragments. Must be set to generate m3u8 playlist.
26 | * @param {Number} [options.hlsListSize] - Number of segments to keep in fmp4 m3u8 playlist. Must be an integer ranging from 2 to 10. Defaults to 4 if hlsBase is set and hlsListSize is not set.
27 | * @param {Boolean} [options.hlsListInit] - Indicates that m3u8 playlist should be generated after init segment is created and before media segments are created. Defaults to false.
28 | * @param {Number} [options.bufferListSize] - Number of segments to keep buffered. Must be an integer ranging from 2 to 10. Not related to HLS settings.
29 | * @returns {Mp4Frag} this - Returns reference to new instance of Mp4Frag for chaining event listeners.
30 | */
31 | constructor(options) {
32 | super(options);
33 | if (options) {
34 | if (typeof options.hlsBase === 'string' && /^[a-z0-9]+$/i.exec(options.hlsBase)) {
35 | const hlsListSize = parseInt(options.hlsListSize);
36 | this._hlsListInit = options.hlsListInit === true;
37 | if (isNaN(hlsListSize)) {
38 | this._hlsListSize = 4;
39 | } else if (hlsListSize < 2) {
40 | this._hlsListSize = 2;
41 | } else if (hlsListSize > 10) {
42 | this._hlsListSize = 10;
43 | } else {
44 | this._hlsListSize = hlsListSize;
45 | }
46 | this._hlsList = [];
47 | this._hlsBase = options.hlsBase;
48 | this._sequence = -1;
49 | }
50 | if (options.hasOwnProperty('bufferListSize')) {
51 | const bufferListSize = parseInt(options.bufferListSize);
52 | if (isNaN(bufferListSize) || bufferListSize < 2) {
53 | this._bufferListSize = 2;
54 | } else if (bufferListSize > 10) {
55 | this._bufferListSize = 10;
56 | } else {
57 | this._bufferListSize = bufferListSize;
58 | }
59 | this._bufferList = [];
60 | }
61 | }
62 | this._parseChunk = this._findFtyp;
63 | return this;
64 | }
65 |
66 | /**
67 | * @readonly
68 | * @property {String} mime
69 | * - Returns the mime codec information as a String.
70 | *
71 | * - Returns Null if requested before [initialized event]{@link Mp4Frag#event:initialized}.
72 | * @returns {String}
73 | */
74 | get mime() {
75 | return this._mime || null;
76 | }
77 |
78 | /**
79 | * @readonly
80 | * @property {Buffer} initialization
81 | * - Returns the mp4 initialization fragment as a Buffer.
82 | *
83 | * - Returns Null if requested before [initialized event]{@link Mp4Frag#event:initialized}.
84 | * @returns {Buffer}
85 | */
86 | get initialization() {
87 | return this._initialization || null;
88 | }
89 |
90 | /**
91 | * @readonly
92 | * @property {Buffer} segment
93 | * - Returns the latest Mp4 segment as a Buffer.
94 | *
95 | * - Returns Null if requested before first [segment event]{@link Mp4Frag#event:segment}.
96 | * @returns {Buffer}
97 | */
98 | get segment() {
99 | return this._segment || null;
100 | }
101 |
102 | /**
103 | * @readonly
104 | * @property {Number} timestamp
105 | * - Returns the timestamp of the latest Mp4 segment as an Integer(milliseconds).
106 | *
107 | * - Returns -1 if requested before first [segment event]{@link Mp4Frag#event:segment}.
108 | * @returns {Number}
109 | */
110 | get timestamp() {
111 | return this._timestamp || -1;
112 | }
113 |
114 | /**
115 | * @readonly
116 | * @property {Number} duration
117 | * - Returns the duration of latest Mp4 segment as a Float(seconds).
118 | *
119 | * - Returns -1 if requested before first [segment event]{@link Mp4Frag#event:segment}.
120 | * @returns {Number}
121 | */
122 | get duration() {
123 | return this._duration || -1;
124 | }
125 |
126 | /**
127 | * @readonly
128 | * @property {String} m3u8
129 | * - Returns the fmp4 HLS m3u8 playlist as a String.
130 | *
131 | * - Returns Null if requested before [initialized event]{@link Mp4Frag#event:initialized}.
132 | * @returns {String}
133 | */
134 | get m3u8() {
135 | return this._m3u8 || null;
136 | }
137 |
138 | /**
139 | * @readonly
140 | * @property {Number} sequence
141 | * - Returns the latest sequence of the fmp4 HLS m3u8 playlist as an Integer.
142 | *
143 | * - Returns -1 if requested before first [segment event]{@link Mp4Frag#event:segment}.
144 | * @returns {Number}
145 | */
146 | get sequence() {
147 | return Number.isInteger(this._sequence) ? this._sequence : -1;
148 | }
149 |
150 | /**
151 | * @readonly
152 | * @property {Array} bufferList
153 | * - Returns the buffered mp4 segments as an Array.
154 | *
155 | * - Returns Null if requested before first [segment event]{@link Mp4Frag#event:segment}.
156 | * @returns {Array}
157 | */
158 | get bufferList() {
159 | if (this._bufferList && this._bufferList.length > 0) {
160 | return this._bufferList;
161 | }
162 | return null;
163 | }
164 |
165 | /**
166 | * @readonly
167 | * @property {Buffer} bufferListConcat
168 | * - Returns the [Mp4Frag.bufferList]{@link Mp4Frag#bufferList} concatenated as a Buffer.
169 | *
170 | * - Returns Null if requested before first [segment event]{@link Mp4Frag#event:segment}.
171 | * @returns {Buffer}
172 | */
173 | get bufferListConcat() {
174 | if (this._bufferList && this._bufferList.length > 0) {
175 | return Buffer.concat(this._bufferList);
176 | }
177 | return null;
178 | }
179 |
180 | /**
181 | * @readonly
182 | * @property {Buffer} bufferConcat
183 | * - Returns the [Mp4Frag.initialization]{@link Mp4Frag#initialization} and [Mp4Frag.bufferList]{@link Mp4Frag#bufferList} concatenated as a Buffer.
184 | *
185 | * - Returns Null if requested before first [segment event]{@link Mp4Frag#event:segment}.
186 | * @returns {Buffer}
187 | */
188 | get bufferConcat() {
189 | if (this._initialization && this._bufferList && this._bufferList.length > 0) {
190 | return Buffer.concat([this._initialization, ...this._bufferList]);
191 | }
192 | return null;
193 | }
194 |
195 | /**
196 | * @param {Number|String} sequence
197 | * - Returns the Mp4 segment that corresponds to the HLS numbered sequence as a Buffer.
198 | *
199 | * - Returns Null if there is no .m4s segment that corresponds to sequence number.
200 | * @returns {Buffer}
201 | */
202 | getHlsSegment(sequence) {
203 | return this.getHlsNamedSegment(`${this._hlsBase}${sequence}.m4s`);
204 | }
205 |
206 | /**
207 | * @param {String} name
208 | * - Returns the Mp4 segment that corresponds to the HLS named sequence as a Buffer.
209 | *
210 | * - Returns Null if there is no .m4s segment that corresponds to sequence name.
211 | * @returns {Buffer}
212 | */
213 | getHlsNamedSegment(name) {
214 | if (name && this._hlsList && this._hlsList.length > 0) {
215 | for (let i = 0; i < this._hlsList.length; i++) {
216 | if (this._hlsList[i].name === name) {
217 | return this._hlsList[i].segment;
218 | }
219 | }
220 | }
221 | return null;
222 | }
223 |
224 | /**
225 | * Search buffer for ftyp.
226 | * @private
227 | */
228 | _findFtyp(chunk) {
229 | const chunkLength = chunk.length;
230 | if (chunkLength < 8 || chunk.indexOf(_FTYP) !== 4) {
231 | this.emit('error', new Error(`${_FTYP.toString()} not found.`));
232 | return;
233 | }
234 | this._ftypLength = chunk.readUInt32BE(0, true);
235 | if (this._ftypLength < chunkLength) {
236 | this._ftyp = chunk.slice(0, this._ftypLength);
237 | this._parseChunk = this._findMoov;
238 | this._parseChunk(chunk.slice(this._ftypLength));
239 | } else if (this._ftypLength === chunkLength) {
240 | this._ftyp = chunk;
241 | this._parseChunk = this._findMoov;
242 | } else {
243 | //should not be possible to get here because ftyp is approximately 24 bytes
244 | //will have to buffer this chunk and wait for rest of it on next pass
245 | this.emit('error', new Error(`ftypLength:${this._ftypLength} > chunkLength:${chunkLength}`));
246 | //return;
247 | }
248 | }
249 |
250 | /**
251 | * Search buffer for moov.
252 | * @private
253 | */
254 | _findMoov(chunk) {
255 | const chunkLength = chunk.length;
256 | if (chunkLength < 8 || chunk.indexOf(_MOOV) !== 4) {
257 | this.emit('error', new Error(`${_MOOV.toString()} not found.`));
258 | return;
259 | }
260 | const moovLength = chunk.readUInt32BE(0, true);
261 | if (moovLength < chunkLength) {
262 | this._parseMoov(Buffer.concat([this._ftyp, chunk], this._ftypLength + moovLength));
263 | delete this._ftyp;
264 | delete this._ftypLength;
265 | this._parseChunk = this._findMoof;
266 | this._parseChunk(chunk.slice(moovLength));
267 | } else if (moovLength === chunkLength) {
268 | this._parseMoov(Buffer.concat([this._ftyp, chunk], this._ftypLength + moovLength));
269 | delete this._ftyp;
270 | delete this._ftypLength;
271 | this._parseChunk = this._findMoof;
272 | } else {
273 | //probably should not arrive here here because moov is typically < 800 bytes
274 | //will have to store chunk until size is big enough to have entire moov piece
275 | //ffmpeg may have crashed before it could output moov and got us here
276 | this.emit('error', new Error(`moovLength:${moovLength} > chunkLength:${chunkLength}`));
277 | //return;
278 | }
279 | }
280 |
281 | /**
282 | * Parse moov for mime.
283 | * @fires Mp4Frag#initialized
284 | * @private
285 | */
286 | _parseMoov(value) {
287 | this._initialization = value;
288 | let audioString = '';
289 | if (this._initialization.indexOf(_MP4A) !== -1) {
290 | audioString = ', mp4a.40.2';
291 | }
292 | let index = this._initialization.indexOf(_AVCC);
293 | if (index === -1) {
294 | this.emit('error', new Error(`${_AVCC.toString()} codec info not found.`));
295 | return;
296 | }
297 | index += 5;
298 | this._mime = `video/mp4; codecs="avc1.${this._initialization
299 | .slice(index, index + 3)
300 | .toString('hex')
301 | .toUpperCase()}${audioString}"`;
302 | this._timestamp = Date.now();
303 | if (this._hlsList && this._hlsListInit) {
304 | let m3u8 = '#EXTM3U\n';
305 | m3u8 += '#EXT-X-VERSION:7\n';
306 | //m3u8 += '#EXT-X-ALLOW-CACHE:NO\n';
307 | m3u8 += `#EXT-X-TARGETDURATION:1\n`;
308 | m3u8 += `#EXT-X-MEDIA-SEQUENCE:0\n`;
309 | m3u8 += `#EXT-X-MAP:URI="init-${this._hlsBase}.mp4"\n`;
310 | this._m3u8 = m3u8;
311 | }
312 | /**
313 | * Fires when the init fragment of the Mp4 is parsed from the piped data.
314 | * @event Mp4Frag#initialized
315 | * @type {Event}
316 | * @property {Object} Object
317 | * @property {String} Object.mime - [Mp4Frag.mime]{@link Mp4Frag#mime}
318 | * @property {Buffer} Object.initialization - [Mp4Frag.initialization]{@link Mp4Frag#initialization}
319 | * @property {String} Object.m3u8 - [Mp4Frag.m3u8]{@link Mp4Frag#m3u8}
320 | */
321 | this.emit('initialized', { mime: this._mime, initialization: this._initialization, m3u8: this._m3u8 || null });
322 | }
323 |
324 | /**
325 | * Find moof after miss due to corrupt data in pipe.
326 | * @private
327 | */
328 | _moofHunt(chunk) {
329 | if (this._moofHunts < this._moofHuntsLimit) {
330 | this._moofHunts++;
331 | //console.warn(`MOOF hunt attempt number ${this._moofHunts}.`);
332 | const index = chunk.indexOf(_MOOF);
333 | if (index > 3 && chunk.length > index + 3) {
334 | delete this._moofHunts;
335 | delete this._moofHuntsLimit;
336 | this._parseChunk = this._findMoof;
337 | this._parseChunk(chunk.slice(index - 4));
338 | }
339 | } else {
340 | this.emit('error', new Error(`${_MOOF.toString()} hunt failed after ${this._moofHunts} attempts.`));
341 | //return;
342 | }
343 | }
344 |
345 | /**
346 | * Search buffer for moof.
347 | * @private
348 | */
349 | _findMoof(chunk) {
350 | if (this._moofBuffer) {
351 | this._moofBuffer.push(chunk);
352 | const chunkLength = chunk.length;
353 | this._moofBufferSize += chunkLength;
354 | if (this._moofLength === this._moofBufferSize) {
355 | //todo verify this works
356 | this._moof = Buffer.concat(this._moofBuffer, this._moofLength);
357 | delete this._moofBuffer;
358 | delete this._moofBufferSize;
359 | this._parseChunk = this._findMdat;
360 | } else if (this._moofLength < this._moofBufferSize) {
361 | this._moof = Buffer.concat(this._moofBuffer, this._moofLength);
362 | const sliceIndex = chunkLength - (this._moofBufferSize - this._moofLength);
363 | delete this._moofBuffer;
364 | delete this._moofBufferSize;
365 | this._parseChunk = this._findMdat;
366 | this._parseChunk(chunk.slice(sliceIndex));
367 | }
368 | } else {
369 | const chunkLength = chunk.length;
370 | if (chunkLength < 8 || chunk.indexOf(_MOOF) !== 4) {
371 | //ffmpeg occasionally pipes corrupt data, lets try to get back to normal if we can find next MOOF box before attempts run out
372 | const mfraIndex = chunk.indexOf(_MFRA);
373 | if (mfraIndex !== -1) {
374 | //console.log(`MFRA was found at ${mfraIndex}. This is expected at the end of stream.`);
375 | return;
376 | }
377 | //console.warn('Failed to find MOOF. Starting MOOF hunt. Ignore this if your file stream input has ended.');
378 | this._moofHunts = 0;
379 | this._moofHuntsLimit = 40;
380 | this._parseChunk = this._moofHunt;
381 | this._parseChunk(chunk);
382 | return;
383 | }
384 | this._moofLength = chunk.readUInt32BE(0, true);
385 | if (this._moofLength === 0) {
386 | this.emit('error', new Error(`Bad data from input stream reports ${_MOOF.toString()} length of 0.`));
387 | return;
388 | }
389 | if (this._moofLength < chunkLength) {
390 | this._moof = chunk.slice(0, this._moofLength);
391 | this._parseChunk = this._findMdat;
392 | this._parseChunk(chunk.slice(this._moofLength));
393 | } else if (this._moofLength === chunkLength) {
394 | //todo verify this works
395 | this._moof = chunk;
396 | this._parseChunk = this._findMdat;
397 | } else {
398 | this._moofBuffer = [chunk];
399 | this._moofBufferSize = chunkLength;
400 | }
401 | }
402 | }
403 |
404 | /**
405 | * Process current segment.
406 | * @fires Mp4Frag#segment
407 | * @param chunk {Buffer}
408 | * @private
409 | */
410 | _setSegment(chunk) {
411 | this._segment = chunk;
412 | const currentTime = Date.now();
413 | this._duration = Math.max((currentTime - this._timestamp) / 1000, 1);
414 | this._timestamp = currentTime;
415 | if (this._hlsList) {
416 | this._hlsList.push({
417 | sequence: ++this._sequence,
418 | name: `${this._hlsBase}${this._sequence}.m4s`,
419 | segment: this._segment,
420 | duration: this._duration
421 | });
422 | while (this._hlsList.length > this._hlsListSize) {
423 | this._hlsList.shift();
424 | }
425 | let m3u8 = '#EXTM3U\n';
426 | m3u8 += '#EXT-X-VERSION:7\n';
427 | //m3u8 += '#EXT-X-ALLOW-CACHE:NO\n';
428 | m3u8 += `#EXT-X-TARGETDURATION:${Math.round(this._duration)}\n`;
429 | m3u8 += `#EXT-X-MEDIA-SEQUENCE:${this._hlsList[0].sequence}\n`;
430 | m3u8 += `#EXT-X-MAP:URI="init-${this._hlsBase}.mp4"\n`;
431 | for (let i = 0; i < this._hlsList.length; i++) {
432 | m3u8 += `#EXTINF:${this._hlsList[i].duration.toFixed(6)},\n`;
433 | m3u8 += `${this._hlsList[i].name}\n`;
434 | }
435 | this._m3u8 = m3u8;
436 | }
437 | if (this._bufferList) {
438 | this._bufferList.push(this._segment);
439 | while (this._bufferList.length > this._bufferListSize) {
440 | this._bufferList.shift();
441 | }
442 | }
443 | if (this._readableState.pipesCount > 0) {
444 | this.push(this._segment);
445 | }
446 | /**
447 | * Fires when the latest Mp4 segment is parsed from the piped data.
448 | * @event Mp4Frag#segment
449 | * @type {Event}
450 | * @property {Buffer} segment - [Mp4Frag.segment]{@link Mp4Frag#segment}
451 | */
452 | this.emit('segment', this._segment);
453 | }
454 |
455 | /**
456 | * Search buffer for mdat.
457 | * @private
458 | */
459 | _findMdat(chunk) {
460 | if (this._mdatBuffer) {
461 | this._mdatBuffer.push(chunk);
462 | const chunkLength = chunk.length;
463 | this._mdatBufferSize += chunkLength;
464 | if (this._mdatLength === this._mdatBufferSize) {
465 | this._setSegment(Buffer.concat([this._moof, ...this._mdatBuffer], this._moofLength + this._mdatLength));
466 | delete this._moof;
467 | delete this._mdatBuffer;
468 | delete this._mdatBufferSize;
469 | delete this._mdatLength;
470 | delete this._moofLength;
471 | this._parseChunk = this._findMoof;
472 | } else if (this._mdatLength < this._mdatBufferSize) {
473 | this._setSegment(Buffer.concat([this._moof, ...this._mdatBuffer], this._moofLength + this._mdatLength));
474 | const sliceIndex = chunkLength - (this._mdatBufferSize - this._mdatLength);
475 | delete this._moof;
476 | delete this._mdatBuffer;
477 | delete this._mdatBufferSize;
478 | delete this._mdatLength;
479 | delete this._moofLength;
480 | this._parseChunk = this._findMoof;
481 | this._parseChunk(chunk.slice(sliceIndex));
482 | }
483 | } else {
484 | const chunkLength = chunk.length;
485 | if (chunkLength < 8 || chunk.indexOf(_MDAT) !== 4) {
486 | this.emit('error', new Error(`${_MDAT.toString()} not found.`));
487 | return;
488 | }
489 | this._mdatLength = chunk.readUInt32BE(0, true);
490 | if (this._mdatLength > chunkLength) {
491 | this._mdatBuffer = [chunk];
492 | this._mdatBufferSize = chunkLength;
493 | } else if (this._mdatLength === chunkLength) {
494 | this._setSegment(Buffer.concat([this._moof, chunk], this._moofLength + chunkLength));
495 | delete this._moof;
496 | delete this._moofLength;
497 | delete this._mdatLength;
498 | this._parseChunk = this._findMoof;
499 | } else {
500 | this._setSegment(Buffer.concat([this._moof, chunk], this._moofLength + this._mdatLength));
501 | const sliceIndex = this._mdatLength;
502 | delete this._moof;
503 | delete this._moofLength;
504 | delete this._mdatLength;
505 | this._parseChunk = this._findMoof;
506 | this._parseChunk(chunk.slice(sliceIndex));
507 | }
508 | }
509 | }
510 |
511 | /**
512 | * Required for stream transform.
513 | * @private
514 | */
515 | _transform(chunk, encoding, callback) {
516 | this._parseChunk(chunk);
517 | callback();
518 | }
519 |
520 | /**
521 | * Run cleanup when unpiped.
522 | * @private
523 | */
524 | _flush(callback) {
525 | this.resetCache();
526 | callback();
527 | }
528 |
529 | /**
530 | * Clear cached values
531 | */
532 | resetCache() {
533 | this._parseChunk = this._findFtyp;
534 | delete this._mime;
535 | delete this._initialization;
536 | delete this._segment;
537 | delete this._timestamp;
538 | delete this._duration;
539 | delete this._moof;
540 | delete this._mdatBuffer;
541 | delete this._moofLength;
542 | delete this._mdatLength;
543 | delete this._mdatBufferSize;
544 | delete this._ftyp;
545 | delete this._ftypLength;
546 | delete this._m3u8;
547 | if (this._hlsList) {
548 | this._hlsList = [];
549 | this._sequence = -1;
550 | }
551 | if (this._bufferList) {
552 | this._bufferList = [];
553 | }
554 | }
555 | }
556 |
557 | module.exports = Mp4Frag;
558 |
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/demo.png
--------------------------------------------------------------------------------
/nvrjs.config.example.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /* System Settings */
3 | system: {
4 | /* Username */
5 | username: "admin",
6 | /* bcrypt password (default: admin) */
7 | password: '$2a$10$CnOx/6vFY2ehRDf68yqd..aLlv0UM.zeBLKnRjuU8YykCsC2Ap3iG',
8 | /* bcrypt API Key (default: x7Te9m38JHQq6ddv) */
9 | apiKey: '$2a$10$N53ci.EIQ7JCu6u1HlOjoO//W0Bmp3GrRruyK1Jysr01CQ1rDrVQK',
10 | /* Any random string */
11 | cookieKey: 'f3gi6FLhIPVV31d1TBQUPEAngrI3wAoP',
12 | interfacePort: 7878,
13 | /* location used for 24/7 recording and database generation */
14 | /* This should be the root of a mount point i.e a dedicated HDD for 24/7 recordings */
15 | storageVolume: '/Volumes/CCTV',
16 | /* Continuous recording settings */
17 | ffmpegLocation: 'ffmpeg',
18 | continuousSegTimeMinutes: 15,
19 | continuousDays: 14,
20 | continuousPurgeIntervalHours: 24,
21 | /* event throttle per sensorId */
22 | eventSensorIdCoolOffSeconds: 60
23 | },
24 | /* Cameras */
25 | cameras: {
26 | '66e39d21-72c4-405c-a838-05a8e8fe0742': {
27 | name: 'Garage',
28 | /* Input Source Config */
29 | /* The keys and values represent the ffmpeg options */
30 | inputConfig: {
31 | use_wallclock_as_timestamps: '1',
32 | fflags: '+igndts',
33 | analyzeduration: '1000000',
34 | probesize: '1000000',
35 | rtsp_transport: 'tcp',
36 | stimeout: '30000000'
37 | },
38 | /* Input Address */
39 | input: 'rtsp://user:password@ip:port/live0',
40 | /* Recording 24/7 */
41 | /* Disabling continuous recording, will disable the ability to create events */
42 | continuous: true,
43 | /* Live streaming config */
44 | /* These settings should be good enough for a low delay live stream, providing your camera produces h264 frames */
45 | /* streaming is achieved with websockets and MP4 fragments */
46 | liveConfig: {
47 | codecString: 'video/mp4; codecs="avc1.64001f"',
48 | streamConfig: {
49 | an: '',
50 | vcodec: 'copy',
51 | f: 'mp4',
52 | movflags: '+frag_keyframe+empty_moov+default_base_moof',
53 | reset_timestamps: '1'
54 | }
55 | }
56 | }
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nvr-js",
3 | "version": "2.0.0",
4 | "description": "A simple, lightweight, but very functional NVR aimed at 24/7 recording using nodejs",
5 | "main": "NVRJS.js",
6 | "keywords": [
7 | "nvr",
8 | "cctv",
9 | "record",
10 | "24/7",
11 | "continuous",
12 | "footage",
13 | "streaming",
14 | "nodejs",
15 | "node"
16 | ],
17 | "author": {
18 | "name": "Marcus Davies",
19 | "email": "marcus.davies83@icloud.com"
20 | },
21 | "license": "MIT",
22 | "dependencies": {
23 | "child_process": "^1.0.2",
24 | "cookie-parser": "^1.4.6",
25 | "express": "^4.17.2",
26 | "handlebars": "^4.7.7",
27 | "socket.io": "^4.4.0",
28 | "sqlite3": "^5.0.2",
29 | "dayjs": "^1.10.7",
30 | "bcrypt": "^5.0.1",
31 | "node-os-utils": "^1.3.5",
32 | "queue-fifo":"^0.2.6",
33 | "express-rate-limit":"^5.5.1"
34 | },
35 | "devDependencies": {
36 | "eslint": "^8.5.0",
37 | "prettier": "^2.5.1"
38 | },
39 | "bugs": {
40 | "url": "https://github.com/marcus-j-davies/nvr-js/issues"
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/marcus-j-davies/nvr-js.git"
45 | },
46 | "homepage": "https://github.com/marcus-j-davies/nvr-js#readme"
47 | }
48 |
--------------------------------------------------------------------------------
/readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/readme.png
--------------------------------------------------------------------------------
/web/dash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NVR JS : Dashboard
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |

33 |
34 |
35 |
36 |
37 | |
38 |
39 |
40 | |
41 |
42 |
43 | |
44 |
45 |
46 | |
47 |
48 |
49 | |
50 |
51 |
52 | |
53 |
54 |
55 | |
56 |
57 |
58 | |
59 |
60 |
61 |
62 |
63 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NVR JS : Login
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Login
34 |

35 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/web/static/css/default.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto';
3 | src: url('fonts/Roboto-Light.ttf');
4 | }
5 |
6 | html,
7 | body {
8 | padding: 0;
9 | margin: 0;
10 | height: 100%;
11 | }
12 |
13 | body {
14 | font-size: 16px;
15 | font-family: Roboto, HelveticaNeue-Light, 'Calibri Light';
16 | }
17 |
18 | .floatingDiv {
19 | position: absolute;
20 | left: 50%;
21 | top: 50%;
22 | transform: translate(-50%, -50%);
23 | overflow: hidden;
24 | min-height: 400px;
25 | min-width: 500px;
26 | -webkit-border-radius: 8px;
27 | -moz-border-radius: 8px;
28 | border-radius: 8px;
29 | border-color: #3b5998;
30 | border-width: 2px;
31 | border-style: solid;
32 | -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
33 | -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
34 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
35 | background-color: rgb(235,235,235);
36 | }
37 |
38 | .floatingDiv .title {
39 | width: 100%;
40 | height: 30px;
41 | line-height: 30px;
42 | padding-left: 5px;
43 | color: white;
44 | background-color: #3b5998;
45 | }
46 |
47 | input[type='button'] {
48 | background-color: #3b5998;
49 | color: white;
50 | border: none;
51 | padding: 5px;
52 | min-width: 80px;
53 | -webkit-border-radius: 4px;
54 | -moz-border-radius: 4px;
55 | border-radius: 4px;
56 | }
57 | input[type='password'], input[type='text'] {
58 | border-style: solid;
59 | border-width: 1px;
60 | border-color: #3b5998;
61 | border-spacing: 10px;
62 | border-radius: 4px;
63 | height: 20px;
64 | }
65 |
66 | .topPanel {
67 | color: white;
68 | padding-right: 30px;
69 | padding-left: 30px;
70 | box-sizing: border-box;
71 | background-color: #3b5998;
72 | height: 100px;
73 | border: none;
74 | border-bottom-color: #3b5998;
75 | border-bottom-width: 3px;
76 | border-bottom-style: solid;
77 | -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
78 | -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
79 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
80 | position: relative;
81 | }
82 |
83 | .sideBar {
84 | width: 270px;
85 | height: calc(100% - 120px);
86 | border: none;
87 | border-right-color: #3b5998;
88 | border-right-width: 3px;
89 | border-right-style: solid;
90 | background-color: rgb(235,235,235);
91 | -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
92 | -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
93 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
94 | padding: 10px;
95 | text-align: center;
96 | }
97 |
98 | .sideBar .cameraPanel {
99 | width: 90%;
100 | border-color: #3b5998;
101 | border-style: solid;
102 | height: 130px;
103 | border-width: 2px;
104 | -webkit-border-radius: 6px;
105 | -moz-border-radius: 6px;
106 | border-radius: 6px;
107 | margin: auto;
108 | background-color: white;
109 | -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
110 | -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
111 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
112 | margin-bottom: 10px;
113 | overflow: hidden;
114 | }
115 |
116 | .cameraPanel .title {
117 | width: 100%;
118 | height: 30px;
119 | line-height: 30px;
120 | text-align: left;
121 | padding-left: 5px;
122 | font-family: Roboto;
123 | color: white;
124 | background-color: #3b5998;
125 |
126 | }
127 |
128 | .cameraPanel .snapShot {
129 | width: 120px;
130 | border-radius: 4px;
131 | }
132 | .cameraPanel .cameraPanelOption {
133 | padding: 5px;
134 | margin-top: 5px;
135 | }
136 |
137 | .videoLive {
138 | width: 100%;
139 | height: 95%;
140 | margin: auto;
141 | }
142 | .videoScrub {
143 | width: 100%;
144 | height: 70%;
145 | margin: auto;
146 | }
147 |
148 | .ui-dialog-buttonset {
149 | font-size: 12px;
150 | }
151 |
152 | .videoButton {
153 | width: 100%;
154 | height: 30px;
155 | margin-bottom: 5px;
156 | }
157 |
158 | .ui-widget.ui-widget-content{
159 | border: 2px solid #3b5998 !important;
160 | overflow: hidden !important;
161 | -webkit-border-radius: 6px;
162 | -moz-border-radius: 6px;
163 | border-radius: 6px;
164 | padding: 0px;
165 | -webkit-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
166 | -moz-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
167 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
168 | }
169 |
170 | .ui-widget-header{
171 | border: none !important;
172 | background: #3b5998 !important;
173 | color: white !important;
174 | font-weight: normal !important;
175 | border-radius: 0px !important;
176 | }
177 |
--------------------------------------------------------------------------------
/web/static/css/font-awesome.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
4 | */@font-face{font-family:'FontAwesome';src:url('fonts/fontawesome-webfont.eot?v=4.7.0');src:url('fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
5 |
--------------------------------------------------------------------------------
/web/static/css/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/web/static/css/fonts/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/Roboto-Light.ttf
--------------------------------------------------------------------------------
/web/static/css/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/web/static/css/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/web/static/css/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/web/static/css/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_444444_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_444444_256x240.png
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_555555_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_555555_256x240.png
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_777620_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_777620_256x240.png
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_777777_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_777777_256x240.png
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_cc0000_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_cc0000_256x240.png
--------------------------------------------------------------------------------
/web/static/css/images/ui-icons_ffffff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/css/images/ui-icons_ffffff_256x240.png
--------------------------------------------------------------------------------
/web/static/css/jquery-ui.min.css:
--------------------------------------------------------------------------------
1 | /*! jQuery UI - v1.13.0 - 2021-10-31
2 | * http://jqueryui.com
3 | * Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css
4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */
6 |
7 | .ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;-ms-filter:"alpha(opacity=0)"}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;-ms-filter:"alpha(opacity=25)";opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;-ms-filter:"alpha(opacity=70)";font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;-ms-filter:"alpha(opacity=35)";background-image:none}.ui-state-disabled .ui-icon{-ms-filter:"alpha(opacity=35)"}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;-ms-filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666}
--------------------------------------------------------------------------------
/web/static/images/CPU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/images/CPU.png
--------------------------------------------------------------------------------
/web/static/images/HDD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/images/HDD.png
--------------------------------------------------------------------------------
/web/static/images/LogoSmall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/images/LogoSmall.png
--------------------------------------------------------------------------------
/web/static/images/RAM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/images/RAM.png
--------------------------------------------------------------------------------
/web/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcus-j-davies/nvr-js/8f8922eb6c7303b72d431aebdc23e1e15ae5a606/web/static/images/logo.png
--------------------------------------------------------------------------------
/web/static/js/canvas2image.js:
--------------------------------------------------------------------------------
1 | /**
2 | * covert canvas to image
3 | * and save the image file
4 | */
5 |
6 | var Canvas2Image = function () {
7 |
8 | // check if support sth.
9 | var $support = function () {
10 | var canvas = document.createElement('canvas'),
11 | ctx = canvas.getContext('2d');
12 |
13 | return {
14 | canvas: !!ctx,
15 | imageData: !!ctx.getImageData,
16 | dataURL: !!canvas.toDataURL,
17 | btoa: !!window.btoa
18 | };
19 | }();
20 |
21 | var downloadMime = 'image/octet-stream';
22 |
23 | function scaleCanvas (canvas, width, height) {
24 | var w = canvas.width,
25 | h = canvas.height;
26 | if (width == undefined) {
27 | width = w;
28 | }
29 | if (height == undefined) {
30 | height = h;
31 | }
32 |
33 | var retCanvas = document.createElement('canvas');
34 | var retCtx = retCanvas.getContext('2d');
35 | retCanvas.width = width;
36 | retCanvas.height = height;
37 | retCtx.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
38 | return retCanvas;
39 | }
40 |
41 | function getDataURL (canvas, type, width, height) {
42 | canvas = scaleCanvas(canvas, width, height);
43 | return canvas.toDataURL(type);
44 | }
45 |
46 | function saveFile (strData,filename) {
47 | var save_link = document.createElement('a');
48 | save_link.href = strData;
49 | save_link.download = filename;
50 | var event = new MouseEvent('click',{"bubbles":false, "cancelable":false});
51 | save_link.dispatchEvent(event);
52 |
53 | }
54 |
55 | function genImage(strData) {
56 | var img = document.createElement('img');
57 | img.src = strData;
58 | return img;
59 | }
60 | function fixType (type) {
61 | type = type.toLowerCase().replace(/jpg/i, 'jpeg');
62 | var r = type.match(/png|jpeg|bmp|gif/)[0];
63 | return 'image/' + r;
64 | }
65 | function encodeData (data) {
66 | if (!window.btoa) { throw 'btoa undefined' }
67 | var str = '';
68 | if (typeof data == 'string') {
69 | str = data;
70 | } else {
71 | for (var i = 0; i < data.length; i ++) {
72 | str += String.fromCharCode(data[i]);
73 | }
74 | }
75 |
76 | return btoa(str);
77 | }
78 | function getImageData (canvas) {
79 | var w = canvas.width,
80 | h = canvas.height;
81 | return canvas.getContext('2d').getImageData(0, 0, w, h);
82 | }
83 | function makeURI (strData, type) {
84 | return 'data:' + type + ';base64,' + strData;
85 | }
86 |
87 |
88 | /**
89 | * create bitmap image
90 | * 按照规则生成图片响应头和响应体
91 | */
92 | var genBitmapImage = function (oData) {
93 |
94 | //
95 | // BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
96 | // BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
97 | //
98 |
99 | var biWidth = oData.width;
100 | var biHeight = oData.height;
101 | var biSizeImage = biWidth * biHeight * 3;
102 | var bfSize = biSizeImage + 54; // total header size = 54 bytes
103 |
104 | //
105 | // typedef struct tagBITMAPFILEHEADER {
106 | // WORD bfType;
107 | // DWORD bfSize;
108 | // WORD bfReserved1;
109 | // WORD bfReserved2;
110 | // DWORD bfOffBits;
111 | // } BITMAPFILEHEADER;
112 | //
113 | var BITMAPFILEHEADER = [
114 | // WORD bfType -- The file type signature; must be "BM"
115 | 0x42, 0x4D,
116 | // DWORD bfSize -- The size, in bytes, of the bitmap file
117 | bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
118 | // WORD bfReserved1 -- Reserved; must be zero
119 | 0, 0,
120 | // WORD bfReserved2 -- Reserved; must be zero
121 | 0, 0,
122 | // DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
123 | 54, 0, 0, 0
124 | ];
125 |
126 | //
127 | // typedef struct tagBITMAPINFOHEADER {
128 | // DWORD biSize;
129 | // LONG biWidth;
130 | // LONG biHeight;
131 | // WORD biPlanes;
132 | // WORD biBitCount;
133 | // DWORD biCompression;
134 | // DWORD biSizeImage;
135 | // LONG biXPelsPerMeter;
136 | // LONG biYPelsPerMeter;
137 | // DWORD biClrUsed;
138 | // DWORD biClrImportant;
139 | // } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
140 | //
141 | var BITMAPINFOHEADER = [
142 | // DWORD biSize -- The number of bytes required by the structure
143 | 40, 0, 0, 0,
144 | // LONG biWidth -- The width of the bitmap, in pixels
145 | biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
146 | // LONG biHeight -- The height of the bitmap, in pixels
147 | biHeight & 0xff, biHeight >> 8 & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
148 | // WORD biPlanes -- The number of planes for the target device. This value must be set to 1
149 | 1, 0,
150 | // WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
151 | // has a maximum of 2^24 colors (16777216, Truecolor)
152 | 24, 0,
153 | // DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
154 | 0, 0, 0, 0,
155 | // DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
156 | biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
157 | // LONG biXPelsPerMeter, unused
158 | 0,0,0,0,
159 | // LONG biYPelsPerMeter, unused
160 | 0,0,0,0,
161 | // DWORD biClrUsed, the number of color indexes of palette, unused
162 | 0,0,0,0,
163 | // DWORD biClrImportant, unused
164 | 0,0,0,0
165 | ];
166 |
167 | var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
168 |
169 | var aImgData = oData.data;
170 |
171 | var strPixelData = '';
172 | var biWidth4 = biWidth<<2;
173 | var y = biHeight;
174 | var fromCharCode = String.fromCharCode;
175 |
176 | do {
177 | var iOffsetY = biWidth4*(y-1);
178 | var strPixelRow = '';
179 | for (var x = 0; x < biWidth; x++) {
180 | var iOffsetX = x<<2;
181 | strPixelRow += fromCharCode(aImgData[iOffsetY+iOffsetX+2]) +
182 | fromCharCode(aImgData[iOffsetY+iOffsetX+1]) +
183 | fromCharCode(aImgData[iOffsetY+iOffsetX]);
184 | }
185 |
186 | for (var c = 0; c < iPadding; c++) {
187 | strPixelRow += String.fromCharCode(0);
188 | }
189 |
190 | strPixelData += strPixelRow;
191 | } while (--y);
192 |
193 | var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
194 |
195 | return strEncoded;
196 | };
197 |
198 |
199 | /**
200 | * [saveAsImage]
201 | * @param {[obj]} canvas [canvasElement]
202 | * @param {[Number]} width [optional] png width
203 | * @param {[Number]} height [optional] png height
204 | * @param {[String]} type [image type]
205 | * @param {[String]} filename [image filename]
206 | * @return {[type]} [description]
207 | */
208 | var saveAsImage = function (canvas, width, height, type,filename) {
209 | if ($support.canvas && $support.dataURL) {
210 | if (typeof canvas == "string") { canvas = document.getElementById(canvas); }
211 | if (type == undefined) { type = 'png'; }
212 | filename = filename == undefined||filename.length === 0 ?Date.now()+'.'+type: filename+'.'+type
213 | type = fixType(type);
214 |
215 | if (/bmp/.test(type)) {
216 | var data = getImageData(scaleCanvas(canvas, width, height));
217 | var strData = genBitmapImage(data);
218 |
219 | saveFile(makeURI(strData, downloadMimedownloadMime),filename);
220 | } else {
221 | var strData = getDataURL(canvas, type, width, height);
222 | saveFile(strData.replace(type, downloadMime),filename);
223 | }
224 | }
225 | };
226 |
227 | var convertToImage = function (canvas, width, height, type) {
228 | if ($support.canvas && $support.dataURL) {
229 | if (typeof canvas == "string") { canvas = document.getElementById(canvas); }
230 | if (type == undefined) { type = 'png'; }
231 | type = fixType(type);
232 |
233 | if (/bmp/.test(type)) {
234 | var data = getImageData(scaleCanvas(canvas, width, height));
235 | var strData = genBitmapImage(data);
236 | return genImage(makeURI(strData, 'image/bmp'));
237 | } else {
238 | var strData = getDataURL(canvas, type, width, height);
239 | return genImage(strData);
240 | }
241 | }
242 | };
243 |
244 |
245 | return {
246 | saveAsImage: saveAsImage,
247 | saveAsPNG: function (canvas, width, height, fileName) {
248 | return saveAsImage(canvas, width, height, 'png',fileName);
249 | },
250 | saveAsJPEG: function (canvas, width, height, fileName) {
251 | return saveAsImage(canvas, width, height, 'jpeg',fileName);
252 | },
253 | saveAsGIF: function (canvas, width, height, fileName) {
254 | return saveAsImage(canvas, width, height, 'gif',fileName);
255 | },
256 | saveAsBMP: function (canvas, width, height, fileName) {
257 | return saveAsImage(canvas, width, height, 'bmp',fileName);
258 | },
259 |
260 | convertToImage: convertToImage,
261 | convertToPNG: function (canvas, width, height) {
262 | return convertToImage(canvas, width, height, 'png');
263 | },
264 | convertToJPEG: function (canvas, width, height) {
265 | return convertToImage(canvas, width, height, 'jpeg');
266 | },
267 | convertToGIF: function (canvas, width, height) {
268 | return convertToImage(canvas, width, height, 'gif');
269 | },
270 | convertToBMP: function (canvas, width, height) {
271 | return convertToImage(canvas, width, height, 'bmp');
272 | }
273 | };
274 |
275 | }();
276 |
--------------------------------------------------------------------------------
/web/static/js/customParseFormat.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs_plugin_customParseFormat=e()}(this,(function(){"use strict";var t={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},e=/(\[[^[]*\])|([-:/.()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,n=/\d\d/,r=/\d\d?/,i=/\d*[^\s\d-_:/()]+/,o={},s=function(t){return(t=+t)+(t>68?1900:2e3)};var a=function(t){return function(e){this[t]=+e}},f=[/[+-]\d\d:?(\d\d)?|Z/,function(t){(this.zone||(this.zone={})).offset=function(t){if(!t)return 0;if("Z"===t)return 0;var e=t.match(/([+-]|\d\d)/g),n=60*e[1]+(+e[2]||0);return 0===n?0:"+"===e[0]?-n:n}(t)}],u=function(t){var e=o[t];return e&&(e.indexOf?e:e.s.concat(e.f))},h=function(t,e){var n,r=o.meridiem;if(r){for(var i=1;i<=24;i+=1)if(t.indexOf(r(i,0,e))>-1){n=i>12;break}}else n=t===(e?"pm":"PM");return n},d={A:[i,function(t){this.afternoon=h(t,!1)}],a:[i,function(t){this.afternoon=h(t,!0)}],S:[/\d/,function(t){this.milliseconds=100*+t}],SS:[n,function(t){this.milliseconds=10*+t}],SSS:[/\d{3}/,function(t){this.milliseconds=+t}],s:[r,a("seconds")],ss:[r,a("seconds")],m:[r,a("minutes")],mm:[r,a("minutes")],H:[r,a("hours")],h:[r,a("hours")],HH:[r,a("hours")],hh:[r,a("hours")],D:[r,a("day")],DD:[n,a("day")],Do:[i,function(t){var e=o.ordinal,n=t.match(/\d+/);if(this.day=n[0],e)for(var r=1;r<=31;r+=1)e(r).replace(/\[|\]/g,"")===t&&(this.day=r)}],M:[r,a("month")],MM:[n,a("month")],MMM:[i,function(t){var e=u("months"),n=(u("monthsShort")||e.map((function(t){return t.substr(0,3)}))).indexOf(t)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[i,function(t){var e=u("months").indexOf(t)+1;if(e<1)throw new Error;this.month=e%12||e}],Y:[/[+-]?\d+/,a("year")],YY:[n,function(t){this.year=s(t)}],YYYY:[/\d{4}/,a("year")],Z:f,ZZ:f};function c(n){var r,i;r=n,i=o&&o.formats;for(var s=(n=r.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(e,n,r){var o=r&&r.toUpperCase();return n||i[r]||t[r]||i[o].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(t,e,n){return e||n.slice(1)}))}))).match(e),a=s.length,f=0;f-1)return new Date(("X"===e?1e3:1)*t);var r=c(e)(t),i=r.year,o=r.month,s=r.day,a=r.hours,f=r.minutes,u=r.seconds,h=r.milliseconds,d=r.zone,l=new Date,m=s||(i||o?1:l.getDate()),M=i||l.getFullYear(),Y=0;i&&!o||(Y=o>0?o-1:l.getMonth());var p=a||0,v=f||0,D=u||0,g=h||0;return d?new Date(Date.UTC(M,Y,m,p,v,D,g+60*d.offset*1e3)):n?new Date(Date.UTC(M,Y,m,p,v,D,g)):new Date(M,Y,m,p,v,D,g)}catch(t){return new Date("")}}(e,a,r),this.init(),d&&!0!==d&&(this.$L=this.locale(d).$L),h&&e!=this.format(a)&&(this.$d=new Date("")),o={}}else if(a instanceof Array)for(var l=a.length,m=1;m<=l;m+=1){s[1]=a[m-1];var M=n.apply(this,s);if(M.isValid()){this.$d=M.$d,this.$L=M.$L,this.init();break}m===l&&(this.$d=new Date(""))}else i.call(this,t)}}}));
--------------------------------------------------------------------------------
/web/static/js/dayjs.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",f="month",h="quarter",c="year",d="date",$="Invalid Date",l=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},g={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date() {
8 | $.ajax({
9 | type: 'GET',
10 | url: '/systeminfo',
11 | dataType: 'json',
12 | success: function (data) {
13 | $('#CPU').html(data.CPU + '%');
14 | $('#RAM').html(data.MEM.usedMemPercentage + '%');
15 | $('#DISK').html(data.DISK.usedPercentage + '%');
16 | }
17 | });
18 | };
19 | Update();
20 | setInterval(() => {
21 | Update();
22 | }, 5000);
23 | }
24 |
25 | function StartTimeline(ID, Name) {
26 | const contents = $('#scrub').html();
27 | const copy = $('');
28 | copy.append(contents);
29 | copy.dialog({
30 | width: 855,
31 | height: 630,
32 | modal: true,
33 | title: Name + ' Timeline Viewer',
34 | close: function () {
35 | const VideoElement = copy.find('video');
36 | const VE5 = $(VideoElement)[0];
37 | VE5.pause();
38 | VE5.remove();
39 | }
40 | });
41 |
42 | const TimelineDiv = copy.find('#timeline');
43 | const TL = $(TimelineDiv)[0];
44 |
45 | const Groups = new vis.DataSet([
46 | { content: 'Video', id: 'Video', value: 1 },
47 | { content: 'Events', id: 'Events', value: 2 }
48 | ]);
49 |
50 | const Items = new vis.DataSet([
51 | { id: 1, content: 'Display Fix', start: '2000-01-01' }
52 | ]);
53 |
54 | const Options = {
55 | start: dayjs().subtract(1, 'hour').toDate(),
56 | end: dayjs().add(15, 'minutes').toDate(),
57 | groupOrder: function (a, b) {
58 | return a.value - b.value;
59 | },
60 | height: 150,
61 | editable: false,
62 | groupEditable: false,
63 | stack: false,
64 | rollingMode: {
65 | follow: false,
66 | offset: 0.5
67 | }
68 | };
69 |
70 | // create a Timeline
71 | const timeline = new vis.Timeline(TL, Items, Groups, Options);
72 | Items.remove(1);
73 |
74 | let CommitTimeout;
75 | let isMoving = false;
76 | timeline.on('rangechange', (event) => {
77 | clearTimeout(CommitTimeout);
78 | isMoving = true;
79 | });
80 | timeline.on('rangechanged', (event) => {
81 | CommitTimeout = setTimeout(() => {
82 | const Start = dayjs(event.start)
83 | .subtract(SearchTimebufferHours, 'hour')
84 | .unix();
85 | const End = dayjs(event.end).add(SearchTimebufferHours, 'hour').unix();
86 | GetSegmentsAndEvents(timeline, Items, Start, End, ID);
87 | isMoving = false;
88 | }, 500);
89 | });
90 | timeline.on('click', (event) => {
91 | if (!isMoving) {
92 | timeline.setCurrentTime(event.time);
93 | LoadAndPosition(timeline, event.time, copy, ID);
94 | }
95 | isMoving = false;
96 | });
97 | }
98 |
99 | let VideoFile;
100 | function LoadAndPosition(Timeline, Date, Copy, ID) {
101 | const VideoElement = Copy.find('video');
102 | const VE5 = $(VideoElement)[0];
103 | const Time = dayjs(Date).unix();
104 | const MatchedSegments = Segments.filter(
105 | (S) => S.Start <= Time && S.End >= Time
106 | )[0];
107 | const VideoStart = MatchedSegments.Start;
108 |
109 | const URL = '/segments/' + ID + '/' + MatchedSegments.FileName;
110 |
111 | let StartTime = Time - VideoStart;
112 | if (StartTime < 0) {
113 | StartTime = 0;
114 | }
115 |
116 | if (VideoFile === undefined || VideoFile !== URL) {
117 | if (!VE5.paused) {
118 | VE5.pause();
119 | }
120 |
121 | VideoElement.off('timeupdate');
122 | VideoElement.on('timeupdate', (event) => {
123 | const Date = dayjs
124 | .unix(MatchedSegments.Start)
125 | .add(VE5.currentTime, 'second')
126 | .toDate();
127 | Timeline.setCurrentTime(Date);
128 | });
129 |
130 | VideoElement.one('canplay', () => {
131 | VE5.play().then((R) => {
132 | VE5.currentTime = StartTime;
133 | });
134 | });
135 | VE5.src = '/segments/' + ID + '/' + MatchedSegments.FileName;
136 | VideoFile = URL;
137 | } else {
138 | VE5.currentTime = StartTime;
139 | if (VE5.paused) {
140 | VE5.play();
141 | }
142 | }
143 | }
144 |
145 | function EventSort(a, b) {
146 | return a.Start - b.Start;
147 | }
148 |
149 | function GetSegmentsAndEvents(Timeline, DataSet, Start, End, ID) {
150 | $.getJSON('/geteventdata/' + ID + '/' + Start + '/' + End, function (data) {
151 | data.segments.sort(EventSort);
152 | Segments = data.segments;
153 |
154 | DataSet.clear();
155 | for (let i = 0; i < data.segments.length; i++) {
156 | const Seg = data.segments[i];
157 | const Start = dayjs.unix(Seg.Start);
158 | const End = dayjs.unix(Seg.End);
159 |
160 | DataSet.add({
161 | start: Start.toDate(),
162 | end: End.toDate(),
163 | type: 'background',
164 | group: 'Video',
165 | content: Start.format('YYYY-MM-DD HH:mm:ss'),
166 | style:
167 | 'background-color: rgba(0,0,0,0.5);color: white;border-radius: 6px;',
168 | fileName: Seg.FileName,
169 | cameraId: Seg.CameraID,
170 | segmentId: Seg.SegmentID
171 | });
172 | }
173 |
174 | for (let i = 0; i < data.events.length; i++) {
175 | const Event = data.events[i];
176 | const Start = dayjs.unix(Event.Date);
177 |
178 | DataSet.add({
179 | start: Start.toDate(),
180 | group: 'Events',
181 | content: Event.Name,
182 | style: 'background-color: orangered;color: white;border-radius: 6px;'
183 | });
184 | }
185 |
186 | Timeline.redraw();
187 | });
188 | }
189 |
190 | function StartLive(ID, Name, Codec) {
191 | let buffer;
192 | let socket;
193 |
194 | const contents = $('#liveView').html();
195 | const copy = $('');
196 | copy.append(contents);
197 | const VideoElement = copy.find('video');
198 | const VE5 = $(VideoElement)[0];
199 | copy.dialog({
200 | width: 520,
201 | height: 410,
202 | title: Name + ' (Live)',
203 | close: function () {
204 | socket.disconnect();
205 | VE5.pause();
206 | VE5.remove();
207 | },
208 | buttons: {
209 | 'Full Screen': function () {
210 | goFullscreen(VE5);
211 | },
212 | Snapshot: function () {
213 | const canvas = document.createElement('canvas');
214 | canvas.width = VE5.videoWidth;
215 | canvas.height = VE5.videoHeight;
216 | const ctx = canvas.getContext('2d');
217 | ctx.drawImage(VE5, 0, 0, canvas.width, canvas.height);
218 | Canvas2Image.saveAsJPEG(canvas);
219 | canvas.remove();
220 | }
221 | }
222 | });
223 |
224 | if (!MediaSource.isTypeSupported(Codec)) {
225 | alert('Unsupported mime type');
226 | return;
227 | }
228 |
229 | const mediaSource = new MediaSource();
230 | const DataURL = URL.createObjectURL(mediaSource);
231 | VE5.src = DataURL;
232 |
233 | mediaSource.addEventListener('sourceopen', function (e) {
234 | buffer = mediaSource.addSourceBuffer(Codec);
235 | buffer.mode = 'sequence';
236 | buffer.addEventListener('updateend', function (e) {
237 | if (
238 | mediaSource.duration !== Number.POSITIVE_INFINITY &&
239 | VE5.currentTime === 0 &&
240 | mediaSource.duration > 0
241 | ) {
242 | VE5.currentTime = mediaSource.duration - 1;
243 | mediaSource.duration = Number.POSITIVE_INFINITY;
244 | }
245 |
246 | VE5.play();
247 | });
248 |
249 | socket = io('/', { path: '/streams/' + ID });
250 | socket.on('segment', function (data) {
251 | data = new Uint8Array(data);
252 | buffer.appendBuffer(data);
253 | });
254 | });
255 | }
256 |
257 | function goFullscreen(element) {
258 | if (element.mozRequestFullScreen) {
259 | element.mozRequestFullScreen();
260 | } else if (element.webkitRequestFullScreen) {
261 | element.webkitRequestFullScreen();
262 | }
263 | }
264 |
265 | function Login() {
266 | const Data = {
267 | password: $('#Password').val(),
268 | username: $('#Username').val()
269 | };
270 | $.ajax({
271 | type: 'POST',
272 | url: '/login',
273 | data: JSON.stringify(Data),
274 | contentType: 'application/json; charset=utf-8',
275 | success: function () {
276 | document.location = '/dashboard';
277 | },
278 | error: function () {
279 | alert('Could not login. This may be due to incorrect login details');
280 | }
281 | });
282 | }
283 |
--------------------------------------------------------------------------------
/web/static/js/socket.io.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Socket.IO v4.3.1
3 | * (c) 2014-2021 Guillermo Rauch
4 | * Released under the MIT License.
5 | */
6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).io=e()}(this,(function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,i=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw i}}}}var d=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,y=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],v=function(t){var e=t,n=t.indexOf("["),r=t.indexOf("]");-1!=n&&-1!=r&&(t=t.substring(0,n)+t.substring(n,r).replace(/:/g,";")+t.substring(r,t.length));for(var o,i,s=d.exec(t||""),a={},c=14;c--;)a[y[c]]=s[c]||"";return-1!=n&&-1!=r&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a.pathNames=function(t,e){var n=/\/{2,9}/g,r=e.replace(n,"/").split("/");"/"!=e.substr(0,1)&&0!==e.length||r.splice(0,1);"/"==e.substr(e.length-1,1)&&r.splice(r.length-1,1);return r}(0,a.path),a.queryKey=(o=a.query,i={},o.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,e,n){e&&(i[e]=n)})),i),a};var m={exports:{}};try{m.exports="undefined"!=typeof XMLHttpRequest&&"withCredentials"in new XMLHttpRequest}catch(t){m.exports=!1}var g=m.exports,k="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")();function b(t){var e=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!e||g))return new XMLHttpRequest}catch(t){}if(!e)try{return new(k[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}function w(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r1?{type:C[n],data:t.substring(1)}:{type:C[n]}:S},M=function(t,e){if(I){var n=function(t){var e,n,r,o,i,s=.75*t.length,a=t.length,c=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var u=new ArrayBuffer(s),h=new Uint8Array(u);for(e=0;e>4,h[c++]=(15&r)<<4|o>>2,h[c++]=(3&o)<<6|63&i;return u}(t);return U(n,e)}return{base64:!0,data:t}},U=function(t,e){return"blob"===e&&t instanceof ArrayBuffer?new Blob([t]):t},V=String.fromCharCode(30),H=function(t){i(o,t);var n=h(o);function o(t){var r;return e(this,o),(r=n.call(this)).writable=!1,A(c(r),t),r.opts=t,r.query=t.query,r.readyState="",r.socket=t.socket,r}return r(o,[{key:"onError",value:function(t,e){var n=new Error(t);return n.type="TransportError",n.description=e,f(s(o.prototype),"emit",this).call(this,"error",n),this}},{key:"open",value:function(){return"closed"!==this.readyState&&""!==this.readyState||(this.readyState="opening",this.doOpen()),this}},{key:"close",value:function(){return"opening"!==this.readyState&&"open"!==this.readyState||(this.doClose(),this.onClose()),this}},{key:"send",value:function(t){"open"===this.readyState&&this.write(t)}},{key:"onOpen",value:function(){this.readyState="open",this.writable=!0,f(s(o.prototype),"emit",this).call(this,"open")}},{key:"onData",value:function(t){var e=F(t,this.socket.binaryType);this.onPacket(e)}},{key:"onPacket",value:function(t){f(s(o.prototype),"emit",this).call(this,"packet",t)}},{key:"onClose",value:function(){this.readyState="closed",f(s(o.prototype),"emit",this).call(this,"close")}}]),o}(R),K="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),Y={},z=0,$=0;function W(t){var e="";do{e=K[t%64]+e,t=Math.floor(t/64)}while(t>0);return e}function J(){var t=W(+new Date);return t!==D?(z=0,D=t):t+"."+W(z++)}for(;$<64;$++)Y[K[$]]=$;J.encode=W,J.decode=function(t){var e=0;for($=0;$0&&void 0!==arguments[0]?arguments[0]:{};return o(t,{xd:this.xd,xs:this.xs},this.opts),new nt(this.uri(),t)}},{key:"doWrite",value:function(t,e){var n=this,r=this.request({method:"POST",data:t});r.on("success",e),r.on("error",(function(t){n.onError("xhr post error",t)}))}},{key:"doPoll",value:function(){var t=this,e=this.request();e.on("data",this.onData.bind(this)),e.on("error",(function(e){t.onError("xhr poll error",e)})),this.pollXhr=e}}]),s}(Q),nt=function(t){i(o,t);var n=h(o);function o(t,r){var i;return e(this,o),A(c(i=n.call(this)),r),i.opts=r,i.method=r.method||"GET",i.uri=t,i.async=!1!==r.async,i.data=void 0!==r.data?r.data:null,i.create(),i}return r(o,[{key:"create",value:function(){var t=this,e=w(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");e.xdomain=!!this.opts.xd,e.xscheme=!!this.opts.xs;var n=this.xhr=new b(e);try{n.open(this.method,this.uri,this.async);try{if(this.opts.extraHeaders)for(var r in n.setDisableHeaderCheck&&n.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(r)&&n.setRequestHeader(r,this.opts.extraHeaders[r])}catch(t){}if("POST"===this.method)try{n.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(t){}try{n.setRequestHeader("Accept","*/*")}catch(t){}"withCredentials"in n&&(n.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(n.timeout=this.opts.requestTimeout),n.onreadystatechange=function(){4===n.readyState&&(200===n.status||1223===n.status?t.onLoad():t.setTimeoutFn((function(){t.onError("number"==typeof n.status?n.status:0)}),0))},n.send(this.data)}catch(e){return void this.setTimeoutFn((function(){t.onError(e)}),0)}"undefined"!=typeof document&&(this.index=o.requestsCount++,o.requests[this.index]=this)}},{key:"onSuccess",value:function(){this.emit("success"),this.cleanup()}},{key:"onData",value:function(t){this.emit("data",t),this.onSuccess()}},{key:"onError",value:function(t){this.emit("error",t),this.cleanup(!0)}},{key:"cleanup",value:function(t){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=Z,t)try{this.xhr.abort()}catch(t){}"undefined"!=typeof document&&delete o.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var t=this.xhr.responseText;null!==t&&this.onData(t)}},{key:"abort",value:function(){this.cleanup()}}]),o}(R);if(nt.requestsCount=0,nt.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",rt);else if("function"==typeof addEventListener){addEventListener("onpagehide"in k?"pagehide":"unload",rt,!1)}function rt(){for(var t in nt.requests)nt.requests.hasOwnProperty(t)&&nt.requests[t].abort()}var ot="function"==typeof Promise&&"function"==typeof Promise.resolve?function(t){return Promise.resolve().then(t)}:function(t,e){return e(t,0)},it=k.WebSocket||k.MozWebSocket,st="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),at=function(t){i(o,t);var n=h(o);function o(t){var r;return e(this,o),(r=n.call(this,t)).supportsBinary=!t.forceBase64,r}return r(o,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var t=this.uri(),e=this.opts.protocols,n=st?{}:w(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=st?new it(t,e,n):e?new it(t,e):new it(t)}catch(t){return this.emit("error",t)}this.ws.binaryType=this.socket.binaryType||"arraybuffer",this.addEventListeners()}}},{key:"addEventListeners",value:function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws._socket.unref(),t.onOpen()},this.ws.onclose=this.onClose.bind(this),this.ws.onmessage=function(e){return t.onData(e.data)},this.ws.onerror=function(e){return t.onError("websocket error",e)}}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=function(n){var r=t[n],o=n===t.length-1;x(r,e.supportsBinary,(function(t){try{e.ws.send(t)}catch(t){}o&&ot((function(){e.writable=!0,e.emit("drain")}),e.setTimeoutFn)}))},r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return e(this,a),r=s.call(this),n&&"object"===t(n)&&(i=n,n=null),n?(n=v(n),i.hostname=n.host,i.secure="https"===n.protocol||"wss"===n.protocol,i.port=n.port,n.query&&(i.query=n.query)):i.host&&(i.hostname=v(i.host).host),A(c(r),i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=i.transports||["polling","websocket"],r.readyState="",r.writeBuffer=[],r.prevBufferLen=0,r.opts=o({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!0},i),r.opts.path=r.opts.path.replace(/\/$/,"")+"/","string"==typeof r.opts.query&&(r.opts.query=G.decode(r.opts.query)),r.id=null,r.upgrades=null,r.pingInterval=null,r.pingTimeout=null,r.pingTimeoutTimer=null,"function"==typeof addEventListener&&(r.opts.closeOnBeforeunload&&addEventListener("beforeunload",(function(){r.transport&&(r.transport.removeAllListeners(),r.transport.close())}),!1),"localhost"!==r.hostname&&(r.offlineEventListener=function(){r.onClose("transport close")},addEventListener("offline",r.offlineEventListener,!1))),r.open(),r}return r(a,[{key:"createTransport",value:function(t){var e=function(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}(this.opts.query);e.EIO=4,e.transport=t,this.id&&(e.sid=this.id);var n=o({},this.opts.transportOptions[t],this.opts,{query:e,socket:this,hostname:this.hostname,secure:this.secure,port:this.port});return new ct[t](n)}},{key:"open",value:function(){var t,e=this;if(this.opts.rememberUpgrade&&a.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket"))t="websocket";else{if(0===this.transports.length)return void this.setTimeoutFn((function(){e.emitReserved("error","No transports available")}),0);t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(t){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)}},{key:"setTransport",value:function(t){var e=this;this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",this.onDrain.bind(this)).on("packet",this.onPacket.bind(this)).on("error",this.onError.bind(this)).on("close",(function(){e.onClose("transport close")}))}},{key:"probe",value:function(t){var e=this,n=this.createTransport(t),r=!1;a.priorWebsocketSuccess=!1;var o=function(){r||(n.send([{type:"ping",data:"probe"}]),n.once("packet",(function(t){if(!r)if("pong"===t.type&&"probe"===t.data){if(e.upgrading=!0,e.emitReserved("upgrading",n),!n)return;a.priorWebsocketSuccess="websocket"===n.name,e.transport.pause((function(){r||"closed"!==e.readyState&&(f(),e.setTransport(n),n.send([{type:"upgrade"}]),e.emitReserved("upgrade",n),n=null,e.upgrading=!1,e.flush())}))}else{var o=new Error("probe error");o.transport=n.name,e.emitReserved("upgradeError",o)}})))};function i(){r||(r=!0,f(),n.close(),n=null)}var s=function(t){var r=new Error("probe error: "+t);r.transport=n.name,i(),e.emitReserved("upgradeError",r)};function c(){s("transport closed")}function u(){s("socket closed")}function h(t){n&&t.name!==n.name&&i()}var f=function(){n.removeListener("open",o),n.removeListener("error",s),n.removeListener("close",c),e.off("close",u),e.off("upgrading",h)};n.once("open",o),n.once("error",s),n.once("close",c),this.once("close",u),this.once("upgrading",h),n.open()}},{key:"onOpen",value:function(){if(this.readyState="open",a.priorWebsocketSuccess="websocket"===this.transport.name,this.emitReserved("open"),this.flush(),"open"===this.readyState&&this.opts.upgrade&&this.transport.pause)for(var t=0,e=this.upgrades.length;t0;case bt.ACK:case bt.BINARY_ACK:return Array.isArray(n)}}}]),a}(R);var Et=function(){function t(n){e(this,t),this.packet=n,this.buffers=[],this.reconPack=n}return r(t,[{key:"takeBinaryData",value:function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=gt(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null}},{key:"finishedReconstruction",value:function(){this.reconPack=null,this.buffers=[]}}]),t}(),At=Object.freeze({__proto__:null,protocol:5,get PacketType(){return bt},Encoder:wt,Decoder:_t});function Rt(t,e,n){return t.on(e,n),function(){t.off(e,n)}}var Tt=Object.freeze({connect:1,connect_error:1,disconnect:1,disconnecting:1,newListener:1,removeListener:1}),Ot=function(t){i(o,t);var n=h(o);function o(t,r,i){var s;return e(this,o),(s=n.call(this)).connected=!1,s.disconnected=!0,s.receiveBuffer=[],s.sendBuffer=[],s.ids=0,s.acks={},s.flags={},s.io=t,s.nsp=r,i&&i.auth&&(s.auth=i.auth),s.io._autoConnect&&s.open(),s}return r(o,[{key:"subEvents",value:function(){if(!this.subs){var t=this.io;this.subs=[Rt(t,"open",this.onopen.bind(this)),Rt(t,"packet",this.onpacket.bind(this)),Rt(t,"error",this.onerror.bind(this)),Rt(t,"close",this.onclose.bind(this))]}}},{key:"active",get:function(){return!!this.subs}},{key:"connect",value:function(){return this.connected||(this.subEvents(),this.io._reconnecting||this.io.open(),"open"===this.io._readyState&&this.onopen()),this}},{key:"open",value:function(){return this.connect()}},{key:"send",value:function(){for(var t=arguments.length,e=new Array(t),n=0;n1?e-1:0),r=1;r0&&t.jitter<=1?t.jitter:0,this.attempts=0}St.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},St.prototype.reset=function(){this.attempts=0},St.prototype.setMin=function(t){this.ms=t},St.prototype.setMax=function(t){this.max=t},St.prototype.setJitter=function(t){this.jitter=t};var Nt=function(n){i(s,n);var o=h(s);function s(n,r){var i,a;e(this,s),(i=o.call(this)).nsps={},i.subs=[],n&&"object"===t(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",i.opts=r,A(c(i),r),i.reconnection(!1!==r.reconnection),i.reconnectionAttempts(r.reconnectionAttempts||1/0),i.reconnectionDelay(r.reconnectionDelay||1e3),i.reconnectionDelayMax(r.reconnectionDelayMax||5e3),i.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),i.backoff=new Ct({min:i.reconnectionDelay(),max:i.reconnectionDelayMax(),jitter:i.randomizationFactor()}),i.timeout(null==r.timeout?2e4:r.timeout),i._readyState="closed",i.uri=n;var u=r.parser||At;return i.encoder=new u.Encoder,i.decoder=new u.Decoder,i._autoConnect=!1!==r.autoConnect,i._autoConnect&&i.open(),i}return r(s,[{key:"reconnection",value:function(t){return arguments.length?(this._reconnection=!!t,this):this._reconnection}},{key:"reconnectionAttempts",value:function(t){return void 0===t?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}},{key:"reconnectionDelay",value:function(t){var e;return void 0===t?this._reconnectionDelay:(this._reconnectionDelay=t,null===(e=this.backoff)||void 0===e||e.setMin(t),this)}},{key:"randomizationFactor",value:function(t){var e;return void 0===t?this._randomizationFactor:(this._randomizationFactor=t,null===(e=this.backoff)||void 0===e||e.setJitter(t),this)}},{key:"reconnectionDelayMax",value:function(t){var e;return void 0===t?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,null===(e=this.backoff)||void 0===e||e.setMax(t),this)}},{key:"timeout",value:function(t){return arguments.length?(this._timeout=t,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(t){var e=this;if(~this._readyState.indexOf("open"))return this;this.engine=new ut(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var o=Rt(n,"open",(function(){r.onopen(),t&&t()})),i=Rt(n,"error",(function(n){r.cleanup(),r._readyState="closed",e.emitReserved("error",n),t?t(n):r.maybeReconnectOnOpen()}));if(!1!==this._timeout){var s=this._timeout;0===s&&o();var a=this.setTimeoutFn((function(){o(),n.close(),n.emit("error",new Error("timeout"))}),s);this.opts.autoUnref&&a.unref(),this.subs.push((function(){clearTimeout(a)}))}return this.subs.push(o),this.subs.push(i),this}},{key:"connect",value:function(t){return this.open(t)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var t=this.engine;this.subs.push(Rt(t,"ping",this.onping.bind(this)),Rt(t,"data",this.ondata.bind(this)),Rt(t,"error",this.onerror.bind(this)),Rt(t,"close",this.onclose.bind(this)),Rt(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(t){this.decoder.add(t)}},{key:"ondecoded",value:function(t){this.emitReserved("packet",t)}},{key:"onerror",value:function(t){this.emitReserved("error",t)}},{key:"socket",value:function(t,e){var n=this.nsps[t];return n||(n=new Ot(this,t,e),this.nsps[t]=n),n}},{key:"_destroy",value:function(t){for(var e=0,n=Object.keys(this.nsps);e=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){e.skipReconnect||(t.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((function(n){n?(e._reconnecting=!1,e.reconnect(),t.emitReserved("reconnect_error",n)):e.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){clearTimeout(r)}))}}},{key:"onreconnect",value:function(){var t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}]),s}(R),Bt={};function xt(e,n){"object"===t(e)&&(n=e,e=void 0);var r,o=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=t;n=n||"undefined"!=typeof location&&location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==n?n.protocol+"//"+t:"https://"+t),r=v(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var o=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+o+":"+r.port+e,r.href=r.protocol+"://"+o+(n&&n.port===r.port?"":":"+r.port),r}(e,(n=n||{}).path||"/socket.io"),i=o.source,s=o.id,a=o.path,c=Bt[s]&&a in Bt[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||c?r=new Nt(i,n):(Bt[s]||(Bt[s]=new Nt(i,n)),r=Bt[s]),o.query&&!n.query&&(n.query=o.queryKey),r.socket(o.path,n)}return o(xt,{Manager:Nt,Socket:Ot,io:xt,connect:xt}),"undefined"!=typeof module&&(module.exports=xt),xt}));
7 | //# sourceMappingURL=socket.io.min.js.map
8 |
--------------------------------------------------------------------------------
/web/static/js/vis-timeline-graph2d.min.css:
--------------------------------------------------------------------------------
1 | .vis [class*=span]{min-height:0;width:auto}.vis-current-time{background-color:#ff7f6e;width:2px;z-index:1;pointer-events:none}.vis-rolling-mode-btn{height:40px;width:40px;position:absolute;top:7px;right:20px;border-radius:50%;font-size:28px;cursor:pointer;opacity:.8;color:#fff;font-weight:700;text-align:center;background:#3876c2}.vis-rolling-mode-btn:before{content:"\26F6"}.vis-rolling-mode-btn:hover{opacity:1}.vis-graph-group0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis-graph-group1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis-graph-group2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis-graph-group3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis-graph-group4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis-graph-group5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis-graph-group6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis-graph-group7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis-graph-group8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis-graph-group9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis-timeline .vis-fill{fill-opacity:.1;stroke:none}.vis-timeline .vis-bar{fill-opacity:.5;stroke-width:1px}.vis-timeline .vis-point{stroke-width:2px;fill-opacity:1}.vis-timeline .vis-legend-background{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis-timeline .vis-outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis-timeline .vis-icon-fill{fill-opacity:.3;stroke:none}.vis-timeline{position:relative;border:1px solid #bfbfbf;overflow:hidden;padding:0;margin:0;box-sizing:border-box}.vis-loading-screen{width:100%;height:100%;position:absolute;top:0;left:0}.vis-panel{position:absolute;padding:0;margin:0;box-sizing:border-box}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right,.vis-panel.vis-top{border:1px #bfbfbf}.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis-left.vis-panel.vis-vertical-scroll,.vis-right.vis-panel.vis-vertical-scroll{height:100%;overflow-x:hidden;overflow-y:scroll}.vis-left.vis-panel.vis-vertical-scroll{direction:rtl}.vis-left.vis-panel.vis-vertical-scroll .vis-content{direction:ltr}.vis-right.vis-panel.vis-vertical-scroll{direction:ltr}.vis-right.vis-panel.vis-vertical-scroll .vis-content{direction:rtl}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-top{border-left-style:solid;border-right-style:solid}.vis-background{overflow:hidden}.vis-panel>.vis-content{position:relative}.vis-panel .vis-shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis-panel .vis-shadow.vis-top{top:-1px;left:0}.vis-panel .vis-shadow.vis-bottom{bottom:-1px;left:0}.vis-custom-time{background-color:#6e94ff;width:2px;cursor:move;z-index:1}.vis-custom-time>.vis-custom-time-marker{background-color:inherit;color:#fff;font-size:12px;white-space:nowrap;padding:3px 5px;top:0;cursor:initial;z-index:inherit}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor{border-color:#e5e5e5}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-major{border-color:#bfbfbf}.vis-data-axis .vis-y-axis.vis-major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-major.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-minor.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title{position:absolute;color:#4d4d4d;white-space:nowrap;bottom:20px;text-align:center}.vis-data-axis .vis-y-axis.vis-title.vis-measure{padding:0;margin:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title.vis-left{bottom:0;-webkit-transform-origin:left top;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;transform-origin:left bottom;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.vis-data-axis .vis-y-axis.vis-title.vis-right{bottom:0;-webkit-transform-origin:right bottom;-moz-transform-origin:right bottom;-ms-transform-origin:right bottom;-o-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.vis-legend{background-color:rgba(247,252,255,.65);padding:5px;border:1px solid #b3b3b3;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis-legend-text{white-space:nowrap;display:inline-block}.vis-itemset{position:relative;padding:0;margin:0;box-sizing:border-box}.vis-itemset .vis-background,.vis-itemset .vis-foreground{position:absolute;width:100%;height:100%;overflow:visible}.vis-axis{position:absolute;width:100%;height:0;left:0;z-index:1}.vis-foreground .vis-group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis-foreground .vis-group:last-child{border-bottom:none}.vis-nesting-group{cursor:pointer}.vis-label.vis-nested-group.vis-group-level-unknown-but-gte1{background:#f5f5f5}.vis-label.vis-nested-group.vis-group-level-0{background-color:#fff}.vis-ltr .vis-label.vis-nested-group.vis-group-level-0 .vis-inner{padding-left:0}.vis-rtl .vis-label.vis-nested-group.vis-group-level-0 .vis-inner{padding-right:0}.vis-label.vis-nested-group.vis-group-level-1{background-color:rgba(0,0,0,.05)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-1 .vis-inner{padding-left:15px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-1 .vis-inner{padding-right:15px}.vis-label.vis-nested-group.vis-group-level-2{background-color:rgba(0,0,0,.1)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-2 .vis-inner{padding-left:30px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-2 .vis-inner{padding-right:30px}.vis-label.vis-nested-group.vis-group-level-3{background-color:rgba(0,0,0,.15)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-3 .vis-inner{padding-left:45px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-3 .vis-inner{padding-right:45px}.vis-label.vis-nested-group.vis-group-level-4{background-color:rgba(0,0,0,.2)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-4 .vis-inner{padding-left:60px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-4 .vis-inner{padding-right:60px}.vis-label.vis-nested-group.vis-group-level-5{background-color:rgba(0,0,0,.25)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-5 .vis-inner{padding-left:75px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-5 .vis-inner{padding-right:75px}.vis-label.vis-nested-group.vis-group-level-6{background-color:rgba(0,0,0,.3)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-6 .vis-inner{padding-left:90px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-6 .vis-inner{padding-right:90px}.vis-label.vis-nested-group.vis-group-level-7{background-color:rgba(0,0,0,.35)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-7 .vis-inner{padding-left:105px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-7 .vis-inner{padding-right:105px}.vis-label.vis-nested-group.vis-group-level-8{background-color:rgba(0,0,0,.4)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-8 .vis-inner{padding-left:120px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-8 .vis-inner{padding-right:120px}.vis-label.vis-nested-group.vis-group-level-9{background-color:rgba(0,0,0,.45)}.vis-ltr .vis-label.vis-nested-group.vis-group-level-9 .vis-inner{padding-left:135px}.vis-rtl .vis-label.vis-nested-group.vis-group-level-9 .vis-inner{padding-right:135px}.vis-label.vis-nested-group{background-color:rgba(0,0,0,.5)}.vis-ltr .vis-label.vis-nested-group .vis-inner{padding-left:150px}.vis-rtl .vis-label.vis-nested-group .vis-inner{padding-right:150px}.vis-group-level-unknown-but-gte1{border:1px solid red}.vis-label.vis-nesting-group:before{display:inline-block;width:15px}.vis-label.vis-nesting-group.expanded:before{content:"\25BC"}.vis-label.vis-nesting-group.collapsed:before{content:"\25B6"}.vis-rtl .vis-label.vis-nesting-group.collapsed:before{content:"\25C0"}.vis-ltr .vis-label:not(.vis-nesting-group):not(.vis-group-level-0){padding-left:15px}.vis-rtl .vis-label:not(.vis-nesting-group):not(.vis-group-level-0){padding-right:15px}.vis-overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-labelset{position:relative;overflow:hidden;box-sizing:border-box}.vis-labelset .vis-label{position:relative;left:0;top:0;width:100%;color:#4d4d4d;box-sizing:border-box}.vis-labelset .vis-label{border-bottom:1px solid #bfbfbf}.vis-labelset .vis-label.draggable{cursor:pointer}.vis-group-is-dragging{background:rgba(0,0,0,.1)}.vis-labelset .vis-label:last-child{border-bottom:none}.vis-labelset .vis-label .vis-inner{display:inline-block;padding:5px}.vis-labelset .vis-label .vis-inner.vis-hidden{padding:0}.vis-time-axis{position:relative;overflow:hidden}.vis-time-axis.vis-foreground{top:0;left:0;width:100%}.vis-time-axis.vis-background{position:absolute;top:0;left:0;width:100%;height:100%}.vis-time-axis .vis-text{position:absolute;color:#4d4d4d;padding:3px;overflow:hidden;box-sizing:border-box;white-space:nowrap}.vis-time-axis .vis-text.vis-measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis-time-axis .vis-grid.vis-vertical{position:absolute;border-left:1px solid}.vis-time-axis .vis-grid.vis-vertical-rtl{position:absolute;border-right:1px solid}.vis-time-axis .vis-grid.vis-minor{border-color:#e5e5e5}.vis-time-axis .vis-grid.vis-major{border-color:#bfbfbf}.vis-item{position:absolute;color:#1a1a1a;border-color:#97b0f8;border-width:1px;background-color:#d5ddf6;display:inline-block;z-index:1}.vis-item.vis-selected{border-color:#ffc200;background-color:#fff785;z-index:2}.vis-editable.vis-selected{cursor:move}.vis-item.vis-point.vis-selected{background-color:#fff785}.vis-item.vis-box{text-align:center;border-style:solid;border-radius:2px}.vis-item.vis-point{background:0 0}.vis-item.vis-dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis-item.vis-range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis-item.vis-background{border:none;background-color:rgba(213,221,246,.4);box-sizing:border-box;padding:0;margin:0}.vis-item .vis-item-overflow{position:relative;width:100%;height:100%;padding:0;margin:0;overflow:hidden}.vis-item-visible-frame{white-space:nowrap}.vis-item.vis-range .vis-item-content{position:relative;display:inline-block}.vis-item.vis-background .vis-item-content{position:absolute;display:inline-block}.vis-item.vis-line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis-item .vis-item-content{white-space:nowrap;box-sizing:border-box;padding:5px}.vis-item .vis-onUpdateTime-tooltip{position:absolute;background:#4f81bd;color:#fff;width:200px;text-align:center;white-space:nowrap;padding:5px;border-radius:1px;transition:.4s;-o-transition:.4s;-moz-transition:.4s;-webkit-transition:.4s}.vis-item .vis-delete,.vis-item .vis-delete-rtl{position:absolute;top:0;width:24px;height:24px;box-sizing:border-box;padding:0 5px;cursor:pointer;-webkit-transition:background .2s linear;-moz-transition:background .2s linear;-ms-transition:background .2s linear;-o-transition:background .2s linear;transition:background .2s linear}.vis-item .vis-delete{right:-24px}.vis-item .vis-delete-rtl{left:-24px}.vis-item .vis-delete-rtl:after,.vis-item .vis-delete:after{content:"\00D7";color:red;font-family:arial,sans-serif;font-size:22px;font-weight:700;-webkit-transition:color .2s linear;-moz-transition:color .2s linear;-ms-transition:color .2s linear;-o-transition:color .2s linear;transition:color .2s linear}.vis-item .vis-delete-rtl:hover,.vis-item .vis-delete:hover{background:red}.vis-item .vis-delete-rtl:hover:after,.vis-item .vis-delete:hover:after{color:#fff}.vis-item .vis-drag-center{position:absolute;width:100%;height:100%;top:0;left:0;cursor:move}.vis-item.vis-range .vis-drag-left{position:absolute;width:24px;max-width:20%;min-width:2px;height:100%;top:0;left:-4px;cursor:w-resize}.vis-item.vis-range .vis-drag-right{position:absolute;width:24px;max-width:20%;min-width:2px;height:100%;top:0;right:-4px;cursor:e-resize}.vis-range.vis-item.vis-readonly .vis-drag-left,.vis-range.vis-item.vis-readonly .vis-drag-right{cursor:auto}.vis-item.vis-cluster{vertical-align:center;text-align:center;border-style:solid;border-radius:2px}.vis-item.vis-cluster-line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis-item.vis-cluster-dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}div.vis-configuration{position:relative;display:block;float:left;font-size:12px}div.vis-configuration-wrapper{display:block;width:700px}div.vis-configuration-wrapper::after{clear:both;content:"";display:block}div.vis-configuration.vis-config-option-container{display:block;width:495px;background-color:#fff;border:2px solid #f7f8fa;border-radius:4px;margin-top:20px;left:10px;padding-left:5px}div.vis-configuration.vis-config-button{display:block;width:495px;height:25px;vertical-align:middle;line-height:25px;background-color:#f7f8fa;border:2px solid #ceced0;border-radius:4px;margin-top:20px;left:10px;padding-left:5px;cursor:pointer;margin-bottom:30px}div.vis-configuration.vis-config-button.hover{background-color:#4588e6;border:2px solid #214373;color:#fff}div.vis-configuration.vis-config-item{display:block;float:left;width:495px;height:25px;vertical-align:middle;line-height:25px}div.vis-configuration.vis-config-item.vis-config-s2{left:10px;background-color:#f7f8fa;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s3{left:20px;background-color:#e4e9f0;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s4{left:30px;background-color:#cfd8e6;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-header{font-size:18px;font-weight:700}div.vis-configuration.vis-config-label{width:120px;height:25px;line-height:25px}div.vis-configuration.vis-config-label.vis-config-s3{width:110px}div.vis-configuration.vis-config-label.vis-config-s4{width:100px}div.vis-configuration.vis-config-colorBlock{top:1px;width:30px;height:19px;border:1px solid #444;border-radius:2px;padding:0;margin:0;cursor:pointer}input.vis-configuration.vis-config-checkbox{left:-5px}input.vis-configuration.vis-config-rangeinput{position:relative;top:-5px;width:60px;padding:1px;margin:0;pointer-events:none}input.vis-configuration.vis-config-range{-webkit-appearance:none;border:0 solid #fff;background-color:rgba(0,0,0,0);width:300px;height:20px}input.vis-configuration.vis-config-range::-webkit-slider-runnable-track{width:300px;height:5px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-webkit-slider-thumb{-webkit-appearance:none;border:1px solid #14334b;height:17px;width:17px;border-radius:50%;background:#3876c2;background:-moz-linear-gradient(top,#3876c2 0,#385380 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#3876c2),color-stop(100%,#385380));background:-webkit-linear-gradient(top,#3876c2 0,#385380 100%);background:-o-linear-gradient(top,#3876c2 0,#385380 100%);background:-ms-linear-gradient(top,#3876c2 0,#385380 100%);background:linear-gradient(to bottom,#3876c2 0,#385380 100%);box-shadow:#111927 0 0 1px 0;margin-top:-7px}input.vis-configuration.vis-config-range:focus{outline:0}input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track{background:#9d9d9d;background:-moz-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#9d9d9d),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-o-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:linear-gradient(to bottom,#9d9d9d 0,#c8c8c8 99%)}input.vis-configuration.vis-config-range::-moz-range-track{width:300px;height:10px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}input.vis-configuration.vis-config-range::-ms-track{width:300px;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}input.vis-configuration.vis-config-range::-ms-fill-lower{background:#777;border-radius:10px}input.vis-configuration.vis-config-range::-ms-fill-upper{background:#ddd;border-radius:10px}input.vis-configuration.vis-config-range::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:focus::-ms-fill-lower{background:#888}input.vis-configuration.vis-config-range:focus::-ms-fill-upper{background:#ccc}.vis-configuration-popup{position:absolute;background:rgba(57,76,89,.85);border:2px solid #f2faff;line-height:30px;height:30px;width:150px;text-align:center;color:#fff;font-size:14px;border-radius:4px;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out}.vis-configuration-popup:after,.vis-configuration-popup:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.vis-configuration-popup:after{border-color:rgba(136,183,213,0);border-left-color:rgba(57,76,89,.85);border-width:8px;margin-top:-8px}.vis-configuration-popup:before{border-color:rgba(194,225,245,0);border-left-color:#f2faff;border-width:12px;margin-top:-12px}.vis .overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}div.vis-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;font-family:verdana;font-size:14px;color:#000;background-color:#f5f4ed;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid #808074;box-shadow:3px 3px 10px rgba(0,0,0,.2);pointer-events:none;z-index:5}
--------------------------------------------------------------------------------