UNPKG

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
/* ------------------------------------------------------------------ * 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;