Source: cam.js

  1. /**
  2. * @namespace cam
  3. * @description Common camera module
  4. * @author Andrew D.Laptev <a.d.laptev@gmail.com>
  5. * @licence MIT
  6. */
  7. const http = require('http'),
  8. https = require('https'),
  9. crypto = require('crypto'),
  10. events = require('events'),
  11. url = require('url'),
  12. util = require('util'),
  13. linerase = require('./utils').linerase,
  14. parseSOAPString = require('./utils').parseSOAPString,
  15. parseString = require('xml2js').parseString,
  16. stripPrefix = require('xml2js').processors.stripPrefix,
  17. emptyFn = function() {};
  18. /**
  19. * @callback Cam~MessageCallback
  20. * @property {?Error} error
  21. * @property {?string} message
  22. */
  23. /**
  24. * @callback Cam~ConnectionCallback
  25. * @property {?Error} error
  26. */
  27. /**
  28. * @typedef Cam~Options
  29. * @property {boolean} [useSecure] Set true if `https:`, defaults to false
  30. * @property {object} [secureOpts] Set options for https like ca, cert, ciphers, rejectUnauthorized, secureOptions, secureProtocol, etc.
  31. * @property {boolean} [useWSSecurity] Use WS-Security SOAP headers
  32. * @property {string} hostname
  33. * @property {string} [username]
  34. * @property {string} [password]
  35. * @property {number} [port=80]
  36. * @property {string} [path=/onvif/device_service]
  37. * @property {number} [timeout=120000]
  38. * @property {boolean} [autoconnect=true] Set false if the camera should not connect automatically. The callback will not be executed.
  39. * @property {boolean} [preserveAddress=false] Force using hostname and port from constructor for the services
  40. */
  41. /**
  42. * Camera class
  43. * @param {Cam~Options} options
  44. * @param {Cam~ConnectionCallback} [callback]
  45. * @fires Cam#rawRequest
  46. * @fires Cam#rawResponse
  47. * @fires Cam#connect
  48. * @fires Cam#event
  49. * @fires Cam#warning
  50. * @property presets
  51. * @class
  52. * @constructor
  53. * @extends events.EventEmitter
  54. * @example
  55. * var
  56. * http = require('http'),
  57. * Cam = require('onvif').Cam;
  58. *
  59. * new Cam({
  60. * useSecure: <IS_SECURE>,
  61. * secureOpts: {...<SECURE_OPTIONS>}
  62. * hostname: <CAMERA_HOST>,
  63. * username: <USERNAME>,
  64. * password: <PASSWORD>
  65. * }, function(err) {
  66. * this.absoluteMove({x: 1, y: 1, zoom: 1});
  67. * this.getStreamUri({protocol:'RTSP'}, function(err, stream) {
  68. * http.createServer(function (req, res) {
  69. * res.writeHead(200, {'Content-Type': 'text/html'});
  70. * res.end('<html><body>' +
  71. * '<embed type="application/x-vlc-plugin" target="' + stream.uri + '"></embed>' +
  72. * '</body></html>');
  73. * }).listen(3030);
  74. * });
  75. * });
  76. */
  77. var Cam = function(options, callback) {
  78. events.EventEmitter.call(this);
  79. callback = callback || emptyFn;
  80. this.useSecure = options.useSecure || false;
  81. this.secureOpts = options.secureOpts || {};
  82. this.hostname = options.hostname;
  83. this.username = options.username;
  84. this.password = options.password;
  85. this.port = options.port || (options.useSecure ? 443 : 80);
  86. this.path = options.path || '/onvif/device_service';
  87. this.timeout = options.timeout || 120000;
  88. this.agent = options.agent || false;
  89. /**
  90. * Use WS-Security SOAP header
  91. * @type {boolean}
  92. */
  93. this.useWSSecurity = typeof options.useWSSecurity === 'boolean' ? options.useWSSecurity : true;
  94. this._nc = 0;
  95. /**
  96. * Force using hostname and port from constructor for the services
  97. * @type {boolean}
  98. */
  99. this.preserveAddress = options.preserveAddress || false;
  100. this.events = {};
  101. /**
  102. * Bind event handling to the `event` event
  103. */
  104. this.on('newListener', function(name) {
  105. // if this is the first listener, start pulling subscription
  106. if (name === 'event' && this.listeners(name).length === 0) {
  107. setImmediate(function() {
  108. this._eventRequest();
  109. }.bind(this));
  110. }
  111. }.bind(this));
  112. if (options.autoconnect !== false) {
  113. setImmediate(function() {
  114. this.connect(callback);
  115. }.bind(this));
  116. }
  117. };
  118. // events.EventEmitter inheritance
  119. util.inherits(Cam, events.EventEmitter);
  120. /**
  121. * Connect to the camera and fill device information properties
  122. * @param {Cam~ConnectionCallback} callback
  123. */
  124. Cam.prototype.connect = function(callback) {
  125. // Must execute getSystemDataAndTime (and wait for callback)
  126. // before any other ONVIF commands so that the time of the ONVIF device
  127. // is known
  128. this.getSystemDateAndTime(function(err, date, xml) {
  129. if (err) {
  130. return callback.call(this, err, null, xml);
  131. }
  132. // Next Step - Execute GetServices
  133. this.getServices(true, function(err) {
  134. if (err) {
  135. // Fall back to GetCapabilities. The original ONVIF spec did not include GetServices
  136. return this.getCapabilities(function(err, data, xml) {
  137. if (err) {
  138. return callback.call(this, err, null, xml);
  139. }
  140. return callUpstartFunctions.call(this);
  141. }.bind(this));
  142. }
  143. if (this.media2Support) {
  144. // Check Media2 works (workaround a buggy D-Link DCS-8635LH camera). Disable Media2 if there is an error.
  145. // This is inefficient as we also call GetProfiles in the upstart function
  146. this.getProfiles(function(err) {
  147. if (err) {
  148. this.media2Support = false; // don't use media2
  149. }
  150. return callUpstartFunctions.call(this);
  151. }.bind(this));
  152. } else {
  153. return callUpstartFunctions.call(this);
  154. }
  155. }.bind(this));
  156. function callUpstartFunctions() {
  157. var upstartFunctions = [];
  158. // Profile S
  159. if (this.uri && this.uri.media) {
  160. upstartFunctions.push(this.getProfiles);
  161. upstartFunctions.push(this.getVideoSources);
  162. }
  163. var count = upstartFunctions.length;
  164. var errCall = false;
  165. if (count > 0) {
  166. upstartFunctions.forEach(function(fun) {
  167. fun.call(this, function(err) {
  168. if (err) {
  169. if (callback && !errCall) {
  170. callback.call(this, err);
  171. errCall = true;
  172. }
  173. } else {
  174. if (!--count) {
  175. this.getActiveSources();
  176. /**
  177. * Indicates that device is connected.
  178. * @event Cam#connect
  179. */
  180. this.emit('connect');
  181. if (callback) {
  182. return callback.call(this, err);
  183. }
  184. }
  185. }
  186. }.bind(this));
  187. }.bind(this));
  188. } else {
  189. this.emit('connect');
  190. if (callback) {
  191. return callback.call(this, false);
  192. }
  193. }
  194. }
  195. }.bind(this));
  196. };
  197. /**
  198. * @callback Cam~RequestCallback
  199. * @param {Error} err
  200. * @param {object} [response] message
  201. * @param {string} [xml] response
  202. */
  203. /**
  204. * Common camera request
  205. * @param {object} options
  206. * @param {string} [options.service] Name of service (ptz, media, etc)
  207. * @param {string} [options.action] Name of the action. Not required, but desired
  208. * @param {string} options.body SOAP body
  209. * @param {string} [options.url] Defines another url to request instead of using the URLs from GetCapabilities/GetServices
  210. * @param {number} options.replyTimeout timeout in milliseconds for the reply (after the socket has connected)
  211. * @param {Cam~RequestCallback} callback response callback
  212. * @private
  213. */
  214. Cam.prototype._request = function(options, callback) {
  215. if (typeof callback !== 'function') {
  216. throw new Error('\'callback\' must be a function');
  217. }
  218. if (options.body === undefined) {
  219. throw new Error('\'options.body\' must be a defined');
  220. }
  221. // Generate the HTTP Content-Type 'action' string by parsing our outgoing ONVIF XML message with xml2js
  222. // xml2js returns a callback, so call _requestPart2 from the callback
  223. if (options.action === undefined || options.action === null) {
  224. let xml2jsOptions = {
  225. 'xmlns': true, // Creates the $ns object that contains the full namepace and local name of each XML Tag
  226. tagNameProcessors: [stripPrefix] // Removes the namespace from each tag so we can parse Key:Value pairs of Tags
  227. };
  228. parseString(options.body, xml2jsOptions, (err, result) => {
  229. // Check for parsing errors
  230. if (err) {
  231. return callback(err);
  232. }
  233. // look for first Key:Value pairs that do not start with $ or $ns
  234. // for (const [key, value] of Object.entries(result['Envelope']['Body'][0])) { // Node 7 or higher
  235. for (const key of Object.keys(result.Envelope.Body[0])) {
  236. const value = result.Envelope.Body[0][key];
  237. if (key.startsWith('$') === false) {
  238. options.action = value[0]['$ns'].uri + '/' + value[0]['$ns'].local;
  239. break;
  240. }
  241. }
  242. this._requestPart2.call(this, options, callback);
  243. });
  244. } else {
  245. this._requestPart2.call(this, options, callback);
  246. }
  247. };
  248. /**
  249. * Common camera request part 2
  250. * @param {Object} options
  251. * @param {Object} options.headers If some of the headers must present (Digest auth)
  252. * @param {string} options.action Name of the action
  253. * @param {string} [options.service] Name of service (ptz, media, etc)
  254. * @param {string} options.body SOAP body
  255. * @param {string} [options.url] Defines another url to request instead of using the URLs from GetCapabilities/GetServices
  256. * @param {number} options.replyTimeout timeout in milliseconds for the reply (after the socket has connected)
  257. * @param {Cam~RequestCallback} callback response callback
  258. * @private
  259. */
  260. Cam.prototype._requestPart2 = function(options, callback) {
  261. const _this = this;
  262. let callbackExecuted = false;
  263. options.headers = options.headers || {};
  264. let reqOptions = options.url || {
  265. hostname: this.hostname,
  266. port: this.port,
  267. agent: this.agent //Supports things like https://www.npmjs.com/package/proxy-agent which provide SOCKS5 and other connections
  268. ,
  269. path: options.service ?
  270. (this.uri && this.uri[options.service] ? this.uri[options.service].path : options.service) : this.path,
  271. timeout: this.timeout
  272. };
  273. reqOptions.headers = options.headers;
  274. Object.assign(reqOptions.headers, {
  275. 'Content-Type': `application/soap+xml;charset=utf-8;action="${options.action}"`,
  276. 'Content-Length': Buffer.byteLength(options.body, 'utf8') //options.body.length chinese will be wrong here
  277. ,
  278. charset: 'utf-8'
  279. });
  280. reqOptions.method = 'POST';
  281. const httpLib = this.useSecure ? https : http;
  282. reqOptions = this.useSecure ? Object.assign({}, this.secureOpts, reqOptions) : reqOptions;
  283. const req = httpLib.request(reqOptions, function(res) {
  284. const wwwAuthenticate = res.headers['www-authenticate'];
  285. const statusCode = res.statusCode;
  286. if (statusCode === 401 && wwwAuthenticate !== undefined) {
  287. // Re-request with digest auth header
  288. res.destroy();
  289. options.headers.Authorization = _this.digestAuth(wwwAuthenticate, reqOptions);
  290. _this._requestPart2(options, callback);
  291. }
  292. const bufs = [];
  293. let length = 0;
  294. res.on('data', function(chunk) {
  295. bufs.push(chunk);
  296. length += chunk.length;
  297. });
  298. res.on('end', function() {
  299. if (callbackExecuted === true) {
  300. return;
  301. }
  302. callbackExecuted = true;
  303. var xml = Buffer.concat(bufs, length).toString('utf8');
  304. /**
  305. * Indicates raw xml response from device.
  306. * @event Cam#rawResponse
  307. * @type {string} xml received from device
  308. * @type {number} HTTP statusCode received from device
  309. */
  310. _this.emit('rawResponse', xml, statusCode);
  311. parseSOAPString(xml, callback, statusCode); // xml and statusCode are passed in. Callback is the function called after the XML is parsed
  312. });
  313. });
  314. // Set the timeout for the reply (for data being received on the socket).
  315. // This timeout is used _after_ the socket has connected.
  316. // When pulling ONVIF Events, this will need to be set to a time larger than the Pull Request timeout
  317. // to ensure the socket does not get closed too early.
  318. // eg replyTimeout must be set to 75 seconds when the Pull Events timeout is 60 seconds
  319. req.setTimeout((options.replyTimeout ? options.replyTimeout : this.timeout), function() {
  320. if (callbackExecuted === true) {
  321. return;
  322. } else {
  323. callbackExecuted = true;
  324. }
  325. callback(new Error('Network timeout'));
  326. req.destroy();
  327. });
  328. req.on('error', function(err) {
  329. if (callbackExecuted === true) {
  330. return;
  331. }
  332. callbackExecuted = true;
  333. /* address, port number or IPCam error */
  334. if (err.code === 'ECONNREFUSED' && err.errno === 'ECONNREFUSED' && err.syscall === 'connect') {
  335. callback(err);
  336. /* network error */
  337. } else if (err.code === 'ECONNRESET' && err.errno === 'ECONNRESET' && err.syscall === 'read') {
  338. callback(err);
  339. } else {
  340. callback(err);
  341. }
  342. });
  343. /**
  344. * Indicates raw xml request to device.
  345. * @event Cam#rawRequest
  346. * @type {Object}
  347. */
  348. this.emit('rawRequest', options.body);
  349. req.write(options.body);
  350. req.end();
  351. };
  352. Cam.prototype._parseChallenge = function(digest) {
  353. const prefix = 'Digest ';
  354. const challenge = digest.substring(digest.indexOf(prefix) + prefix.length);
  355. const parts = challenge.split(',')
  356. .map(part => part.match(/^\s*?([a-zA-Z0-9]+)="?([^"]*)"?\s*?$/).slice(1));
  357. return Object.fromEntries(parts);
  358. };
  359. Cam.prototype.updateNC = function() {
  360. this._nc += 1;
  361. if (this._nc > 99999999) {
  362. this._nc = 1;
  363. }
  364. return String(this._nc).padStart(8, '0');
  365. };
  366. Cam.prototype.digestAuth = function(wwwAuthenticate, reqOptions) {
  367. const challenge = this._parseChallenge(wwwAuthenticate);
  368. const ha1 = crypto.createHash('md5');
  369. ha1.update([this.username, challenge.realm, this.password].join(':'));
  370. const ha2 = crypto.createHash('md5');
  371. ha2.update([reqOptions.method, reqOptions.path].join(':'));
  372. let cnonce = null;
  373. let nc = null;
  374. if (typeof challenge.qop === 'string') {
  375. const cnonceHash = crypto.createHash('md5');
  376. cnonceHash.update(Math.random().toString(36));
  377. cnonce = cnonceHash.digest('hex').substring(0, 8);
  378. nc = this.updateNC();
  379. }
  380. const response = crypto.createHash('md5');
  381. const responseParams = [
  382. ha1.digest('hex'),
  383. challenge.nonce
  384. ];
  385. if (cnonce) {
  386. responseParams.push(nc);
  387. responseParams.push(cnonce);
  388. }
  389. responseParams.push(challenge.qop);
  390. responseParams.push(ha2.digest('hex'));
  391. response.update(responseParams.join(':'));
  392. const authParams = {
  393. username: this.username,
  394. realm: challenge.realm,
  395. nonce: challenge.nonce,
  396. uri: reqOptions.path,
  397. qop: challenge.qop,
  398. response: response.digest('hex'),
  399. };
  400. if (challenge.opaque) {
  401. authParams.opaque = challenge.opaque;
  402. }
  403. if (cnonce) {
  404. authParams.nc = nc;
  405. authParams.cnonce = cnonce;
  406. }
  407. return 'Digest ' + Object.entries(authParams).map(([key, value]) => `${key}="${value}"`).join(',');
  408. };
  409. /**
  410. * @callback Cam~DateTimeCallback
  411. * @property {?Error} error
  412. * @property {Date} dateTime Date object of current device's dateTime
  413. * @property {string} xml Raw SOAP response
  414. */
  415. /**
  416. * Receive date and time from cam
  417. * @param {Cam~DateTimeCallback} callback
  418. */
  419. Cam.prototype.getSystemDateAndTime = function(callback) {
  420. // The ONVIF spec says this should work without a Password as we need to know any difference in the
  421. // remote NVT's time relative to our own time clock (called the timeShift) before we can calculate the
  422. // correct timestamp in nonce SOAP Authentication header.
  423. // But.. Panasonic and Digital Barriers both have devices that implement ONVIF that only work with
  424. // authenticated getSystemDateAndTime. So for these devices we need to do an authenticated getSystemDateAndTime.
  425. // As 'timeShift' is not set, the local clock MUST be set to the correct time AND the NVT/Camera MUST be set to the correct time
  426. // if the camera implements Replay Attack Protection (eg Axis)
  427. this._request({
  428. // Try the Unauthenticated Request first. Do not use this._envelopeHeader() as we don't have timeShift yet.
  429. body: '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' +
  430. '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
  431. '<GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  432. '</s:Body>' +
  433. '</s:Envelope>'
  434. }, function(err, data, xml, statusCode) {
  435. if (!err) {
  436. try {
  437. let systemDateAndTime = data[0]['getSystemDateAndTimeResponse'][0]['systemDateAndTime'][0];
  438. let dateTime = systemDateAndTime['UTCDateTime'] || systemDateAndTime['localDateTime'];
  439. let time;
  440. if (dateTime === undefined) {
  441. // Seen on a cheap Chinese camera from GWellTimes-IPC. Use the current time.
  442. time = new Date();
  443. } else {
  444. let dt = linerase(dateTime[0]);
  445. time = new Date(Date.UTC(dt.date.year, dt.date.month - 1, dt.date.day, dt.time.hour, dt.time.minute, dt.time.second));
  446. }
  447. if (!this.timeShift) {
  448. this.timeShift = time - (process.uptime() * 1000);
  449. }
  450. callback.call(this, err, time, xml);
  451. } catch (err) {
  452. callback.call(this, err, null, xml);
  453. }
  454. }
  455. if (err) {
  456. // some cameras return XML with 'sender not authorized'
  457. // One HikVision camera returned a 401 error code and the following HTML...
  458. // <!DOCTYPE html><html><head><title>Document Error: Unauthorized</title></head><body><h2>Access Error: 401 -- Unauthorized</h2>
  459. // <p>Authentication Error: Access Denied! Authorization required.</p></body></html>
  460. let xmlLowerCase = '';
  461. if (xml) {
  462. xmlLowerCase = xml.toLowerCase();
  463. }
  464. if (
  465. (statusCode && statusCode === 401)
  466. || (xml && (xmlLowerCase.includes('sender not authorized') || xmlLowerCase.includes('unauthorized') || xmlLowerCase.includes('notauthorized')))
  467. ) {
  468. // Try again with a Username and Password
  469. this._request({
  470. body: this._envelopeHeader() +
  471. '<GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  472. this._envelopeFooter()
  473. }, function(err, data, xml) {
  474. try {
  475. let systemDateAndTime = data[0]['getSystemDateAndTimeResponse'][0]['systemDateAndTime'][0];
  476. let dateTime = systemDateAndTime['UTCDateTime'] || systemDateAndTime['localDateTime'];
  477. let time;
  478. if (dateTime === undefined) {
  479. // Seen on a cheap Chinese camera from GWellTimes-IPC. Use the current time.
  480. time = new Date();
  481. } else {
  482. let dt = linerase(dateTime[0]);
  483. time = new Date(Date.UTC(dt.date.year, dt.date.month - 1, dt.date.day, dt.time.hour, dt.time.minute, dt.time.second));
  484. }
  485. if (!this.timeShift) {
  486. this.timeShift = time - (process.uptime() * 1000);
  487. }
  488. callback.call(this, err, time, xml);
  489. } catch (err) {
  490. callback.call(this, err, null, xml);
  491. }
  492. }.bind(this));
  493. } else {
  494. callback.call(this, err, null, xml);
  495. }
  496. }
  497. }.bind(this));
  498. };
  499. /**
  500. * @typedef {object} Cam~SystemDateAndTime
  501. * @property {string} dayTimeType (Manual | NTP)
  502. * @property {boolean} daylightSavings
  503. * @property {string} timezone in POSIX 1003.1 format
  504. * @property {number} hour
  505. * @property {number} minute
  506. * @property {number} second
  507. * @property {number} year
  508. * @property {number} month
  509. * @property {number} day
  510. */
  511. /**
  512. * Set the device system date and time
  513. * @param {object} options
  514. * @param {Date} [options.dateTime]
  515. * @param {string} options.dateTimeType (Manual | NTP)
  516. * @param {boolean} [options.daylightSavings=false]
  517. * @patam {string} [options.timezone]
  518. * @param {Cam~DateTimeCallback} callback
  519. */
  520. Cam.prototype.setSystemDateAndTime = function(options, callback) {
  521. if (['Manual', 'NTP'].indexOf(options.dateTimeType) === -1) {
  522. return callback(new Error('DateTimeType should be `Manual` or `NTP`'));
  523. }
  524. this._request({
  525. body: this._envelopeHeader() +
  526. '<SetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl">' +
  527. '<DateTimeType>' +
  528. options.dateTimeType +
  529. '</DateTimeType>' +
  530. '<DaylightSavings>' +
  531. (!!options.daylightSavings) +
  532. '</DaylightSavings>' +
  533. (options.timezone !== undefined ? '<TimeZone>' +
  534. '<TZ xmlns="http://www.onvif.org/ver10/schema">' +
  535. options.timezone +
  536. '</TZ>' +
  537. '</TimeZone>' : '') +
  538. // ( options.dateTime !== undefined && options.dateTime.getDate instanceof Date ?
  539. (options.dateTime !== undefined && options.dateTime instanceof Date ? '<UTCDateTime>' +
  540. '<Time xmlns="http://www.onvif.org/ver10/schema">' +
  541. '<Hour>' + options.dateTime.getUTCHours() + '</Hour>' +
  542. '<Minute>' + options.dateTime.getUTCMinutes() + '</Minute>' +
  543. '<Second>' + options.dateTime.getUTCSeconds() + '</Second>' +
  544. '</Time>' +
  545. '<Date xmlns="http://www.onvif.org/ver10/schema">' +
  546. '<Year>' + options.dateTime.getUTCFullYear() + '</Year>' +
  547. '<Month>' + (options.dateTime.getUTCMonth() + 1) + '</Month>' +
  548. '<Day>' + options.dateTime.getUTCDate() + '</Day>' +
  549. '</Date>' +
  550. '</UTCDateTime>' : '') +
  551. '</SetSystemDateAndTime>' +
  552. this._envelopeFooter()
  553. }, function(err, data, xml) {
  554. if (err || linerase(data).setSystemDateAndTimeResponse !== '') {
  555. return callback.call(this, linerase(data).setSystemDateAndTimeResponse !== '' ?
  556. new Error('Wrong `SetSystemDateAndTime` response') :
  557. err, data, xml);
  558. }
  559. //get new system time from device
  560. this.getSystemDateAndTime(callback);
  561. }.bind(this));
  562. };
  563. /**
  564. * Capability list
  565. * @typedef {object} Cam~Capabilities
  566. * @property {object} device Device capabilities
  567. * @property {string} device.XAddr Device service URI
  568. * @property {object} [device.network] Network capabilities
  569. * @property {boolean} device.network.IPFilter Indicates support for IP filtering
  570. * @property {boolean} device.network.zeroConfiguration Indicates support for zeroconf
  571. * @property {boolean} device.network.IPVersion6 Indicates support for IPv6
  572. * @property {boolean} device.network.dynDNS Indicates support for dynamic DNS configuration
  573. * @property {object} [device.system] System capabilities
  574. * @property {boolean} device.system.discoveryResolve Indicates support for WS Discovery resolve requests
  575. * @property {boolean} device.system.discoveryBye Indicates support for WS-Discovery Bye
  576. * @property {boolean} device.system.remoteDiscovery Indicates support for remote discovery
  577. * @property {boolean} device.system.systemBackup Indicates support for system backup through MTOM
  578. * @property {boolean} device.system.systemLogging Indicates support for retrieval of system logging through MTOM
  579. * @property {boolean} device.system.firmwareUpgrade Indicates support for firmware upgrade through MTOM
  580. * @property {boolean} device.system.httpFirmwareUpgrade Indicates support for firmware upgrade through HTTP
  581. * @property {boolean} device.system.httpSystemBackup Indicates support for system backup through HTTP
  582. * @property {boolean} device.system.httpSystemLogging Indicates support for retrieval of system logging through HTTP
  583. * @property {object} [device.IO] I/O capabilities
  584. * @property {number} device.IO.inputConnectors Number of input connectors
  585. * @property {number} device.IO.relayOutputs Number of relay outputs
  586. * @property {object} [device.IO.extension]
  587. * @property {boolean} device.IO.extension.auxiliary
  588. * @property {object} device.IO.extension.auxiliaryCommands
  589. * @property {object} [device.security] Security capabilities
  590. * @property {boolean} device.security.'TLS1.1' Indicates support for TLS 1.1
  591. * @property {boolean} device.security.'TLS1.2' Indicates support for TLS 1.2
  592. * @property {boolean} device.security.onboardKeyGeneration Indicates support for onboard key generation
  593. * @property {boolean} device.security.accessPolicyConfig Indicates support for access policy configuration
  594. * @property {boolean} device.security.'X.509Token' Indicates support for WS-Security X.509 token
  595. * @property {boolean} device.security.SAMLToken Indicates support for WS-Security SAML token
  596. * @property {boolean} device.security.kerberosToken Indicates support for WS-Security Kerberos token
  597. * @property {boolean} device.security.RELToken Indicates support for WS-Security REL token
  598. * @property {object} events Event capabilities
  599. * @property {string} events.XAddr Event service URI
  600. * @property {boolean} events.WSSubscriptionPolicySupport Indicates whether or not WS Subscription policy is supported
  601. * @property {boolean} events.WSPullPointSupport Indicates whether or not WS Pull Point is supported
  602. * @property {boolean} events.WSPausableSubscriptionManagerInterfaceSupport Indicates whether or not WS Pausable Subscription Manager Interface is supported
  603. * @property {object} imaging Imaging capabilities
  604. * @property {string} imaging.XAddr Imaging service URI
  605. * @property {object} media Media capabilities
  606. * @property {string} media.XAddr Media service URI
  607. * @property {object} media.streamingCapabilities Streaming capabilities
  608. * @property {boolean} media.streamingCapabilities.RTPMulticast Indicates whether or not RTP multicast is supported
  609. * @property {boolean} media.streamingCapabilities.RTP_TCP Indicates whether or not RTP over TCP is supported
  610. * @property {boolean} media.streamingCapabilities.RTP_RTSP_TCP Indicates whether or not RTP/RTSP/TCP is supported
  611. * @property {object} media.streamingCapabilities.extension
  612. * @property {object} PTZ PTZ capabilities
  613. * @property {string} PTZ.XAddr PTZ service URI
  614. * @property {object} [extension]
  615. * @property {object} extension.deviceIO DeviceIO capabilities
  616. * @property {string} extension.deviceIO.XAddr DeviceIO service URI
  617. * @property {number} extension.deviceIO.videoSources
  618. * @property {number} extension.deviceIO.videoOutputs
  619. * @property {number} extension.deviceIO.audioSources
  620. * @property {number} extension.deviceIO.audioOutputs
  621. * @property {number} extension.deviceIO.relayOutputs
  622. * @property {object} [extension.extensions]
  623. * @property {object} [extension.extensions.telexCapabilities]
  624. * @property {object} [extension.extensions.scdlCapabilities]
  625. */
  626. /**
  627. * @callback Cam~GetCapabilitiesCallback
  628. * @property {?Error} error
  629. * @property {Cam~Capabilities} capabilities
  630. * @property {string} xml Raw SOAP response
  631. */
  632. /**
  633. * This method has been replaced by the more generic GetServices method. For capabilities of individual services refer to the GetServiceCapabilities methods.
  634. * @param {Cam~GetCapabilitiesCallback} [callback]
  635. */
  636. Cam.prototype.getCapabilities = function(callback) {
  637. this._request({
  638. body: this._envelopeHeader() +
  639. '<GetCapabilities xmlns="http://www.onvif.org/ver10/device/wsdl">' +
  640. '<Category>All</Category>' +
  641. '</GetCapabilities>' +
  642. this._envelopeFooter()
  643. }, function(err, data, xml) {
  644. if (!err) {
  645. /**
  646. * Device capabilities
  647. * @name Cam#capabilities
  648. * @type {Cam~Capabilities}
  649. */
  650. this.capabilities = linerase(data[0]['getCapabilitiesResponse'][0]['capabilities'][0]);
  651. // fill Cam#uri property
  652. if (!this.uri) {
  653. /**
  654. * Device service URIs
  655. * @name Cam#uri
  656. * @property {url} [PTZ]
  657. * @property {url} [media]
  658. * @property {url} [imaging]
  659. * @property {url} [events]
  660. * @property {url} [device]
  661. */
  662. this.uri = {};
  663. }
  664. ['PTZ', 'media', 'imaging', 'events', 'device'].forEach(function(name) {
  665. if (this.capabilities[name] && this.capabilities[name].XAddr) {
  666. this.uri[name.toLowerCase()] = this._parseUrl(this.capabilities[name].XAddr);
  667. }
  668. }.bind(this));
  669. // extensions, eg. deviceIO
  670. if (this.capabilities.extension) {
  671. Object.keys(this.capabilities.extension).forEach(function(ext) {
  672. // TODO think about complex extensions like `telexCapabilities` and `scdlCapabilities`
  673. if (this.capabilities.extension[ext].XAddr) {
  674. this.uri[ext] = url.parse(this.capabilities.extension[ext].XAddr);
  675. }
  676. }.bind(this));
  677. }
  678. // HACK for a Profile G NVR that has 'replay' but did not have 'recording' in GetCapabilities
  679. if ((this.uri['replay']) && !this.uri['recording']) {
  680. var tempRecorderXaddr = this.uri['replay'].href.replace('replay', 'recording');
  681. console.warn("WARNING: Adding " + tempRecorderXaddr + " for bad Profile G device");
  682. this.uri['recording'] = url.parse(tempRecorderXaddr);
  683. }
  684. }
  685. if (callback) {
  686. callback.call(this, err, this.capabilities, xml);
  687. }
  688. }.bind(this));
  689. };
  690. /**
  691. * Returns the capabilities of the device service
  692. * @param [callback]
  693. */
  694. Cam.prototype.getServiceCapabilities = function(callback) {
  695. this._request({
  696. body: this._envelopeHeader() +
  697. '<GetServiceCapabilities xmlns="http://www.onvif.org/ver10/device/wsdl" />' +
  698. this._envelopeFooter()
  699. }, function(err, data, xml) {
  700. if (!err) {
  701. data = linerase(data);
  702. this.serviceCapabilities = {
  703. network: data.getServiceCapabilitiesResponse.capabilities.network.$,
  704. security: data.getServiceCapabilitiesResponse.capabilities.security.$,
  705. system: data.getServiceCapabilitiesResponse.capabilities.system.$
  706. };
  707. if (data.getServiceCapabilitiesResponse.capabilities.misc) {
  708. this.serviceCapabilities.auxiliaryCommands = data.getServiceCapabilitiesResponse.capabilities.misc.$.AuxiliaryCommands.split(' ');
  709. }
  710. }
  711. if (callback) {
  712. callback.call(this, err, this.serviceCapabilities, xml);
  713. }
  714. }.bind(this));
  715. };
  716. /**
  717. * Active source
  718. * @typedef {object} Cam~ActiveSource
  719. * @property {string} sourceToken video source token
  720. * @property {string} profileToken profile token
  721. * @property {string} videoSourceConfigurationToken video source configuration token
  722. * @property {object} [ptz] PTZ-object
  723. * @property {string} ptz.name PTZ configuration name
  724. * @property {string} ptz.token PTZ token
  725. */
  726. /**
  727. * Get active sources
  728. * @private
  729. */
  730. Cam.prototype.getActiveSources = function() {
  731. //NVT is a camera with one video source
  732. if (this.videoSources.$) {
  733. this.videoSources = [this.videoSources];
  734. }
  735. //The following code block supports a camera with a single video source
  736. //as well as encoders with multiple sources. By default, the first source is set to the activeSource.
  737. /**
  738. * Default profiles for the device
  739. * @name Cam#defaultProfiles
  740. * @type {Array.<Cam~Profile>}
  741. */
  742. this.defaultProfiles = [];
  743. /**
  744. * Active video sources
  745. * @name Cam#activeSources
  746. * @type {Array.<Cam~ActiveSource>}
  747. */
  748. this.activeSources = [];
  749. this.videoSources.forEach(function(videoSource, idx) {
  750. // let's choose first appropriate profile for our video source and make it default
  751. var videoSrcToken = videoSource.$.token,
  752. appropriateProfiles = this.profiles.filter(function(profile) {
  753. if (profile.videoSourceConfiguration && profile.videoEncoderConfiguration) {
  754. if (profile.videoSourceConfiguration.sourceToken === videoSrcToken || profile.videoSourceConfiguration.sourceToken === profile.videoSourceConfiguration.name) {
  755. return true;
  756. }
  757. }
  758. });
  759. if (appropriateProfiles.length === 0) {
  760. if (idx === 0) {
  761. throw new Error('Unrecognized configuration');
  762. } else {
  763. return;
  764. }
  765. }
  766. if (idx === 0) {
  767. /**
  768. * Default selected profile for the device
  769. * @name Cam#defaultProfile
  770. * @type {Cam~Profile}
  771. */
  772. this.defaultProfile = appropriateProfiles[0];
  773. }
  774. this.defaultProfiles[idx] = appropriateProfiles[0];
  775. this.activeSources[idx] = {
  776. sourceToken: videoSource.$.token,
  777. profileToken: this.defaultProfiles[idx].$.token,
  778. videoSourceConfigurationToken: this.defaultProfiles[idx].videoSourceConfiguration.$.token
  779. };
  780. if (this.defaultProfiles[idx].videoEncoderConfiguration) {
  781. var configuration = this.defaultProfiles[idx].videoEncoderConfiguration;
  782. this.activeSources[idx].encoding = configuration.encoding;
  783. this.activeSources[idx].width = configuration.resolution ? configuration.resolution.width : '';
  784. this.activeSources[idx].height = configuration.resolution ? configuration.resolution.height : '';
  785. this.activeSources[idx].fps = configuration.rateControl ? configuration.rateControl.frameRateLimit : '';
  786. this.activeSources[idx].bitrate = configuration.rateControl ? configuration.rateControl.bitrateLimit : '';
  787. }
  788. if (idx === 0) {
  789. /**
  790. * Current active video source
  791. * @name Cam#activeSource
  792. * @type {Cam~ActiveSource}
  793. */
  794. this.activeSource = this.activeSources[idx];
  795. }
  796. if (this.defaultProfiles[idx].PTZConfiguration) {
  797. this.activeSources[idx].ptz = {
  798. name: this.defaultProfiles[idx].PTZConfiguration.name,
  799. token: this.defaultProfiles[idx].PTZConfiguration.$.token
  800. };
  801. /* TODO Think about it
  802. if (idx === 0) {
  803. this.defaultProfile.PTZConfiguration = this.activeSources[idx].PTZConfiguration;
  804. }
  805. */
  806. }
  807. }.bind(this));
  808. // If we haven't got any active source, send a warning
  809. if (this.activeSources.length === 0) {
  810. /**
  811. * Indicates any warning.
  812. * @event Cam#warning
  813. * @type {string}
  814. */
  815. this.emit('warning', 'There are no active sources at this device');
  816. }
  817. };
  818. /**
  819. * @typedef {object} Cam~Service
  820. * @property {string} namespace Namespace uri
  821. * @property {string} XAddr Uri for requests
  822. * @property {number} version.minor Minor version
  823. * @property {number} version.major Major version
  824. */
  825. /**
  826. * @callback Cam~GetServicesCallback
  827. * @property {?Error} error
  828. * @property {Array.<Cam~Service>} services
  829. * @property {string} xml Raw SOAP response
  830. */
  831. /**
  832. * Returns information about services on the device.
  833. * @param {boolean} [includeCapability=true] Indicates if the service capabilities (untyped) should be included in the response.
  834. * @param {Cam~GetServicesCallback} [callback]
  835. */
  836. Cam.prototype.getServices = function(includeCapability, callback) {
  837. if (typeof includeCapability == 'function') {
  838. callback = includeCapability;
  839. includeCapability = true;
  840. }
  841. this._request({
  842. body: this._envelopeHeader() +
  843. '<GetServices xmlns="http://www.onvif.org/ver10/device/wsdl">' +
  844. '<IncludeCapability>' + includeCapability + '</IncludeCapability>' +
  845. '</GetServices>' +
  846. this._envelopeFooter(),
  847. }, function(err, data, xml) {
  848. if (!err) {
  849. /**
  850. * Device services
  851. * @name Cam#services
  852. * @type {Array<Cam~Service>}
  853. */
  854. this.services = linerase(data).getServicesResponse.service;
  855. // ONVIF Profile T introduced Media2 (ver20) so cameras from around 2020/2021 will have
  856. // two media entries in the ServicesResponse, one for Media (ver10/media) and one for Media2 (ver20/media)
  857. // This is so that existing VMS software can still access the video via the orignal ONVIF Media API
  858. // fill Cam#uri property
  859. if (!this.uri) {
  860. /**
  861. * Device service URIs
  862. * @name Cam#uri
  863. * @property {url} [PTZ]
  864. * @property {url} [media]
  865. * @property {url} [media2]
  866. * @property {url} [imaging]
  867. * @property {url} [events]
  868. * @property {url} [device]
  869. */
  870. this.uri = {};
  871. }
  872. this.media2Support = false;
  873. this.services.forEach((service) => {
  874. // Look for services with namespaces and XAddr values
  875. if (Object.prototype.hasOwnProperty.call(service, 'namespace') && Object.prototype.hasOwnProperty.call(service, 'XAddr')) {
  876. // Only parse ONVIF namespaces. Axis cameras return Axis namespaces in GetServices
  877. let parsedNamespace = url.parse(service.namespace);
  878. if (parsedNamespace.hostname === 'www.onvif.org') {
  879. let namespaceSplitted = parsedNamespace.path.substring(1).split('/'); // remove leading Slash, then split
  880. // special case for Media and Media2 where cameras supporting Profile S and Profile T (2020/2021 models) have two media services
  881. if (namespaceSplitted[1] === 'media' && namespaceSplitted[0] === 'ver20') {
  882. this.media2Support = true;
  883. namespaceSplitted[1] = 'media2';
  884. }
  885. this.uri[namespaceSplitted[1]] = this._parseUrl(service.XAddr);
  886. }
  887. }
  888. });
  889. }
  890. if (callback) {
  891. callback.call(this, err, this.services, xml);
  892. }
  893. }.bind(this));
  894. };
  895. /**
  896. * @typedef {object} Cam~DeviceInformation
  897. * @property {string} manufacturer The manufactor of the device
  898. * @property {string} model The device model
  899. * @property {string} firmwareVersion The firmware version in the device
  900. * @property {string} serialNumber The serial number of the device
  901. * @property {string} hardwareId The hardware ID of the device
  902. */
  903. /**
  904. * @callback Cam~GetDeviceInformationCallback
  905. * @property {?Error} error
  906. * @property {Cam~DeviceInformation} deviceInformation Device information
  907. * @property {string} xml Raw SOAP response
  908. */
  909. /**
  910. * Receive device information
  911. * @param {Cam~GetDeviceInformationCallback} [callback]
  912. */
  913. Cam.prototype.getDeviceInformation = function(callback) {
  914. this._request({
  915. body: this._envelopeHeader() +
  916. '<GetDeviceInformation xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  917. this._envelopeFooter()
  918. }, function(err, data, xml) {
  919. if (!err) {
  920. this.deviceInformation = linerase(data).getDeviceInformationResponse;
  921. }
  922. if (callback) {
  923. callback.call(this, err, this.deviceInformation, xml);
  924. }
  925. }.bind(this));
  926. };
  927. /**
  928. * @typedef {object} Cam~HostnameInformation
  929. * @property {boolean} fromDHCP Indicates whether the hostname is obtained from DHCP or not
  930. * @property {string} [name] Indicates the hostname
  931. */
  932. /**
  933. * @callback Cam~GetHostnameCallback
  934. * @property {?Error} error
  935. * @property {Cam~HostnameInformation} hostnameInformation Hostname information
  936. * @property {string} xml Raw SOAP response
  937. */
  938. /**
  939. * Receive hostname information
  940. * @param {Cam~GetHostnameCallback} [callback]
  941. */
  942. Cam.prototype.getHostname = function(callback) {
  943. this._request({
  944. body: this._envelopeHeader() +
  945. '<GetHostname xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  946. this._envelopeFooter()
  947. }, function(err, data, xml) {
  948. if (callback) {
  949. callback.call(this, err, err ? null : linerase(data).getHostnameResponse.hostnameInformation, xml);
  950. }
  951. }.bind(this));
  952. };
  953. /**
  954. * @typedef {object} Cam~Scope
  955. * @property {string} scopeDef Indicates if the scope is fixed or configurable
  956. * @property {string} scopeItem Scope item URI
  957. */
  958. /**
  959. * @callback Cam~getScopesCallback
  960. * @property {?Error} error
  961. * @property {Array<Cam~Scope>} scopes Scopes
  962. * @property {string} xml Raw SOAP response
  963. */
  964. /**
  965. * Receive the scope parameters of a device
  966. * @param {Cam~getScopesCallback} callback
  967. */
  968. Cam.prototype.getScopes = function(callback) {
  969. this._request({
  970. body: this._envelopeHeader() +
  971. '<GetScopes xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  972. this._envelopeFooter()
  973. }, function(err, data, xml) {
  974. if (!err) {
  975. /**
  976. * Device scopes
  977. * @type {undefined|Array<Cam~Scope>}
  978. */
  979. this.scopes = linerase(data).getScopesResponse.scopes;
  980. if (this.scopes === undefined) {
  981. this.scopes = [];
  982. } else if (!Array.isArray(this.scopes)) {
  983. this.scopes = [this.scopes];
  984. }
  985. }
  986. if (callback) {
  987. callback.call(this, err, this.scopes, xml);
  988. }
  989. }.bind(this));
  990. };
  991. /**
  992. * Set the scope parameters of a device
  993. * @param {Array<string>} scopes array of scope's uris
  994. * @param {Cam~getScopesCallback} callback
  995. */
  996. Cam.prototype.setScopes = function(scopes, callback) {
  997. this._request({
  998. body: this._envelopeHeader() +
  999. '<SetScopes xmlns="http://www.onvif.org/ver10/device/wsdl">' +
  1000. scopes.map(function(uri) { return '<Scopes>' + uri + '</Scopes>'; }).join('') +
  1001. '</SetScopes>' +
  1002. this._envelopeFooter()
  1003. }, function(err, data, xml) {
  1004. if (err || linerase(data).setScopesResponse !== '') {
  1005. return callback(linerase(data).setScopesResponse !== '' ? new Error('Wrong `SetScopes` response') : err, data, xml);
  1006. }
  1007. // get new scopes from device
  1008. this.getScopes(callback);
  1009. }.bind(this));
  1010. };
  1011. /**
  1012. * /Device/ Reboot the device
  1013. * @param {Cam~MessageCallback} callback
  1014. */
  1015. Cam.prototype.systemReboot = function(callback) {
  1016. this._request({
  1017. service: 'deviceIO',
  1018. body: this._envelopeHeader() +
  1019. '<SystemReboot xmlns="http://www.onvif.org/ver10/device/wsdl"/>' +
  1020. this._envelopeFooter()
  1021. }, function(err, res, xml) {
  1022. if (!err) {
  1023. res = res[0].systemRebootResponse[0].message[0];
  1024. }
  1025. callback.call(this, err, res, xml);
  1026. });
  1027. };
  1028. /**
  1029. * @callback Cam~SetSystemFactoryDefaultCallback
  1030. * @property {?Error} error
  1031. * @property {null}
  1032. * @property {string} xml Raw SOAP response
  1033. */
  1034. /**
  1035. * Reset camera to factory default
  1036. * @param {boolean} [hard=false] Reset network settings
  1037. * @param {Cam~SetSystemFactoryDefaultCallback} callback
  1038. */
  1039. Cam.prototype.setSystemFactoryDefault = function(hard, callback) {
  1040. if (callback === undefined) {
  1041. callback = hard;
  1042. hard = false;
  1043. }
  1044. let body = this._envelopeHeader() +
  1045. '<SetSystemFactoryDefault xmlns="http://www.onvif.org/ver10/device/wsdl">' +
  1046. '<FactoryDefault>' + (hard ? 'Hard' : 'Soft') + '</FactoryDefault>' +
  1047. '</SetSystemFactoryDefault>' +
  1048. this._envelopeFooter();
  1049. this._request({
  1050. service: 'device',
  1051. body: body,
  1052. }, function(err, res, xml) {
  1053. if (callback) {
  1054. callback.call(this, err, null, xml);
  1055. }
  1056. });
  1057. };
  1058. /**
  1059. * Generate arguments for digest auth
  1060. * @return {{passdigest: *, nonce: (*|String), timestamp: string}}
  1061. * @private
  1062. */
  1063. Cam.prototype._passwordDigest = function() {
  1064. let timestamp = (new Date((process.uptime() * 1000) + (this.timeShift || 0))).toISOString();
  1065. let nonce = Buffer.allocUnsafe(16);
  1066. nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 0, 4);
  1067. nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 4, 4);
  1068. nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 8, 4);
  1069. nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 12, 4);
  1070. let cryptoDigest = crypto.createHash('sha1');
  1071. cryptoDigest.update(Buffer.concat([nonce, Buffer.from(timestamp, 'ascii'), Buffer.from(this.password, 'ascii')]));
  1072. let passdigest = cryptoDigest.digest('base64');
  1073. return {
  1074. passdigest: passdigest,
  1075. nonce: nonce.toString('base64'),
  1076. timestamp: timestamp
  1077. };
  1078. };
  1079. /**
  1080. * Envelope header for all SOAP messages
  1081. * @property {boolean} [openHeader=false]
  1082. * @returns {string}
  1083. * @private
  1084. */
  1085. Cam.prototype._envelopeHeader = function(openHeader) {
  1086. let header = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">' +
  1087. '<s:Header>';
  1088. // Only insert Security if there is a username and password
  1089. if (this.useWSSecurity && this.username && this.password) {
  1090. const req = this._passwordDigest();
  1091. header += '<Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">' +
  1092. '<UsernameToken>' +
  1093. '<Username>' + this.username + '</Username>' +
  1094. '<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + req.passdigest + '</Password>' +
  1095. '<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + req.nonce + '</Nonce>' +
  1096. '<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + req.timestamp + '</Created>' +
  1097. '</UsernameToken>' +
  1098. '</Security>';
  1099. }
  1100. if (!(openHeader !== undefined && openHeader)) {
  1101. header += '</s:Header>' +
  1102. '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">';
  1103. }
  1104. return header;
  1105. };
  1106. /**
  1107. * Envelope footer for all SOAP messages
  1108. * @returns {string}
  1109. * @private
  1110. */
  1111. Cam.prototype._envelopeFooter = function() {
  1112. return '</s:Body>' +
  1113. '</s:Envelope>';
  1114. };
  1115. /**
  1116. * Parse url with an eye on `preserveAddress` property
  1117. * @param {string} address
  1118. * @returns {URL}
  1119. * @private
  1120. */
  1121. Cam.prototype._parseUrl = function(address) {
  1122. const parsedAddress = url.parse(address);
  1123. // If host for service and default host differs, also if preserve address property set
  1124. // we substitute host, hostname and port from settings then rebuild the href using .format
  1125. if (this.preserveAddress && (this.hostname !== parsedAddress.hostname || this.port !== parsedAddress.port)) {
  1126. parsedAddress.hostname = this.hostname;
  1127. parsedAddress.host = this.hostname + ':' + this.port;
  1128. parsedAddress.port = this.port;
  1129. parsedAddress.href = url.format(parsedAddress);
  1130. }
  1131. return parsedAddress;
  1132. };
  1133. module.exports = {
  1134. Cam: Cam
  1135. };
  1136. // extending Camera prototype
  1137. require('./device')(Cam);
  1138. require('./events')(Cam);
  1139. require('./media')(Cam);
  1140. require('./ptz')(Cam);
  1141. require('./imaging')(Cam);
  1142. require('./recording')(Cam);
  1143. require('./replay')(Cam);