diff --git a/README.md b/README.md
index 4a13955..68ea498 100644
--- a/README.md
+++ b/README.md
@@ -24,12 +24,14 @@ configure a camera
Options:
--help Show help [boolean]
--version Show version number [boolean]
- --debug [boolean] [default: false]
+ --debug Log debug messages [boolean] [default: false]
--ansi [boolean] [default: false]
- --discovery_ip [default: "192.168.1.255"]
+ --discovery_ip Camera discovery IP address
+ [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
- --ssid [string] [required]
- --password [string] [required]
+ --ssid Wifi network for the camera to connect to
+ [string] [required]
+ --password Wifi network password [string] [required]
```
## Running
@@ -44,10 +46,12 @@ start http server
Options:
--help Show help [boolean]
--version Show version number [boolean]
- --debug [boolean] [default: false]
+ --debug Log debug messages [boolean] [default: false]
--ansi [boolean] [default: false]
- --audio [boolean] [default: false]
- --discovery_ip [default: "192.168.1.255"]
+ --audio Also stream audio from camera
+ [boolean] [default: true]
+ --discovery_ip Camera discovery IP address
+ [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
--port HTTP Port to listen on [number] [default: 5000]
```
@@ -58,9 +62,11 @@ There's a basic UI which can display multiple cameras:

-The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras.
+The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras. For debugging purposes, you can send the packets to a specific camera by setting `--discovery_ip` to its IP address.
-Clicking on the image will take you to a page that has audio streaming enabled.
+Clicking on the image will take you to a page that has audio streaming. Click the button below the image to mute/unmute the audio.
+
+To customize the camera names, edit the file `cameras.txt` and add the names of your cameras, one per line, in the format `CAMERA_ID=descriptive name` (you must restart the HTTP server for the changes to take effect).
## Protocol
diff --git a/asd.html b/asd.html
index f523ff4..50e8e53 100644
--- a/asd.html
+++ b/asd.html
@@ -1,40 +1,72 @@
-
${id}
+
+
+ ${name}
+
+
${name}
+
diff --git a/cam.ico.gz b/cam.ico.gz
new file mode 100644
index 0000000..733fad7
Binary files /dev/null and b/cam.ico.gz differ
diff --git a/cameras.txt b/cameras.txt
new file mode 100644
index 0000000..55655ac
--- /dev/null
+++ b/cameras.txt
@@ -0,0 +1,10 @@
+# This is a simple name mapping for cameras, in the form
+#
+# CAMERA_ID=descriptive name
+#
+# Blank lines and lines starting with "#" are ignored.
+#
+# Any updates to this file require restarting the HTTP server.
+# This file is meant to be updated manually, it is NOT updated dynamically.
+
+FTYC811847AGFDZ=Office camera
diff --git a/cmd/bin.ts b/cmd/bin.ts
index cdd28b9..ca6a1d2 100644
--- a/cmd/bin.ts
+++ b/cmd/bin.ts
@@ -11,11 +11,11 @@ yargs(hideBin(process.argv))
"start http server",
(yargs) => {
return yargs
- .option("debug", { default: false })
+ .option("debug", { describe: "Log debug messages", default: false })
.option("ansi", { default: false })
- .option("audio", { default: false })
+ .option("audio", { describe: "Also stream audio from camera", default: true })
.boolean(["debug", "ansi", "audio"])
- .option("discovery_ip", { default: "192.168.1.255" })
+ .option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
.number(["port"])
@@ -31,13 +31,14 @@ yargs(hideBin(process.argv))
"configure a camera",
(yargs) => {
return yargs
- .option("debug", { default: false })
+ .option("debug", { describe: "Log debug messages", default: false })
.option("ansi", { default: false })
.boolean(["debug", "ansi"])
- .option("discovery_ip", { default: "192.168.1.255" })
+ .option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
- .demandOption("ssid")
- .demandOption("password")
+ .option("ssid", { describe: "Wifi network for the camera to connect to" })
+ .option("password", { describe: "Wifi network password" })
+ .demandOption(["ssid", "password"])
.string(["ssid", "password"]);
},
(argv) => {
diff --git a/handlers.ts b/handlers.ts
index 44523be..fb97ade 100644
--- a/handlers.ts
+++ b/handlers.ts
@@ -68,12 +68,12 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
if (cmd_id == ControlCommands.DevStatusAck) {
// ParseDevStatus -> offset relevant?
- let charging = u32_swap(dv.add(0x28).readU32()) & 1; // 0x14000101 v 0x14000100
- let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
+ let charging = u32_swap(dv.add(0x28).readU32()) & 1 ? "" : "not "; // 0x14000101 v 0x14000100
+ let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
- console.log(`charging? ${charging}, batlevel? ${power}, wifi dbm: ${dbm}`);
+ console.log(`Camera ${session.devName}: ${charging}charging, battery at ${power/1000}V, Wifi ${dbm} dBm`);
}
if (cmd_id == ControlCommands.WifiSettingsAck) {
@@ -93,7 +93,7 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
dns2: dv.add(0x10c).readString(0x10),
};
const buf = SendListWifi(session);
- console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
+ console.log(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
return [buf];
}
diff --git a/http_server.ts b/http_server.ts
index de4ef72..262e913 100644
--- a/http_server.ts
+++ b/http_server.ts
@@ -1,17 +1,35 @@
import { RemoteInfo } from "dgram";
-import { readFileSync } from "node:fs";
+import { readFileSync, existsSync } from "node:fs";
import http from "node:http";
import { opt } from "./options.js";
import { discoverDevices } from "./discovery.js";
-import { DevSerial } from "./impl.js";
+import { DevSerial, SendDevStatus } from "./impl.js";
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
-let BOUNDARY = "a very good boundary line";
-let responses: Record = {};
-let audioResponses: Record = {};
-let sessions: Record = {};
+const BOUNDARY = "a very good boundary line";
+const responses: Record = {};
+const audioResponses: Record = {};
+const sessions: Record = {};
+// Text file containing the mapping of camera names.
+const nameFile = "cameras.txt";
+
+// Reads the simple mapping of camera names from the text file.
+const cameraNames = Object.assign({},
+ ...(existsSync(nameFile) ? readFileSync(nameFile, "utf8") : "").toString()
+ .replace(/\r\n/g, "\n").split("\n")
+ .filter(l => !l.startsWith("#"))
+ .filter(l => l.trim() != "")
+ .map(l => { let kv = l.split("="); return ({ [kv[0]]: kv[1] }); }));
+
+// Returns the camera name (custom name, if it exists, otherwise its ID).
+const cameraName = (id: string): string => cameraNames[id] || id;
+
+// Page's favicon.
+const favicon = readFileSync("cam.ico.gz");
+
+// The HTTP server.
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
const server = http.createServer((req, res) => {
if (req.url.startsWith("/ui/")) {
@@ -27,8 +45,12 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
res.end("Nothing online");
return;
}
- const ui = readFileSync("asd.html").toString();
- res.end(ui.replace(/\${id}/g, devId));
+ const ui = readFileSync("asd.html").toString()
+ .replace(/\${id}/g, devId)
+ .replace(/\${name}/g, cameraName(devId))
+ .replace(/\${audio}/g, with_audio.toString())
+ .replace(/\${debug}/g, opts.debug.toString());
+ res.end(ui);
return;
}
if (req.url.startsWith("/audio/")) {
@@ -46,22 +68,30 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
}
res.setHeader("Content-Type", `text/event-stream`);
audioResponses[devId].push(res);
+ console.log(`Audio stream requested for camera ${devId}`);
+ return;
+ }
+
+ if (req.url.startsWith("/favicon.ico")) {
+ res.setHeader("Content-Type", "image/x-icon");
+ res.setHeader("Content-Encoding", "gzip");
+ res.end(favicon);
return;
}
if (req.url.startsWith("/camera/")) {
let devId = req.url.split("/")[2];
- console.log("requested for", devId);
+ console.log(`Video stream requested for camera ${devId}`);
let s = sessions[devId];
if (s === undefined) {
res.writeHead(400);
- res.end("invalid ID");
+ res.end(`Camera ${devId} not discovered`);
return;
}
if (!s.connected) {
res.writeHead(400);
- res.end("Nothing online");
+ res.end(`Camera ${devId} offline`);
return;
}
@@ -69,29 +99,43 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
responses[devId].push(res);
res.on("close", () => {
responses[devId] = responses[devId].filter((r) => r !== res);
- console.log("Conn closed, kicked");
+ console.log(`Video stream closed for camera ${devId}`);
});
} else {
- res.write(``);
+ res.write("");
+ res.write("");
+ res.write(``);
+ res.write("All cameras");
+ res.write("");
+ res.write("");
+ res.write("