node-onvif
Version:
The node-onvif is a Node.js module which allows you to communicate with the network camera which supports the ONVIF specifications.
288 lines (268 loc) • 9.15 kB
JavaScript
/* ------------------------------------------------------------------
* node-onvif - node-onvif.js
*
* Copyright (c) 2016 - 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-09-30
* ---------------------------------------------------------------- */
;
const mDgram = require('dgram');
const mCrypto = require('crypto');
/* ------------------------------------------------------------------
* Constructor: Onvif()
* ---------------------------------------------------------------- */
function Onvif() {
// Public
this.OnvifDevice = require('./modules/device.js');
// Private
this._OnvifSoap = require('./modules/soap.js');
this._MULTICAST_ADDRESS = '239.255.255.250';
this._PORT = 3702;
this._udp = null;
this._devices = {};
this._DISCOVERY_INTERVAL = 150; // ms
this._DISCOVERY_RETRY_MAX = 3;
this._DISCOVERY_WAIT = 3000; // ms
this._discovery_interval_timer = null;
this._discovery_wait_timer = null;
}
/* ------------------------------------------------------------------
* Method: startDiscovery(callback)
* [Caution]
* This method has been depricated.
* Use the startProbe() method instead of this method.
* ---------------------------------------------------------------- */
Onvif.prototype.startDiscovery = function(callback) {
this.startProbe().then((list) => {
let execCallback = () => {
let d = list.shift();
if(d) {
callback(d);
setTimeout(() => {
execCallback();
}, 100);
}
}
execCallback();
}).catch((error) => {
callback(error);
});
};
/* ------------------------------------------------------------------
* Method: startProbe([callback])
* ---------------------------------------------------------------- */
Onvif.prototype.startProbe = function(callback) {
let promise = new Promise((resolve, reject) => {
this._devices = {};
this._udp = mDgram.createSocket('udp4');
this._udp.once('error', (error) => {
reject(error);
});
this._udp.on('message', (buf, device_info) => {
this._OnvifSoap.parse(buf.toString()).then((result) => {
let type = '';
let urn = '';
let xaddrs = [];
let scopes = [];
let types = '';
try {
let probe_matches = result['Body']['ProbeMatches']
// make sure the right data exists
if(probe_matches !== undefined) {
let probe_match = probe_matches['ProbeMatch'];
urn = probe_match['EndpointReference']['Address'];
xaddrs = probe_match['XAddrs'].split(/\s+/);
if(typeof(probe_match['Scopes']) === 'string') {
scopes = probe_match['Scopes'].split(/\s+/);
} else if(typeof(probe_match['Scopes']) === 'object' && typeof(probe_match['Scopes']['_']) === 'string') {
scopes = probe_match['Scopes']['_'].split(/\s+/);
}
// modified to support Pelco cameras
if(typeof(probe_match['Types']) === 'string') {
types = probe_match['Types'].split(/\s+/);
} else if(typeof(probe_match['Types']) === 'object' && typeof(probe_match['Types']['_']) === 'string') {
types = probe_match['Types']['_'].split(/\s+/)
}
}
} catch(e) {
return;
};
if(urn && xaddrs.length > 0 && scopes.length > 0) {
if(!this._devices[urn]) {
let name = '';
let hardware = '';
let location = '';
scopes.forEach((s) => {
if(s.indexOf('onvif://www.onvif.org/hardware/') === 0) {
hardware = s.split('/').pop();
} else if(s.indexOf('onvif://www.onvif.org/location/') === 0) {
location = s.split('/').pop();
} else if(s.indexOf('onvif://www.onvif.org/name/') === 0) {
name = s.split('/').pop();
name = name.replace(/_/g, ' ');
}
});
let probe = {
'urn' : urn,
'name' : name,
'hardware': hardware,
'location': location,
'types' : types,
'xaddrs' : xaddrs,
'scopes' : scopes
};
this._devices[urn] = probe;
}
}
}).catch((error) => {
// Do nothing.
});
});
this._udp.bind(() => {
this._udp.removeAllListeners('error');
this._sendProbe().then(() => {
// Do nothing.
}).catch((error) => {
reject(error);
});
this._discovery_wait_timer = setTimeout(() => {
this.stopProbe().then(() => {
let device_list = [];
Object.keys(this._devices).forEach((urn) => {
device_list.push(this._devices[urn]);
});
resolve(device_list);
}).catch((error) => {
reject(error);
});
}, this._DISCOVERY_WAIT);
});
});
if(this._isValidCallback(callback)) {
promise.then((device_list) => {
callback(null, device_list);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
Onvif.prototype._isValidCallback = function(callback) {
return (callback && typeof(callback) === 'function') ? true : false;
};
Onvif.prototype._execCallback = function(callback, arg1, arg2) {
if(this._isValidCallback(callback)) {
callback(arg1, arg2);
}
};
Onvif.prototype._sendProbe = function(callback) {
let soap_tmpl = '';
soap_tmpl += '<?xml version="1.0" encoding="UTF-8"?>';
soap_tmpl += '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">';
soap_tmpl += ' <s:Header>';
soap_tmpl += ' <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>';
soap_tmpl += ' <a:MessageID>uuid:__uuid__</a:MessageID>';
soap_tmpl += ' <a:ReplyTo>';
soap_tmpl += ' <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>';
soap_tmpl += ' </a:ReplyTo>';
soap_tmpl += ' <a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>';
soap_tmpl += ' </s:Header>';
soap_tmpl += ' <s:Body>';
soap_tmpl += ' <Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery">';
soap_tmpl += ' <d:Types xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dp0="http://www.onvif.org/ver10/network/wsdl">dp0:__type__</d:Types>';
soap_tmpl += ' </Probe>';
soap_tmpl += ' </s:Body>';
soap_tmpl += '</s:Envelope>';
soap_tmpl = soap_tmpl.replace(/\>\s+\</g, '><');
soap_tmpl = soap_tmpl.replace(/\s+/, ' ');
let soap_set = [];
['NetworkVideoTransmitter', 'Device', 'NetworkVideoDisplay'].forEach((type) => {
let s = soap_tmpl;
s = s.replace('__type__', type);
s = s.replace('__uuid__', this._createUuidV4());
soap_set.push(s);
});
let soap_list = [];
for(let i=0; i<this._DISCOVERY_RETRY_MAX; i++) {
soap_set.forEach((s) => {
soap_list.push(s);
});
}
let promise = new Promise((resolve, reject) => {
if (!this._udp) {
reject(new Error('No UDP connection is available. The init() method might not be called yet.'));
}
let send = () => {
let soap = soap_list.shift();
if(soap) {
let buf = Buffer.from(soap, 'utf8');
this._udp.send(buf, 0, buf.length, this._PORT, this._MULTICAST_ADDRESS, (error, bytes) => {
this._discovery_interval_timer = setTimeout(() => {
send();
}, this._DISCOVERY_INTERVAL);
});
} else {
resolve();
}
};
send();
});
return promise;
};
Onvif.prototype._createUuidV4 = function() {
let clist = mCrypto.randomBytes(16).toString('hex').toLowerCase().split('');
clist[12] = '4';
clist[16] = (parseInt(clist[16], 16) & 3 | 8).toString(16);
let m = clist.join('').match(/^(.{8})(.{4})(.{4})(.{4})(.{12})/);
let uuid = [m[1], m[2], m[3], m[4], m[5]].join('-');
this._uuid = uuid;
return uuid;
};
/* ------------------------------------------------------------------
* Method: stopDiscovery([callback])
* [Caution]
* This method has been depricated.
* Use the stopProbe() method instead of this method.
* ---------------------------------------------------------------- */
Onvif.prototype.stopDiscovery = function(callback) {
this.stopProbe().then(() => {
this._execCallback(callback);
}).catch((error) => {
this._execCallback(callback, error);
});
};
/* ------------------------------------------------------------------
* Method: stopProbe([callback])
* ---------------------------------------------------------------- */
Onvif.prototype.stopProbe = function(callback) {
if(this._discovery_interval_timer !== null) {
clearTimeout(this._discovery_interval_timer);
this._discovery_interval_timer = null;
}
if(this._discovery_wait_timer !== null) {
clearTimeout(this._discovery_wait_timer);
this._discovery_wait_timer = null;
}
let promise = new Promise((resolve, reject) => {
if(this._udp) {
this._udp.close(() => {
this._udp.unref()
this._udp = null;
resolve();
});
} else {
resolve();
}
});
if(this._isValidCallback(callback)) {
promise.then(() => {
callback(null);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
module.exports = new Onvif();