From 8ac9b98ca4ec0a4fa9d93e81b486caae7b1c9208 Mon Sep 17 00:00:00 2001 From: YeonV Date: Mon, 24 Mar 2025 06:57:12 +0100 Subject: [PATCH 1/2] blade-ify --- asd.html | 414 +++++++++++++++++++++++++++++++++++++------------ cmd/bin.ts | 2 +- discovery.ts | 133 +++++++++++----- http_server.ts | 309 ++++++++++++++++++++++++++++++++++-- logger.ts | 4 +- package.json | 1 + 6 files changed, 700 insertions(+), 163 deletions(-) diff --git a/asd.html b/asd.html index 87006d4..491c9e2 100644 --- a/asd.html +++ b/asd.html @@ -1,103 +1,313 @@ - - - - ${name} - -

${name}


- - - - + + + + + + Camera: ${name} + + + +
+ +

Camera: ${name}

+ +
+
+ Camera ${name} +
+ + + + +
+
+ + diff --git a/cmd/bin.ts b/cmd/bin.ts index e4f03f2..9c23d9e 100644 --- a/cmd/bin.ts +++ b/cmd/bin.ts @@ -68,7 +68,7 @@ yargs(hideBin(process.argv)) if (majorVersion < 16) { logger.error(`Node version ${majorVersion} is not supported, may malfunction`); } - if (argv.discovery_ip !== undefined) { + if (argv.discovery_ip !== undefined && typeof argv.discovery_ip === 'string') { config.discovery_ips = [argv.discovery_ip]; } pair({ ssid: argv.ssid, password: argv.password }); diff --git a/discovery.ts b/discovery.ts index 2ec5a70..8a5d6a9 100644 --- a/discovery.ts +++ b/discovery.ts @@ -5,54 +5,101 @@ import { Commands } from "./datatypes.js"; import { create_LanSearch, parse_PunchPkt } from "./impl.js"; import { logger } from "./logger.js"; import { config } from "./settings.js"; +import { DevSerial } from "./impl.js"; const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => { - const ab = new Uint8Array(msg).buffer; - const dv = new DataView(ab); - const cmd_id = dv.readU16(); - if (cmd_id != Commands.PunchPkt) { - return; - } - if (config.blacklisted_ips.indexOf(rinfo.address) !== -1) { - logger.debug(`Dropping packet of blacklisted IP: ${rinfo.address}`); - return; - } - logger.debug("Received a PunchPkt message"); - ee.emit("discover", rinfo, parse_PunchPkt(dv)); + const ab = new Uint8Array(msg).buffer; + const dv = new DataView(ab); + const cmd_id = dv.readU16(); + if (cmd_id != Commands.PunchPkt) { + return; + } + if (config.blacklisted_ips.indexOf(rinfo.address) !== -1) { + logger.debug(`Dropping packet of blacklisted IP: ${rinfo.address}`); + return; + } + logger.debug("Received a PunchPkt message"); + ee.emit("discover", rinfo, parse_PunchPkt(dv)); }; export const discoverDevices = (discovery_ips: string[]): EventEmitter => { - const sock = createSocket("udp4"); - const SEND_PORT = 32108; - const ee = new EventEmitter(); - - sock.on("error", (err) => { - console.error(`sock error:\n${err.stack}`); - sock.close(); - }); - - sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo)); - - let timers = []; - sock.on("listening", () => { - let ls_buf = create_LanSearch(); - sock.setBroadcast(true); - discovery_ips.forEach((discovery_ip) => { - logger.info(`Searching for devices on ${discovery_ip}`); - - let int = setInterval(() => { - logger.log("trace", `>> LanSearch [${discovery_ip}]`); - sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip); - }, 3000); - timers.push(int); - logger.log("trace", `>> LanSearch [${discovery_ip}]`); - sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip); + const sock = createSocket("udp4"); + const SEND_PORT = 32108; + const ee = new EventEmitter(); + let devicesDiscovered: Record = {}; // Track discovered devices + let discoveryRunning = true; // Flag to control discovery + let discoveryInterval: NodeJS.Timeout | null = null; // Store the interval ID + let discoveryTimeout: NodeJS.Timeout | null = null; // Timeout to stop discovery after 10 seconds + + sock.on("error", (err) => { + console.error(`sock error:\n${err.stack}`); + sock.close(); }); - }); - sock.bind(); + sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo)); - sock.on("close", () => timers.forEach((timer) => clearInterval(timer))); - ee.on("close", () => sock.close()); - return ee; -}; + sock.on("listening", () => { + let ls_buf = create_LanSearch(); + sock.setBroadcast(true); + + const sendLanSearch = () => { + if (!discoveryRunning) { + return; // Stop if discovery is no longer running + } + discovery_ips.forEach((discovery_ip) => { + logger.log("trace", `>> LanSearch [${discovery_ip}]`); + sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip); + }); + }; + + discovery_ips.forEach((discovery_ip) => { + logger.info(`Searching for devices on ${discovery_ip}`); + }); + + discoveryInterval = setInterval(sendLanSearch, 3000); + sendLanSearch(); // Send the first LanSearch immediately + + // Stop discovery after 10 seconds + discoveryTimeout = setTimeout(() => { + discoveryRunning = false; + if (discoveryInterval) { + clearInterval(discoveryInterval); + } + sock.close(); + logger.info("Discovery process stopped after 10 seconds."); + }, 10000); + }); + + sock.bind(); + + sock.on("close", () => { + if (discoveryInterval) { + clearInterval(discoveryInterval); + } + if (discoveryTimeout) { + clearTimeout(discoveryTimeout); + } + }); + + ee.on("close", () => { + discoveryRunning = false; + if (discoveryInterval) { + clearInterval(discoveryInterval); + } + if (discoveryTimeout) { + clearTimeout(discoveryTimeout); + } + sock.close(); + }); + + ee.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => { + if (devicesDiscovered[dev.devId]) { + logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`); + } else { + devicesDiscovered[dev.devId] = true; + logger.info(`Discovered new camera: ${dev.devId} at ${rinfo.address}`); + } + }); + + return ee; +}; \ No newline at end of file diff --git a/http_server.ts b/http_server.ts index eba7cc4..f8cff3b 100644 --- a/http_server.ts +++ b/http_server.ts @@ -34,23 +34,30 @@ const cameraName = (id: string): string => config.cameras[id].alias || id; export const serveHttp = (port: number) => { const server = http.createServer((req, res) => { if (req.url.startsWith("/ui/")) { - let devId = req.url.split("/")[2]; - let s = sessions[devId]; - if (s === undefined) { + const url = new URL(req.url, `http://${req.headers.host}`); + const devId = url.pathname.split("/")[2]; + const session = sessions[devId]; + + if (!session) { res.writeHead(400); - res.end("invalid ID"); + res.end("Invalid ID"); return; } - if (!s.connected) { + + if (!session.connected) { res.writeHead(400); - res.end("Nothing online"); + res.end("Camera is offline"); return; } + + const cameraData = config.cameras[devId]; const ui = html_template .toString() .replace(/\${id}/g, devId) .replace(/\${name}/g, cameraName(devId)) - .replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false"); + .replace(/\${audio}/g, cameraData.audio ? "true" : "false"); + + res.writeHead(200, { "Content-Type": "text/html" }); res.end(ui); return; } @@ -118,17 +125,289 @@ export const serveHttp = (port: number) => { responses[devId] = responses[devId].filter((r) => r !== res); logger.info(`Video stream closed for camera ${devId}`); }); + } else if (req.url.startsWith("/discover")) { + logger.info("Discovery triggered by client."); + const devEv = discoverDevices(config.discovery_ips); + + devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => { + logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`); + }); + + devEv.on("close", () => { + logger.info("Discovery process completed."); + }); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ message: "Discovery started for 10 seconds." })); + return; } else { res.write(""); res.write(""); res.write(``); res.write("All cameras"); + res.write(` + + `); res.write(""); res.write(""); - res.write("

All cameras


"); - Object.keys(sessions).forEach((id) => - res.write(`

${cameraName(id)}


`), - ); + res.write(` +
+

All Cameras

+
+ + + +
+
+
+ `); + + // Dynamically generate camera items + Object.keys(sessions).forEach((id) => { + const session = sessions[id]; + const cameraData = config.cameras[id]; + res.write(` + + Camera ${cameraName(id)} +
+
+
ID: ${id}
+
Name: ${cameraName(id)}
+
Label: ${id}
+
IP: ${session.dst_ip}
+ +
+
${cameraName(id)}
+
+
+ `); + }); + + res.write(` +
+ + `); + res.write(""); res.write(""); res.end(); @@ -164,10 +443,10 @@ export const serveHttp = (port: number) => { const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]); const header = Buffer.from(`\r\n--${BOUNDARY}\r\nContent-Length: ${assembled.length}\r\nContent-Type: image/jpeg\r\n\r\n`); responses[dev.devId].forEach((res) => { - res.write(header); - res.write(assembled); + res.write(header); + res.write(assembled); }); - }); + }); s.eventEmitter.on("disconnect", () => { logger.info(`Camera ${dev.devId} disconnected`); @@ -188,4 +467,4 @@ export const serveHttp = (port: number) => { logger.info(`Starting HTTP server on port ${port}`); server.listen(port); -}; +}; \ No newline at end of file diff --git a/logger.ts b/logger.ts index 56b59bb..228a5b1 100644 --- a/logger.ts +++ b/logger.ts @@ -1,5 +1,5 @@ import { isatty } from "node:tty"; -import { addColors, config, createLogger, format, transports as wtransports } from "winston"; +import winston, { addColors, config, createLogger, format, transports as wtransports } from "winston"; const myFormat = format.printf(({ level, message, timestamp }) => { return `${timestamp} [${level}] ${message}`; @@ -9,7 +9,7 @@ const transports = { console: new wtransports.Console(), }; -export let logger = undefined; +export let logger: winston.Logger = undefined; export const buildLogger = (level: string, colorize: boolean | undefined) => { let use_color = colorize === undefined ? isatty(1) : colorize; diff --git a/package.json b/package.json index 8340e77..d87f1ea 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12 --loader:.gz=binary --loader:.html=text" }, "devDependencies": { + "@types/node": "^22.13.11", "@types/yargs": "^17.0.32", "esbuild": "^0.20.2", "mocha": "^10.2.0", From b335224aeed4e19b6b76c017f481438aad20b31c Mon Sep 17 00:00:00 2001 From: YeonV Date: Mon, 24 Mar 2025 07:07:42 +0100 Subject: [PATCH 2/2] regenerate package-lock.json --- package-lock.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ffa07c..d84099c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "yargs": "^17.7.2" }, "devDependencies": { + "@types/node": "^22.13.11", "@types/yargs": "^17.0.32", "esbuild": "^0.20.2", "mocha": "^10.2.0", @@ -468,13 +469,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", - "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", + "version": "22.13.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz", + "integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/triple-beam": { @@ -1628,11 +1629,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2",