camera-probe
Version:
Realtime scanning and discovery of networked cameras.
254 lines (237 loc) • 12.5 kB
JavaScript
import { map, shareReplay, mapTo, takeUntil, scan, distinctUntilChanged, share } from 'rxjs/operators';
import { maybe } from 'typescript-monads';
import { createSocket } from 'dgram';
import { Observable, fromEvent, Subject, timer } from 'rxjs';
import { DOMParser } from '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 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 maybe(elm.getElementsByTagNameNS(ns, node).item(0)); }; }; };
var maybeRootProbeElement = 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 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 = 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(map(function (a) { return Object.keys(a).map(function (b) { return a[b]; }); })); };
var flattenDocumentStrings = function (source) { return source.pipe(map(function (a) { return a.map(flattenXml); })); };
var timestamp = function (source) { return source.pipe(map(function (a) { return ({ msg: a.toString(), ts: Date.now() }); })); };
var distinctUntilObjectChanged = function (source) { return source.pipe(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(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(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 Observable.create(function (obs) {
var cfg = __assign(__assign({}, DEFAULT_PROBE_CONFIG), (config || {}));
var socket = createSocket({ type: 'udp4' });
var socketMessages$ = fromEvent(socket, 'message').pipe(map(function (a) { return a[0]; }), shareReplay(1));
var internalLimit = new Subject();
socket.on('err', function (err) { return obs.error(err); });
socket.on('close', function () { return obs.complete(); });
timer(0, cfg.PROBE_REQUEST_SAMPLE_RATE_MS).pipe(mapTo(flattenBuffersWithInfo(cfg.PORTS)(cfg.MULTICAST_ADDRESS)(messages.map(mapStringToBuffer))), 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, 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 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(map(function (b) {
return b.map(function (raw) {
return {
raw: raw,
doc: cfg.PARSER(raw)
};
});
}));
};
var onvifProbe = function (config) {
return wsProbe(config)
.pipe(map(function (res) { return res.map(function (a) {
return __assign(__assign({}, a), { device: xmlToOnvifDevice(a.doc)() });
}); }));
};
var onvifProbe$ = function () { return onvifProbe().pipe(share()); };
var onvifDevices$ = function () { return onvifProbe$().pipe(map(function (a) { return a.map(function (b) { return b.device; }); })); };
var onvifResponses$ = function () { return onvifProbe$().pipe(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'
// }
// ]
// }
export { cli, onvifDevices$, onvifProbe$, onvifResponses$ };