/**
* @namespace cam
* @description PTZ section for Cam class
* @author Andrew D.Laptev <a.d.laptev@gmail.com>
* @licence MIT
*/
const Cam = require('./cam').Cam
, extend = require('util')._extend
, linerase = require('./utils').linerase
, url = require('url')
;
/**
* Receive cam presets
* @param {object} [options]
* @param {string} [options.profileToken]
* @param [callback]
*/
Cam.prototype.getPresets = function(options, callback) {
if (callback === undefined) { callback = options; options = {}; }
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GetPresets xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) +'</ProfileToken>' +
'</GetPresets>' +
this._envelopeFooter()
}, function(err, data, xml) {
if (!err) {
this.presets = {};
var presets = linerase(data).getPresetsResponse.preset;
if (typeof presets !== 'undefined') {
if (!Array.isArray(presets)) {
presets = [presets];
}
presets.forEach(function(preset) {
this.presets[preset.name] = preset.$.token;
}.bind(this));
}
}
if (callback) {
callback.call(this, err, this.presets, xml);
}
}.bind(this));
};
/**
* /PTZ/ Go to preset
* @param {object} options
* @param {string} [options.profileToken]
* @param {string} options.preset PresetName from {@link Cam#presets} property
* @param {function} callback
*/
Cam.prototype.gotoPreset = function(options, callback) {
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GotoPreset xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) + '</ProfileToken>' +
'<PresetToken>' + options.preset + '</PresetToken>' +
(options.speed ? '<Speed>' + this._panTiltZoomVectors(options.speed) + '</Speed>' : '') +
'</GotoPreset>' +
this._envelopeFooter()
}, callback.bind(this));
};
/**
* @typedef {object} Cam~PTZStatus
* @property {object} position
* @property {number} position.x
* @property {number} position.y
* @property {number} position.zoom
* @property {string} moveStatus
* @property {?Error} error
* @property {Date} utcTime
*/
/**
* @callback Cam~GetPTZStatusCallback
* @property {?Error} error
* @property {Cam~PTZStatus} status
*/
/**
* /PTZ/ Receive cam status
* @param {Object} [options]
* @param {string} [options.profileToken={@link Cam#activeSource.profileToken}]
* @param {Cam~GetPTZStatusCallback} callback
*/
Cam.prototype.getStatus = function(options, callback) {
if (callback === undefined) { callback = options; options = {}; }
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GetStatus xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) +'</ProfileToken>' +
'</GetStatus>' +
this._envelopeFooter()
}, function(err, data, xml) {
if (!err) {
var res = linerase(data).getStatusResponse.PTZStatus;
var status = {
position: {
x: res.position.panTilt.$.x
, y: res.position.panTilt.$.y
, zoom: res.position.zoom.$.x
}
, moveStatus: res.moveStatus
, error: res.error
, utcTime: res.utcTime
};
}
callback.call(this, err, err ? null : status, xml);
}.bind(this));
};
/**
* /PTZ/ Returns the properties of the requested PTZ node, if it exists.
* Use this function to get maximum number of presets, ranges of admitted values for x, y, zoom, iris, focus
* @param {Function} callback
*/
Cam.prototype.getNodes = function(callback) {
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GetNodes xmlns="http://www.onvif.org/ver20/ptz/wsdl" />' +
this._envelopeFooter()
}, function(err, data, xml) {
if (!err) {
var nodes = this.nodes = {};
data[0]['getNodesResponse'].forEach(function(ptzNode) {
nodes[ptzNode['PTZNode'][0].$.token] = linerase(ptzNode['PTZNode'][0]);
delete nodes[ptzNode['PTZNode'][0].$.token].$;
});
}
callback.call(this, err, nodes, xml);
}.bind(this));
};
/**
* /PTZ/ Get an array with configuration names
* @param {Function} callback
*/
Cam.prototype.getConfigurations = function(callback) {
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GetConfigurations xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'</GetConfigurations>' +
this._envelopeFooter()
}, function(err, data, xml) {
if (!err) {
var configurations = {};
data[0]['getConfigurationsResponse'].forEach(function(configuration) {
configurations[configuration['PTZConfiguration'][0]['name']] = {
useCount: parseInt(configuration['PTZConfiguration'][0]['useCount'])
, nodeToken: configuration['PTZConfiguration'][0]['nodeToken'][0]
, defaultPTZSpeed: {
x: configuration['PTZConfiguration'][0]['defaultPTZSpeed'][0]['panTilt'][0].$.x
, y: configuration['PTZConfiguration'][0]['defaultPTZSpeed'][0]['panTilt'][0].$.y
, zoom: configuration['PTZConfiguration'][0]['defaultPTZSpeed'][0]['zoom'][0].$.x
}
, defaultPTZTimeout: configuration['PTZConfiguration'][0]['defaultPTZTimeout'][0]
};
});
this.configurations = configurations;
}
if (callback) {
callback.call(this, err, this.configurations, xml);
}
}.bind(this));
};
/**
* /PTZ/ Get options for the PTZ configuration
* @param {string} configurationToken
* @param {Function} callback
*/
Cam.prototype.getConfigurationOptions = function(configurationToken, callback) {
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<GetConfigurationOptions xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ConfigurationToken>' + configurationToken + '</ConfigurationToken>' +
'</GetConfigurationOptions>' +
this._envelopeFooter()
}, function(err, data, xml) {
var configOptions;
if (!err) {
var sp = data[0]['getConfigurationOptionsResponse'][0]['PTZConfigurationOptions'][0]['spaces'][0];
configOptions = {
ptzTimeout: {
min: data[0]['getConfigurationOptionsResponse'][0]['PTZConfigurationOptions'][0]['PTZTimeout'][0]['min']
, max: data[0]['getConfigurationOptionsResponse'][0]['PTZConfigurationOptions'][0]['PTZTimeout'][0]['max']
}
, spaces: {
absolute: {
x: {
min: parseFloat(sp['absolutePanTiltPositionSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['absolutePanTiltPositionSpace'][0]['XRange'][0]['max'][0])
, uri: sp['absolutePanTiltPositionSpace'][0]['URI']
}
, y: {
min: parseFloat(sp['absolutePanTiltPositionSpace'][0]['YRange'][0]['min'][0])
, max: parseFloat(sp['absolutePanTiltPositionSpace'][0]['YRange'][0]['max'][0])
, uri: sp['absolutePanTiltPositionSpace'][0]['URI']
}
, zoom: {
min: parseFloat(sp['absoluteZoomPositionSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['absoluteZoomPositionSpace'][0]['XRange'][0]['max'][0])
, uri: sp['absoluteZoomPositionSpace'][0]['URI']
}
}
, relative: {
x: {
min: parseFloat(sp['relativePanTiltTranslationSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['relativePanTiltTranslationSpace'][0]['XRange'][0]['max'][0])
, uri: sp['relativePanTiltTranslationSpace'][0]['URI']
}
, y: {
min: parseFloat(sp['relativePanTiltTranslationSpace'][0]['YRange'][0]['min'][0])
, max: parseFloat(sp['relativePanTiltTranslationSpace'][0]['YRange'][0]['max'][0])
, uri: sp['relativePanTiltTranslationSpace'][0]['URI']
}
, zoom: {
min: parseFloat(sp['relativeZoomTranslationSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['relativeZoomTranslationSpace'][0]['XRange'][0]['max'][0])
, uri: sp['relativeZoomTranslationSpace'][0]['URI']
}
}
, continuous: {
x: {
min: parseFloat(sp['continuousPanTiltVelocitySpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['continuousPanTiltVelocitySpace'][0]['XRange'][0]['max'][0])
, uri: sp['continuousPanTiltVelocitySpace'][0]['URI']
}
, y: {
min: parseFloat(sp['continuousPanTiltVelocitySpace'][0]['YRange'][0]['min'][0])
, max: parseFloat(sp['continuousPanTiltVelocitySpace'][0]['YRange'][0]['max'][0])
, uri: sp['continuousPanTiltVelocitySpace'][0]['URI']
}
, zoom: {
min: parseFloat(sp['continuousZoomVelocitySpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['continuousZoomVelocitySpace'][0]['XRange'][0]['max'][0])
, uri: sp['continuousZoomVelocitySpace'][0]['URI']
}
}
, common: {
x: {
min: parseFloat(sp['panTiltSpeedSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['panTiltSpeedSpace'][0]['XRange'][0]['max'][0])
, uri: sp['panTiltSpeedSpace'][0]['URI']
}
, zoom: {
min: parseFloat(sp['zoomSpeedSpace'][0]['XRange'][0]['min'][0])
, max: parseFloat(sp['zoomSpeedSpace'][0]['XRange'][0]['max'][0])
, uri: sp['zoomSpeedSpace'][0]['URI']
}
}
}
};
if (this.configurations[configurationToken]) {
extend(this.configurations[configurationToken], configOptions);
}
}
if (callback) {
callback.call(this, err, configOptions, xml);
}
}.bind(this));
};
/**
* /PTZ/ relative move
* @param {object} options
* @param {string} [options.profileToken=Cam#activeSource.profileToken]
* @param {number} [options.x=0] Pan, float within -1 to 1
* @param {number} [options.y=0] Tilt, float within -1 to 1
* @param {number} [options.zoom=0] Zoom, float within 0 to 1
* @param {object} [options.speed] If the speed argument is omitted, the default speed set by the PTZConfiguration will be used.
* @param {number} [options.speed.x] Pan speed, float within 0 to 1
* @param {number} [options.speed.y] Tilt speed, float within 0 to 1
* @param {number} [options.speed.zoom] Zoom speed, float within 0 to 1
* @param {Cam~RequestCallback} [callback]
*/
Cam.prototype.relativeMove = function(options, callback) {
callback = callback ? callback.bind(this) : function() {};
// Due to some glitches in testing cam forcibly set undefined move parameters to zero
options.x = options.x || 0;
options.y = options.y || 0;
options.zoom = options.zoom || 0;
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<RelativeMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) + '</ProfileToken>' +
'<Translation>' +
this._panTiltZoomVectors(options) +
'</Translation>' +
(options.speed ? '<Speed>' + this._panTiltZoomVectors(options.speed) + '</Speed>' : '') +
'</RelativeMove>' +
this._envelopeFooter()
}, callback.bind(this));
};
/**
* /PTZ/ absolute move
* @param {object} options
* @param {string} [options.profileToken=Cam#activeSource.profileToken]
* @param {number} [options.x] Pan, float within -1 to 1
* @param {number} [options.y] Tilt, float within -1 to 1
* @param {number} [options.zoom] Zoom, float within 0 to 1
* @param {object} [options.speed] If the speed argument is omitted, the default speed set by the PTZConfiguration will be used.
* @param {number} [options.speed.x] Pan speed, float within 0 to 1
* @param {number} [options.speed.y] Tilt speed, float within 0 to 1
* @param {number} [options.speed.zoom] Zoom speed, float within 0 to 1
* @param {Cam~RequestCallback} [callback]
*/
Cam.prototype.absoluteMove = function(options, callback) {
callback = callback ? callback.bind(this) : function() {};
// Due to some glitches in testing cam forcibly set undefined move parameters to zero
options.x = options.x || 0;
options.y = options.y || 0;
options.zoom = options.zoom || 0;
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<AbsoluteMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) + '</ProfileToken>' +
'<Position>' +
this._panTiltZoomVectors(options) +
'</Position>' +
(options.speed ? '<Speed>' + this._panTiltZoomVectors(options.speed) + '</Speed>' : '') +
'</AbsoluteMove>' +
this._envelopeFooter()
}, callback.bind(this));
};
/**
* /PTZ/ Operation for continuous Pan/Tilt and Zoom movements
* @param options
* @param {string} [options.profileToken=Cam#activeSource.profileToken]
* @param {number} [options.x=0] pan velocity, float within 0 to 1
* @param {number} [options.y=0] tilt velocity, float within 0 to 1
* @param {number} [options.zoom=0] zoom velocity, float within 0 to 1
* @param {number} [options.timeout=Infinity] timeout in milliseconds
* @param {Cam~RequestCallback} callback
*/
Cam.prototype.continuousMove = function(options, callback) {
callback = callback ? callback.bind(this) : function() {};
// Due to some glitches in testing cam forcibly set undefined move parameters to zero
options.x = options.x || 0;
options.y = options.y || 0;
options.zoom = options.zoom || 0;
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<ContinuousMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) + '</ProfileToken>' +
'<Velocity>' +
this._panTiltZoomVectors(options) +
'</Velocity>' +
(options.timeout ? '<Timeout>PT' + (options.timeout / 1000) + 'S</Timeout>' : '') +
'</ContinuousMove>' +
this._envelopeFooter()
}, callback.bind(this));
};
/**
* Stop ongoing pan, tilt and zoom movements of absolute relative and continuous type
* @param {object} [options]
* @param {string} [options.profileToken]
* @param {boolean|string} [options.panTilt]
* @param {boolean|string} [options.zoom]
* @param {Cam~RequestCallback} [callback]
*/
Cam.prototype.stop = function(options, callback) {
if (callback === undefined) {
switch (typeof options) {
case 'object': callback = function() {}; break;
case 'function': callback = options; options = {}; break;
default: callback = function() {}; options = {};
}
}
this._request({
service: 'ptz'
, body: this._envelopeHeader() +
'<Stop xmlns="http://www.onvif.org/ver20/ptz/wsdl">' +
'<ProfileToken>' + (options.profileToken || this.activeSource.profileToken) + '</ProfileToken>' +
(options.panTilt ? '<PanTilt>' + options.panTilt + '</PanTilt>' : '') +
(options.zoom ? '<Zoom>' + options.zoom + '</Zoom>' : '') +
'</Stop>' +
this._envelopeFooter()
}, callback.bind(this));
};
/**
* Create ONVIF soap vector
* @param [ptz.x]
* @param [ptz.y]
* @param [ptz.zoom]
* @return {string}
* @private
*/
Cam.prototype._panTiltZoomVectors = function(ptz) {
return ptz
?
(ptz.x !== undefined && ptz.y !== undefined
? '<PanTilt x="' + ptz.x
+ '" y="' + ptz.y + '" xmlns="http://www.onvif.org/ver10/schema"/>'
: '') +
(ptz.zoom !== undefined
? '<Zoom x="'
+ ptz.zoom + '" xmlns="http://www.onvif.org/ver10/schema"/>'
: '')
: '';
};