node-red-contrib-hikvision-ultimate
Version:
A native set of nodes for Hikvision (and compatible) Cameras, Alarms, Radars, NVR, Doorbells, etc.
315 lines (271 loc) • 14.2 kB
JavaScript
module.exports = function (RED) {
const jimp = require("jimp"); // Resize
function hikvisionUltimatePicture(config) {
RED.nodes.createNode(this, config);
var node = this;
node.topic = config.topic || config.name;
node.server = RED.nodes.getNode(config.server)
const logDebug = (text) => {
if (node.server && node.server.debug) RED.log.info(`hikvisionUltimatePicture: ${text}`);
};
node.picture; // Stores the cam image
node.pictureLarghezza = 0;
node.pictureAltezza = 0;
node.urlImageCurrentIndex = config.urlImageCurrentIndex === undefined ? 0 : config.urlImageCurrentIndex; // Stores the valid URL
node.previousInputMessage = {}; // 01/09/2022 previous msg input to be passet to the output
node.setNodeStatus = ({ fill, shape, text }) => {
var dDate = new Date();
node.status({ fill: fill, shape: shape, text: text + " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" })
}
// 15/12/2020 apply the manipulation to the node's variable
node.variabilizeManipulation = (_config) => {
node.channelID = (_config.channelID === null || _config.channelID === undefined) ? "1" : _config.channelID;
node.qualityimage = (_config.qualityimage === null || _config.qualityimage === undefined || _config.qualityimage.trim() === "") ? "80" : _config.qualityimage;
node.rotateimage = (_config.rotateimage === null || _config.rotateimage === undefined) ? "0" : _config.rotateimage;
node.widthimage = (_config.widthimage === null || _config.widthimage === undefined || _config.widthimage.trim() === "") ? "0" : _config.widthimage;
node.heightimage = (_config.heightimage === null || _config.heightimage === undefined || _config.heightimage.trim() === "") ? "0" : _config.heightimage;
node.cropimage = (_config.cropimage === null || _config.cropimage === undefined || _config.cropimage.trim() === "") ? "" : _config.cropimage;
// 27/01/2021 Fonts
node.textoverlay = (_config.textoverlay === null || _config.textoverlay === undefined || _config.textoverlay.trim() === "") ? "" : _config.textoverlay;
node.textoverlayXY = (_config.textoverlayXY === null || _config.textoverlayXY === undefined || _config.textoverlayXY.trim() === "") ? "0,0" : _config.textoverlayXY;
node.textoverlayWH = (_config.textoverlayWH === null || _config.textoverlayWH === undefined || _config.textoverlayWH.trim() === "") ? "" : _config.textoverlayWH;
node.textoverlayFont = (_config.textoverlayFont === null || _config.textoverlayFont === undefined || _config.textoverlayFont.trim() === "") ? "FONT_SANS_32_WHITE" : _config.textoverlayFont;
node.urlImage = ["/ISAPI/Streaming/channels/" + node.channelID + "01/picture", "/ISAPI/ContentMgmt/StreamingProxy/channels/" + node.channelID + "01/picture"]; // Stores all URLS the node will try to get images from
//node.urlImage = ["/ISAPI/ContentMgmt/StreamingProxy/channels/" + node.channelID + "01/picture", "/ISAPI/Streaming/channels/" + node.channelID + "01/picture"]; // Stores all URLS the node will try to get images from
if (node.urlImageCurrentIndex > node.urlImage.length - 1) node.urlImageCurrentIndex = 0;
if (node.cropimage !== "" && node.cropimage.split(",").length === 4) {
node.cropimage = {
x: Number(node.cropimage.split(",")[0].trim()),
y: Number(node.cropimage.split(",")[1].trim()),
w: Number(node.cropimage.split(",")[2].trim()),
h: Number(node.cropimage.split(",")[3].trim())
}
} else {
node.cropimage = "";
}
if (node.textoverlayXY !== "" && node.textoverlayXY.split(",").length === 2) {
node.textoverlayXY = {
x: Number(node.textoverlayXY.split(",")[0].trim()),
y: Number(node.textoverlayXY.split(",")[1].trim())
}
} else {
node.textoverlayXY = "";
}
if (node.textoverlayWH !== "" && node.textoverlayWH.split(",").length === 2) {
node.textoverlayWH = {
w: Number(node.textoverlayWH.split(",")[0].trim()),
h: Number(node.textoverlayWH.split(",")[1].trim())
}
} else {
node.textoverlayWH = "";
}
}
node.variabilizeManipulation(config);
// 14/12/2020 Get the picture image from camera
RED.httpAdmin.get("/hikvisionUltimateGetPicture", RED.auth.needsPermission('hikvisionUltimatePicture.read'), function (req, res) {
if (typeof req.query.serverID !== "undefined" && req.query.serverID !== null && req.query.serverID !== "") {
var _nodeServer = RED.nodes.getNode(req.query.serverID);// Retrieve node.id of the config node.
// Create the config object
//#region CREATING CONFIG
var sManipulate = req.query.manipulate;
var oConfig = { channelID: sManipulate.split("-SEP-")[0].toString().trim() };
oConfig.qualityimage = sManipulate.split("-SEP-")[1].toString().trim();
oConfig.rotateimage = sManipulate.split("-SEP-")[2].toString().trim();
oConfig.widthimage = sManipulate.split("-SEP-")[3].toString().trim();
oConfig.heightimage = sManipulate.split("-SEP-")[4].toString().trim();
oConfig.cropimage = sManipulate.split("-SEP-")[5].toString().trim().replace(/-/g, ",");
oConfig.textoverlay = sManipulate.split("-SEP-")[6].toString().trim();
oConfig.textoverlayXY = sManipulate.split("-SEP-")[7].toString().trim().replace(/-/g, ",");
oConfig.textoverlayWH = sManipulate.split("-SEP-")[8].toString().trim().replace(/-/g, ",");
oConfig.textoverlayFont = sManipulate.split("-SEP-")[9].toString().trim();
node.urlImageCurrentIndex = (sManipulate.split("-SEP-")[10].toString().trim() === "YES" ? 0 : node.urlImageCurrentIndex); // Retry from beginning, to find the right image url
node.variabilizeManipulation(oConfig);
//#endregion
//console.log("MAN " + JSON.stringify(oConfig))
if (_nodeServer === null) {
logDebug("No server configured for picture request");
res.json({ picture: "", width: 0, height: 0 });
} else {
node.picture = null;
node.server = _nodeServer;
logDebug(`Requesting snapshot from URL index ${node.urlImageCurrentIndex}: ${node.urlImage[node.urlImageCurrentIndex]}`);
// Call the request, that then sends the result via node.sendPayload function
node.server.request(node, "GET", node.urlImage[node.urlImageCurrentIndex], null).then(success => {
// Wait until the server has called node.sendPayload and node.sendPayload has populated the node.picture variable
var iTimeout = 0;
var CB = () => {
iTimeout += 1;
if (iTimeout >= 15) {
// Set the URL to the next in the list, so it can retry with that url in SendPayload
node.urlImageCurrentIndex += 1;
if (node.urlImageCurrentIndex > node.urlImage.length - 1) {
// No more URLs to try
node.urlImageCurrentIndex = 0;
logDebug("Snapshot timed out, no more URLs to try");
res.json({ picture: "", width: " !Error getting picture Timeout! ", height: 0 });
} else {
// Cycled through all URLS
logDebug(`Snapshot timed out, retrying with URL index ${node.urlImageCurrentIndex}`);
res.json({ picture: "", width: " !Retry new URL... ", height: 0 });
}
return;
} else {
if (node.picture !== null) {
logDebug(`Snapshot received (w:${node.pictureLarghezza} h:${node.pictureAltezza}) from URL index ${node.urlImageCurrentIndex}`);
res.json({ picture: node.picture, width: node.pictureLarghezza, height: node.pictureAltezza, urlImageCurrentIndex: node.urlImageCurrentIndex });
return;
}
setTimeout(CB, 500);
}
}
setTimeout(CB, 500);
}).catch(error => {
// No more URLs to try
node.urlImageCurrentIndex = 0;
logDebug(`Snapshot request error: ${error.message || error}`);
res.json({ picture: "", width: " !Error getting picture! ", height: " !" + error.message + "! " });
});
}
} else { res.json({ picture: "", width: 0, height: 0 }); }
});
// Get picture
// node.getPicture = (_picBase64) => new Promise(function (resolve, reject) {
// try {
// jimp.read(_picBase64)
// .then(image => {
// if (node.rotateimage !== 0) image = image.rotate(Number(node.rotateimage));
// if (node.cropimage !== "") image = image.crop(node.cropimage.x, node.cropimage.y, node.cropimage.w, node.cropimage.h);
// if (node.heightimage !== "0" && node.widthimage !== "0") image = image.resize(Number(node.widthimage), Number(node.heightimage));
// if (node.qualityimage !== 100) image = image.quality(Number(node.qualityimage));
// node.pictureLarghezza = image.bitmap.width;
// node.pictureAltezza = image.bitmap.height;
// jimp.loadFont(jimp.FONT_SANS_32_WHITE).then(font => {
// image = image.print(font, 10, 10, 'Hello World!');
// }).catch(err => { });
// image.getBufferAsync(jimp.MIME_JPEG).then(picture => {
// node.picture = "data:image/jpg;base64," + picture.toString("base64");
// resolve(node.picture);
// }).catch(error => {
// reject(error);
// });
// });
// } catch (error) {
// reject(error);
// }
// });
// Get picture
async function getPicture(_picBase64) {
try {
image = await jimp.read(_picBase64);
if (node.rotateimage !== 0) image = await image.rotate(Number(node.rotateimage));
if (node.cropimage !== "") image = await image.crop(node.cropimage.x, node.cropimage.y, node.cropimage.w, node.cropimage.h);
if (node.heightimage !== "0" && node.widthimage !== "0") image = await image.resize(Number(node.widthimage), Number(node.heightimage));
if (node.qualityimage !== 100) image = await image.quality(Number(node.qualityimage));
node.pictureLarghezza = image.bitmap.width;
node.pictureAltezza = image.bitmap.height;
// 27/01/2021 FONTS
if (node.textoverlay !== "") {
const oFont = jimp[node.textoverlayFont];
try {
const font = await jimp.loadFont(oFont);
if (node.textoverlayWH === "") {
image = await image.print(font, node.textoverlayXY.x, node.textoverlayXY.y, {
text: node.textoverlay,
alignmentX: jimp.HORIZONTAL_ALIGN_LEFT,
alignmentY: jimp.VERTICAL_ALIGN_TOP
});
} else {
image = await image.print(font, node.textoverlayXY.x, node.textoverlayXY.y, {
text: node.textoverlay,
alignmentX: jimp.HORIZONTAL_ALIGN_LEFT,
alignmentY: jimp.VERTICAL_ALIGN_TOP
}, node.textoverlayWH.w, node.textoverlayWH.h);
}
} catch (error) {
}
}
let picture = await image.getBufferAsync(jimp.MIME_JPEG);
let b64 = picture.toString("base64");
node.picture = "data:image/jpg;base64," + b64;
// Return various type of picture formats
return ({
forUI: node.picture, forEmail: picture, base64: b64
});
} catch (error) {
return (error);
}
};
// Called from config node, to send output to the flow
node.sendPayload = (_msg) => {
if (_msg === null || _msg === undefined) return;
if (_msg.type !== undefined && _msg.type === 'img') {
// Coming from an event containing an image
return;
}
// 01/09/2022 Add the previous input message
_msg.previousInputMessage = node.previousInputMessage;
_msg.topic = node.topic;
if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg]); return; }; // It's a connection error/restore comunication.
if (!_msg.hasOwnProperty("payload") || (_msg.hasOwnProperty("payload") && _msg.payload === undefined)) return;
// 07/04/2021 The server has got a wrong response from camera/NVR
if (_msg.hasOwnProperty("wrongResponse")) {
// Maybe the URL was not working because the NVR is old or requires another URL
// Try with another URL
node.setNodeStatus({ fill: "yellow", shape: "ring", text: "Got wrong response. Trying wit another URL." });
// Call the request, that then sends the result via node.sendPayload function
//console.log("BANANA FIGA NON VA CON", node.urlImage[node.urlImageCurrentIndex]);
if (node.server.debug) RED.log.info("BANANA TRYING GETTING IMAGE WITH", node.urlImage[node.urlImageCurrentIndex]);
//console.log("BANANA PROVA CON", node.urlImage[node.urlImageCurrentIndex]);
node.server.request(node, "GET", node.urlImage[node.urlImageCurrentIndex], null); // Hybrid NVR get image from an IP camera
return;
}
if (_msg.payload.hasOwnProperty("eventType")) {
// Chech if it's only a hearbeat alarm
try {
var sAlarmType = _msg.payload.eventType.toString().toLowerCase();
if (sAlarmType === "videoloss" && _msg.payload.hasOwnProperty("activePostCount") && _msg.payload.activePostCount == "0") {
node.setNodeStatus({ fill: "green", shape: "ring", text: "Received HeartBeat (the device is online)" });
return; // It's a Heartbeat
}
} catch (error) { }
return;
}
getPicture(_msg.payload).then(data => {
_msg.payload = data.forUI;
_msg.forEmail = data.forEmail;
_msg.base64 = data.base64;
node.send(_msg, null);
node.setNodeStatus({ fill: "green", shape: "dot", text: "Picture received" });
}).catch(error => {
node.setNodeStatus({ fill: "red", shape: "dot", text: "GetBuffer error: " + error.message });
})
}
// On each deploy, unsubscribe+resubscribe
if (node.server) {
node.server.removeClient(node);
node.server.addClient(node);
}
this.on('input', function (msg) {
if (msg === null || msg === undefined) return;
// 01/09/2022 Save the original message to be passed through and sent out
node.previousInputMessage = msg;
if (msg.hasOwnProperty("textoverlay")) node.textoverlay = msg.textoverlay;
if (msg.hasOwnProperty("payload") && msg.payload !== null && msg.payload !== undefined) {
if (msg.payload === true) {
try {
// Call the request, that then sends the result via node.sendPayload function
node.server.request(node, "GET", node.urlImage[node.urlImageCurrentIndex], null);
} catch (error) {
}
}
}
});
node.on("close", function (done) {
if (node.server) {
node.server.removeClient(node);
}
done();
});
}
RED.nodes.registerType("hikvisionUltimatePicture", hikvisionUltimatePicture);
}