UNPKG

camera-probe

Version:

Realtime scanning and discovery of networked cameras.

261 lines (242 loc) 12.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var operators = require('rxjs/operators'); var typescriptMonads = require('typescript-monads'); var dgram = require('dgram'); var rxjs = require('rxjs'); var xmldom = require('xmldom'); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArrays() { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; } var SCHEMAS = { addressing: 'http://schemas.xmlsoap.org/ws/2004/08/addressing', discovery: 'http://schemas.xmlsoap.org/ws/2005/04/discovery' }; var maybeIpAddress = function (str) { return typescriptMonads.maybe(str.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/)).map(function (a) { return a[0]; }); }; var xmlToOnvifDevice = function (doc) { return function (notfoundStr) { if (notfoundStr === void 0) { notfoundStr = 'unknown'; } var simpleParse = function (elm) { return function (ns) { return function (node) { return typescriptMonads.maybe(elm.getElementsByTagNameNS(ns, node).item(0)); }; }; }; var maybeRootProbeElement = typescriptMonads.maybe(doc.getElementsByTagNameNS(SCHEMAS.discovery, 'ProbeMatch').item(0)); return maybeRootProbeElement.map(function (rootElement) { var parseProbeElements = simpleParse(rootElement); var parseProbeDiscoveryElements = parseProbeElements(SCHEMAS.discovery); var parseProbeAddressingElements = parseProbeElements(SCHEMAS.addressing); var scopeParser = function (scopes) { return function (pattern) { return typescriptMonads.maybe(scopes.find(function (a) { return a.toLowerCase().includes(("onvif://www.onvif.org/" + pattern).toLocaleLowerCase()); })).flatMapAuto(function (a) { return a.split('/').pop(); }); }; }; var scopes = parseProbeDiscoveryElements('Scopes').flatMapAuto(function (a) { return a.textContent; }).map(function (a) { return a.split(' '); }).valueOr([]); var xaddrs = parseProbeDiscoveryElements('XAddrs').flatMapAuto(function (a) { return a.textContent; }).map(function (a) { return a.split(' '); }).valueOr([]); var metadataVersion = parseProbeDiscoveryElements('MetadataVersion').flatMapAuto(function (a) { return a.textContent; }).valueOr(notfoundStr); var scopeParse = scopeParser(scopes); var valueFromScope = function (str) { return scopeParse(str).valueOr(notfoundStr); }; var urn = parseProbeAddressingElements('Address') .flatMapAuto(function (a) { return a.textContent; }) .map(function (a) { return a.split(':').pop() || ''; }) .valueOr(notfoundStr); var profiles = scopes .filter(function (a) { return a.includes("onvif://www.onvif.org/Profile"); }) .map(function (b) { return b.split('/').pop(); }) .filter(Boolean); var deviceServiceUri = typescriptMonads.maybe(xaddrs .find(function (a) { return a.includes("onvif/device_service"); })) .valueOr('0.0.0.0'); var ip = maybeIpAddress(deviceServiceUri).valueOr(deviceServiceUri); return { name: valueFromScope('name'), hardware: scopeParse('hardware').match({ none: function () { return valueFromScope('model'); }, some: function (val) { return val; } }), location: valueFromScope('location'), deviceServiceUri: deviceServiceUri, ip: ip, metadataVersion: metadataVersion, urn: urn, scopes: scopes, profiles: profiles, xaddrs: xaddrs }; }).valueOr({ name: notfoundStr, hardware: notfoundStr, location: notfoundStr, deviceServiceUri: notfoundStr, ip: notfoundStr, metadataVersion: notfoundStr, urn: notfoundStr, scopes: [], profiles: [], xaddrs: [] }); }; }; var generateWsDiscoveryProbePayload = function (uuid) { return function (type) { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Envelope xmlns=\"http://www.w3.org/2003/05/soap-envelope\">\n <Header xmlns:a=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">\n <a:Action mustUnderstand=\"1\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>\n <a:MessageID>uuid:" + uuid + "</a:MessageID>\n <a:ReplyTo>\n <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>\n </a:ReplyTo>\n <a:To mustUnderstand=\"1\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>\n </Header>\n <Body>\n <Probe xmlns=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\">\n <Types xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">\n dp0:" + type + "\n </Types>\n </Probe>\n </Body>\n</Envelope>"; }; }; var generateGuid = function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; var DEFAULT_PROBE_CONFIG = { PORTS: [], SOCKET_PROTOCOL: 'udp4', MULTICAST_ADDRESS: '239.255.255.250', PROBE_REQUEST_SAMPLE_RATE_MS: 3000, PROBE_RESPONSE_FALLOUT_MS: 4000, PROBE_RESPONSE_TIMEOUT_MS: 6000, RESULT_DEDUPE_FN: function (msg) { return msg.reduce(function (acc, curr) { return __assign(__assign({}, acc), { curr: curr }); }, {}); } }; var mapStringToBuffer = function (str) { return Buffer.from(str, 'utf8'); }; var flattenXml = function (str) { return str.replace(/>\s*/g, '>').replace(/\s*</g, '<'); }; var toArrayOfValues = function (source) { return source.pipe(operators.map(function (a) { return Object.keys(a).map(function (b) { return a[b]; }); })); }; var flattenDocumentStrings = function (source) { return source.pipe(operators.map(function (a) { return a.map(flattenXml); })); }; var timestamp = function (source) { return source.pipe(operators.map(function (a) { return ({ msg: a.toString(), ts: Date.now() }); })); }; var distinctUntilObjectChanged = function (source) { return source.pipe(operators.distinctUntilChanged(function (a, b) { var keys1 = Object.keys(a); var keys2 = Object.keys(b); return keys1.length === keys2.length && keys1.reduce(function (acc, curr) { return acc === false ? false : keys2.includes(curr); }, true); })); }; var accumulateFreshMessages = function (falloutTime) { return function (source) { return source.pipe(operators.scan(function (acc, val) { return __spreadArrays(acc, [val]).filter(function (a) { return a.ts > Date.now() - falloutTime; }); }, [])); }; }; var mapStrToDictionary = function (mapFn) { return function (source) { return source.pipe(operators.map(mapFn)); }; }; var flattenBuffersWithInfo = function (ports) { return function (address) { return function (buffers) { return ports.reduce(function (acc, port) { return __spreadArrays(acc, buffers.map(function (buffer) { return ({ buffer: buffer, port: port, address: address }); })); }, []); }; }; }; var probe = function (config) { return function (messages) { return rxjs.Observable.create(function (obs) { var cfg = __assign(__assign({}, DEFAULT_PROBE_CONFIG), (config || {})); var socket = dgram.createSocket({ type: 'udp4' }); var socketMessages$ = rxjs.fromEvent(socket, 'message').pipe(operators.map(function (a) { return a[0]; }), operators.shareReplay(1)); var internalLimit = new rxjs.Subject(); socket.on('err', function (err) { return obs.error(err); }); socket.on('close', function () { return obs.complete(); }); rxjs.timer(0, cfg.PROBE_REQUEST_SAMPLE_RATE_MS).pipe(operators.mapTo(flattenBuffersWithInfo(cfg.PORTS)(cfg.MULTICAST_ADDRESS)(messages.map(mapStringToBuffer))), operators.takeUntil(internalLimit)) .subscribe(function (bfrPorts) { bfrPorts.forEach(function (mdl) { return socket.send(mdl.buffer, 0, mdl.buffer.length, mdl.port, mdl.address); }); }); socketMessages$.pipe(timestamp, accumulateFreshMessages(cfg.PROBE_RESPONSE_FALLOUT_MS), mapStrToDictionary(cfg.RESULT_DEDUPE_FN), distinctUntilObjectChanged, toArrayOfValues, flattenDocumentStrings, operators.takeUntil(internalLimit)).subscribe(function (msg) { return obs.next(msg); }, function (err) { return obs.next(err); }); return function unsubscribe() { internalLimit.next(); internalLimit.complete(); socket.close(); }; }); }; }; var dom = new xmldom.DOMParser(); var XML_PARSER_FN = function (str) { return dom.parseFromString(str, 'application/xml'); }; var wsDiscoveryParseToDict = function (fn) { return function (msg) { return msg.reduce(function (acc, curr) { var _a; return __assign(__assign({}, acc), (_a = {}, _a[xmlToOnvifDevice(fn(curr.msg))().urn] = curr.msg, _a)); }, {}); }; }; var DEFAULT_WS_PROBE_CONFIG = { PORTS: [3702], DEVICES: ['NetworkVideoTransmitter', 'Device', 'NetworkVideoDisplay'], PARSER: XML_PARSER_FN, RESULT_DEDUPE_FN: wsDiscoveryParseToDict(XML_PARSER_FN), }; var mapDeviceStrToPayload = function (str) { return generateWsDiscoveryProbePayload(generateGuid())(str); }; var mapDevicesToPayloads = function (devices) { return devices.map(mapDeviceStrToPayload); }; var wsProbe = function (config) { var cfg = __assign(__assign({}, DEFAULT_WS_PROBE_CONFIG), config); return probe(cfg)(mapDevicesToPayloads(cfg.DEVICES)) .pipe(operators.map(function (b) { return b.map(function (raw) { return { raw: raw, doc: cfg.PARSER(raw) }; }); })); }; var onvifProbe = function (config) { return wsProbe(config) .pipe(operators.map(function (res) { return res.map(function (a) { return __assign(__assign({}, a), { device: xmlToOnvifDevice(a.doc)() }); }); })); }; var onvifProbe$ = function () { return onvifProbe().pipe(operators.share()); }; var onvifDevices$ = function () { return onvifProbe$().pipe(operators.map(function (a) { return a.map(function (b) { return b.device; }); })); }; var onvifResponses$ = function () { return onvifProbe$().pipe(operators.map(function (a) { return a.map(function (b) { return b.raw; }); })); }; var cli = function () { return onvifDevices$() .subscribe(function (res) { console.clear(); console.log('Camera Probe'); console.table(res.map(function (device) { return { Name: device.name, Model: device.hardware, IP: device.ip, URN: device.urn, Endpoint: device.deviceServiceUri }; })); }); }; // interface IReponse { // devices: [ // { // raw: 'string', // document: 'Maybe<Document>', // device, // scanType: 'discovery' | 'ipscan', // protocol: 'onvif' | 'upnp' | 'mdns' // } // ] // } exports.cli = cli; exports.onvifDevices$ = onvifDevices$; exports.onvifProbe$ = onvifProbe$; exports.onvifResponses$ = onvifResponses$;