init
This commit is contained in:
29 files changed
+6054
No files matched your search
@@ -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)
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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')
|
||||
};
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"connectionString": "mongodb://your-mongo-address/db-name",
|
||||
"secret": "your db-secret"
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Generated
+3053
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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", {});
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
var JSMpeg={Player:null,VideoElement:null,BitBuffer:null,Source:{},Demuxer:{},Decoder:{},Renderer:{},AudioOutput:{},Now:function(){return window.performance?window.performance.now()/1e3:Date.now()/1e3},CreateVideoElements:function(){var elements=document.querySelectorAll(".jsmpeg");for(var i=0;i<elements.length;i++){new JSMpeg.VideoElement(elements[i])}},Fill:function(array,value){if(array.fill){array.fill(value)}else{for(var i=0;i<array.length;i++){array[i]=value}}}};if(document.readyState==="complete"){JSMpeg.CreateVideoElements()}else{document.addEventListener("DOMContentLoaded",JSMpeg.CreateVideoElements)}JSMpeg.VideoElement=function(){"use strict";var VideoElement=function(element){var url=element.dataset.url;if(!url){throw"VideoElement has no `data-url` attribute"}var addStyles=function(element,styles){for(var name in styles){element.style[name]=styles[name]}};this.container=element;addStyles(this.container,{display:"inline-block",position:"relative",minWidth:"80px",minHeight:"80px"});this.canvas=document.createElement("canvas");this.canvas.width=960;this.canvas.height=540;addStyles(this.canvas,{display:"block",width:"100%"});this.container.appendChild(this.canvas);this.playButton=document.createElement("div");this.playButton.innerHTML=VideoElement.PLAY_BUTTON;addStyles(this.playButton,{zIndex:2,position:"absolute",top:"0",bottom:"0",left:"0",right:"0",maxWidth:"75px",maxHeight:"75px",margin:"auto",opacity:"0.7",cursor:"pointer"});this.container.appendChild(this.playButton);var options={canvas:this.canvas};for(var option in element.dataset){try{options[option]=JSON.parse(element.dataset[option])}catch(err){options[option]=element.dataset[option]}}this.player=new JSMpeg.Player(url,options);element.playerInstance=this.player;if(options.poster&&!options.autoplay&&!this.player.options.streaming){options.decodeFirstFrame=false;this.poster=new Image;this.poster.src=options.poster;this.poster.addEventListener("load",this.posterLoaded);addStyles(this.poster,{display:"block",zIndex:1,position:"absolute",top:0,left:0,bottom:0,right:0});this.container.appendChild(this.poster)}if(!this.player.options.streaming){this.container.addEventListener("click",this.onClick.bind(this))}if(options.autoplay||this.player.options.streaming){this.playButton.style.display="none"}if(this.player.audioOut&&!this.player.audioOut.unlocked){var unlockAudioElement=this.container;if(options.autoplay||this.player.options.streaming){this.unmuteButton=document.createElement("div");this.unmuteButton.innerHTML=VideoElement.UNMUTE_BUTTON;addStyles(this.unmuteButton,{zIndex:2,position:"absolute",bottom:"10px",right:"20px",width:"75px",height:"75px",margin:"auto",opacity:"0.7",cursor:"pointer"});this.container.appendChild(this.unmuteButton);unlockAudioElement=this.unmuteButton}this.unlockAudioBound=this.onUnlockAudio.bind(this,unlockAudioElement);unlockAudioElement.addEventListener("touchstart",this.unlockAudioBound,false);unlockAudioElement.addEventListener("click",this.unlockAudioBound,true)}};VideoElement.prototype.onUnlockAudio=function(element,ev){if(this.unmuteButton){ev.preventDefault();ev.stopPropagation()}this.player.audioOut.unlock(function(){if(this.unmuteButton){this.unmuteButton.style.display="none"}element.removeEventListener("touchstart",this.unlockAudioBound);element.removeEventListener("click",this.unlockAudioBound)}.bind(this))};VideoElement.prototype.onClick=function(ev){if(this.player.isPlaying){this.player.pause();this.playButton.style.display="block"}else{this.player.play();this.playButton.style.display="none";if(this.poster){this.poster.style.display="none"}}};VideoElement.PLAY_BUTTON='<svg style="max-width: 75px; max-height: 75px;" '+'viewBox="0 0 200 200" alt="Play video">'+'<circle cx="100" cy="100" r="90" fill="none" '+'stroke-width="15" stroke="#fff"/>'+'<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>'+"</svg>";VideoElement.UNMUTE_BUTTON='<svg style="max-width: 75px; max-height: 75px;" viewBox="0 0 75 75">'+'<polygon class="audio-speaker" stroke="none" fill="#fff" '+'points="39,13 22,28 6,28 6,47 21,47 39,62 39,13"/>'+'<g stroke="#fff" stroke-width="5">'+'<path d="M 49,50 69,26"/>'+'<path d="M 69,50 49,26"/>'+"</g>"+"</svg>";return VideoElement}();JSMpeg.Player=function(){"use strict";var Player=function(url,options){this.options=options||{};if(options.source){this.source=new options.source(url,options);options.streaming=!!this.source.streaming}else if(url.match(/^wss?:\/\//)){this.source=new JSMpeg.Source.WebSocket(url,options);options.streaming=true}else if(options.progressive!==false){this.source=new JSMpeg.Source.AjaxProgressive(url,options);options.streaming=false}else{this.source=new JSMpeg.Source.Ajax(url,options);options.streaming=false}this.maxAudioLag=options.maxAudioLag||.25;this.loop=options.loop!==false;this.autoplay=!!options.autoplay||options.streaming;this.demuxer=new JSMpeg.Demuxer.TS(options);this.source.connect(this.demuxer);if(options.video!==false){this.video=new JSMpeg.Decoder.MPEG1VideoLine truncated
|
||||
src++;y|=sY[src]<<16;src++;y|=sY[src]<<24;src++;dY[dest++]=y}dest+=scan>>2;src+=scan}}}width=this.halfWidth;scan=width-8;H=motionH/2>>1;V=motionV/2>>1;oddH=(motionH/2&1)===1;oddV=(motionV/2&1)===1;src=((this.mbRow<<3)+V)*width+(this.mbCol<<3)+H;dest=this.mbRow*width+this.mbCol<<1;last=dest+(width<<1);var cr1,cr2,cr,cb1,cb2,cb;if(oddH){if(oddV){while(dest<last){cr1=sCr[src]+sCr[src+width];cb1=sCb[src]+sCb[src+width];src++;for(x=0;x<2;x++){cr2=sCr[src]+sCr[src+width];cb2=sCb[src]+sCb[src+width];src++;cr=cr1+cr2+2>>2&255;cb=cb1+cb2+2>>2&255;cr1=sCr[src]+sCr[src+width];cb1=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<6&65280;cb|=cb1+cb2+2<<6&65280;cr2=sCr[src]+sCr[src+width];cb2=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<14&16711680;cb|=cb1+cb2+2<<14&16711680;cr1=sCr[src]+sCr[src+width];cb1=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<22&4278190080;cb|=cb1+cb2+2<<22&4278190080;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan-1}}else{while(dest<last){cr1=sCr[src];cb1=sCb[src];src++;for(x=0;x<2;x++){cr2=sCr[src];cb2=sCb[src++];cr=cr1+cr2+1>>1&255;cb=cb1+cb2+1>>1&255;cr1=sCr[src];cb1=sCb[src++];cr|=cr1+cr2+1<<7&65280;cb|=cb1+cb2+1<<7&65280;cr2=sCr[src];cb2=sCb[src++];cr|=cr1+cr2+1<<15&16711680;cb|=cb1+cb2+1<<15&16711680;cr1=sCr[src];cb1=sCb[src++];cr|=cr1+cr2+1<<23&4278190080;cb|=cb1+cb2+1<<23&4278190080;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan-1}}}else{if(oddV){while(dest<last){for(x=0;x<2;x++){cr=sCr[src]+sCr[src+width]+1>>1&255;cb=sCb[src]+sCb[src+width]+1>>1&255;src++;cr|=sCr[src]+sCr[src+width]+1<<7&65280;cb|=sCb[src]+sCb[src+width]+1<<7&65280;src++;cr|=sCr[src]+sCr[src+width]+1<<15&16711680;cb|=sCb[src]+sCb[src+width]+1<<15&16711680;src++;cr|=sCr[src]+sCr[src+width]+1<<23&4278190080;cb|=sCb[src]+sCb[src+width]+1<<23&4278190080;src++;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan}}else{while(dest<last){for(x=0;x<2;x++){cr=sCr[src];cb=sCb[src];src++;cr|=sCr[src]<<8;cb|=sCb[src]<<8;src++;cr|=sCr[src]<<16;cb|=sCb[src]<<16;src++;cr|=sCr[src]<<24;cb|=sCb[src]<<24;src++;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan}}}};MPEG1.prototype.dcPredictorY=0;MPEG1.prototype.dcPredictorCr=0;MPEG1.prototype.dcPredictorCb=0;MPEG1.prototype.blockData=null;MPEG1.prototype.decodeBlock=function(block){var n=0,quantMatrix;if(this.macroblockIntra){var predictor,dctSize;if(block<4){predictor=this.dcPredictorY;dctSize=this.readHuffman(MPEG1.DCT_DC_SIZE_LUMINANCE)}else{predictor=block===4?this.dcPredictorCr:this.dcPredictorCb;dctSize=this.readHuffman(MPEG1.DCT_DC_SIZE_CHROMINANCE)}if(dctSize>0){var differential=this.bits.read(dctSize);if((differential&1<<dctSize-1)!==0){this.blockData[0]=predictor+differential}else{this.blockData[0]=predictor+(-1<<dctSize|differential+1)}}else{this.blockData[0]=predictor}if(block<4){this.dcPredictorY=this.blockData[0]}else if(block===4){this.dcPredictorCr=this.blockData[0]}else{this.dcPredictorCb=this.blockData[0]}this.blockData[0]<<=3+5;quantMatrix=this.intraQuantMatrix;n=1}else{quantMatrix=this.nonIntraQuantMatrix}var level=0;while(true){var run=0,coeff=this.readHuffman(MPEG1.DCT_COEFF);if(coeff===1&&n>0&&this.bits.read(1)===0){break}if(coeff===65535){run=this.bits.read(6);level=this.bits.read(8);if(level===0){level=this.bits.read(8)}else if(level===128){level=this.bits.read(8)-256}else if(level>128){level=level-256}}else{run=coeff>>8;level=coeff&255;if(this.bits.read(1)){level=-level}}n+=run;var dezigZagged=MPEG1.ZIG_ZAG[n];n++;level<<=1;if(!this.macroblockIntra){level+=level<0?-1:1}level=level*this.quantizerScale*quantMatrix[dezigZagged]>>4;if((level&1)===0){level-=level>0?1:-1}if(level>2047){level=2047}else if(level<-2048){level=-2048}this.blockData[dezigZagged]=level*MPEG1.PREMULTIPLIER_MATRIX[dezigZagged]}var destArray,destIndex,scan;if(block<4){destArray=this.currentY;scan=this.codedWidth-8;destIndex=this.mbRow*this.codedWidth+this.mbCol<<4;if((block&1)!==0){destIndex+=8}if((block&2)!==0){destIndex+=this.codedWidth<<3}}else{destArray=block===4?this.currentCb:this.currentCr;scan=(this.codedWidth>>1)-8;destIndex=(this.mbRow*this.codedWidth<<2)+(this.mbCol<<3)}if(this.macroblockIntra){if(n===1){MPEG1.CopyValueToDestination(this.blockData[0]+128>>8,destArray,destIndex,scan);this.blockData[0]=0}else{MPEG1.IDCT(this.blockData);MPEG1.CopyBlockToDestination(this.blockData,destArray,destIndex,scan);JSMpeg.Fill(this.blockData,0)}}else{if(n===1){MPEG1.AddValueToDestination(this.blockData[0]+128>>8,destArray,destIndex,scan);this.blockData[0]=0}else{MPEG1.IDCT(this.blockData);MPEG1.AddBlockToDestination(this.blockData,destArray,destIndex,scan);JSMpeg.Fill(this.blockData,0)}}n=0};MPEG1.CopyBlockToDestination=function(block,dest,index,scan){for(var n=0;n<64;n+=8,index+=scan+8){dest[index+0]=block[n+0];dest[index+1]=block[n+1];dest[index+2]=block[n+2];dest[index+3]=block[n+3];dest[index+4]=block[n+4];dest[index+5]=block[n+5];dest[index+6]=block[n+6];dest[index+7]=block[n+7]}};MPEG1.AddBlockToDestination=function(block,dest,index,scan){for(var n=0;n<64;n+=8,index+Line truncated
|
||||
gl.uniform1i(gl.getUniformLocation(this.program,name),index);return texture};WebGLRenderer.prototype.createProgram=function(vsh,fsh){var gl=this.gl;var program=gl.createProgram();gl.attachShader(program,this.compileShader(gl.VERTEX_SHADER,vsh));gl.attachShader(program,this.compileShader(gl.FRAGMENT_SHADER,fsh));gl.linkProgram(program);gl.useProgram(program);return program};WebGLRenderer.prototype.compileShader=function(type,source){var gl=this.gl;var shader=gl.createShader(type);gl.shaderSource(shader,source);gl.compileShader(shader);if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS)){throw new Error(gl.getShaderInfoLog(shader))}return shader};WebGLRenderer.prototype.allowsClampedTextureData=function(){var gl=this.gl;var texture=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,texture);gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,1,1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,new Uint8ClampedArray([0]));return gl.getError()===0};WebGLRenderer.prototype.renderProgress=function(progress){var gl=this.gl;gl.useProgram(this.loadingProgram);var loc=gl.getUniformLocation(this.loadingProgram,"progress");gl.uniform1f(loc,progress);gl.drawArrays(gl.TRIANGLE_STRIP,0,4)};WebGLRenderer.prototype.render=function(y,cb,cr){if(!this.enabled){return}var gl=this.gl;var w=this.width+15>>4<<4,h=this.height,w2=w>>1,h2=h>>1;if(this.shouldCreateUnclampedViews){y=new Uint8Array(y.buffer),cb=new Uint8Array(cb.buffer),cr=new Uint8Array(cr.buffer)}gl.useProgram(this.program);this.updateTexture(gl.TEXTURE0,this.textureY,w,h,y);this.updateTexture(gl.TEXTURE1,this.textureCb,w2,h2,cb);this.updateTexture(gl.TEXTURE2,this.textureCr,w2,h2,cr);gl.drawArrays(gl.TRIANGLE_STRIP,0,4)};WebGLRenderer.prototype.updateTexture=function(unit,texture,w,h,data){var gl=this.gl;gl.activeTexture(unit);gl.bindTexture(gl.TEXTURE_2D,texture);gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data)};WebGLRenderer.IsSupported=function(){try{if(!window.WebGLRenderingContext){return false}var canvas=document.createElement("canvas");return!!(canvas.getContext("webgl")||canvas.getContext("experimental-webgl"))}catch(err){return false}};WebGLRenderer.SHADER={FRAGMENT_YCRCB_TO_RGBA:["precision mediump float;","uniform sampler2D textureY;","uniform sampler2D textureCb;","uniform sampler2D textureCr;","varying vec2 texCoord;","mat4 rec601 = mat4(","1.16438, 0.00000, 1.59603, -0.87079,","1.16438, -0.39176, -0.81297, 0.52959,","1.16438, 2.01723, 0.00000, -1.08139,","0, 0, 0, 1",");","void main() {","float y = texture2D(textureY, texCoord).r;","float cb = texture2D(textureCb, texCoord).r;","float cr = texture2D(textureCr, texCoord).r;","gl_FragColor = vec4(y, cr, cb, 1.0) * rec601;","}"].join("\n"),FRAGMENT_LOADING:["precision mediump float;","uniform float progress;","varying vec2 texCoord;","void main() {","float c = ceil(progress-(1.0-texCoord.y));","gl_FragColor = vec4(c,c,c,1);","}"].join("\n"),VERTEX_IDENTITY:["attribute vec2 vertex;","varying vec2 texCoord;","void main() {","texCoord = vertex;","gl_Position = vec4((vertex * 2.0 - 1.0) * vec2(1, -1), 0.0, 1.0);","}"].join("\n")};return WebGLRenderer}();JSMpeg.Renderer.Canvas2D=function(){"use strict";var CanvasRenderer=function(options){this.canvas=options.canvas||document.createElement("canvas");this.width=this.canvas.width;this.height=this.canvas.height;this.enabled=true;this.context=this.canvas.getContext("2d")};CanvasRenderer.prototype.destroy=function(){};CanvasRenderer.prototype.resize=function(width,height){this.width=width|0;this.height=height|0;this.canvas.width=this.width;this.canvas.height=this.height;this.imageData=this.context.getImageData(0,0,this.width,this.height);JSMpeg.Fill(this.imageData.data,255)};CanvasRenderer.prototype.renderProgress=function(progress){var w=this.canvas.width,h=this.canvas.height,ctx=this.context;ctx.fillStyle="#222";ctx.fillRect(0,0,w,h);ctx.fillStyle="#fff";ctx.fillRect(0,h-h*progress,w,h*progress)};CanvasRenderer.prototype.render=function(y,cb,cr){this.YCbCrToRGBA(y,cb,cr,this.imageData.data);this.context.putImageData(this.imageData,0,0)};CanvasRenderer.prototype.YCbCrToRGBA=function(y,cb,cr,rgba){if(!this.enabled){return}var w=this.width+15>>4<<4,w2=w>>1;var yIndex1=0,yIndex2=w,yNext2Lines=w+(w-this.width);var cIndex=0,cNextLine=w2-(this.width>>1);var rgbaIndex1=0,rgbaIndex2=this.width*4,rgbaNext2Lines=this.width*4;var cols=this.width>>1,rows=this.height>>1;var ccb,ccr,r,g,b;for(var row=0;row<rows;row++){for(var col=0;col<cols;col++){ccb=cb[cIndex];ccr=cr[cIndex];cIndex++;r=ccb+(ccb*103>>8)-179;g=(ccr*88>>8)-44+(ccb*183>>8)-91;b=ccr+(ccr*198>>8)-227;var y1=y[yIndex1++];var y2=y[yIndex1++];rgba[rgbaIndex1]=y1+r;rgba[rgbaIndex1+1]=y1-g;rgba[rgbaIndex1+2]=y1+b;rgba[rgbaIndex1+4]=y2+r;rgba[rgbaIndex1+5]=y2-g;rgba[rgbaIndex1+6]=y2+b;rgbaIndex1+=8;var y3=y[yIndex2++];var y4=y[yIndex2++];rgba[rgbaIndex2]=y3+r;rgba[rgbaIndex2+1]=y3-g;rgba[rgbaIndex2+2]=y3+b;rgba[rgbaIndex2+4]=y4+r;rgba[rgbaIndex2+5]=y4-g;rgba[rgbaIndex2+6]=y4+b;rgbaIndex2+=8}yIndex1+=yNext2LineLine truncated
|
||||
+149
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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});
|
||||
});
|
||||
}
|
||||
@@ -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