init
This commit is contained in:
		
							
								
								
									
										39
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
# PTZOptics Node Server
 | 
			
		||||
 | 
			
		||||
The PTZOptics Node Server is a simple skeleton Express server to control your PTZOptics camera via visca commands.
 | 
			
		||||
 | 
			
		||||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
You need to have at least [Node.js version: 8.12.0](https://nodejs.org/en/download/), [MongoDB version: 4.0.3](https://www.mongodb.com/download-center?initial=true#community), and a [PTZOptics camera](https://ptzoptics.com/).
 | 
			
		||||
 | 
			
		||||
## Installing
 | 
			
		||||
1. Configure your PTZOptics camera to your local network. [PTZOptics Knowledge Base](https://help.ptzoptics.com/support/solutions/folders/13000001062)
 | 
			
		||||
2. Clone this repo and then extract to your preferred location
 | 
			
		||||
3. Update the mongoDB connection information inside `/app/config.json`.  
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
{  
 | 
			
		||||
    connectionString: "mongodb://your-mongo-address/db-name",  
 | 
			
		||||
    secret: "your db-secret"  
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
4. Start the server
 | 
			
		||||
 | 
			
		||||
    `cd /The/path/to/the/repo`  
 | 
			
		||||
    `npm start`
 | 
			
		||||
 | 
			
		||||
5. Head to `http://localhost:4000`
 | 
			
		||||
6. Click 'Add Camera' and enter your camera's information.
 | 
			
		||||
 | 
			
		||||
## Contributing
 | 
			
		||||
 | 
			
		||||
1. Fork it!
 | 
			
		||||
2. Create your feature branch: `git checkout -b my-new-feature`
 | 
			
		||||
3. Commit your changes: `git commit -m 'Add some feature'`
 | 
			
		||||
4. Push to the branch: `git push origin my-new-feature`
 | 
			
		||||
5. Submit a pull request :D
 | 
			
		||||
 | 
			
		||||
## Authors
 | 
			
		||||
 | 
			
		||||
[**PTZOptics**](https://github.com/PTZOptics)
 | 
			
		||||
							
								
								
									
										180
									
								
								app/_helpers/cameraHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								app/_helpers/cameraHelper.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const http = require('http');
 | 
			
		||||
const tcpPortUsed =require('tcp-port-used');
 | 
			
		||||
const db = require(path.resolve(__dirname, './db.js'));
 | 
			
		||||
const Camera = db.Camera;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getDeviceModel,
 | 
			
		||||
    checkPortUse,
 | 
			
		||||
    createNewCameraStreamPort
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function checkPortUse(port) {
 | 
			
		||||
    return tcpPortUsed.check(port, '127.0.0.1')
 | 
			
		||||
        .then((inuse) => {
 | 
			
		||||
            return inuse;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
            throw err;
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getDeviceModel(ip) {
 | 
			
		||||
    const options = {
 | 
			
		||||
        hostname: ip,
 | 
			
		||||
        path: '/cgi-bin/param.cgi?get_serial_number',
 | 
			
		||||
        method: 'GET'
 | 
			
		||||
    };
 | 
			
		||||
    return sendCgiReq(options)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            const rawData = res.toString().replace(/\n/g, '').toUpperCase();
 | 
			
		||||
            return calcCamModel(rawData, rawData.substring(0, 1), rawData.substring(0, 2))
 | 
			
		||||
                .then(res => {
 | 
			
		||||
                    return res;
 | 
			
		||||
                })
 | 
			
		||||
                .catch(err => {
 | 
			
		||||
                    throw err;
 | 
			
		||||
                });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
            throw err;
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function calcCamModel(serial, first_letter, twoFirst_letter) {
 | 
			
		||||
    let model = '';
 | 
			
		||||
    // Check 12x
 | 
			
		||||
    if (["1", "A", "B", "C", "D", "N", "O"].includes(first_letter)) {
 | 
			
		||||
        model = "PT12X-";
 | 
			
		||||
        (first_letter === "1") ? model += serial.slice(3, 6) + "-XX-" + serial.slice(8, 10) + checkPoe(serial.slice(10)) : model += newSerialAnsBuilder(serial);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check 20x
 | 
			
		||||
    if (["2", "E", "F", "G", "H", "P", "Q"].includes(first_letter) && twoFirst_letter !== "PT" ) {
 | 
			
		||||
        model = "PT20X-";
 | 
			
		||||
        (first_letter === "2") ? model += serial.slice(3, 6) + "-XX-" + serial.slice(8, 10) + checkPoe(serial.slice(10)) : model += newSerialAnsBuilder(serial);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check 30x
 | 
			
		||||
    if (["W", "X", "R", "S"].includes(first_letter)) {
 | 
			
		||||
        model = "PT30X-" + newSerialAnsBuilder(serial);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check zcams
 | 
			
		||||
    if (["J", "U", "I", "T"].includes(first_letter)) {
 | 
			
		||||
        (["J", "U"].includes(first_letter)) ? model = "PT20X-ZCAM-" : model = "PTVL-ZCAM";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ('PT' == twoFirst_letter) {
 | 
			
		||||
        (serial.slice(0, 4) === "PTVL") ? model = "PTVL-ZCAM-" : model = "PT20X-ZCAM-";
 | 
			
		||||
    }
 | 
			
		||||
    return [model, serial];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function newSerialAnsBuilder(serial) {
 | 
			
		||||
    let ans = '';
 | 
			
		||||
    switch(serial.substring(0, 1)) {
 | 
			
		||||
        // 12X
 | 
			
		||||
        case "A":
 | 
			
		||||
        case "B":
 | 
			
		||||
            ans = "SDI-XX-G2" + checkPoe(serial.slice(1));
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "C":
 | 
			
		||||
        case "D":
 | 
			
		||||
            ans = "USB-XX-G2";
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "N":
 | 
			
		||||
        case "O":
 | 
			
		||||
            ans = "SDI-XX-G2 POE";
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        // 20X
 | 
			
		||||
        case "E":
 | 
			
		||||
        case "F":
 | 
			
		||||
            ans = "SDI-XX-G2" + checkPoe(serial.slice(1));
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "G":
 | 
			
		||||
        case "H":
 | 
			
		||||
            ans = "USB-XX-G2";
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "P":
 | 
			
		||||
        case "Q":
 | 
			
		||||
            ans = "SDI-XX-G2 POE";
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        // 30X
 | 
			
		||||
        case "W":
 | 
			
		||||
        case "X":
 | 
			
		||||
            ans = "SDI-XX-G2 POE";
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "R":
 | 
			
		||||
        case "S":
 | 
			
		||||
            ans = "NDI-XX-G2";
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
    return ans;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkPoe (serialNum) {
 | 
			
		||||
    if (serialNum <= "B1025000") {
 | 
			
		||||
        return '';
 | 
			
		||||
    } else if (serialNum >= "B1025001" && serialNum <= "D0129000") {
 | 
			
		||||
        return "-POE";
 | 
			
		||||
    } else if(serialNum >= "D0129001") {
 | 
			
		||||
        return "-POE";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function createNewCameraStreamPort() {
 | 
			
		||||
    let port = 5000;
 | 
			
		||||
    let cameraStreamPorts;
 | 
			
		||||
    try {
 | 
			
		||||
        // Returns all saved camera stream ports
 | 
			
		||||
        cameraStreamPorts = await currentCameraStreamPorts();
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (cameraStreamPorts.includes(port)) {
 | 
			
		||||
        ++port;
 | 
			
		||||
    }
 | 
			
		||||
    return port;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function currentCameraStreamPorts() {
 | 
			
		||||
    return Camera.find().select('streamPort').lean()
 | 
			
		||||
        .then((cameras) => {
 | 
			
		||||
            return cameras.map((camera) => {
 | 
			
		||||
                return camera.streamPort;
 | 
			
		||||
            });
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            throw err;
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendCgiReq(options) {
 | 
			
		||||
    return new Promise(function(resolve, reject) {
 | 
			
		||||
        const req = http.request(options, (res) => {
 | 
			
		||||
            let rawData = '';
 | 
			
		||||
            res.setEncoding('utf8');
 | 
			
		||||
            res.on('data', (chunk) => {
 | 
			
		||||
                rawData += chunk;
 | 
			
		||||
            });
 | 
			
		||||
            res.on('end', () => {
 | 
			
		||||
                resolve(rawData);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        req.on('error', function(err) {
 | 
			
		||||
            reject(err);
 | 
			
		||||
        });
 | 
			
		||||
        req.end();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								app/_helpers/db.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/_helpers/db.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const config = require(path.resolve(__dirname, '../config.json'));
 | 
			
		||||
const mongoose = require('mongoose');
 | 
			
		||||
 | 
			
		||||
mongoose.connect(config.connectionString, {useNewUrlParser: true });
 | 
			
		||||
mongoose.Promise = global.Promise;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    Camera: require('../camera/camera.model')
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										9
									
								
								app/_helpers/error-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/_helpers/error-handler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
module.exports = errorHandler;
 | 
			
		||||
 | 
			
		||||
function errorHandler(err, req, res, next) {
 | 
			
		||||
    console.log(err);
 | 
			
		||||
    if (typeof err === 'string') {
 | 
			
		||||
        return res.status(409).json({message: err});
 | 
			
		||||
    }
 | 
			
		||||
    return res.status(500).json({message: err.message});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								app/_helpers/ptzHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/_helpers/ptzHelper.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    translateDirection,
 | 
			
		||||
    hexStrToNum,
 | 
			
		||||
    numToHexStr,
 | 
			
		||||
    sanitizeSpeed,
 | 
			
		||||
    getCurrentPos
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function translateDirection(direction) {
 | 
			
		||||
    let hexStr;
 | 
			
		||||
    switch(direction.toLowerCase()) {
 | 
			
		||||
        case "stop":
 | 
			
		||||
            hexStr =  "0303FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "up":
 | 
			
		||||
            hexStr =  "0301FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "down":
 | 
			
		||||
            hexStr =  "0302FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "right":
 | 
			
		||||
            hexStr =  "0203FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "left":
 | 
			
		||||
            hexStr =  "0103FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "upleft":
 | 
			
		||||
            hexStr =  "0101FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "upright":
 | 
			
		||||
            hexStr =  "0201FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "downleft":
 | 
			
		||||
            hexStr =  "0102FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "downright":
 | 
			
		||||
            hexStr =  "0202FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The direction " + direction + " is not a valid movement direction";
 | 
			
		||||
    }
 | 
			
		||||
    return hexStr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hexStrToNum(str) {
 | 
			
		||||
    return parseInt(str, 16);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function numToHexStr(num) {
 | 
			
		||||
    if (num == 0) {
 | 
			
		||||
        return '00';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (typeof num === 'string') {
 | 
			
		||||
        num = parseInt(num);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return num.toString(16).toUpperCase().split('').reduce(function(str, char) {
 | 
			
		||||
        return '0' + char;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sanitizeSpeed(pan, tilt) {
 | 
			
		||||
    let sanitizedPan, sanitizedTilt;
 | 
			
		||||
 | 
			
		||||
    if (pan >= 1 && pan <= 18) {
 | 
			
		||||
        sanitizedPan = ("0" + pan).slice(-2);
 | 
			
		||||
    } else {
 | 
			
		||||
        throw 'The pan speed value must be greater than or equal to 1 and less than or equal to 18';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tilt >= 1 && tilt <= 14) {
 | 
			
		||||
        sanitizedTilt = ("0" + pan).slice(-2);
 | 
			
		||||
    } else {
 | 
			
		||||
        throw 'The tilt speed value must be greater than or equal to 1 and less than or equal to 14';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [sanitizedPan, sanitizedTilt];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getCurrentPos(id) {
 | 
			
		||||
    return numToHexStr(await socket.sendCmd(id, "81090612FF"));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								app/_helpers/socket.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/_helpers/socket.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
 | 
			
		||||
const Camera = db.Camera;
 | 
			
		||||
const net = require('net');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    sendCmd: _sendCmd
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function _sendCmd(id, cmd) {
 | 
			
		||||
    const camera = await Camera.findById(id);
 | 
			
		||||
    const buffer = Buffer.from(cmd, 'hex');
 | 
			
		||||
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        const socket = new net.Socket({allowHalfOpen: true});
 | 
			
		||||
        const conn = net.createConnection(Number(camera.port), camera.ip);
 | 
			
		||||
        conn.setNoDelay();
 | 
			
		||||
        conn.setEncoding('hex');
 | 
			
		||||
 | 
			
		||||
        conn.on('connect', () => {
 | 
			
		||||
            conn.write(buffer);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        conn.on('error', (error) => {
 | 
			
		||||
            reject(error);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        conn.on('data', (buf) => {
 | 
			
		||||
            if (conn.bytesRead >= 3) {
 | 
			
		||||
                conn.end();
 | 
			
		||||
                decode(buf.toString('hex'))
 | 
			
		||||
                    .then((res) => {
 | 
			
		||||
                        resolve(res);
 | 
			
		||||
                    })
 | 
			
		||||
                    .catch(err => {
 | 
			
		||||
                        reject(err);
 | 
			
		||||
                    });
 | 
			
		||||
            } else {
 | 
			
		||||
                reject("Unusual Camera Response: " + buf.toString('hex') + " connection bytes Read: " + conn.bytesRead);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function decode(hexStr) {
 | 
			
		||||
    let decoded = '';
 | 
			
		||||
    switch (hexStr) {
 | 
			
		||||
        case "9041ff":
 | 
			
		||||
        case "9042ff":
 | 
			
		||||
            decoded = "Command Accepted";
 | 
			
		||||
            break;
 | 
			
		||||
        case "9051ff":
 | 
			
		||||
        case "9041ff9051ff":
 | 
			
		||||
            decoded = "Socket1 Cmd Done";
 | 
			
		||||
            break;
 | 
			
		||||
        case "9052ff":
 | 
			
		||||
        case "9042ff9052ff":
 | 
			
		||||
            decoded = "Socket2 Cmd Done";
 | 
			
		||||
            break;
 | 
			
		||||
        case "906002ff":
 | 
			
		||||
            throw "Command Syntax Error";
 | 
			
		||||
        case "906003ff":
 | 
			
		||||
            throw "Command Buffer Full";
 | 
			
		||||
        case "906104ff":
 | 
			
		||||
            throw "Socket1 Cmd Cancelled";
 | 
			
		||||
        case "906204ff":
 | 
			
		||||
            throw "Socket2 Cmd Cancelled";
 | 
			
		||||
        case "906105ff":
 | 
			
		||||
        case "906205ff":
 | 
			
		||||
            throw "No Socket";
 | 
			
		||||
        case "906141ff":
 | 
			
		||||
            throw "Socket1 Cmd Not Executable";
 | 
			
		||||
        case "906241ff":
 | 
			
		||||
            throw "Socket2 Cmd Not Executable";
 | 
			
		||||
        default:
 | 
			
		||||
            throw "Unusual Camera Response: " + hexStr;
 | 
			
		||||
    }
 | 
			
		||||
    return decoded;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								app/camera/camera.controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/camera/camera.controller.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
const cameraService = require(path.resolve(__dirname, './camera.service'));
 | 
			
		||||
 | 
			
		||||
router.post('/create', _create);
 | 
			
		||||
router.get('/cameras', getAll);
 | 
			
		||||
router.get('/', getById);
 | 
			
		||||
router.put('/', update);
 | 
			
		||||
router.delete('/', _delete);
 | 
			
		||||
router.post('/osd', _osd);
 | 
			
		||||
router.post('/stream', _stream);
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
 | 
			
		||||
function _create(req, res, next) {
 | 
			
		||||
    cameraService.create(req.body)
 | 
			
		||||
        .then((camera) => res.json(camera))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function update(req, res, next) {
 | 
			
		||||
    cameraService.update(req.body.id)
 | 
			
		||||
        .then((camera) => res.json(camera))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAll(req, res, next) {
 | 
			
		||||
    cameraService.getAll()
 | 
			
		||||
        .then((cameras) => res.json(cameras))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getById(req, res, next) {
 | 
			
		||||
    cameraService.getById(req.body.id)
 | 
			
		||||
        .then((camera) => res.json(camera))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _delete(req, res, next) {
 | 
			
		||||
    cameraService.delete(req.body.id)
 | 
			
		||||
        .then((id) => res.json(id))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _osd(req, res, next) {
 | 
			
		||||
    cameraService.osd(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _stream(req, res, next) {
 | 
			
		||||
    console.log("Stream Requester ip: " + req.ip);
 | 
			
		||||
    cameraService.stream(req.body)
 | 
			
		||||
        .then((streamPort) => res.json(streamPort))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								app/camera/camera.model.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/camera/camera.model.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
const mongoose = require('mongoose');
 | 
			
		||||
const Schema = mongoose.Schema;
 | 
			
		||||
 | 
			
		||||
const schema = new Schema({
 | 
			
		||||
    _id: Schema.Types.ObjectId,
 | 
			
		||||
    ip: {type: String, unique: true, required: true},
 | 
			
		||||
    port: {type: String, required: true},
 | 
			
		||||
    rtsp: {type: String, required: true},
 | 
			
		||||
    model: {type: String, required: true},
 | 
			
		||||
    serial: {type: String, required: true},
 | 
			
		||||
    name: {type: String, unique: true},
 | 
			
		||||
    streamPort: {type: Number, unique: true},
 | 
			
		||||
    presets: [
 | 
			
		||||
        {
 | 
			
		||||
            memNum: {type: Number, max: 127},
 | 
			
		||||
            name: {type: String},
 | 
			
		||||
            location: {
 | 
			
		||||
                pan: {type: String},
 | 
			
		||||
                tilt: {type: String},
 | 
			
		||||
                focus: {type: String},
 | 
			
		||||
                zoom: {type: String}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
schema.set('toJson', {virtuals: true});
 | 
			
		||||
module.exports = mongoose.model('Camera', schema);
 | 
			
		||||
							
								
								
									
										127
									
								
								app/camera/camera.service.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								app/camera/camera.service.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const Stream = require('../stream/videoStream');
 | 
			
		||||
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
 | 
			
		||||
const mongoose = require('mongoose');
 | 
			
		||||
const cameraHelper = require(path.resolve(__dirname, '../_helpers/cameraHelper.js'));
 | 
			
		||||
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
 | 
			
		||||
const Camera = db.Camera;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    create: _create,
 | 
			
		||||
    getAll: _getAll,
 | 
			
		||||
    getById: _getById,
 | 
			
		||||
    update: _update,
 | 
			
		||||
    delete: _delete,
 | 
			
		||||
    osd: _osd,
 | 
			
		||||
    stream: _stream
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function _getAll() {
 | 
			
		||||
    try {
 | 
			
		||||
        return await Camera.find().lean();
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _getById(id) {
 | 
			
		||||
    try {
 | 
			
		||||
        return await Camera.findById(id).lean();
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _create(cameraParams) {
 | 
			
		||||
    if (await Camera.findOne({ip: cameraParams.ip})) {
 | 
			
		||||
        throw 'There is already a camera with ip ' + cameraParams.ip;
 | 
			
		||||
    } else {
 | 
			
		||||
        const modelSerialArr = await cameraHelper.getDeviceModel(cameraParams.ip);
 | 
			
		||||
        const camId = new mongoose.Types.ObjectId();
 | 
			
		||||
        const streamPort = await cameraHelper.createNewCameraStreamPort();
 | 
			
		||||
        return new Camera({
 | 
			
		||||
            ...cameraParams,
 | 
			
		||||
            _id: camId,
 | 
			
		||||
            model: modelSerialArr[0],
 | 
			
		||||
            serial: modelSerialArr[1],
 | 
			
		||||
            streamPort: streamPort
 | 
			
		||||
        })
 | 
			
		||||
        .save()
 | 
			
		||||
        .then((camera) => {
 | 
			
		||||
            return camera.toObject();
 | 
			
		||||
        }).catch((err) => {
 | 
			
		||||
            throw err;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _update(cameraParams) {
 | 
			
		||||
    try {
 | 
			
		||||
        return await Camera.findByIdAndUpdate(cameraParams._id, cameraParams, {new: true}).save();
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _delete(id) {
 | 
			
		||||
    try {
 | 
			
		||||
        return await Camera.findOneAndDelete(id);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _osd({id, option}) {
 | 
			
		||||
    let osdHex = "8101";
 | 
			
		||||
    switch(option.toLowerCase()) {
 | 
			
		||||
        case "openToggle":
 | 
			
		||||
            osdHex += "043F025FFF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "up":
 | 
			
		||||
            osdHex += "06010E0E0301FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "down":
 | 
			
		||||
            osdHex += "06010E0E0302FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "left":
 | 
			
		||||
            osdHex += "06010E0E0103FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "right":
 | 
			
		||||
            osdHex += "06010E0E0203FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "enter":
 | 
			
		||||
            osdHex += "060605FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "return":
 | 
			
		||||
            osdHex += "060604FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The OSD option " + option + " is not a recognizable OSD option.";
 | 
			
		||||
    }
 | 
			
		||||
    return await socket.sendCmd(id, osdHex);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _stream({id, width = 1920, height = 1080}) {
 | 
			
		||||
    const camera = await Camera.findById(id).lean();
 | 
			
		||||
 | 
			
		||||
    // In case a seperate user wants to reach same camera stream
 | 
			
		||||
    let inUse;
 | 
			
		||||
    try {
 | 
			
		||||
        inUse = await cameraHelper.checkPortUse(camera.streamPort);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!inUse) {
 | 
			
		||||
        const stream = new Stream({
 | 
			
		||||
            name: camera.name || "stream: " + camera.rtsp,
 | 
			
		||||
            url: 'rtsp://' + camera.rtsp,
 | 
			
		||||
            port: camera.streamPort,
 | 
			
		||||
            width: width,
 | 
			
		||||
            height: height
 | 
			
		||||
        });
 | 
			
		||||
        stream.start();
 | 
			
		||||
    }
 | 
			
		||||
    return camera.streamPort;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								app/config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/config.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
    "connectionString": "mongodb://your-mongo-address/db-name",
 | 
			
		||||
    "secret": "your db-secret"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								app/image/image.controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/image/image.controller.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
const imageService = require(path.resolve(__dirname, './image.service.js'));
 | 
			
		||||
module.exports = router;
 | 
			
		||||
 | 
			
		||||
router.post('/bright', _brightness);
 | 
			
		||||
router.post('/contrast', _contrast);
 | 
			
		||||
router.post('/wb', _whiteBalance);
 | 
			
		||||
router.post('/rgain', _rgain);
 | 
			
		||||
router.post('/bgain', _bgain);
 | 
			
		||||
router.post('/shutter', _shutter);
 | 
			
		||||
router.post('/iris', _iris);
 | 
			
		||||
router.post('/gain', _gain);
 | 
			
		||||
router.post('/backLight', _backLight);
 | 
			
		||||
router.post('/bw', _blackWhite);
 | 
			
		||||
router.post('/flicker', _flicker);
 | 
			
		||||
router.post('/imgFlip', _imgFlip);
 | 
			
		||||
router.post('/colorHue', _colorHue);
 | 
			
		||||
router.post('/ae', _autoExp);
 | 
			
		||||
router.post('/save', _save);
 | 
			
		||||
 | 
			
		||||
function _brightness(req, res, next) {
 | 
			
		||||
    imageService.brightness(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _contrast(req, res, next) {
 | 
			
		||||
    imageService.contrast(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _whiteBalance(req, res, next) {
 | 
			
		||||
    imageService.whiteBalance(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _rgain(req, res, next) {
 | 
			
		||||
    imageService.rgain(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _bgain(req, res, next) {
 | 
			
		||||
    imageService.bgain(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _shutter(req, res, next) {
 | 
			
		||||
    imageService.shutter(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _iris(req, res, next) {
 | 
			
		||||
    imageService.iris(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _gain(req, res, next) {
 | 
			
		||||
    imageService.gain(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _backLight(req, res, next) {
 | 
			
		||||
    imageService.backLight(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _blackWhite(req, res, next) {
 | 
			
		||||
    imageService.blackWhite(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _flicker(req, res, next) {
 | 
			
		||||
    imageService.flicker(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _imgFlip(req, res, next) {
 | 
			
		||||
    imageService.imgFlip(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _colorHue(req, res, next) {
 | 
			
		||||
    imageService.colorHue(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _autoExp(req, res, next) {
 | 
			
		||||
    imageService.ae(req.body)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _save(req, res, next) {
 | 
			
		||||
    imageService.save(req.body.id)
 | 
			
		||||
        .then((socket) => res.json(socket))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										479
									
								
								app/image/image.service.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								app/image/image.service.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,479 @@
 | 
			
		||||
 /*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
 | 
			
		||||
const ptzHelper = require(path.resolve(__dirname, '../_helpers/ptzHelper.js'));
 | 
			
		||||
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
 | 
			
		||||
const Camera = db.Camera;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    brightness: _brightness,
 | 
			
		||||
    contrast: _contrast,
 | 
			
		||||
    whiteBalance: whiteBal,
 | 
			
		||||
    rgain: _rgain,
 | 
			
		||||
    bgain: _bgain,
 | 
			
		||||
    shutter: _shutter,
 | 
			
		||||
    iris: _iris,
 | 
			
		||||
    gain: _gain,
 | 
			
		||||
    backLight: _backLight,
 | 
			
		||||
    blackWhite: _blackWhite,
 | 
			
		||||
    flicker: _flicker,
 | 
			
		||||
    imgFlip: _imgFlip,
 | 
			
		||||
    colorHue: _colorHue,
 | 
			
		||||
    ae: _autoExp,
 | 
			
		||||
    save: saveSetting
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function _brightness({id, pos}) {
 | 
			
		||||
    let brightnessHex = "810104A10000";
 | 
			
		||||
    let brightnessPos = ptzHelper.numToHexStr(pos).padStart(4, "0");
 | 
			
		||||
    brightnessHex += brightnessPos + "FF";
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, brightnessHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _contrast(contrastParams) {
 | 
			
		||||
    const {id, pos} = contrastParams;
 | 
			
		||||
    let contrastHex = "810104A20000";
 | 
			
		||||
    let contrastPos = ptzHelper.numToHexStr(pos).padStart(4, "0");
 | 
			
		||||
    contrastHex += contrastPos + 'FF';
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, contrastHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function whiteBal({id, option, mode}) {
 | 
			
		||||
    let wbHex = "810104";
 | 
			
		||||
 | 
			
		||||
    if (mode.toLowerCase() === 'wbmode') {
 | 
			
		||||
        switch(option.toLowerCase()) {
 | 
			
		||||
            case "auto":
 | 
			
		||||
                wbHex += "3500FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "indoor":
 | 
			
		||||
                wbHex += "3501FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "outdoor":
 | 
			
		||||
                wbHex  += "3502FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "onepush":
 | 
			
		||||
                wbHex += "3503FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "manual":
 | 
			
		||||
                wbHex += "3505FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "onepush-trigger":
 | 
			
		||||
                wbHex += "1005FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The white balance mode " + option + " is not a recognizable white balance mode.";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode.toLowerCase() === 'awbsenstivity') {
 | 
			
		||||
        switch(option.toLowerCase()) {
 | 
			
		||||
            case "high":
 | 
			
		||||
                wbHex += "A900FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "normal":
 | 
			
		||||
                wbHex += "A901FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "low":
 | 
			
		||||
                wbHex += "A902FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The auto white balance mode " + option + " is not a recognizable auto white balance mode.";
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        throw mode + "is not a recognizable white balance method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, wbHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _rgain({id, mode, option = null, pos = null}) {
 | 
			
		||||
    let rgainHex = "810104";
 | 
			
		||||
 | 
			
		||||
    if (mode.toLowerCase() === 'standard') {
 | 
			
		||||
        switch (option.toLowerCase()) {
 | 
			
		||||
            case "reset":
 | 
			
		||||
                rgainHex += "0300FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "up":
 | 
			
		||||
                rgainHex += "0302FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "down":
 | 
			
		||||
                rgainHex += "0303FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The rgain option " + option + " is not a recognizable standard rgain option";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode.toLowerCase() === 'direct') {
 | 
			
		||||
        rgainHex = "430000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The rgain mode " + mode + " is not a recognizable rgain method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, rgainHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _bgain({id, mode, option = null, pos = null}) {
 | 
			
		||||
    let bgainHex = "810104";
 | 
			
		||||
 | 
			
		||||
    if (mode.toLowerCase() === 'standard') {
 | 
			
		||||
        switch (option.toLowerCase()) {
 | 
			
		||||
            case "reset":
 | 
			
		||||
                bgainHex += "0400FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "up":
 | 
			
		||||
                bgainHex += "0402FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "down":
 | 
			
		||||
                bgainHex += "0403FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The bgain option " + option + " is not a recognizable standard bgain option";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode.toLowerCase() === 'direct') {
 | 
			
		||||
        rgainHex = "430000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The bgain mode " + mode + " is not a recognizable bgain method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, bgainHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _shutter({id, option, mode}) {
 | 
			
		||||
    let shutterHex = "8101040A";
 | 
			
		||||
    if (mode.toLowerCase() === 'standard') {
 | 
			
		||||
        switch (option.toLowerCase()) {
 | 
			
		||||
            case "reset":
 | 
			
		||||
                shutterHex += "00FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "up":
 | 
			
		||||
                shutterHex += "02FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "down":
 | 
			
		||||
                shutterHex += "03FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The shutter option " + option + " is not a recognizable standard shutter option";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode.toLowerCase() === 'direct') {
 | 
			
		||||
        shutterHex += "0000";
 | 
			
		||||
        switch (option) {
 | 
			
		||||
            case "1/30":
 | 
			
		||||
                shutterHex += "0001FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/60":
 | 
			
		||||
                shutterHex += "0002FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/90":
 | 
			
		||||
                shutterHex += "0003FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/100":
 | 
			
		||||
                shutterHex += "0004FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/125":
 | 
			
		||||
                shutterHex += "0005FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/180":
 | 
			
		||||
                shutterHex += "0006FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/250":
 | 
			
		||||
                shutterHex += "0007FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/350":
 | 
			
		||||
                shutterHex += "0008FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/500":
 | 
			
		||||
                shutterHex += "0009FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/725":
 | 
			
		||||
                shutterHex += "000AFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/1000":
 | 
			
		||||
                shutterHex += "000BFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/1500":
 | 
			
		||||
                shutterHex += "000CFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/2000":
 | 
			
		||||
                shutterHex += "000DFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/3000":
 | 
			
		||||
                shutterHex += "000EFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/4000":
 | 
			
		||||
                shutterHex += "000FFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/6000":
 | 
			
		||||
                shutterHex += "0100FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "1/10000":
 | 
			
		||||
                shutterHex += "0101FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The shutter option " + option + " is not a recognizable direct shutter option";
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The shutter mode " + mode + " is not a recognizable shutter mode";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, shutterHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
    console.log("reached end");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _iris({id, mode, option}) {
 | 
			
		||||
    let irisHex = "8101040B";
 | 
			
		||||
 | 
			
		||||
    if (mode.toLowerCase() === 'standard') {
 | 
			
		||||
        switch (option.toLowerCase()) {
 | 
			
		||||
            case "reset":
 | 
			
		||||
                irisHex += "00FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "up":
 | 
			
		||||
                irisHex += "02FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "down":
 | 
			
		||||
                irisHex += "03FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The iris option " + option + " is not a recognizable standard iris option";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode.toLowerCase() === 'direct') {
 | 
			
		||||
        irisHex += "0000";
 | 
			
		||||
        switch (option.toLowerCase()) {
 | 
			
		||||
            case "close":
 | 
			
		||||
                irisHex += "0000FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f11":
 | 
			
		||||
                irisHex += "0006FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f9.6":
 | 
			
		||||
                irisHex += "0007FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f8.0":
 | 
			
		||||
                irisHex += "0008FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f6.8":
 | 
			
		||||
                irisHex += "0009FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f5.6":
 | 
			
		||||
                irisHex += "000AFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f4.8":
 | 
			
		||||
                irisHex += "000BFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f4.0":
 | 
			
		||||
                irisHex += "000CFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f3.4":
 | 
			
		||||
                irisHex += "000DFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f2.8":
 | 
			
		||||
                irisHex += "000EFF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f2.0":
 | 
			
		||||
                irisHex += "0100FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "f1.8":
 | 
			
		||||
                irisHex += "0200FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The iris direct option " + option + " is not a recognizable option";
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The iris option " + option + " is not a recognizable direct iris option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, irisHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _gain({id, mode, option = null, pos = null}) {
 | 
			
		||||
    let gainHex = "8101040C";
 | 
			
		||||
    if (mode.toLowerCase() === "standard") {
 | 
			
		||||
        switch(option.toLowerCase()) {
 | 
			
		||||
            case "reset":
 | 
			
		||||
                gainHex += "00FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "up":
 | 
			
		||||
                gainHex += "02FF";
 | 
			
		||||
                break;
 | 
			
		||||
            case "down":
 | 
			
		||||
                gainHex += "03FF";
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw "The gain option " + option + " is not a recognizable standard gain option";
 | 
			
		||||
        }
 | 
			
		||||
    } else if (mode === "direct") {
 | 
			
		||||
        gainHex += "0000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The gain mode " + mode + " is not a recognizable gain mode";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, gainHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _backLight({id, option}) {
 | 
			
		||||
    let backlightHex = "81010433";
 | 
			
		||||
    if (option.toLowerCase() === 'on') {
 | 
			
		||||
        backlightHex += "02FF";
 | 
			
		||||
    } else if (option.toLowerCase() === 'off') {
 | 
			
		||||
        backlightHex += "03FF";
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The back light option " + option + " is not a recognizable back light option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, backlightHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _blackWhite({id, option}) {
 | 
			
		||||
    let bwHex = "81010463";
 | 
			
		||||
 | 
			
		||||
    if (option.toLowerCase() === 'on') {
 | 
			
		||||
        bwHex += "04FF";
 | 
			
		||||
    } else if (option.toLowerCase() === 'off') {
 | 
			
		||||
        bwHex += "00FF";
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The back light option " + option + " is not a recognizable back light option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, bwHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _flicker({id, option}) {
 | 
			
		||||
    let flickerHex = "810104230";
 | 
			
		||||
    switch(option.toLowerCase()) {
 | 
			
		||||
        case "off":
 | 
			
		||||
            flickerHex += "0FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "50hz":
 | 
			
		||||
            flickerHex += "1FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case "60hz":
 | 
			
		||||
            flickerHex += "2FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The flicker option " + option + " is not a recognizable standard flicker option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, flickerHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _imgFlip({id, mode, option}) {
 | 
			
		||||
    let imgFlipHex = "810104";
 | 
			
		||||
 | 
			
		||||
    if (mode.toLowerCase() === 'lr') {
 | 
			
		||||
        imgFlipHex += '61';
 | 
			
		||||
    } else if (mode.toLowerCase() === 'pf') {
 | 
			
		||||
        imgFlipHex += '66';
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The image flip mode " + mode + " is not a recognizable image flip mode";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (option.toLowerCase() === 'off') {
 | 
			
		||||
        imgFlipHex += '03FF';
 | 
			
		||||
    } else if (option.toLowerCase() === 'on') {
 | 
			
		||||
        imgFlipHex += '02FF';
 | 
			
		||||
    } else {
 | 
			
		||||
        throw "The image flip option " + option + " is not a recognizable image flip option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, imgFlipHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _colorHue({id, pos}) {
 | 
			
		||||
    let colorHueHex = '8101044F';
 | 
			
		||||
    let colorHuePos = ptzHelper.numToHexStr(pos).padStart(8, "0");
 | 
			
		||||
    colorHueHex += colorHuePos + "FF";
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, colorHueHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _autoExp({id, option}) {
 | 
			
		||||
    let aeHex = '81010439';
 | 
			
		||||
 | 
			
		||||
    switch (option.toLowerCase()) {
 | 
			
		||||
        case "fullauto":
 | 
			
		||||
            aeHex += '00FF';
 | 
			
		||||
            break;
 | 
			
		||||
        case "manual":
 | 
			
		||||
            aeHex += '03FF';
 | 
			
		||||
            break;
 | 
			
		||||
        case "shutter":
 | 
			
		||||
            aeHex += '0AFF';
 | 
			
		||||
            break;
 | 
			
		||||
        case "iris":
 | 
			
		||||
            aeHex += '0BFF';
 | 
			
		||||
            break;
 | 
			
		||||
        case "bright":
 | 
			
		||||
            aeHex += '0DFF';
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The autoexp option " + option + " is not a recognizable autoexp option";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, aeHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveSetting(id) {
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, "81010604FF");
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								app/ptz/ptz.controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/ptz/ptz.controller.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
const ptzService = require(path.resolve(__dirname, './ptz.service.js'));
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
 | 
			
		||||
router.post('/motion', _motion);
 | 
			
		||||
router.post('/presets', _presets);
 | 
			
		||||
router.post('/focus', _focus);
 | 
			
		||||
router.post('/zoom', _zoom);
 | 
			
		||||
 | 
			
		||||
function _presets(req, res, next) {
 | 
			
		||||
    ptzService.preset(req.body)
 | 
			
		||||
        .then((response) => res.send(response))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _motion(req, res, next) {
 | 
			
		||||
    ptzService.motion(req.body)
 | 
			
		||||
        .then((response) => res.send(response))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _focus(req, res, next) {
 | 
			
		||||
    ptzService.focus(req.body)
 | 
			
		||||
        .then((response) => res.send(response))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _zoom(req, res, next) {
 | 
			
		||||
    ptzService.zoom(req.body)
 | 
			
		||||
        .then((response) => res.send(response))
 | 
			
		||||
        .catch(err => next(err));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										194
									
								
								app/ptz/ptz.service.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								app/ptz/ptz.service.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
 | 
			
		||||
const ptzHelper = require(path.resolve(__dirname, '../_helpers/ptzHelper.js'));
 | 
			
		||||
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
 | 
			
		||||
const Camera = db.Camera;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    preset: _presets,
 | 
			
		||||
    motion: _motion,
 | 
			
		||||
    focus: _focus,
 | 
			
		||||
    zoom: _zoom
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function _presets({id, mode, speed = null, memNum = null}) {
 | 
			
		||||
    let memCmd;
 | 
			
		||||
 | 
			
		||||
    switch(mode.toLowerCase()) {
 | 
			
		||||
        case 'speed':
 | 
			
		||||
            const recallSpeed = ptzHelper.numToHexStr(speed).padStart(2, "0");
 | 
			
		||||
            memCmd = "81010601" + recallSpeed + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'call':
 | 
			
		||||
            memCmd = "8101043F02" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'set':
 | 
			
		||||
            memCmd = "8101043F01" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'reset':
 | 
			
		||||
            memCmd = "8101043F00" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The preset mode " + mode + " is not a recognizable preset method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, memCmd);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _motion({id, mode, pan = null, tilt = null, panSpeed = null, tiltSpeed = null, direction = null}) {
 | 
			
		||||
    if (mode === 'absolute' || mode === 'relative' || mode === 'standard') {
 | 
			
		||||
        panTiltSpeedArr = ptzHelper.sanitizeSpeed(panSpeed, tiltSpeed);
 | 
			
		||||
        if (mode === 'absolute' || mode === 'relative') {
 | 
			
		||||
            pan = ptzHelper.numToHexStr(pan);
 | 
			
		||||
            tilt = ptzHelper.numToHexStr(tilt);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let motionHex;
 | 
			
		||||
    switch(mode.toLowerCase()) {
 | 
			
		||||
        case 'home':
 | 
			
		||||
            motionHex = "81010604FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'absolute':
 | 
			
		||||
            motionHex = "81010602" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + pan + tilt +"FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'relative':
 | 
			
		||||
            motionHex = "81010603" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + pan + tilt +"FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'standard':
 | 
			
		||||
            motionHex = "81010601" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + ptzHelper.translateDirection(direction);
 | 
			
		||||
            break;
 | 
			
		||||
        case 'current':
 | 
			
		||||
            motionHex = "81090612FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The option " + mode + " is not a recognizable motion method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, motionHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _focus({id, mode, option = null, focusPos = null, intensity = null}) {
 | 
			
		||||
    let focusHex = '';
 | 
			
		||||
    switch(mode.toLowerCase()) {
 | 
			
		||||
        case 'standard':
 | 
			
		||||
            focusHex = "81010408";
 | 
			
		||||
 | 
			
		||||
            if (option === 'stop') {
 | 
			
		||||
                focusHex += "00FF";
 | 
			
		||||
            } else if (option === 'tele') {
 | 
			
		||||
                focusHex += "03FF";
 | 
			
		||||
            } else if (option === 'wide') {
 | 
			
		||||
                focusHex += "02FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The focus option " + option + " is not a recognizable standard focus option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 'variable':
 | 
			
		||||
            focusHex = "81010408";
 | 
			
		||||
            if (option === 'tele') {
 | 
			
		||||
                focusHex += '3' + intensity + "FF";
 | 
			
		||||
            } else if (option === 'wide') {
 | 
			
		||||
                focusHex += '2' + intensity + "FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The focus option " + option + " is not a recognizable variable focus option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 'direct':
 | 
			
		||||
            focusPos = numToHexStr(focusPos).split('').reduce((str, char) => {
 | 
			
		||||
                return '0' + char;
 | 
			
		||||
            });
 | 
			
		||||
            focusPos = focusPos.padStart(8, "0");
 | 
			
		||||
            focusHex = "81010448" + focusPos + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        case 'focusmode':
 | 
			
		||||
            focusHex = "810";
 | 
			
		||||
            if (option === 'auto') {
 | 
			
		||||
                focusHex += "1043802FF";
 | 
			
		||||
            } else if (option === 'manual') {
 | 
			
		||||
                focusHex += "1043803FF";
 | 
			
		||||
            } else if (option === 'toggle') {
 | 
			
		||||
                focusHex += "1043810FF";
 | 
			
		||||
            } else if (option === 'lock') {
 | 
			
		||||
                focusHex += "A046802FF";
 | 
			
		||||
            } else if (option === 'unlock') {
 | 
			
		||||
                focusHex += "A046803FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The focus option " + option + " is not a recognizable focus mode option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 'afzone':
 | 
			
		||||
            focusHex = "810104AA";
 | 
			
		||||
            if (option === 'top') {
 | 
			
		||||
                focusHex += "00FF";
 | 
			
		||||
            } else if (option === 'center') {
 | 
			
		||||
                focusHex += "01FF";
 | 
			
		||||
            } else if (option === 'bottom') {
 | 
			
		||||
                focusHex += "02FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The focus option " + option + " is not a recognizable auto focus-zone option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The focus mode " + mode + " is not a recognizable focus method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, focusHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _zoom({id, mode, option = null, zoomPos = null, intensity}) {
 | 
			
		||||
    let zoomHex = '';
 | 
			
		||||
 | 
			
		||||
    switch (mode) {
 | 
			
		||||
        case 'standard':
 | 
			
		||||
            zoomHex = "81010407";
 | 
			
		||||
            if (option === "stop") {
 | 
			
		||||
                zoomHex += "00FF";
 | 
			
		||||
            } else if (option === "tele") {
 | 
			
		||||
                zoomHex += "02FF";
 | 
			
		||||
            } else if (option === 'wide') {
 | 
			
		||||
                zoomHex += "03FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The zoom option " + option + " is not a recognizable standard zoom option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 'variable':
 | 
			
		||||
            zoomHex = "81010407";
 | 
			
		||||
            if (option === 'tele') {
 | 
			
		||||
                zoomHex += '2' + intensity + "FF";
 | 
			
		||||
            } else if (option === 'wide') {
 | 
			
		||||
                zoomHex += '3' + intensity + "FF";
 | 
			
		||||
            } else {
 | 
			
		||||
                throw "The zoom option " + option + " is not a recognizable variable zoom option";
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 'direct':
 | 
			
		||||
            zoomPos = numToHexStr(zoomPos).split('').reduce((str, char) => {
 | 
			
		||||
                return '0' + char;
 | 
			
		||||
            });
 | 
			
		||||
            zoomPos = zoomPos.padStart(8, "0");
 | 
			
		||||
            zoomHex = "81010447" + zoomPos + "FF";
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            throw "The zoom mode " + mode + " is not a recognizable zoom method.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await socket.sendCmd(id, zoomHex);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								app/stream/mpeg1muxer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/stream/mpeg1muxer.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
 | 
			
		||||
const child_process = require('child_process');
 | 
			
		||||
const EventEmitter = require('events');
 | 
			
		||||
const spawn = require('cross-spawn');
 | 
			
		||||
 | 
			
		||||
class Mpeg1Muxer extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
    constructor(options) {
 | 
			
		||||
        super(options);
 | 
			
		||||
 | 
			
		||||
        this.url = options.url;
 | 
			
		||||
        this.width = options.width;
 | 
			
		||||
        // this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-vf', 'yadif', '-f', 'mpegts', '-r', '30',  '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '4096k', '-muxdelay', '0', '-', './app/stream/stream.ts'], {
 | 
			
		||||
        //     detached: false
 | 
			
		||||
        // });
 | 
			
		||||
        this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-filter:v', 'scale=1280:-1', '-f', 'mpegts', '-r', '30',  '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '1500k', '-', './app/stream/stream.ts'], {
 | 
			
		||||
            detached: false
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.inputStreamStarted = true;
 | 
			
		||||
        this.stream.stdout.on('data', (data) => { return this.emit('mpeg1data', data); });
 | 
			
		||||
        this.stream.stderr.on('data', (data) => { return this.emit('ffmpegError', data); });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stop() {
 | 
			
		||||
        try {
 | 
			
		||||
            this.stream.stdout.removeAllListeners();
 | 
			
		||||
        } catch(err) {
 | 
			
		||||
            console.log("Muxer: " + err);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.stream.kill();
 | 
			
		||||
        this.stream = undefined;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Mpeg1Muxer;
 | 
			
		||||
							
								
								
									
										124
									
								
								app/stream/videoStream.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								app/stream/videoStream.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
// Thanks to:  https://github.com/Wifsimster/node-rtsp-stream-es6
 | 
			
		||||
const WebSocket = require('ws');
 | 
			
		||||
const EventEmitter = require('events');
 | 
			
		||||
const STREAM_MAGIC_BYTES = "jsmp";
 | 
			
		||||
const Mpeg1Muxer = require('./mpeg1muxer');
 | 
			
		||||
 | 
			
		||||
class VideoStream extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
    constructor(options) {
 | 
			
		||||
        super(options);
 | 
			
		||||
        this.name = options.name;
 | 
			
		||||
        this.url = options.url;
 | 
			
		||||
        this.width = options.width;
 | 
			
		||||
        this.height = options.height;
 | 
			
		||||
        this.port = options.port;
 | 
			
		||||
        this.stream = void 0;
 | 
			
		||||
        this.stream2Socket();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stream2Socket() {
 | 
			
		||||
        this.server = new WebSocket.Server({
 | 
			
		||||
            port: this.port
 | 
			
		||||
        });
 | 
			
		||||
        this.server.on('connection', (socket) => {
 | 
			
		||||
            console.log(`New connection: ${this.name}`);
 | 
			
		||||
            let streamHeader = new Buffer(8);
 | 
			
		||||
            streamHeader.write(STREAM_MAGIC_BYTES);
 | 
			
		||||
            streamHeader.writeUInt16BE(this.width, 4);
 | 
			
		||||
            streamHeader.writeUInt16BE(this.height, 6);
 | 
			
		||||
            socket.send(streamHeader);
 | 
			
		||||
 | 
			
		||||
            socket.on('close', () => {
 | 
			
		||||
                console.log(`${this.name} disconnected !`);
 | 
			
		||||
                if (this.server.clients.length <= 0) {
 | 
			
		||||
                    this.mpeg1Muxer.stop();
 | 
			
		||||
                    this.server.close();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.on('camdata', (data) => {
 | 
			
		||||
            for (let i in this.server.clients) {
 | 
			
		||||
                let client = this.server.clients[i];
 | 
			
		||||
                if (client.readyState === WebSocket.OPEN) {
 | 
			
		||||
                    client.send(data);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onSocketConnect(socket) {
 | 
			
		||||
        let streamHeader = new Buffer(8);
 | 
			
		||||
        streamHeader.write(STREAM_MAGIC_BYTES);
 | 
			
		||||
        streamHeader.writeUInt16BE(this.width, 4);
 | 
			
		||||
        streamHeader.writeUInt16BE(this.height, 6);
 | 
			
		||||
        socket.send(streamHeader, {
 | 
			
		||||
            binary: true
 | 
			
		||||
        });
 | 
			
		||||
        console.log(`New connection: ${this.name} - ${this.wsServer.clients.length} total`);
 | 
			
		||||
        return socket.on("close", function(code, message) {
 | 
			
		||||
            return console.log(`${this.name} disconnected - ${this.wsServer.clients.length} total`);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    start() {
 | 
			
		||||
        this.mpeg1Muxer = new Mpeg1Muxer({
 | 
			
		||||
            url: this.url,
 | 
			
		||||
            width: this.width
 | 
			
		||||
        });
 | 
			
		||||
        this.mpeg1Muxer.on('mpeg1data', (data) => {
 | 
			
		||||
            return this.emit('camdata', data);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let gettingInputData = false;
 | 
			
		||||
        let gettingOutputData = false;
 | 
			
		||||
        let inputData = [];
 | 
			
		||||
        let outputData = [];
 | 
			
		||||
 | 
			
		||||
        this.mpeg1Muxer.on('ffmpegError', (data) => {
 | 
			
		||||
            data = data.toString();
 | 
			
		||||
            if (data.indexOf('Input #') !== -1) {
 | 
			
		||||
                gettingInputData = true;
 | 
			
		||||
            }
 | 
			
		||||
            if (data.indexOf('Output #') !== -1) {
 | 
			
		||||
                gettingInputData = false;
 | 
			
		||||
                gettingOutputData = true;
 | 
			
		||||
            }
 | 
			
		||||
            if (data.indexOf('frame') === 0) {
 | 
			
		||||
                gettingOutputData = false;
 | 
			
		||||
            }
 | 
			
		||||
            if (gettingInputData) {
 | 
			
		||||
                inputData.push(data.toString());
 | 
			
		||||
                let size = data.match(/\d+x\d+/);
 | 
			
		||||
                if (size != null) {
 | 
			
		||||
                    size = size[0].split('x');
 | 
			
		||||
                    if (this.width == null) {
 | 
			
		||||
                        this.width = parseInt(size[0], 10);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (this.height == null) {
 | 
			
		||||
                        return this.height = parseInt(size[1], 10);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this.mpeg1Muxer.on('ffmpegError', (data) => {
 | 
			
		||||
            return global.process.stderr.write(data);
 | 
			
		||||
        });
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stop(serverCloseCallback) {
 | 
			
		||||
        this.server.close(serverCloseCallback);
 | 
			
		||||
        this.server.removeAllListeners();
 | 
			
		||||
        this.server = undefined;
 | 
			
		||||
 | 
			
		||||
        this.mpeg1Muxer.stop();
 | 
			
		||||
        this.mpeg1Muxer.removeAllListeners();
 | 
			
		||||
        this.mpeg1Muxer = undefined;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = VideoStream;
 | 
			
		||||
							
								
								
									
										3053
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3053
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ptzoptics_server_api",
 | 
			
		||||
  "version": "0.0.1",
 | 
			
		||||
  "description": "Skeleton webapp to control PTZOptics cameras via the visca protocol",
 | 
			
		||||
  "main": "server.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node server.js",
 | 
			
		||||
    "devStart": "nodemon server.js",
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "PTZOptics",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@ffmpeg-installer/ffmpeg": "^1.0.15",
 | 
			
		||||
    "body-parser": "^1.18.3",
 | 
			
		||||
    "compression": "^1.7.3",
 | 
			
		||||
    "connect-mongo": "^2.0.1",
 | 
			
		||||
    "cors": "^2.8.4",
 | 
			
		||||
    "cross-spawn": "^6.0.5",
 | 
			
		||||
    "express": "^4.16.3",
 | 
			
		||||
    "mongoose": "^5.2.1",
 | 
			
		||||
    "nodemon": "^1.17.5",
 | 
			
		||||
    "path": "^0.12.7",
 | 
			
		||||
    "tcp-port-used": "^1.0.0",
 | 
			
		||||
    "ws": "1.1.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								pub/PTZ-Optics-Favicon.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								pub/PTZ-Optics-Favicon.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										205
									
								
								pub/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								pub/css/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
			
		||||
html, body {
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    width: 100vw;
 | 
			
		||||
}
 | 
			
		||||
body{
 | 
			
		||||
    font-size:.875rem;
 | 
			
		||||
    background-color:#ececec;
 | 
			
		||||
    max-width:1440px;
 | 
			
		||||
    max-height: 100vh;
 | 
			
		||||
}
 | 
			
		||||
option {
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    display: block;
 | 
			
		||||
    white-space: pre;
 | 
			
		||||
    min-height: 1.2em;
 | 
			
		||||
}
 | 
			
		||||
.wrapper{
 | 
			
		||||
    width:100%;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    align-items:stretch;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Sidebar & Content */
 | 
			
		||||
 | 
			
		||||
#content,#sidebar{
 | 
			
		||||
    min-height:100vh;
 | 
			
		||||
    max-height:100vh;
 | 
			
		||||
    transition:all .3s;
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
}
 | 
			
		||||
#sidebar{
 | 
			
		||||
    width:30%;
 | 
			
		||||
    min-width:30%;
 | 
			
		||||
    max-width:30%;
 | 
			
		||||
    position:fixed;
 | 
			
		||||
    top:0;
 | 
			
		||||
    left:0;
 | 
			
		||||
    z-index:999;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
#sidebar.active{
 | 
			
		||||
    margin-left:-30%;
 | 
			
		||||
}
 | 
			
		||||
#content{
 | 
			
		||||
    width:65%;
 | 
			
		||||
    padding-right: 5%;
 | 
			
		||||
    margin-right: 1%;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
}
 | 
			
		||||
#content.active {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
.optionview.row{
 | 
			
		||||
    margin-bottom: 1.0em;
 | 
			
		||||
}
 | 
			
		||||
ul.nav {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
}
 | 
			
		||||
#optionsNav > ul, #optionsNav > .nav,  #optionsNav > ul > li {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    float: none;
 | 
			
		||||
    transition:all .3s;
 | 
			
		||||
}
 | 
			
		||||
#optionWindow {
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
.highlight {
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    background-color: #93CCEA;
 | 
			
		||||
}
 | 
			
		||||
.topBtns {
 | 
			
		||||
    margin-bottom: 1.0em;
 | 
			
		||||
}
 | 
			
		||||
.imageRow {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
    margin-left: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Movement buttons & option nav */
 | 
			
		||||
 | 
			
		||||
.movementBtn,.nav-item{
 | 
			
		||||
    cursor:pointer;
 | 
			
		||||
}
 | 
			
		||||
.movementBtn{
 | 
			
		||||
    width:2.7em;
 | 
			
		||||
    height:2.7em;
 | 
			
		||||
    color:#999;
 | 
			
		||||
    margin:.4em;
 | 
			
		||||
    transition: transform .2s;
 | 
			
		||||
}
 | 
			
		||||
.movementBtn:hover{
 | 
			
		||||
    color:#000;
 | 
			
		||||
    transform: scale(1.2);
 | 
			
		||||
}
 | 
			
		||||
.movementBtn:active{
 | 
			
		||||
    color:green;
 | 
			
		||||
    transform: scale(1.2);
 | 
			
		||||
    text-shadow:0 0 6px green;
 | 
			
		||||
}
 | 
			
		||||
#ptzSpeedBtnRow,#setCallBtnRow{
 | 
			
		||||
    padding-top:.8em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Message & Stream Stage */
 | 
			
		||||
 | 
			
		||||
#stageContainer {
 | 
			
		||||
    height: 80%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    position: static;
 | 
			
		||||
}
 | 
			
		||||
#streamStage, #msgStage {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
}
 | 
			
		||||
#streamStage {
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
    right: 0;
 | 
			
		||||
}
 | 
			
		||||
#msgStage {
 | 
			
		||||
    transition:all .4s;
 | 
			
		||||
    display:none;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
.errMsg {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    color: #721c24;
 | 
			
		||||
    background-color: #f8d7da;
 | 
			
		||||
    border-color: #f5c6cb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.successMsg {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    color: #155724;
 | 
			
		||||
    background-color: #d4edda;
 | 
			
		||||
    border-color: #c3e6cb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Mobile Devices */
 | 
			
		||||
 | 
			
		||||
@media (max-width:414px){
 | 
			
		||||
    .wrapper{
 | 
			
		||||
        width:100vw;
 | 
			
		||||
        height: 100vh;
 | 
			
		||||
        margin-right: auto;
 | 
			
		||||
        margin-left: auto;
 | 
			
		||||
    }
 | 
			
		||||
    /* Sidebar & Content */
 | 
			
		||||
    #content,#sidebar{
 | 
			
		||||
        transition:all .3s;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        z-index: auto;
 | 
			
		||||
        right: auto;
 | 
			
		||||
        left: auto;
 | 
			
		||||
        min-width: 100vw;
 | 
			
		||||
        max-width: 100vw;
 | 
			
		||||
    }
 | 
			
		||||
    #sidebar{
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 50%;
 | 
			
		||||
        width:100%;
 | 
			
		||||
        padding: 20px;
 | 
			
		||||
        overflow-y: scroll;
 | 
			
		||||
        top: 50%;
 | 
			
		||||
        max-height: 50%;
 | 
			
		||||
        min-height: 50%;
 | 
			
		||||
    }
 | 
			
		||||
    #sidebar.active{
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
    #content{
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        width:100%;
 | 
			
		||||
        max-width: 100vw;
 | 
			
		||||
        height: 50%;
 | 
			
		||||
        min-height: 50%;
 | 
			
		||||
        max-height: 50%;
 | 
			
		||||
        top:0px;
 | 
			
		||||
        overflow-x: hidden;
 | 
			
		||||
    }
 | 
			
		||||
    #content.active {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        max-height: 100%;
 | 
			
		||||
        min-height: 100%;
 | 
			
		||||
        max-width: 100vw;
 | 
			
		||||
    }
 | 
			
		||||
    #optionWindow {
 | 
			
		||||
        overflow-y: scroll;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* stage */
 | 
			
		||||
    #stageContainer {
 | 
			
		||||
        height: 70%;
 | 
			
		||||
        position: inherit;
 | 
			
		||||
    }
 | 
			
		||||
    #streamStage {
 | 
			
		||||
        position: static;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										400
									
								
								pub/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								pub/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,400 @@
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 | 
			
		||||
    <meta name="description" content="">
 | 
			
		||||
    <meta name="author" content="">
 | 
			
		||||
    <title>Visca API</title>
 | 
			
		||||
    <base href="/" />
 | 
			
		||||
    <link rel="icon" href="PTZ-Optics-Favicon.jpg" />
 | 
			
		||||
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" />
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="/css/style.css" />
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="wrapper">
 | 
			
		||||
        <nav id="sidebar">
 | 
			
		||||
            <div id="options">
 | 
			
		||||
                <h3 class="text-center mx-auto">Option Panel</h3>
 | 
			
		||||
                <nav id="optionsNav" class="navbar navbar-default">
 | 
			
		||||
                    <ul class="nav text-center mx-auto">
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link active highlight" value="ctrl" onclick="optionView('ctrl');">PTZ</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link" value="focus" onclick="optionView('focus');">Focus</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link" value="camImage" onclick="optionView('camImage');">Image</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link" value="preset" onclick="optionView('preset');">Presets</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </nav>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="optionWindow">
 | 
			
		||||
                <div id="ctrl" class="optionView">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div id="arrowControl" class="col">
 | 
			
		||||
                            <div class="row">
 | 
			
		||||
                                <div class="col text-center mx-auto">
 | 
			
		||||
                                    <div class="btn-group pb-1" role="group" aria-label="Focus">
 | 
			
		||||
                                        <button id="focusWide" type="button" class="btn btn-sm btn-outline-dark" data-value="wide"><i class="fas fa-minus"></i></button>
 | 
			
		||||
                                        <button type="button" class="btn btn-small btn-dark disabled">Focus</button>
 | 
			
		||||
                                        <button id="focusTele" type="button" class="btn btn-sm btn-outline-dark" data-value="tele"><i class="fas fa-plus"></i></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="row">
 | 
			
		||||
                                <div class="col">
 | 
			
		||||
                                    <div class="row justify-content-md-center">
 | 
			
		||||
                                        <div class="col mx-auto text-center">
 | 
			
		||||
                                            <i id="motionUp" data-value="up" class="camera-ctrl movementBtn fas fa-sm fa-arrow-up"></i>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="row justify-content-md-center">
 | 
			
		||||
                                        <div class="col text-center">
 | 
			
		||||
                                            <i id="motionLeft" data-value="left" class="camera-ctrl movementBtn fas fa-sm fa-arrow-left"></i>
 | 
			
		||||
                                            <i class="camera-ctrl movementBtn fas fa-sm fa-home" onclick="moveHome()"></i>
 | 
			
		||||
                                            <i id="motionRight" data-value="right" class="camera-ctrl movementBtn fas fa-sm fa-arrow-right"></i>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="row justify-content-md-center">
 | 
			
		||||
                                        <div class="col mx-auto text-center">
 | 
			
		||||
                                            <i id="motionDown" data-value="down" class="camera-ctrl movementBtn fas fa-sm fa-arrow-down"></i>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="row">
 | 
			
		||||
                                <div class="col text-center">
 | 
			
		||||
                                    <div class="btn-group pb-1" role="group" aria-label="Focus">
 | 
			
		||||
                                        <button id="zoomWide" type="button" class="btn btn-sm btn-outline-dark" data-value="wide"><i class="fas fa-minus"></i></button>
 | 
			
		||||
                                        <button type="button" class="btn btn-small btn-dark disabled">Zoom</button>
 | 
			
		||||
                                        <button id="zoomTele" type="button" class="btn btn-sm btn-outline-dark" data-value="tele"><i class="fas fa-plus"></i></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="row pr-1 pt-1">
 | 
			
		||||
                                <div class="col-lg-10 mx-auto">
 | 
			
		||||
                                    <div class="form-group">
 | 
			
		||||
                                        <label id="panSpeedLabel" for="formControlRange">Pan Speed: 10</label>
 | 
			
		||||
                                        <input id="panSpeedInput" data-html="pan" type="range" class="form-control-range panTiltInput" max="18" min="1" value="10">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-group">
 | 
			
		||||
                                        <label id="tiltSpeedLabel" for="formControlRange">Tilt Speed: 10</label>
 | 
			
		||||
                                        <input id="tiltSpeedInput" data-html="tilt" type="range" class="form-control-range panTiltInput" max="14" min="1" value="10">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-group">
 | 
			
		||||
                                        <label id="zoomSpeedLabel" for="formControlRange">Zoom Speed: 5</label>
 | 
			
		||||
                                        <input id="zoomSpeedInput" data-html="zoom" type="range" class="form-control-range panTiltInput" max="7" min="0" value="5">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-group">
 | 
			
		||||
                                        <label id="focusSpeedLabel" for="formControlRange">Focus Speed: 5</label>
 | 
			
		||||
                                        <input id="focusSpeedInput" data-html="focus" type="range" class="form-control-range panTiltInput" max="7" min="0" value="5">
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="focus" class="optionView" style="display:none;">
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col-lg-6">
 | 
			
		||||
                            <label for="focusModeSel">Focus Mode:</label>
 | 
			
		||||
                            <select id="focusModeSel" class="custom-select" onchange="focusMode()">
 | 
			
		||||
                                <option value="auto">Auto</option>
 | 
			
		||||
                                <option value="manual">Manual</option>
 | 
			
		||||
                                <option value="toggle">toggle</option>
 | 
			
		||||
                                <option value="lock">Lock</option>
 | 
			
		||||
                                <option value="unlock">Unlock</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-lg-6">
 | 
			
		||||
                            <label for="afZoneSel">AF Zone:</label>
 | 
			
		||||
                            <select id="afZoneSel" class="custom-select" onchange="afZone()">
 | 
			
		||||
                                <option value="top">Top</option>
 | 
			
		||||
                                <option value="center">Center</option>
 | 
			
		||||
                                <option value="bottom">Bottom</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="camImage" class="optionView" style="display:none;">
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="wbModeSel">White Balance:</label>
 | 
			
		||||
                            <select id="wbModeSel" class="custom-select" onchange="whiteBalance('wbmode')">
 | 
			
		||||
                                <option value="auto">Auto</option>
 | 
			
		||||
                                <option value="indoor">Indoor</option>
 | 
			
		||||
                                <option value="outdoor">Outdoor</option>
 | 
			
		||||
                                <option value="onepush">Onepush</option>
 | 
			
		||||
                                <option value="manual">Manual</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="awbSel">AWB Sensitivity:</label>
 | 
			
		||||
                            <select id="awbSel" class="custom-select" onchange="whiteBalance('awbsenstivity')">
 | 
			
		||||
                                <option value="high">High</option>
 | 
			
		||||
                                <option value="normal" selected>Normal</option>
 | 
			
		||||
                                <option value="low">Low</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="bgainBtn">Bgain:</label>
 | 
			
		||||
                            <div id="bgainBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Bgain Button Group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="bgain('standard', 'up')">Up</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="bgain('standard', 'down')">Down</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="bgain('standard', 'reset')">Reset</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="rgainBtn">Rgain:</label>
 | 
			
		||||
                            <div id="rgainBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Rgain Button Group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="rgain('standard', 'up')">Up</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="rgain('standard', 'down')">Down</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="rgain('standard', 'reset')">Reset</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="gainBtn">Gain:</label>
 | 
			
		||||
                            <div id="gainBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Gain Button Group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="gain('standard', 'up')">Up</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="gain('standard', 'down')">Down</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="gain('standard', 'reset')">Reset</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="blRadios">Backlight:</label>
 | 
			
		||||
                            <div id="blRadios" class="mx-auto text-center">
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <label class="form-check-label" for="blOn">On</label>
 | 
			
		||||
                                    <input class="form-check-input" onclick="backlight('on')" type="radio" name="blRadio" id="blOn" value="on">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <label class="form-check-label" for="blOff"> Off </label>
 | 
			
		||||
                                    <input class="form-check-input" onclick="backlight('off')" type="radio" name="blRadio" id="blOff" checked>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="bwRadios">Black White:</label>
 | 
			
		||||
                            <div id="bwRadios" class="mx-auto text-center">
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <label class="form-check-label" for="bwOn">On</label>
 | 
			
		||||
                                    <input class="form-check-input" onclick="blackWhite('on')" type="radio" name="bwRadio" id="blOn" value="on">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <label class="form-check-label" for="bwOff">Off</label>
 | 
			
		||||
                                    <input class="form-check-input" onclick="blackWhite('off')" type="radio" name="bwRadio" id="blOff" value="off" checked>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="flickerBtn">Flicker:</label>
 | 
			
		||||
                            <div id="flickerBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Camera Flicker">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="flicker('off')">Off</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="flicker('50Hz')">50Hz</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="flicker('60Hz')">60hz</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="imageFlipCb">Image Flip Dimension:</label>
 | 
			
		||||
                            <div id="imageFlipCb" class="mx-auto text-center">
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="lr" onchange="imgFlip('lr')">
 | 
			
		||||
                                    <label class="form-check-label" for="lr">Horizontal</label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check form-check-inline">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="pf" onchange="imgFlip('pf')">
 | 
			
		||||
                                    <label class="form-check-label" for="pf">Vertical</label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label id="colorHueInputLabel" for="colorHueInput">ColorHue: 8</label>
 | 
			
		||||
                                <input id="colorHueInput" type="range" class="form-control-range" max="14" min="0" value="8" onchange="colorHue()">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label id="brightInputLabel" for="brightInput">Brightness: 8</label>
 | 
			
		||||
                                <input id="brightInput" type="range" class="form-control-range" max="14" min="0" value="8" onchange="brightness()">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
                                <label id="contrastInputLabel" for="contrastInput">Contrast: 9</label>
 | 
			
		||||
                                <input id="contrastInput" type="range" class="form-control-range" max="14" min="0" value="9" onchange="contrast()">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="aeSel">Auto Exp:</label>
 | 
			
		||||
                            <select id="aeSel" class="custom-select" onchange="autoExp()">
 | 
			
		||||
                                <option value="fullauto">Full Auto</option>
 | 
			
		||||
                                <option value="manual" selected>Manual</option>
 | 
			
		||||
                                <option value="shutter">Shutter</option>
 | 
			
		||||
                                <option value="iris">Iris</option>
 | 
			
		||||
                                <option value="bright">Bright</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="shutterBtn">Shutter:</label>
 | 
			
		||||
                            <div id="shutterBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Shutter Button Group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="shutter('standard', 'up')">Up</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="shutter('standard', 'down')">Down</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="shutter('standard', 'reset')">Reset</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <label for="irisBtn">Iris:</label>
 | 
			
		||||
                            <div id="irisBtn" class="mx-auto">
 | 
			
		||||
                                <div class="btn-group" role="group" aria-label="Iris Button Group">
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="iris('standard', 'up')">Up</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="iris('standard', 'down')">Down</button>
 | 
			
		||||
                                    <button type="button" class="btn btn-outline-dark btn-sm text-center" onclick="iris('standard', 'reset')">Reset</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="preset" class="optionView" style="display:none;">
 | 
			
		||||
                    <div class="row mb-2 text-center imageRow">
 | 
			
		||||
                        <div class="col-lg-12 mx-auto">
 | 
			
		||||
                            <label id="presetSpeedLabel" for="presetSpeedInput">Preset Recall Speed: 5</label>
 | 
			
		||||
                            <input id="presetSpeedInput" type="range" class="form-control-range" max="18" min="1" value="5" onchange="presetAction('speed', 'presetSpeedInput')">
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col-lg-12 mx-auto">
 | 
			
		||||
                            <div class="input-group mb-3">
 | 
			
		||||
                                <div class="input-group-prepend">
 | 
			
		||||
                                    <span class="input-group-text" id="basic-addon1">Preset:</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <input id="presetInput" class="form-control" placeholder="Preset Number (0 - 127)" aria-label="Preset number Input" aria-describedby="Camera Preset">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="row text-center imageRow">
 | 
			
		||||
                        <div class="col mx-auto text-center">
 | 
			
		||||
                            <button id="setPresetBtn" type="button" class="btn btn-sm btn-outline-dark" onclick="presetAction('set')">Set</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col mx-auto text-center">
 | 
			
		||||
                            <button id="callPresetBtn" type="button" class="btn btn-sm btn-outline-dark" onclick="presetAction('call')">Call</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col mx-auto text-center">
 | 
			
		||||
                            <button id="recallPresetBtn" type="button" class="btn btn-sm btn-outline-dark" onclick="presetAction('reset')">Reset</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
        <div id="content" class="container-fluid">
 | 
			
		||||
            <div class="modal fade" id="newCameraModal" tabindex="-1" role="dialog" aria-labelledby="cameraModalLabel" aria-hidden="true">
 | 
			
		||||
                <div class="modal-dialog" role="document">
 | 
			
		||||
                    <div class="modal-content">
 | 
			
		||||
                        <div class="modal-header">
 | 
			
		||||
                            <h5 class="modal-title" id="cameraModalLabel">New Camera</h5>
 | 
			
		||||
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                                <span aria-hidden="true">×</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-body">
 | 
			
		||||
                            <form class="needs-validation">
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="cameraNickname">Name:</label>
 | 
			
		||||
                                    <input type="text" name="name" class="form-control" id="cameraNickname" placeholder="Camera Nickname">
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="cameraIpAddy">IP Address:</label>
 | 
			
		||||
                                    <input type="text" name="ip" class="form-control" id="cameraIpAddy" placeholder="xxx.xxx.xxx.x" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="cameraPort">Port:</label>
 | 
			
		||||
                                    <input type="text" name="port" class="form-control" id="cameraPort" placeholder="5678" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    <label for="cameraRtsp">RTSP:</label>
 | 
			
		||||
                                    <input type="text" name="rtsp" class="form-control" id="cameraRtsp" placeholder="xxx.xxx.xxx.x:554/1" required>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="modal-footer">
 | 
			
		||||
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
 | 
			
		||||
                            <button type="button" class="btn btn-outline-dark" onclick="addCamera();" data-dismiss="modal">Save Camera</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="row topBtns">
 | 
			
		||||
                <div class="col-lg-8 text-center mx-auto">
 | 
			
		||||
                    <button type="button" class="btn btn-sm btn-outline-info" onclick="toggleSideBar()">
 | 
			
		||||
                        <i class="fas fa-align-left"></i>
 | 
			
		||||
                        <span>Toggle Sidebar</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button type="button" class="btn btn-sm btn-outline-dark" data-toggle="modal" data-target="#newCameraModal"><span>Add Camera</span></button>
 | 
			
		||||
                    <button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteCamera();"><span>Delete Camera</span></button>
 | 
			
		||||
                    <button id="fullscreenToggle" type="button" class="btn btn-sm btn-outline-dark" onclick="reqFullScreen()"><i class="fas fa-arrows-alt"></i><span></span></button>
 | 
			
		||||
                    <select id="cameraList" class="custom-select" onchange="changeStream();"></select>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-lg-12">
 | 
			
		||||
                    <div id="stageContainer" class="text-center">
 | 
			
		||||
                        <div id="msgStage" class="alert text-center mt-1"></div>
 | 
			
		||||
                        <canvas id="streamStage" class="img-fluid mx-auto text-center" controls></canvas>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <script type="text/javascript" src="/js/main.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/motion.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/image.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/focusZoom.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/jsmpeg.min.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/stream.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="/js/preset.js"></script>
 | 
			
		||||
    <script type="text/javascript">
 | 
			
		||||
        getAllSavedCameras()
 | 
			
		||||
            .then((cameras) => {
 | 
			
		||||
                const camObj = JSON.parse(cameras);
 | 
			
		||||
                camObj.forEach(function(camera, index) {
 | 
			
		||||
                    addCameraTag(camera, index);
 | 
			
		||||
                });
 | 
			
		||||
            }).then(() => {
 | 
			
		||||
                startStream();
 | 
			
		||||
                motionTouchEvents();
 | 
			
		||||
                focusTouchEvents('focus');
 | 
			
		||||
            })
 | 
			
		||||
            .catch((err => console.log(err)));
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
 | 
			
		||||
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
							
								
								
									
										166
									
								
								pub/js/focusZoom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								pub/js/focusZoom.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
function focusTouchEvents() {
 | 
			
		||||
    const opts = ['tele', 'wide'];
 | 
			
		||||
    opts.forEach((opt) => {
 | 
			
		||||
        const eleId = 'focus' + opt.charAt(0).toUpperCase() + opt.slice(1);
 | 
			
		||||
        const ele = document.getElementById(eleId);
 | 
			
		||||
        ele.addEventListener('mousedown', variableFocus);
 | 
			
		||||
        ele.addEventListener('mouseup', stopFocus);
 | 
			
		||||
        ele.addEventListener('touchstart', variableFocus);
 | 
			
		||||
        ele.addEventListener('touchend', stopFocus);
 | 
			
		||||
    });
 | 
			
		||||
    zoomTouchEvents();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function zoomTouchEvents() {
 | 
			
		||||
    const opts = ['tele', 'wide'];
 | 
			
		||||
    opts.forEach((opt) => {
 | 
			
		||||
        const eleId = 'zoom' + opt.charAt(0).toUpperCase() + opt.slice(1);
 | 
			
		||||
        const ele = document.getElementById(eleId);
 | 
			
		||||
        ele.addEventListener('mousedown', variableZoom);
 | 
			
		||||
        ele.addEventListener('mouseup', stopZoom);
 | 
			
		||||
        ele.addEventListener('touchstart', variableZoom);
 | 
			
		||||
        ele.addEventListener('touchend', stopZoom);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getFocusSpeed() {
 | 
			
		||||
    return document.getElementById('focusSpeedInput').value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getZoomSpeed() {
 | 
			
		||||
    return document.getElementById('zoomSpeedInput').value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopFocus(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    stdFocus('stop');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopZoom(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    stdZoom('stop');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function variableFocus(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const opt = e.currentTarget.getAttribute('data-value');
 | 
			
		||||
    const focusSpeed = getFocusSpeed();
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "variable",
 | 
			
		||||
        "option": opt,
 | 
			
		||||
        "intensity": focusSpeed
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/focus", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("variableFocus res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function directFocus(focusPos) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "direct",
 | 
			
		||||
        "focusPos": focusPos
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/focus", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("directFocus res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stdFocus(opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "standard",
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/focus", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("stdFocus res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focusMode() {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "focusmode",
 | 
			
		||||
        "option": document.getElementById('focusModeSel').selectedOptions[0].value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/focus", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("focusmode res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function afZone() {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "afzone",
 | 
			
		||||
        "option": document.getElementById('afZoneSel').selectedOptions[0].value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/focus", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("afzone res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// **************************************** ZOOM *************************************
 | 
			
		||||
 | 
			
		||||
function variableZoom(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const opt = e.currentTarget.getAttribute('data-value');
 | 
			
		||||
    const zoomSpeed = getZoomSpeed();
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "variable",
 | 
			
		||||
        "option": opt,
 | 
			
		||||
        "intensity": zoomSpeed
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/zoom", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("variableZoom res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stdZoom(opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "standard",
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/zoom", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("stdZoom res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function directZoom(zoomPos) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "direct",
 | 
			
		||||
        "zoomPos": zoomPos
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/zoom", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("directZoom res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										237
									
								
								pub/js/image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								pub/js/image.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
function whiteBalance(mode) {
 | 
			
		||||
    let id;
 | 
			
		||||
    if (mode === 'wbmode') {
 | 
			
		||||
        id = "wbModeSel";
 | 
			
		||||
    } else {
 | 
			
		||||
        id = "awbSel";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode,
 | 
			
		||||
        "option": document.getElementById(id).selectedOptions[0].value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/wb", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("white bal res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function autoExp() {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "option": document.getElementById("aeSel").selectedOptions[0].value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/ae", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("auto exp res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rgain(mode, opt, pos) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode
 | 
			
		||||
    };
 | 
			
		||||
    if (mode === "direct") {
 | 
			
		||||
        payload.pos = pos;
 | 
			
		||||
    } else if (mode === "standard"){
 | 
			
		||||
        payload.option = opt;
 | 
			
		||||
    }
 | 
			
		||||
    sendCmd("/image/rgain", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("rgain res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function bgain(mode, opt, pos) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode
 | 
			
		||||
    };
 | 
			
		||||
    if (mode === "direct") {
 | 
			
		||||
        payload.pos = pos;
 | 
			
		||||
    } else if (mode === "standard"){
 | 
			
		||||
        payload.option = opt;
 | 
			
		||||
    }
 | 
			
		||||
    sendCmd("/image/bgain", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("bgain res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function shutter(mode, option) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode,
 | 
			
		||||
        "option": option
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sendCmd("/image/shutter", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("shutter res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function iris(mode, option) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (mode === "direct") {
 | 
			
		||||
        payload.pos = pos;
 | 
			
		||||
    } else if (mode === "standard"){
 | 
			
		||||
        payload.option = option;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sendCmd("/image/iris", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("iris res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function gain(mode, opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode,
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sendCmd("/image/gain", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("gain res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function backlight(opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/backlight", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("backlight res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function blackWhite(opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/bw", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("blackwhite res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function flicker(opt) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "option": opt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/flicker", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("flicker res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function imgFlip(mode) {
 | 
			
		||||
    let opt;
 | 
			
		||||
    if (document.getElementById(mode).checked) {
 | 
			
		||||
        opt = 'on';
 | 
			
		||||
    } else {
 | 
			
		||||
        opt = 'off';
 | 
			
		||||
    }
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "option": opt,
 | 
			
		||||
        "mode": mode
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/imgFlip", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("image flip res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function changeInputLabel(setting) {
 | 
			
		||||
    const input = document.getElementById(setting + 'Input');
 | 
			
		||||
    const label = document.getElementById(setting + "InputLabel");
 | 
			
		||||
    if (setting === 'bright') {
 | 
			
		||||
        label.innerHTML = "Brightness: " + input.value;
 | 
			
		||||
    } else {
 | 
			
		||||
        label.innerHTML =  setting.charAt(0).toUpperCase() + setting.slice(1) + ": " + input.value;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function colorHue() {
 | 
			
		||||
    changeInputLabel('colorHue');
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "pos": document.getElementById('colorHueInput').value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/colorHue", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("color hue res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function brightness() {
 | 
			
		||||
    changeInputLabel('bright');
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "pos": document.getElementById('brightInput').value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/bright", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("bright res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function contrast() {
 | 
			
		||||
    changeInputLabel('contrast');
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "pos": document.getElementById('contrastInput').value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/image/contrast", "POST", payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            alertMsg("contrast res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveSetting() {
 | 
			
		||||
    sendCmd("/image/save", "POST", {});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								pub/js/jsmpeg.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pub/js/jsmpeg.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										149
									
								
								pub/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								pub/js/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
function sendCmd(path, method, payload = {}) {
 | 
			
		||||
    const server = 'http://' + document.location.hostname + ':4000';
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open(method, server + path);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
 | 
			
		||||
        xhr.onload = function() {
 | 
			
		||||
            if (this.status >= 200 && this.status < 300) {
 | 
			
		||||
                resolve(xhr.response);
 | 
			
		||||
            } else {
 | 
			
		||||
                reject(xhr.response);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        xhr.onerror = function() {
 | 
			
		||||
            reject(xhr.response);
 | 
			
		||||
        };
 | 
			
		||||
        if (method !== "GET") {
 | 
			
		||||
            payload.id = getCurrentCamera().value;
 | 
			
		||||
            xhr.send(JSON.stringify(payload));
 | 
			
		||||
        } else {
 | 
			
		||||
            xhr.send();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function alertMsg(msg, err) {
 | 
			
		||||
    const msgStage = document.getElementById('msgStage');
 | 
			
		||||
    msgStage.style.display = "block";
 | 
			
		||||
 | 
			
		||||
    if (err) {
 | 
			
		||||
        msgStage.classList.add("errMsg");
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            msgStage.classList.remove("errMsg");
 | 
			
		||||
            msgStage.style.display = "none";
 | 
			
		||||
        }, 4000);
 | 
			
		||||
    } else {
 | 
			
		||||
        msgStage.classList.add("successMsg");
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            msgStage.classList.remove("successMsg");
 | 
			
		||||
            msgStage.style.display = "none";
 | 
			
		||||
        }, 4000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    msgStage.innerHTML = msg;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAllSavedCameras() {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        resolve(sendCmd('/camera/cameras', 'GET', null));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCurrentCamera() {
 | 
			
		||||
    const cameraSelectTag = document.getElementById('cameraList');
 | 
			
		||||
    return cameraSelectTag.options[cameraSelectTag.selectedIndex];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function optionView(view) {
 | 
			
		||||
    document.getElementById("options").querySelectorAll("a.active")[0].classList.remove("active", "highlight");
 | 
			
		||||
    document.querySelectorAll("a[value=" + view + "]")[0].classList.add("active", "highlight");
 | 
			
		||||
 | 
			
		||||
    const allOptionViews = document.getElementsByClassName('optionView');
 | 
			
		||||
    Array.prototype.forEach.call(allOptionViews, function(view) {
 | 
			
		||||
        view.style.display = "none";
 | 
			
		||||
    });
 | 
			
		||||
    document.getElementById(view).style.display = "block";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Array.prototype.forEach.call(document.getElementsByClassName('panTiltInput'), function(input) {
 | 
			
		||||
    input.addEventListener('change', changeRangeInputLabel, false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function changeRangeInputLabel(e) {
 | 
			
		||||
    const identity = e.currentTarget.getAttribute('data-html');
 | 
			
		||||
    document.getElementById(identity + "SpeedLabel").innerHTML = identity.charAt(0).toUpperCase() + identity.slice(1) + " Speed: " + e.currentTarget.value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addCamera() {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        name: document.getElementById('cameraNickname').value,
 | 
			
		||||
        ip: document.getElementById('cameraIpAddy').value,
 | 
			
		||||
        port: document.getElementById('cameraPort').value,
 | 
			
		||||
        rtsp: document.getElementById('cameraRtsp').value
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd('/camera/create', 'POST', payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            const camera = JSON.parse(res);
 | 
			
		||||
            addCameraTag(camera);
 | 
			
		||||
            alertMsg("Camera Successfully Added");
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
            alertMsg(err);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSideBar() {
 | 
			
		||||
    const sidebar = document.getElementById('sidebar');
 | 
			
		||||
    const content = document.getElementById('content');
 | 
			
		||||
    if(sidebar.classList.contains('active')) {
 | 
			
		||||
        sidebar.classList.remove('active');
 | 
			
		||||
        content.classList.remove('active');
 | 
			
		||||
    } else {
 | 
			
		||||
        sidebar.classList.add('active');
 | 
			
		||||
        content.classList.add('active');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addCameraTag(camera, index = 1) {
 | 
			
		||||
    const optTag = document.createElement("option");
 | 
			
		||||
    const eleId = camera.name || camera.ip;
 | 
			
		||||
    optTag.dataset.streamPort = camera.streamPort;
 | 
			
		||||
    if (index === 0) {
 | 
			
		||||
        optTag.setAttribute("selected", "selected");
 | 
			
		||||
    }
 | 
			
		||||
    optTag.appendChild(document.createTextNode(eleId));
 | 
			
		||||
    optTag.setAttribute("value", camera._id);
 | 
			
		||||
    document.getElementById('cameraList').appendChild(optTag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteCamera() {
 | 
			
		||||
    const camera = getCurrentCamera();
 | 
			
		||||
    if(confirm("Would you like to delete " + camera.innerHTML + "?")) {
 | 
			
		||||
        sendCmd('/camera/', "DELETE")
 | 
			
		||||
            .then(() => {
 | 
			
		||||
                const cameraSelectTag = document.getElementById('cameraList');
 | 
			
		||||
                cameraSelectTag.remove(cameraSelectTag.selectedIndex);
 | 
			
		||||
                changeStream();
 | 
			
		||||
            })
 | 
			
		||||
            .catch((err) => {
 | 
			
		||||
                console.log(err);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reqFullScreen() {
 | 
			
		||||
    const doc = window.document;
 | 
			
		||||
    const docEl = doc.documentElement;
 | 
			
		||||
 | 
			
		||||
    const requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
 | 
			
		||||
    const cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
 | 
			
		||||
 | 
			
		||||
    if(!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
 | 
			
		||||
        requestFullScreen.call(docEl);
 | 
			
		||||
    } else {
 | 
			
		||||
        cancelFullScreen.call(doc);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								pub/js/motion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								pub/js/motion.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
function motionTouchEvents() {
 | 
			
		||||
    const directions = ['right', 'left', 'up', 'down'];
 | 
			
		||||
    directions.forEach((direction) => {
 | 
			
		||||
        const eleId = 'motion' + direction.charAt(0).toUpperCase() + direction.slice(1);
 | 
			
		||||
        const ele = document.getElementById(eleId);
 | 
			
		||||
        ele.addEventListener('mousedown', stdMove);
 | 
			
		||||
        ele.addEventListener('mouseup', stopMotion);
 | 
			
		||||
        ele.addEventListener('touchstart', stdMove);
 | 
			
		||||
        ele.addEventListener('touchend', stopMotion);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function moveHome() {
 | 
			
		||||
    const payload = {mode: "home"};
 | 
			
		||||
    sendCmd("/ptz/motion", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("home res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopMotion(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    const panTiltArr = getPanTiltSpeeds();
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "standard",
 | 
			
		||||
        "direction": "stop",
 | 
			
		||||
        "panSpeed": panTiltArr[0],
 | 
			
		||||
        "tiltSpeed": panTiltArr[1]
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/motion", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("continousMove_" + direction + " res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stdMove(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const direction = e.target.getAttribute('data-value');
 | 
			
		||||
    const panTiltArr = getPanTiltSpeeds();
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "standard",
 | 
			
		||||
        "direction": direction,
 | 
			
		||||
        "panSpeed": panTiltArr[0],
 | 
			
		||||
        "tiltSpeed": panTiltArr[1]
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/motion", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("continousMove_" + direction + " res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPanTiltSpeeds() {
 | 
			
		||||
    return [document.getElementById('panSpeedInput').value,  document.getElementById('tiltSpeedInput').value];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function moveAbsPos(pan, tilt) {
 | 
			
		||||
    const panTiltArr = getPanTiltSpeeds();
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "absolute",
 | 
			
		||||
        "panSpeed": panTiltArr[0],
 | 
			
		||||
        "tiltSpeed": panTiltArr[1],
 | 
			
		||||
        "pan": pan,
 | 
			
		||||
        "tilt": tilt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/motion", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("moveAbsPos res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function moveRelPos(pan, tilt) {
 | 
			
		||||
    const panTiltArr = getPanTiltSpeeds();
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": "relative",
 | 
			
		||||
        "panSpeed": panTiltArr[0],
 | 
			
		||||
        "tiltSpeed": panTiltArr[1],
 | 
			
		||||
        "pan": pan,
 | 
			
		||||
        "tilt": tilt
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("/ptz/motion", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("moveRelPos res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function presets(mode, memNum) {
 | 
			
		||||
    const payload = {
 | 
			
		||||
        "mode": mode,
 | 
			
		||||
        "num": memNum
 | 
			
		||||
    };
 | 
			
		||||
    sendCmd("ptz/presets", "POST", payload)
 | 
			
		||||
        .then(function(res) {
 | 
			
		||||
            alertMsg("Preset res: " + res);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function(err) {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								pub/js/preset.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								pub/js/preset.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
 | 
			
		||||
function presetAction(action, eleId = 'presetInput') {
 | 
			
		||||
    let payload = {
 | 
			
		||||
        mode: action
 | 
			
		||||
    };
 | 
			
		||||
    if (action === 'speed') {
 | 
			
		||||
        payload.speed = document.getElementById(eleId).value;
 | 
			
		||||
        changePresetSpeedLabel(payload.speed);
 | 
			
		||||
    } else {
 | 
			
		||||
        animateActionBtn(action);
 | 
			
		||||
        payload.memNum = document.getElementById(eleId).value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sendCmd('/ptz/presets', 'POST', payload)
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
            console.log(res);
 | 
			
		||||
            if (action !== 'speed' && res === "Socket1 Cmd Done" || action !== 'speed' && res === "Socket2 Cmd Done") {
 | 
			
		||||
                stopPresetBtnAnimate(action);
 | 
			
		||||
            }
 | 
			
		||||
            alertMsg("Preset Action res: " + res + " Payload: " + payload);
 | 
			
		||||
        }).catch((err) => {
 | 
			
		||||
            alertMsg(err, true);
 | 
			
		||||
        });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function animateActionBtn(action) {
 | 
			
		||||
    const btn = document.getElementById(action + 'PresetBtn');
 | 
			
		||||
    btn.innerHTML = '<i class="fa fa-spinner fa-pulse"></i>';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopPresetBtnAnimate(action) {
 | 
			
		||||
    const btn = document.getElementById(action + 'PresetBtn');
 | 
			
		||||
    btn.innerHTML = action.charAt(0).toUpperCase() + action.slice(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function changePresetSpeedLabel(speed) {
 | 
			
		||||
    document.getElementById("presetSpeedLabel").innerHTML = "Preset Recall Speed: " + speed;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								pub/js/stream.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pub/js/stream.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
let player;
 | 
			
		||||
 | 
			
		||||
function changeStream() {
 | 
			
		||||
    player.destroy();
 | 
			
		||||
    startStream();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function startStream() {
 | 
			
		||||
    const canvas = document.getElementById('streamStage');
 | 
			
		||||
    const payload = {
 | 
			
		||||
        width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
 | 
			
		||||
        height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sendCmd('/camera/stream', 'POST', payload).then((camera) => {
 | 
			
		||||
        const streamPort = JSON.parse(camera);
 | 
			
		||||
        player = new JSMpeg.Player('ws://' + document.location.hostname + ':' + streamPort, {canvas:canvas});
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
/*jshint esversion: 6 */
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const express = require('express');
 | 
			
		||||
const app = express();
 | 
			
		||||
const cors = require('cors');
 | 
			
		||||
const bodyParser = require('body-parser');
 | 
			
		||||
const errorhandler = require(path.resolve( __dirname, 'app/_helpers/error-handler.js'));
 | 
			
		||||
const compression = require('compression');
 | 
			
		||||
 | 
			
		||||
app.use(compression());
 | 
			
		||||
app.use(bodyParser.urlencoded({extended: true}));
 | 
			
		||||
app.use(bodyParser.json());
 | 
			
		||||
app.use(cors());
 | 
			
		||||
 | 
			
		||||
app.use(express.static(__dirname + '/pub'));
 | 
			
		||||
app.get('/', (req, res) => {
 | 
			
		||||
    res.sendFile(path.resolve(__dirname + '/pub/index.html'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.use('/camera', require(path.resolve( __dirname, 'app/camera/camera.controller.js')));
 | 
			
		||||
app.use('/ptz', require(path.resolve( __dirname, 'app/ptz/ptz.controller.js')));
 | 
			
		||||
app.use('/image', require(path.resolve( __dirname, 'app/image/image.controller.js')));
 | 
			
		||||
app.get('*', function(req, res){
 | 
			
		||||
  res.send('what???', 404);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.use(errorhandler);
 | 
			
		||||
 | 
			
		||||
const port = process.env.NODE_ENV === 'production' ? 80 : 4000;
 | 
			
		||||
const server = app.listen(port, function() {
 | 
			
		||||
    console.log('Server listening on port ' + port);
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user