UNPKG

node-red-contrib-onvif-nodes

Version:

Node Red nodes for communicating with OnVif compliant IP devices

332 lines (299 loc) 17.3 kB
/** * Copyright 2018 Bart Butenaers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { var settings = RED.settings; const onvif = require('onvif'); const url = require('url'); const https = require('http'); const utils = require('./utils'); function OnVifMediaNode(config) { RED.nodes.createNode(this, config); this.action = config.action; this.profileToken = config.profileToken; this.profileName = config.profileName; this.videoEncoderConfigToken = config.videoEncoderConfigToken; this.protocol = config.protocol; this.stream = config.stream; this.snapshotUriMap = new Map(); var node = this; // Retrieve the config node, where the device is configured node.deviceConfig = RED.nodes.getNode(config.deviceConfig); if (node.deviceConfig) { node.listener = function(onvifStatus) { utils.setNodeStatus(node, 'media', onvifStatus); } // Start listening for Onvif config nodes status changes node.deviceConfig.addListener("onvif_status", node.listener); // Show the current Onvif config node status already utils.setNodeStatus(node, 'media', node.deviceConfig.onvifStatus); node.deviceConfig.initialize(); } function getSnapshot(uri, newMsg) { require('request').get(uri, { 'auth': { 'user': node.deviceConfig.credentials.user, 'pass': node.deviceConfig.credentials.password, 'sendImmediately': false } }).on('response', function(response) { if(response.statusCode === 200) { // Concatenate all the data chunks, to get a complete image (as a Buffer) newMsg.payload = response.body; newMsg.contentType = response.headers['content-type']; if(!newMsg.contentType) { newMsg.contentType = 'image/jpeg'; } // Remember all body chunks and concatenate them into a single buffer when entire image received var bodyChunks = []; response.on("data", function (chunk) { bodyChunks.push(chunk); }); response.on("end", function () { newMsg.payload = Buffer.concat(bodyChunks); node.send(newMsg); }); } else { console.log(response.statusCode + ' ' + response.statusMessage); } }).on('error', function(err) { console.error(err.message) }) } node.on("input", function(msg) { var newMsg = {}; var action = node.action || msg.action; if (!action) { // When no action specified in the node, it should be specified in the msg.action node.error("No action specified (in node or msg)"); return; } // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet) if (action !== "reconnect") { if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") { node.error("This node is not connected to a device"); return; } if (!utils.hasService(node.deviceConfig.cam, 'media')) { node.error("The device has no support for a media service"); return; } } var protocol = node.protocol || msg.protocol; var stream = node.stream || msg.stream; var profileToken = node.profileToken || msg.profileToken; var profileName = node.profileName || msg.profileName; var videoEncoderConfigToken = node.videoEncoderConfigToken || msg.videoEncoderConfigToken; // TODO check this only for actions where profileToken is needed // TODO when device disconnected, this gives "Cannot read property '$' of undefined" due to missing videosources... /*if (!profileToken) { if (!node.deviceConfig.cam.getActiveSources()) { console.warn('No default video source available'); return; } }*/ // To make things easier for a user, we will let the user specify profile names. // The corresponding profile token (which is required in Onvif to work with profiles) will be searched here... if (!profileToken && profileName) { profileToken = node.deviceConfig.getProfileTokenByName(profileName); } newMsg.xaddr = this.deviceConfig.xaddress; newMsg.action = action; try { switch (action) { case "getStreamUri": // All these 3 fields are optional. If not specified, the device will use the defaults. var options = { 'stream': stream, 'profileToken': profileToken, 'protocol': protocol }; node.deviceConfig.cam.getStreamUri(options, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getSnapshotUri": // The profileToken is optional. When not specified, the device will use the default profile token. var options = { 'profileToken': profileToken }; node.deviceConfig.cam.getSnapshotUri(options, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getVideoEncoderConfiguration": // If the videoEncoderConfigToken doesn't exist, the first element from the videoEncoderConfigurations array will be returned node.deviceConfig.cam.getVideoEncoderConfiguration(videoEncoderConfigToken, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getVideoEncoderConfigurations": node.deviceConfig.cam.getVideoEncoderConfigurations(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getVideoEncoderConfigurationOptions": // If the videoEncoderConfigToken doesn't exist, the first element from the videoEncoderConfigurations array will be used node.deviceConfig.cam.getVideoEncoderConfigurationOptions(videoEncoderConfigToken, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getProfiles": node.deviceConfig.cam.getProfiles(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getVideoSources": node.deviceConfig.cam.getVideoSources(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getVideoSourceConfigurations": node.deviceConfig.cam.getVideoSourceConfigurations(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getAudioSources": node.deviceConfig.cam.getAudioSources(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getAudioSourceConfigurations": node.deviceConfig.cam.getAudioSourceConfigurations(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getAudioEncoderConfigurations": node.deviceConfig.cam.getAudioEncoderConfigurations(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getAudioOutputs": node.deviceConfig.cam.getAudioOutputs(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getAudioOutputConfigurations": node.deviceConfig.cam.getAudioOutputConfigurations(function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getOSDs": // If the videoEncoderConfigToken doesn't exist, all available OSD's will be requested node.deviceConfig.cam.getOSDs(videoEncoderConfigToken, function(err, stream, xml) { // Onvif OSD implementation is optional and most manufactures did not implement it. // This means we will arrive here often ... // Most camera's will offer another REST interface to check and configure OSD texts, but not based on OnVif. utils.handleResult(node, err, stream, xml, newMsg); }); break; case "getSnapshot": var snapshotUri = node.snapshotUriMap.get(profileToken); // Get the uri once (and cache it), when it hasn't been retrieved yet if (!snapshotUri) { // When no token is specified, the camera will use the default token var options = { 'profileToken': profileToken }; node.deviceConfig.cam.getSnapshotUri(options, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, null); if (!err) { // Cache the URL for the next time node.snapshotUriMap.set(profileToken, stream.uri); getSnapshot(stream.uri, newMsg); } }); } else { getSnapshot(snapshotUri, newMsg); } break; case "createProfile": // The profile name is optional var options = { 'name': profileName, 'profileToken': profileToken // TODO This is not being used (the device generates a new token) ... }; // Create an empty new deletable media profile node.deviceConfig.cam.createProfile(options, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; case "deleteProfile": node.deviceConfig.cam.deleteProfile(profileToken, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); }); break; /* TODO case "setVideoEncoderConfiguration": var options = { 'token': node.videoEncoderConfigToken, 'name': node.videoEncoderConfigName, //'useCount': ???, 'encoding': node.videoEncoderConfigEncoding, //JPEG | H264 | MPEG4 'resolution': { 'width': node.videoEncoderConfigWidth, 'height': node.videoEncoderConfigHeight, }, //'quality': ?????, 'rateControl': { 'frameRateLimit': node.videoEncoderConfigFrameRateLimit, 'encodingInterval': node.videoEncoderConfigEncodingInterval, 'bitrateLimit': node.videoEncoderConfigBitrateLimit }, 'MPEG4': { 'govLength': node.videoEncoderConfigMpeg4GovLength, 'profile': node.videoEncoderConfigMpeg4Profile // SP | ASP }, 'H264': { 'govLength': node.videoEncoderConfigH264GovLength, 'profile': node.videoEncoderConfigH264Profile // Baseline | Main | Extended | High }, 'multicast': { 'address': node.videoEncoderConfigMulticastAddress, 'type': node.videoEncoderConfigMulticastType, // IPv4 | IPv6 'IPv4Address': node.videoEncoderConfigMulticastIpv4Address, 'IPv6Address': node.videoEncoderConfigMulticastIpv6Address, 'port': node.videoEncoderConfigMulticastPort, 'TTL': node.videoEncoderConfigMulticastTtl, 'autoStart': node.videoEncoderConfigMulticastAutoStart, }, 'sessionTimeout': node.videoEncoderConfigName }; node.deviceConfig.cam.setVideoEncoderConfiguration(options, function(err, stream, xml) { utils.handleResult(node, err, stream, xml, newMsg); });; break; */ case "reconnect": node.deviceConfig.cam.connect(); break; default: //node.status({fill:"red",shape:"dot",text: "unsupported action"}); node.error("Action " + action + " is not supported"); } } catch (exc) { node.error("Action " + action + " failed: " + exc); } }); node.on("close",function() { if (node.listener) { node.deviceConfig.removeListener("onvif_status", node.listener); } }); } RED.nodes.registerType("onvif-media",OnVifMediaNode); }