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.
698 lines (662 loc) • 21.2 kB
JavaScript
/* ------------------------------------------------------------------
* node-onvif - device.js
*
* Copyright (c) 2016-2018, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2018-08-13
* ---------------------------------------------------------------- */
'use strict';
const mCrypto = require('crypto');
const mUrl = require('url');
const mUtil = require('util');
const mEventEmitter = require('events').EventEmitter;
const mOnvifServiceDevice = require('./service-device.js');
const mOnvifServiceMedia = require('./service-media.js');
const mOnvifServicePtz = require('./service-ptz.js');
const mOnvifServiceEvents = require('./service-events.js');
const mOnvifHttpAuth = require('./http-auth.js');
/* ------------------------------------------------------------------
* Constructor: OnvifDevice(params)
* - params:
* - address : IP address of the targeted device
* (Required if the `xaddr` is not specified)
* - xaddr : URL of the entry point for the device management service
* (Required if the `address' is not specified)
* If the `xaddr` is specified, the `address` is ignored.
* - user : User name (Optional)
* - pass : Password (Optional)
* ---------------------------------------------------------------- */
function OnvifDevice(params) {
if (!params || typeof (params) !== 'object') {
throw new Error('The parameter was invalid.');
}
this.address = '';
this.xaddr = '';
this.user = '';
this.pass = '';
this.keepAddr = false;
this.lastResponse = null; // for debug
if (('xaddr' in params) && typeof (params['xaddr']) === 'string') {
this.xaddr = params['xaddr'];
let ourl = mUrl.parse(this.xaddr);
this.address = ourl.hostname;
} else if (('address' in params) && typeof (params['address']) === 'string') {
this.keepAddr = true;
this.address = params['address'];
this.xaddr = 'http://' + this.address + '/onvif/device_service';
} else {
throw new Error('The parameter was invalid.');
}
if (('user' in params) && typeof (params['user']) === 'string') {
this.user = params['user'] || '';
}
if (('pass' in params) && typeof (params['pass']) === 'string') {
this.pass = params['pass'] || '';
}
this.oxaddr = mUrl.parse(this.xaddr);
if (this.user) {
this.oxaddr.auth = this.user + ':' + this.pass;
}
this.time_diff = 0;
this.information = null;
this.services = {
'device': new mOnvifServiceDevice({ 'xaddr': this.xaddr, 'user': this.user, 'pass': this.pass }),
'events': null,
'imaging': null,
'media': null,
'ptz': null
};
this.profile_list = [];
this.current_profile = null;
this.ptz_moving = false;
mEventEmitter.call(this);
};
mUtil.inherits(OnvifDevice, mEventEmitter);
OnvifDevice.prototype._isValidCallback = function (callback) {
return (callback && typeof (callback) === 'function') ? true : false;
};
OnvifDevice.prototype._execCallback = function (callback, arg1, arg2) {
if (this._isValidCallback(callback)) {
callback(arg1, arg2);
}
};
/* ------------------------------------------------------------------
* Method: getInformation()
* ---------------------------------------------------------------- */
OnvifDevice.prototype.getInformation = function () {
let o = this.information;
if (o) {
return JSON.parse(JSON.stringify(o));
} else {
return null;
}
};
/* ------------------------------------------------------------------
* Method: getCurrentProfile()
* ---------------------------------------------------------------- */
OnvifDevice.prototype.getCurrentProfile = function () {
let o = this.current_profile;
if (o) {
return JSON.parse(JSON.stringify(o));
} else {
return null;
}
};
/* ------------------------------------------------------------------
* Method: getProfileList()
* ---------------------------------------------------------------- */
OnvifDevice.prototype.getProfileList = function () {
return JSON.parse(JSON.stringify(this.profile_list));
};
/* ------------------------------------------------------------------
* Method: changeProfile(index|token)
* ---------------------------------------------------------------- */
OnvifDevice.prototype.changeProfile = function (index) {
if (typeof (index) === 'number' && index >= 0 && index % 1 === 0) {
let p = this.profile_list[index];
if (p) {
this.current_profile = p;
return this.getCurrentProfile();
} else {
return null;
}
} else if (typeof (index) === 'string' && index.length > 0) {
let new_profile = null;
for (let i = 0; i < this.profile_list.length; i++) {
if (this.profile_list[i]['token'] === index) {
new_profile = this.profile_list[i];
break;
}
}
if (new_profile) {
this.current_profile = new_profile;
return this.getCurrentProfile();
} else {
return null;
}
} else {
return null;
}
};
/* ------------------------------------------------------------------
* Method: getUdpStreamUrl()
* ---------------------------------------------------------------- */
OnvifDevice.prototype.getUdpStreamUrl = function () {
if (!this.current_profile) {
return '';
}
let url = this.current_profile['stream']['udp'];
return url ? url : '';
};
/* ------------------------------------------------------------------
* Method: fetchSnapshot()
* ---------------------------------------------------------------- */
OnvifDevice.prototype.fetchSnapshot = function (callback) {
let promise = new Promise((resolve, reject) => {
if (!this.current_profile) {
reject(new Error('No media profile is selected.'));
return;
}
if (!this.current_profile['snapshot']) {
reject(new Error('The device does not support snapshot or you have not authorized by the device.'));
return;
}
let ourl = mUrl.parse(this.current_profile['snapshot']);
let options = {
protocol: ourl.protocol,
auth: this.user + ':' + this.pass,
hostname: ourl.hostname,
port: ourl.port || 80,
path: ourl.path,
method: 'GET'
};
let req = mOnvifHttpAuth.request(options, (res) => {
let buffer_list = [];
res.on('data', (buf) => {
buffer_list.push(buf);
});
res.on('end', () => {
if (res.statusCode === 200) {
let buffer = Buffer.concat(buffer_list);
let ct = res.headers['content-type'];
if (!ct) { // workaround for DBPOWER
ct = 'image/jpeg';
}
if (ct.match(/image\//)) {
resolve({ 'headers': res.headers, 'body': buffer });
} else if (ct.match(/^text\//)) {
reject(new Error(buffer.toString()));
} else {
reject(new Error('Unexpected data: ' + ct));
}
} else {
reject(new Error(res.statusCode + ' ' + res.statusMessage));
}
});
req.on('error', (error) => {
reject(error);
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
if (this._isValidCallback(callback)) {
promise.then((res) => {
callback(null, res);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
/* ------------------------------------------------------------------
* Method: ptzMove(params[, callback])
* - params:
* - speed:
* - x | Float | required | speed for pan (in the range of -1.0 to 1.0)
* - y | Float | required | speed for tilt (in the range of -1.0 to 1.0)
* - z | Float | required | speed for zoom (in the range of -1.0 to 1.0)
* - timeout | Integer | optional | seconds (Default 1)
* ---------------------------------------------------------------- */
OnvifDevice.prototype.ptzMove = function (params, callback) {
let promise = new Promise((resolve, reject) => {
if (!this.current_profile) {
reject(new Error('No media profile is selected.'));
return;
}
if (!this.services['ptz']) {
reject(new Error('The device does not support PTZ.'));
return;
}
let speed = params['speed'];
if (!speed) {
speed = {};
}
let x = speed['x'] || 0;
let y = speed['y'] || 0;
let z = speed['z'] || 0;
let timeout = params['timeout'];
if (!timeout || typeof (timeout) !== 'number') {
timeout = 1;
}
let p = {
'ProfileToken': this.current_profile['token'],
'Velocity': { 'x': x, 'y': y, 'z': z },
'Timeout': timeout
};
this.ptz_moving = true;
this.services['ptz'].continuousMove(p).then(() => {
resolve();
}).catch((error) => {
reject(error);
});
});
if (this._isValidCallback(callback)) {
promise.then(() => {
callback(null);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
/* ------------------------------------------------------------------
* Method: ptzStop([callback])
* ---------------------------------------------------------------- */
OnvifDevice.prototype.ptzStop = function (callback) {
let promise = new Promise((resolve, reject) => {
if (!this.current_profile) {
reject(new Error('No media profile is selected.'));
return;
}
if (!this.services['ptz']) {
reject(new Error('The device does not support PTZ.'));
return;
}
this.ptz_moving = false;
let p = {
'ProfileToken': this.current_profile['token'],
'PanTilt': true,
'Zoom': true
};
this.services['ptz'].stop(p).then((result) => {
resolve(result);
}).catch((error) => {
reject(error);
});
});
if (this._isValidCallback(callback)) {
promise.then((res) => {
callback(null, res);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
/* ------------------------------------------------------------------
* Method: setAuth(user, pass)
* ---------------------------------------------------------------- */
OnvifDevice.prototype.setAuth = function (user, pass) {
this.user = user || '';
this.pass = pass || '';
if (this.user) {
this.oxaddr.auth = this.user + ':' + this.pass;
}
for (let k in this.services) {
let s = this.services[k];
if (s) {
this.services[k].setAuth(user, pass);
}
}
};
/* ------------------------------------------------------------------
* Method: init([callback])
* ---------------------------------------------------------------- */
OnvifDevice.prototype.init = function (callback) {
let promise = new Promise((resolve, reject) => {
this._getSystemDateAndTime().then(() => {
return this._getCapabilities();
}).then(() => {
return this._getDeviceInformation();
}).then(() => {
return this._mediaGetProfiles();
}).then(() => {
return this._mediaGetStreamURI();
}).then(() => {
return this._mediaGetSnapshotUri();
}).then(() => {
let info = this.getInformation();
resolve(info);
}).catch((error) => {
reject(error);
});
});
if (this._isValidCallback(callback)) {
promise.then((info) => {
callback(null, info);
}).catch((error) => {
callback(error);
});
} else {
return promise;
}
};
// GetSystemDateAndTime (Access Class: PRE_AUTH)
OnvifDevice.prototype._getSystemDateAndTime = function () {
let promise = new Promise((resolve, reject) => {
this.services.device.getSystemDateAndTime((error, result) => {
// Ignore the error becase some devices do not support
// the GetSystemDateAndTime command and the error does
// not cause any trouble.
if (!error) {
this.time_diff = this.services.device.getTimeDiff();
}
resolve();
});
});
return promise;
};
// GetCapabilities (Access Class: PRE_AUTH)
OnvifDevice.prototype._getCapabilities = function () {
let promise = new Promise((resolve, reject) => {
this.services.device.getCapabilities((error, result) => {
this.lastResponse = result;
if (error) {
reject(new Error('Failed to initialize the device: ' + error.toString()));
return;
}
let c = result['data']['GetCapabilitiesResponse']['Capabilities'];
if (!c) {
reject(new Error('Failed to initialize the device: No capabilities were found.'));
return;
}
let events = c['Events'];
if (events && events['XAddr']) {
this.services.events = new mOnvifServiceEvents({
'xaddr': this._getXaddr(events['XAddr']),
'time_diff': this.time_diff,
'user': this.user,
'pass': this.pass
});
}
let imaging = c['Imaging'];
if (imaging && imaging['XAddr']) {
/*
this.services.imaging = new mOnvifServiceImaging({
'xaddr' : imaging['XAddr'],
'time_diff': this.time_diff,
'user' : this.user,
'pass' : this.pass
});
*/
}
let media = c['Media'];
if (media && media['XAddr']) {
this.services.media = new mOnvifServiceMedia({
'xaddr': this._getXaddr(media['XAddr']),
'time_diff': this.time_diff,
'user': this.user,
'pass': this.pass
});
}
let ptz = c['PTZ'];
if (ptz && ptz['XAddr']) {
this.services.ptz = new mOnvifServicePtz({
'xaddr': this._getXaddr(ptz['XAddr']),
'time_diff': this.time_diff,
'user': this.user,
'pass': this.pass
});
}
resolve();
});
});
return promise;
};
// GetDeviceInformation (Access Class: READ_SYSTEM)
OnvifDevice.prototype._getDeviceInformation = function () {
let promise = new Promise((resolve, reject) => {
this.services.device.getDeviceInformation((error, result) => {
if (error) {
reject(new Error('Failed to initialize the device: ' + error.toString()));
} else {
this.information = result['data']['GetDeviceInformationResponse'];
resolve();
}
});
});
return promise;
};
// Media::GetProfiles (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetProfiles = function () {
let promise = new Promise((resolve, reject) => {
this.services.media.getProfiles((error, result) => {
this.lastResponse = result;
if (error) {
reject(new Error('Failed to initialize the device: ' + error.toString()));
return;
}
let profiles = result['data']['GetProfilesResponse']['Profiles'];
if (!profiles) {
reject(new Error('Failed to initialize the device: The targeted device does not any media profiles.'));
return;
}
profiles = [].concat(profiles) // in case profiles is not a list, then forEach below will report an error
profiles.forEach((p) => {
let profile = {
'token': p['$']['token'],
'name': p['Name'],
'snapshot': '',
'stream': {
'udp': '',
'http': '',
'rtsp': ''
},
'video': {
'source': null,
'encoder': null
},
'audio': {
'source': null,
'encoder': null
},
'ptz': {
'range': {
'x': {
'min': 0,
'max': 0
},
'y': {
'min': 0,
'max': 0
},
'z': {
'min': 0,
'max': 0
}
}
}
};
if (p['VideoSourceConfiguration']) {
profile['video']['source'] = {
'token': p['VideoSourceConfiguration']['$']['token'],
'name': p['VideoSourceConfiguration']['Name'],
'bounds': {
'width': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['width'], 10),
'height': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['height'], 10),
'x': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['x'], 10),
'y': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['y'], 10)
}
};
}
if (p['VideoEncoderConfiguration']) {
profile['video']['encoder'] = {
'token': p['VideoEncoderConfiguration']['$']['token'],
'name': p['VideoEncoderConfiguration']['Name'],
'resolution': {
'width': parseInt(p['VideoEncoderConfiguration']['Resolution']['Width'], 10),
'height': parseInt(p['VideoEncoderConfiguration']['Resolution']['Height'], 10),
},
'quality': parseInt(p['VideoEncoderConfiguration']['Quality'], 10),
'framerate': parseInt(p['VideoEncoderConfiguration']['RateControl']['FrameRateLimit'], 10),
'bitrate': parseInt(p['VideoEncoderConfiguration']['RateControl']['BitrateLimit'], 10),
'encoding': p['VideoEncoderConfiguration']['Encoding']
};
}
if (p['AudioSourceConfiguration']) {
profile['audio']['source'] = {
'token': p['AudioSourceConfiguration']['$']['token'],
'name': p['AudioSourceConfiguration']['Name']
};
}
if (p['AudioEncoderConfiguration']) {
profile['audio']['encoder'] = {
'token': ('$' in p['AudioEncoderConfiguration']) ? p['AudioEncoderConfiguration']['$']['token'] : '',
'name': p['AudioEncoderConfiguration']['Name'],
'bitrate': parseInt(p['AudioEncoderConfiguration']['Bitrate'], 10),
'samplerate': parseInt(p['AudioEncoderConfiguration']['SampleRate'], 10),
'encoding': p['AudioEncoderConfiguration']['Encoding']
};
}
if (p['PTZConfiguration']) {
try {
let r = p['PTZConfiguration']['PanTiltLimits']['Range'];
let xr = r['XRange'];
let x = profile['ptz']['range']['x'];
x['min'] = parseFloat(xr['Min']);
x['max'] = parseFloat(xr['Max']);
} catch (e) { }
try {
let r = p['PTZConfiguration']['PanTiltLimits']['Range'];
let yr = r['YRange'];
let y = profile['ptz']['range']['y'];
y['min'] = parseFloat(yr['Min']);
y['max'] = parseFloat(yr['Max']);
} catch (e) { }
try {
let r = p['PTZConfiguration']['ZoomLimits']['Range'];
let zr = r['XRange'];
let z = profile['ptz']['range']['z'];
z['min'] = parseFloat(zr['Min']);
z['max'] = parseFloat(zr['Max']);
} catch (e) { }
}
this.profile_list.push(profile);
if (!this.current_profile) {
this.current_profile = profile;
}
});
resolve();
});
});
return promise;
};
// Media::GetStreamURI (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetStreamURI = function () {
let protocol_list = ['UDP', 'HTTP', 'RTSP'];
let promise = new Promise((resolve, reject) => {
let profile_index = 0;
let protocol_index = 0;
let getStreamUri = () => {
let profile = this.profile_list[profile_index];
if (profile) {
let protocol = protocol_list[protocol_index];
if (protocol) {
let token = profile['token'];
let params = {
'ProfileToken': token,
'Protocol': protocol
};
this.services.media.getStreamUri(params, (error, result) => {
this.lastResponse = result;
if (!error) {
let uri = result['data']['GetStreamUriResponse']['MediaUri']['Uri'];
uri = this._getUri(uri);
this.profile_list[profile_index]['stream'][protocol.toLowerCase()] = uri;
}
protocol_index++;
getStreamUri();
});
} else {
profile_index++;
protocol_index = 0;
getStreamUri();
}
} else {
resolve();
return;
}
};
getStreamUri();
});
return promise;
};
// Media::GetSnapshotUri (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetSnapshotUri = function () {
let promise = new Promise((resolve, reject) => {
let profile_index = 0;
let getSnapshotUri = () => {
let profile = this.profile_list[profile_index];
if (profile) {
let params = { 'ProfileToken': profile['token'] };
this.services.media.getSnapshotUri(params, (error, result) => {
this.lastResponse = result;
if (!error) {
try {
let snapshotUri = result['data']['GetSnapshotUriResponse']['MediaUri']['Uri'];
snapshotUri = this._getSnapshotUri(snapshotUri);
profile['snapshot'] = snapshotUri;
} catch (e) { console.log(e); }
}
profile_index++;
getSnapshotUri();
});
} else {
resolve();
}
};
getSnapshotUri();
});
return promise;
};
OnvifDevice.prototype._getXaddr = function (directXaddr) {
if (!this.keepAddr) return directXaddr;
const path = mUrl.parse(directXaddr).path;
return 'http://' + this.address + path;
}
OnvifDevice.prototype._getUri = function (directUri) {
if(typeof(directUri) === 'object' && directUri['_']) {
directUri = directUri['_'];
}
if (!this.keepAddr) return directUri;
const base = mUrl.parse('http://' + this.address);
const parts = mUrl.parse(directUri);
const newParts = {
host: base.host,
pathname: base.pathname + parts.pathname
};
const newUri = mUrl.format(newParts);
return newUri;
}
OnvifDevice.prototype._getSnapshotUri = function (directUri) {
if(typeof(directUri) === 'object' && directUri['_']) {
directUri = directUri['_'];
}
if (!this.keepAddr) return directUri;
const base = mUrl.parse('http://' + this.address);
const parts = mUrl.parse(directUri);
const newParts = {
protocol: parts.protocol,
host: base.host,
pathname: base.pathname + parts.pathname
};
const newUri = mUrl.format(newParts);
return newUri;
}
module.exports = OnvifDevice;