This commit is contained in:
PTZOptics 2018-11-02 15:24:39 -04:00 committed by GitHub
commit b799070fd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 6054 additions and 0 deletions

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# PTZOptics Node Server
The PTZOptics Node Server is a simple skeleton Express server to control your PTZOptics camera via visca commands.
## Prerequisites
You need to have at least [Node.js version: 8.12.0](https://nodejs.org/en/download/), [MongoDB version: 4.0.3](https://www.mongodb.com/download-center?initial=true#community), and a [PTZOptics camera](https://ptzoptics.com/).
## Installing
1. Configure your PTZOptics camera to your local network. [PTZOptics Knowledge Base](https://help.ptzoptics.com/support/solutions/folders/13000001062)
2. Clone this repo and then extract to your preferred location
3. Update the mongoDB connection information inside `/app/config.json`.
```
{
connectionString: "mongodb://your-mongo-address/db-name",
secret: "your db-secret"
}
```
4. Start the server
`cd /The/path/to/the/repo`
`npm start`
5. Head to `http://localhost:4000`
6. Click 'Add Camera' and enter your camera's information.
## Contributing
1. Fork it!
2. Create your feature branch: `git checkout -b my-new-feature`
3. Commit your changes: `git commit -m 'Add some feature'`
4. Push to the branch: `git push origin my-new-feature`
5. Submit a pull request :D
## Authors
[**PTZOptics**](https://github.com/PTZOptics)

View File

@ -0,0 +1,180 @@
/*jshint esversion: 6 */
const path = require('path');
const http = require('http');
const tcpPortUsed =require('tcp-port-used');
const db = require(path.resolve(__dirname, './db.js'));
const Camera = db.Camera;
module.exports = {
getDeviceModel,
checkPortUse,
createNewCameraStreamPort
};
async function checkPortUse(port) {
return tcpPortUsed.check(port, '127.0.0.1')
.then((inuse) => {
return inuse;
})
.catch(err => {
throw err;
});
}
async function getDeviceModel(ip) {
const options = {
hostname: ip,
path: '/cgi-bin/param.cgi?get_serial_number',
method: 'GET'
};
return sendCgiReq(options)
.then((res) => {
const rawData = res.toString().replace(/\n/g, '').toUpperCase();
return calcCamModel(rawData, rawData.substring(0, 1), rawData.substring(0, 2))
.then(res => {
return res;
})
.catch(err => {
throw err;
});
})
.catch(err => {
throw err;
});
}
async function calcCamModel(serial, first_letter, twoFirst_letter) {
let model = '';
// Check 12x
if (["1", "A", "B", "C", "D", "N", "O"].includes(first_letter)) {
model = "PT12X-";
(first_letter === "1") ? model += serial.slice(3, 6) + "-XX-" + serial.slice(8, 10) + checkPoe(serial.slice(10)) : model += newSerialAnsBuilder(serial);
}
// Check 20x
if (["2", "E", "F", "G", "H", "P", "Q"].includes(first_letter) && twoFirst_letter !== "PT" ) {
model = "PT20X-";
(first_letter === "2") ? model += serial.slice(3, 6) + "-XX-" + serial.slice(8, 10) + checkPoe(serial.slice(10)) : model += newSerialAnsBuilder(serial);
}
// Check 30x
if (["W", "X", "R", "S"].includes(first_letter)) {
model = "PT30X-" + newSerialAnsBuilder(serial);
}
// Check zcams
if (["J", "U", "I", "T"].includes(first_letter)) {
(["J", "U"].includes(first_letter)) ? model = "PT20X-ZCAM-" : model = "PTVL-ZCAM";
}
if ('PT' == twoFirst_letter) {
(serial.slice(0, 4) === "PTVL") ? model = "PTVL-ZCAM-" : model = "PT20X-ZCAM-";
}
return [model, serial];
}
function newSerialAnsBuilder(serial) {
let ans = '';
switch(serial.substring(0, 1)) {
// 12X
case "A":
case "B":
ans = "SDI-XX-G2" + checkPoe(serial.slice(1));
break;
case "C":
case "D":
ans = "USB-XX-G2";
break;
case "N":
case "O":
ans = "SDI-XX-G2 POE";
break;
// 20X
case "E":
case "F":
ans = "SDI-XX-G2" + checkPoe(serial.slice(1));
break;
case "G":
case "H":
ans = "USB-XX-G2";
break;
case "P":
case "Q":
ans = "SDI-XX-G2 POE";
break;
// 30X
case "W":
case "X":
ans = "SDI-XX-G2 POE";
break;
case "R":
case "S":
ans = "NDI-XX-G2";
break;
}
return ans;
}
function checkPoe (serialNum) {
if (serialNum <= "B1025000") {
return '';
} else if (serialNum >= "B1025001" && serialNum <= "D0129000") {
return "-POE";
} else if(serialNum >= "D0129001") {
return "-POE";
}
}
async function createNewCameraStreamPort() {
let port = 5000;
let cameraStreamPorts;
try {
// Returns all saved camera stream ports
cameraStreamPorts = await currentCameraStreamPorts();
} catch(err) {
throw err;
}
while (cameraStreamPorts.includes(port)) {
++port;
}
return port;
}
async function currentCameraStreamPorts() {
return Camera.find().select('streamPort').lean()
.then((cameras) => {
return cameras.map((camera) => {
return camera.streamPort;
});
})
.catch((err) => {
throw err;
});
}
function sendCgiReq(options) {
return new Promise(function(resolve, reject) {
const req = http.request(options, (res) => {
let rawData = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
rawData += chunk;
});
res.on('end', () => {
resolve(rawData);
});
});
req.on('error', function(err) {
reject(err);
});
req.end();
});
}

11
app/_helpers/db.js Normal file
View File

@ -0,0 +1,11 @@
/*jshint esversion: 6 */
const path = require('path');
const config = require(path.resolve(__dirname, '../config.json'));
const mongoose = require('mongoose');
mongoose.connect(config.connectionString, {useNewUrlParser: true });
mongoose.Promise = global.Promise;
module.exports = {
Camera: require('../camera/camera.model')
};

View File

@ -0,0 +1,9 @@
module.exports = errorHandler;
function errorHandler(err, req, res, next) {
console.log(err);
if (typeof err === 'string') {
return res.status(409).json({message: err});
}
return res.status(500).json({message: err.message});
}

85
app/_helpers/ptzHelper.js Normal file
View File

@ -0,0 +1,85 @@
/*jshint esversion: 6 */
module.exports = {
translateDirection,
hexStrToNum,
numToHexStr,
sanitizeSpeed,
getCurrentPos
};
function translateDirection(direction) {
let hexStr;
switch(direction.toLowerCase()) {
case "stop":
hexStr = "0303FF";
break;
case "up":
hexStr = "0301FF";
break;
case "down":
hexStr = "0302FF";
break;
case "right":
hexStr = "0203FF";
break;
case "left":
hexStr = "0103FF";
break;
case "upleft":
hexStr = "0101FF";
break;
case "upright":
hexStr = "0201FF";
break;
case "downleft":
hexStr = "0102FF";
break;
case "downright":
hexStr = "0202FF";
break;
default:
throw "The direction " + direction + " is not a valid movement direction";
}
return hexStr;
}
function hexStrToNum(str) {
return parseInt(str, 16);
}
function numToHexStr(num) {
if (num == 0) {
return '00';
}
if (typeof num === 'string') {
num = parseInt(num);
}
return num.toString(16).toUpperCase().split('').reduce(function(str, char) {
return '0' + char;
});
}
function sanitizeSpeed(pan, tilt) {
let sanitizedPan, sanitizedTilt;
if (pan >= 1 && pan <= 18) {
sanitizedPan = ("0" + pan).slice(-2);
} else {
throw 'The pan speed value must be greater than or equal to 1 and less than or equal to 18';
}
if (tilt >= 1 && tilt <= 14) {
sanitizedTilt = ("0" + pan).slice(-2);
} else {
throw 'The tilt speed value must be greater than or equal to 1 and less than or equal to 14';
}
return [sanitizedPan, sanitizedTilt];
}
async function getCurrentPos(id) {
return numToHexStr(await socket.sendCmd(id, "81090612FF"));
}

81
app/_helpers/socket.js Normal file
View File

@ -0,0 +1,81 @@
/*jshint esversion: 6 */
const path = require('path');
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
const Camera = db.Camera;
const net = require('net');
module.exports = {
sendCmd: _sendCmd
};
async function _sendCmd(id, cmd) {
const camera = await Camera.findById(id);
const buffer = Buffer.from(cmd, 'hex');
return new Promise((resolve, reject) => {
const socket = new net.Socket({allowHalfOpen: true});
const conn = net.createConnection(Number(camera.port), camera.ip);
conn.setNoDelay();
conn.setEncoding('hex');
conn.on('connect', () => {
conn.write(buffer);
});
conn.on('error', (error) => {
reject(error);
});
conn.on('data', (buf) => {
if (conn.bytesRead >= 3) {
conn.end();
decode(buf.toString('hex'))
.then((res) => {
resolve(res);
})
.catch(err => {
reject(err);
});
} else {
reject("Unusual Camera Response: " + buf.toString('hex') + " connection bytes Read: " + conn.bytesRead);
}
});
});
}
async function decode(hexStr) {
let decoded = '';
switch (hexStr) {
case "9041ff":
case "9042ff":
decoded = "Command Accepted";
break;
case "9051ff":
case "9041ff9051ff":
decoded = "Socket1 Cmd Done";
break;
case "9052ff":
case "9042ff9052ff":
decoded = "Socket2 Cmd Done";
break;
case "906002ff":
throw "Command Syntax Error";
case "906003ff":
throw "Command Buffer Full";
case "906104ff":
throw "Socket1 Cmd Cancelled";
case "906204ff":
throw "Socket2 Cmd Cancelled";
case "906105ff":
case "906205ff":
throw "No Socket";
case "906141ff":
throw "Socket1 Cmd Not Executable";
case "906241ff":
throw "Socket2 Cmd Not Executable";
default:
throw "Unusual Camera Response: " + hexStr;
}
return decoded;
}

View File

@ -0,0 +1,58 @@
/*jshint esversion: 6 */
const path = require('path');
const express = require('express');
const router = express.Router();
const cameraService = require(path.resolve(__dirname, './camera.service'));
router.post('/create', _create);
router.get('/cameras', getAll);
router.get('/', getById);
router.put('/', update);
router.delete('/', _delete);
router.post('/osd', _osd);
router.post('/stream', _stream);
module.exports = router;
function _create(req, res, next) {
cameraService.create(req.body)
.then((camera) => res.json(camera))
.catch(err => next(err));
}
function update(req, res, next) {
cameraService.update(req.body.id)
.then((camera) => res.json(camera))
.catch(err => next(err));
}
function getAll(req, res, next) {
cameraService.getAll()
.then((cameras) => res.json(cameras))
.catch(err => next(err));
}
function getById(req, res, next) {
cameraService.getById(req.body.id)
.then((camera) => res.json(camera))
.catch(err => next(err));
}
function _delete(req, res, next) {
cameraService.delete(req.body.id)
.then((id) => res.json(id))
.catch(err => next(err));
}
function _osd(req, res, next) {
cameraService.osd(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _stream(req, res, next) {
console.log("Stream Requester ip: " + req.ip);
cameraService.stream(req.body)
.then((streamPort) => res.json(streamPort))
.catch(err => next(err));
}

View File

@ -0,0 +1,30 @@
/*jshint esversion: 6 */
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const schema = new Schema({
_id: Schema.Types.ObjectId,
ip: {type: String, unique: true, required: true},
port: {type: String, required: true},
rtsp: {type: String, required: true},
model: {type: String, required: true},
serial: {type: String, required: true},
name: {type: String, unique: true},
streamPort: {type: Number, unique: true},
presets: [
{
memNum: {type: Number, max: 127},
name: {type: String},
location: {
pan: {type: String},
tilt: {type: String},
focus: {type: String},
zoom: {type: String}
}
}
]
});
schema.set('toJson', {virtuals: true});
module.exports = mongoose.model('Camera', schema);

View File

@ -0,0 +1,127 @@
/*jshint esversion: 6 */
const path = require('path');
const Stream = require('../stream/videoStream');
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
const mongoose = require('mongoose');
const cameraHelper = require(path.resolve(__dirname, '../_helpers/cameraHelper.js'));
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
const Camera = db.Camera;
module.exports = {
create: _create,
getAll: _getAll,
getById: _getById,
update: _update,
delete: _delete,
osd: _osd,
stream: _stream
};
async function _getAll() {
try {
return await Camera.find().lean();
} catch(err) {
throw err;
}
}
async function _getById(id) {
try {
return await Camera.findById(id).lean();
} catch(err) {
throw err;
}
}
async function _create(cameraParams) {
if (await Camera.findOne({ip: cameraParams.ip})) {
throw 'There is already a camera with ip ' + cameraParams.ip;
} else {
const modelSerialArr = await cameraHelper.getDeviceModel(cameraParams.ip);
const camId = new mongoose.Types.ObjectId();
const streamPort = await cameraHelper.createNewCameraStreamPort();
return new Camera({
...cameraParams,
_id: camId,
model: modelSerialArr[0],
serial: modelSerialArr[1],
streamPort: streamPort
})
.save()
.then((camera) => {
return camera.toObject();
}).catch((err) => {
throw err;
});
}
}
async function _update(cameraParams) {
try {
return await Camera.findByIdAndUpdate(cameraParams._id, cameraParams, {new: true}).save();
} catch(err) {
throw err;
}
}
async function _delete(id) {
try {
return await Camera.findOneAndDelete(id);
} catch(err) {
throw err;
}
}
async function _osd({id, option}) {
let osdHex = "8101";
switch(option.toLowerCase()) {
case "openToggle":
osdHex += "043F025FFF";
break;
case "up":
osdHex += "06010E0E0301FF";
break;
case "down":
osdHex += "06010E0E0302FF";
break;
case "left":
osdHex += "06010E0E0103FF";
break;
case "right":
osdHex += "06010E0E0203FF";
break;
case "enter":
osdHex += "060605FF";
break;
case "return":
osdHex += "060604FF";
break;
default:
throw "The OSD option " + option + " is not a recognizable OSD option.";
}
return await socket.sendCmd(id, osdHex);
}
async function _stream({id, width = 1920, height = 1080}) {
const camera = await Camera.findById(id).lean();
// In case a seperate user wants to reach same camera stream
let inUse;
try {
inUse = await cameraHelper.checkPortUse(camera.streamPort);
} catch(err) {
throw err;
}
if (!inUse) {
const stream = new Stream({
name: camera.name || "stream: " + camera.rtsp,
url: 'rtsp://' + camera.rtsp,
port: camera.streamPort,
width: width,
height: height
});
stream.start();
}
return camera.streamPort;
}

4
app/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"connectionString": "mongodb://your-mongo-address/db-name",
"secret": "your db-secret"
}

View File

@ -0,0 +1,112 @@
/*jshint esversion: 6 */
const path = require('path');
const express = require('express');
const router = express.Router();
const imageService = require(path.resolve(__dirname, './image.service.js'));
module.exports = router;
router.post('/bright', _brightness);
router.post('/contrast', _contrast);
router.post('/wb', _whiteBalance);
router.post('/rgain', _rgain);
router.post('/bgain', _bgain);
router.post('/shutter', _shutter);
router.post('/iris', _iris);
router.post('/gain', _gain);
router.post('/backLight', _backLight);
router.post('/bw', _blackWhite);
router.post('/flicker', _flicker);
router.post('/imgFlip', _imgFlip);
router.post('/colorHue', _colorHue);
router.post('/ae', _autoExp);
router.post('/save', _save);
function _brightness(req, res, next) {
imageService.brightness(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _contrast(req, res, next) {
imageService.contrast(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _whiteBalance(req, res, next) {
imageService.whiteBalance(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _rgain(req, res, next) {
imageService.rgain(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _bgain(req, res, next) {
imageService.bgain(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _shutter(req, res, next) {
imageService.shutter(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _iris(req, res, next) {
imageService.iris(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _gain(req, res, next) {
imageService.gain(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _backLight(req, res, next) {
imageService.backLight(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _blackWhite(req, res, next) {
imageService.blackWhite(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _flicker(req, res, next) {
imageService.flicker(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _imgFlip(req, res, next) {
imageService.imgFlip(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _colorHue(req, res, next) {
imageService.colorHue(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _autoExp(req, res, next) {
imageService.ae(req.body)
.then((socket) => res.json(socket))
.catch(err => next(err));
}
function _save(req, res, next) {
imageService.save(req.body.id)
.then((socket) => res.json(socket))
.catch(err => next(err));
}

479
app/image/image.service.js Normal file
View File

@ -0,0 +1,479 @@
/*jshint esversion: 6 */
const path = require('path');
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
const ptzHelper = require(path.resolve(__dirname, '../_helpers/ptzHelper.js'));
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
const Camera = db.Camera;
module.exports = {
brightness: _brightness,
contrast: _contrast,
whiteBalance: whiteBal,
rgain: _rgain,
bgain: _bgain,
shutter: _shutter,
iris: _iris,
gain: _gain,
backLight: _backLight,
blackWhite: _blackWhite,
flicker: _flicker,
imgFlip: _imgFlip,
colorHue: _colorHue,
ae: _autoExp,
save: saveSetting
};
async function _brightness({id, pos}) {
let brightnessHex = "810104A10000";
let brightnessPos = ptzHelper.numToHexStr(pos).padStart(4, "0");
brightnessHex += brightnessPos + "FF";
try {
return await socket.sendCmd(id, brightnessHex);
} catch(err) {
throw err;
}
}
async function _contrast(contrastParams) {
const {id, pos} = contrastParams;
let contrastHex = "810104A20000";
let contrastPos = ptzHelper.numToHexStr(pos).padStart(4, "0");
contrastHex += contrastPos + 'FF';
try {
return await socket.sendCmd(id, contrastHex);
} catch(err) {
throw err;
}
}
async function whiteBal({id, option, mode}) {
let wbHex = "810104";
if (mode.toLowerCase() === 'wbmode') {
switch(option.toLowerCase()) {
case "auto":
wbHex += "3500FF";
break;
case "indoor":
wbHex += "3501FF";
break;
case "outdoor":
wbHex += "3502FF";
break;
case "onepush":
wbHex += "3503FF";
break;
case "manual":
wbHex += "3505FF";
break;
case "onepush-trigger":
wbHex += "1005FF";
break;
default:
throw "The white balance mode " + option + " is not a recognizable white balance mode.";
}
} else if (mode.toLowerCase() === 'awbsenstivity') {
switch(option.toLowerCase()) {
case "high":
wbHex += "A900FF";
break;
case "normal":
wbHex += "A901FF";
break;
case "low":
wbHex += "A902FF";
break;
default:
throw "The auto white balance mode " + option + " is not a recognizable auto white balance mode.";
}
} else {
throw mode + "is not a recognizable white balance method.";
}
try {
return await socket.sendCmd(id, wbHex);
} catch(err) {
throw err;
}
}
async function _rgain({id, mode, option = null, pos = null}) {
let rgainHex = "810104";
if (mode.toLowerCase() === 'standard') {
switch (option.toLowerCase()) {
case "reset":
rgainHex += "0300FF";
break;
case "up":
rgainHex += "0302FF";
break;
case "down":
rgainHex += "0303FF";
break;
default:
throw "The rgain option " + option + " is not a recognizable standard rgain option";
}
} else if (mode.toLowerCase() === 'direct') {
rgainHex = "430000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
} else {
throw "The rgain mode " + mode + " is not a recognizable rgain method.";
}
try {
return await socket.sendCmd(id, rgainHex);
} catch(err) {
throw err;
}
}
async function _bgain({id, mode, option = null, pos = null}) {
let bgainHex = "810104";
if (mode.toLowerCase() === 'standard') {
switch (option.toLowerCase()) {
case "reset":
bgainHex += "0400FF";
break;
case "up":
bgainHex += "0402FF";
break;
case "down":
bgainHex += "0403FF";
break;
default:
throw "The bgain option " + option + " is not a recognizable standard bgain option";
}
} else if (mode.toLowerCase() === 'direct') {
rgainHex = "430000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
} else {
throw "The bgain mode " + mode + " is not a recognizable bgain method.";
}
try {
return await socket.sendCmd(id, bgainHex);
} catch(err) {
throw err;
}
}
async function _shutter({id, option, mode}) {
let shutterHex = "8101040A";
if (mode.toLowerCase() === 'standard') {
switch (option.toLowerCase()) {
case "reset":
shutterHex += "00FF";
break;
case "up":
shutterHex += "02FF";
break;
case "down":
shutterHex += "03FF";
break;
default:
throw "The shutter option " + option + " is not a recognizable standard shutter option";
}
} else if (mode.toLowerCase() === 'direct') {
shutterHex += "0000";
switch (option) {
case "1/30":
shutterHex += "0001FF";
break;
case "1/60":
shutterHex += "0002FF";
break;
case "1/90":
shutterHex += "0003FF";
break;
case "1/100":
shutterHex += "0004FF";
break;
case "1/125":
shutterHex += "0005FF";
break;
case "1/180":
shutterHex += "0006FF";
break;
case "1/250":
shutterHex += "0007FF";
break;
case "1/350":
shutterHex += "0008FF";
break;
case "1/500":
shutterHex += "0009FF";
break;
case "1/725":
shutterHex += "000AFF";
break;
case "1/1000":
shutterHex += "000BFF";
break;
case "1/1500":
shutterHex += "000CFF";
break;
case "1/2000":
shutterHex += "000DFF";
break;
case "1/3000":
shutterHex += "000EFF";
break;
case "1/4000":
shutterHex += "000FFF";
break;
case "1/6000":
shutterHex += "0100FF";
break;
case "1/10000":
shutterHex += "0101FF";
break;
default:
throw "The shutter option " + option + " is not a recognizable direct shutter option";
}
} else {
throw "The shutter mode " + mode + " is not a recognizable shutter mode";
}
try {
return await socket.sendCmd(id, shutterHex);
} catch(err) {
throw err;
}
console.log("reached end");
}
async function _iris({id, mode, option}) {
let irisHex = "8101040B";
if (mode.toLowerCase() === 'standard') {
switch (option.toLowerCase()) {
case "reset":
irisHex += "00FF";
break;
case "up":
irisHex += "02FF";
break;
case "down":
irisHex += "03FF";
break;
default:
throw "The iris option " + option + " is not a recognizable standard iris option";
}
} else if (mode.toLowerCase() === 'direct') {
irisHex += "0000";
switch (option.toLowerCase()) {
case "close":
irisHex += "0000FF";
break;
case "f11":
irisHex += "0006FF";
break;
case "f9.6":
irisHex += "0007FF";
break;
case "f8.0":
irisHex += "0008FF";
break;
case "f6.8":
irisHex += "0009FF";
break;
case "f5.6":
irisHex += "000AFF";
break;
case "f4.8":
irisHex += "000BFF";
break;
case "f4.0":
irisHex += "000CFF";
break;
case "f3.4":
irisHex += "000DFF";
break;
case "f2.8":
irisHex += "000EFF";
break;
case "f2.0":
irisHex += "0100FF";
break;
case "f1.8":
irisHex += "0200FF";
break;
default:
throw "The iris direct option " + option + " is not a recognizable option";
}
} else {
throw "The iris option " + option + " is not a recognizable direct iris option";
}
try {
return await socket.sendCmd(id, irisHex);
} catch(err) {
throw err;
}
}
async function _gain({id, mode, option = null, pos = null}) {
let gainHex = "8101040C";
if (mode.toLowerCase() === "standard") {
switch(option.toLowerCase()) {
case "reset":
gainHex += "00FF";
break;
case "up":
gainHex += "02FF";
break;
case "down":
gainHex += "03FF";
break;
default:
throw "The gain option " + option + " is not a recognizable standard gain option";
}
} else if (mode === "direct") {
gainHex += "0000" + ptzHelper.numToHexStr(pos).padStart(4, "0") + "FF";
} else {
throw "The gain mode " + mode + " is not a recognizable gain mode";
}
try {
return await socket.sendCmd(id, gainHex);
} catch(err) {
throw err;
}
}
async function _backLight({id, option}) {
let backlightHex = "81010433";
if (option.toLowerCase() === 'on') {
backlightHex += "02FF";
} else if (option.toLowerCase() === 'off') {
backlightHex += "03FF";
} else {
throw "The back light option " + option + " is not a recognizable back light option";
}
try {
return await socket.sendCmd(id, backlightHex);
} catch(err) {
throw err;
}
}
async function _blackWhite({id, option}) {
let bwHex = "81010463";
if (option.toLowerCase() === 'on') {
bwHex += "04FF";
} else if (option.toLowerCase() === 'off') {
bwHex += "00FF";
} else {
throw "The back light option " + option + " is not a recognizable back light option";
}
try {
return await socket.sendCmd(id, bwHex);
} catch(err) {
throw err;
}
}
async function _flicker({id, option}) {
let flickerHex = "810104230";
switch(option.toLowerCase()) {
case "off":
flickerHex += "0FF";
break;
case "50hz":
flickerHex += "1FF";
break;
case "60hz":
flickerHex += "2FF";
break;
default:
throw "The flicker option " + option + " is not a recognizable standard flicker option";
}
try {
return await socket.sendCmd(id, flickerHex);
} catch(err) {
throw err;
}
}
async function _imgFlip({id, mode, option}) {
let imgFlipHex = "810104";
if (mode.toLowerCase() === 'lr') {
imgFlipHex += '61';
} else if (mode.toLowerCase() === 'pf') {
imgFlipHex += '66';
} else {
throw "The image flip mode " + mode + " is not a recognizable image flip mode";
}
if (option.toLowerCase() === 'off') {
imgFlipHex += '03FF';
} else if (option.toLowerCase() === 'on') {
imgFlipHex += '02FF';
} else {
throw "The image flip option " + option + " is not a recognizable image flip option";
}
try {
return await socket.sendCmd(id, imgFlipHex);
} catch(err) {
throw err;
}
}
async function _colorHue({id, pos}) {
let colorHueHex = '8101044F';
let colorHuePos = ptzHelper.numToHexStr(pos).padStart(8, "0");
colorHueHex += colorHuePos + "FF";
try {
return await socket.sendCmd(id, colorHueHex);
} catch(err) {
throw err;
}
}
async function _autoExp({id, option}) {
let aeHex = '81010439';
switch (option.toLowerCase()) {
case "fullauto":
aeHex += '00FF';
break;
case "manual":
aeHex += '03FF';
break;
case "shutter":
aeHex += '0AFF';
break;
case "iris":
aeHex += '0BFF';
break;
case "bright":
aeHex += '0DFF';
break;
default:
throw "The autoexp option " + option + " is not a recognizable autoexp option";
}
try {
return await socket.sendCmd(id, aeHex);
} catch(err) {
throw err;
}
}
async function saveSetting(id) {
try {
return await socket.sendCmd(id, "81010604FF");
} catch(err) {
throw err;
}
}

36
app/ptz/ptz.controller.js Normal file
View File

@ -0,0 +1,36 @@
/*jshint esversion: 6 */
const path = require('path');
const express = require('express');
const router = express.Router();
const ptzService = require(path.resolve(__dirname, './ptz.service.js'));
module.exports = router;
router.post('/motion', _motion);
router.post('/presets', _presets);
router.post('/focus', _focus);
router.post('/zoom', _zoom);
function _presets(req, res, next) {
ptzService.preset(req.body)
.then((response) => res.send(response))
.catch(err => next(err));
}
function _motion(req, res, next) {
ptzService.motion(req.body)
.then((response) => res.send(response))
.catch(err => next(err));
}
function _focus(req, res, next) {
ptzService.focus(req.body)
.then((response) => res.send(response))
.catch(err => next(err));
}
function _zoom(req, res, next) {
ptzService.zoom(req.body)
.then((response) => res.send(response))
.catch(err => next(err));
}

194
app/ptz/ptz.service.js Normal file
View File

@ -0,0 +1,194 @@
/*jshint esversion: 6 */
const path = require('path');
const db = require(path.resolve(__dirname, '../_helpers/db.js'));
const ptzHelper = require(path.resolve(__dirname, '../_helpers/ptzHelper.js'));
const socket = require(path.resolve(__dirname, '../_helpers/socket.js'));
const Camera = db.Camera;
module.exports = {
preset: _presets,
motion: _motion,
focus: _focus,
zoom: _zoom
};
async function _presets({id, mode, speed = null, memNum = null}) {
let memCmd;
switch(mode.toLowerCase()) {
case 'speed':
const recallSpeed = ptzHelper.numToHexStr(speed).padStart(2, "0");
memCmd = "81010601" + recallSpeed + "FF";
break;
case 'call':
memCmd = "8101043F02" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
break;
case 'set':
memCmd = "8101043F01" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
break;
case 'reset':
memCmd = "8101043F00" + ptzHelper.numToHexStr(memNum).padStart(2, "0") + "FF";
break;
default:
throw "The preset mode " + mode + " is not a recognizable preset method.";
}
try {
return await socket.sendCmd(id, memCmd);
} catch(err) {
throw err;
}
}
async function _motion({id, mode, pan = null, tilt = null, panSpeed = null, tiltSpeed = null, direction = null}) {
if (mode === 'absolute' || mode === 'relative' || mode === 'standard') {
panTiltSpeedArr = ptzHelper.sanitizeSpeed(panSpeed, tiltSpeed);
if (mode === 'absolute' || mode === 'relative') {
pan = ptzHelper.numToHexStr(pan);
tilt = ptzHelper.numToHexStr(tilt);
}
}
let motionHex;
switch(mode.toLowerCase()) {
case 'home':
motionHex = "81010604FF";
break;
case 'absolute':
motionHex = "81010602" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + pan + tilt +"FF";
break;
case 'relative':
motionHex = "81010603" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + pan + tilt +"FF";
break;
case 'standard':
motionHex = "81010601" + panTiltSpeedArr[0] + panTiltSpeedArr[1] + ptzHelper.translateDirection(direction);
break;
case 'current':
motionHex = "81090612FF";
break;
default:
throw "The option " + mode + " is not a recognizable motion method.";
}
try {
return await socket.sendCmd(id, motionHex);
} catch(err) {
throw err;
}
}
async function _focus({id, mode, option = null, focusPos = null, intensity = null}) {
let focusHex = '';
switch(mode.toLowerCase()) {
case 'standard':
focusHex = "81010408";
if (option === 'stop') {
focusHex += "00FF";
} else if (option === 'tele') {
focusHex += "03FF";
} else if (option === 'wide') {
focusHex += "02FF";
} else {
throw "The focus option " + option + " is not a recognizable standard focus option";
}
break;
case 'variable':
focusHex = "81010408";
if (option === 'tele') {
focusHex += '3' + intensity + "FF";
} else if (option === 'wide') {
focusHex += '2' + intensity + "FF";
} else {
throw "The focus option " + option + " is not a recognizable variable focus option";
}
break;
case 'direct':
focusPos = numToHexStr(focusPos).split('').reduce((str, char) => {
return '0' + char;
});
focusPos = focusPos.padStart(8, "0");
focusHex = "81010448" + focusPos + "FF";
break;
case 'focusmode':
focusHex = "810";
if (option === 'auto') {
focusHex += "1043802FF";
} else if (option === 'manual') {
focusHex += "1043803FF";
} else if (option === 'toggle') {
focusHex += "1043810FF";
} else if (option === 'lock') {
focusHex += "A046802FF";
} else if (option === 'unlock') {
focusHex += "A046803FF";
} else {
throw "The focus option " + option + " is not a recognizable focus mode option";
}
break;
case 'afzone':
focusHex = "810104AA";
if (option === 'top') {
focusHex += "00FF";
} else if (option === 'center') {
focusHex += "01FF";
} else if (option === 'bottom') {
focusHex += "02FF";
} else {
throw "The focus option " + option + " is not a recognizable auto focus-zone option";
}
break;
default:
throw "The focus mode " + mode + " is not a recognizable focus method.";
}
try {
return await socket.sendCmd(id, focusHex);
} catch(err) {
throw err;
}
}
async function _zoom({id, mode, option = null, zoomPos = null, intensity}) {
let zoomHex = '';
switch (mode) {
case 'standard':
zoomHex = "81010407";
if (option === "stop") {
zoomHex += "00FF";
} else if (option === "tele") {
zoomHex += "02FF";
} else if (option === 'wide') {
zoomHex += "03FF";
} else {
throw "The zoom option " + option + " is not a recognizable standard zoom option";
}
break;
case 'variable':
zoomHex = "81010407";
if (option === 'tele') {
zoomHex += '2' + intensity + "FF";
} else if (option === 'wide') {
zoomHex += '3' + intensity + "FF";
} else {
throw "The zoom option " + option + " is not a recognizable variable zoom option";
}
break;
case 'direct':
zoomPos = numToHexStr(zoomPos).split('').reduce((str, char) => {
return '0' + char;
});
zoomPos = zoomPos.padStart(8, "0");
zoomHex = "81010447" + zoomPos + "FF";
break;
default:
throw "The zoom mode " + mode + " is not a recognizable zoom method.";
}
try {
return await socket.sendCmd(id, zoomHex);
} catch(err) {
throw err;
}
}

38
app/stream/mpeg1muxer.js Normal file
View File

@ -0,0 +1,38 @@
/*jshint esversion: 6 */
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
const child_process = require('child_process');
const EventEmitter = require('events');
const spawn = require('cross-spawn');
class Mpeg1Muxer extends EventEmitter {
constructor(options) {
super(options);
this.url = options.url;
this.width = options.width;
// this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-vf', 'yadif', '-f', 'mpegts', '-r', '30', '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '4096k', '-muxdelay', '0', '-', './app/stream/stream.ts'], {
// detached: false
// });
this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-filter:v', 'scale=1280:-1', '-f', 'mpegts', '-r', '30', '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '1500k', '-', './app/stream/stream.ts'], {
detached: false
});
this.inputStreamStarted = true;
this.stream.stdout.on('data', (data) => { return this.emit('mpeg1data', data); });
this.stream.stderr.on('data', (data) => { return this.emit('ffmpegError', data); });
}
stop() {
try {
this.stream.stdout.removeAllListeners();
} catch(err) {
console