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