diff --git a/tools/onvifGetStreamUri.js b/tools/onvifGetStreamUri.js
new file mode 100644
index 00000000..cebcce0e
--- /dev/null
+++ b/tools/onvifGetStreamUri.js
@@ -0,0 +1,164 @@
+const fetch = require('node-fetch');
+const dgram = require('dgram');
+const parseString = require('xml2js').parseString;
+const base64 = require('base-64');
+
+const USERNAME = process.argv[2]
+const PASSWORD = process.argv[3]
+
+if(!USERNAME || !PASSWORD){
+ console.log(`Missing Username and/or Password!`)
+ console.log(`Example : node ./onvifGetStreamUri.js "USERNAME" "PASSWORD"`)
+ console.log(`Put the quotations seen in the example!`)
+ return
+}
+
+function cleanKeys(obj) {
+ const cleanKey = (key) => key.includes(':') ? key.split(':').pop() : key;
+
+ if (Array.isArray(obj)) {
+ return obj.map(cleanKeys);
+ } else if (obj !== null && typeof obj === 'object') {
+ return Object.keys(obj).reduce((newObj, key) => {
+ newObj[cleanKey(key)] = cleanKeys(obj[key]);
+ return newObj;
+ }, {});
+ }
+ return obj;
+}
+async function getStreamUri(deviceXAddr, username, password) {
+ let envelope = `
+
+
+
+
+
+ `;
+
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/soap+xml',
+ 'Authorization': 'Basic ' + base64.encode(username + ':' + password),
+ },
+ body: envelope,
+ };
+
+ let response = await fetch(deviceXAddr, options);
+ let body = await response.text();
+
+ parseString(body, async (err, result) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ const soapBody = cleanKeys(result).Envelope.Body[0]
+ if (soapBody.Fault) {
+ console.log(deviceXAddr,'Not Authorized');
+ return;
+ }
+ try{
+ var profiles = soapBody.GetProfilesResponse[0].Profiles;
+ }catch(err){
+ console.log(err.stack)
+ console.error(deviceXAddr,`getStreamUri soapBody on ERROR`,JSON.stringify(soapBody,null,3))
+ return
+ }
+ if (!profiles || !profiles.length) {
+ console.log(deviceXAddr,'No profiles found');
+ return;
+ }
+
+ const firstProfileToken = profiles[0]['$']['token'];
+
+ envelope = `
+
+
+
+
+ RTP-Unicast
+
+ RTSP
+
+
+ ${firstProfileToken}
+
+
+
+ `;
+
+ options.body = envelope;
+ response = await fetch(deviceXAddr, options);
+ body = await response.text();
+
+ parseString(body, (err, result) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+
+ const uri = result['SOAP-ENV:Envelope']['SOAP-ENV:Body'][0]['trt:GetStreamUriResponse'][0]['trt:MediaUri'][0]['tt:Uri'][0];
+ console.log('Stream URI:', uri);
+ });
+ });
+}
+
+const DISCOVER_MSG = Buffer.from(`
+
+
+ http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
+ uuid:1000-3000-5000-70000000000000
+
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+
+ urn:schemas-xmlsoap-org:ws:2005:04:discovery
+
+
+
+ dn:NetworkVideoTransmitter
+
+
+
+`);
+
+const socket = dgram.createSocket('udp4');
+socket.bind(() => {
+ socket.setBroadcast(true);
+ socket.setMulticastTTL(128);
+ socket.addMembership('239.255.255.250');
+ socket.send(DISCOVER_MSG, 0, DISCOVER_MSG.length, 3702, '239.255.255.250');
+});
+
+socket.on('message', function (message, rinfo) {
+ parseString(message, (err, result) => {
+ if (err) {
+ console.error('Failed to parse XML', err);
+ return;
+ }
+ const cleanJson = cleanKeys(result)
+ if (!cleanJson.Envelope || !cleanJson.Envelope.Body) {
+ console.error('Unexpected message format', result);
+ console.error('cleanJson', cleanJson);
+ return;
+ }
+
+ const soapBody = cleanJson.Envelope.Body[0];
+ const probeMatches = soapBody.ProbeMatches;
+ if (!probeMatches) {
+ console.error('No probe matches in message', soapBody);
+ return;
+ }
+
+ const probeMatch = probeMatches[0].ProbeMatch[0];
+ if (!probeMatch) {
+ console.error('No probe match in probe matches', probeMatches);
+ return;
+ }
+ let xAddrs = probeMatch.XAddrs[0];
+ console.log('Found ONVIF device', xAddrs);
+ getStreamUri(xAddrs, USERNAME, PASSWORD)
+ .catch(err => console.error('Failed to get stream URI', err));
+ });
+});