From 690c1b41465866c874af4e2fcfa098776d7e541d Mon Sep 17 00:00:00 2001
From: Adriano Cunha <35786489+adrcunha@users.noreply.github.com>
Date: Tue, 30 Apr 2024 07:18:10 -0700
Subject: [PATCH] More features in the HTTP server (#16)
* Added more features to HTTP server
* Added a title to the multi-camera view page.
* Support naming cameras through a simple mapping using a text file.
* Updated some logging messages to be more verbose (and match those in pair.ts).
* Confirmed the charging status and battery level values reported back by DevStatusAck.
* Log camera status (charging, battery, Wifi strength) when connected.
* More updates to HTTP server
* Deal with Web Audio autoplay policy by starting camera on mute and adding a button to toggle the audio stream.
* Add a favicon so the server tan can be easily spotted.
The favicon file is a 48x48 32bpp ICO file, gzipped.
* Update README.md
* Describe flags and enable camera audio by default.
* Update audio button based on state change
* Update description of discovery_ip in bin.ts
* Update session start message in http_server.ts
* Update README.md
Update flags and note about --discovery_ip.
---
README.md | 24 ++++++-----
asd.html | 106 ++++++++++++++++++++++++++++++++++++-------------
cam.ico.gz | Bin 0 -> 634 bytes
cameras.txt | 10 +++++
cmd/bin.ts | 15 +++----
handlers.ts | 8 ++--
http_server.ts | 82 +++++++++++++++++++++++++++++---------
session.ts | 4 +-
8 files changed, 181 insertions(+), 68 deletions(-)
create mode 100644 cam.ico.gz
create mode 100644 cameras.txt
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 0000000000000000000000000000000000000000..733fad77813a7054d47dabde0b6a61250f03baac
GIT binary patch
literal 634
zcmV-=0)_n_iwFqfVJ&6=17l%rE@@+L0PUGiOB+EL#@~&>lpr+_u}5>MNDtx%DCF+N
zFW|u*gdXz&gb3|ND1~0zOWR9}_fqf+NDw@EP(aGzBUB7pKqbyaU5_c3qR%uq*R~
zCoemhKfjrs4ZH6WMfj|&5WZXV{xQ)!5iP+eq7BmJK|w+HUR&@t_!?H8&3&H3uXrG(
z)JTq^Xc5g1;otB(;huv}a$rvK1vnnvl^uHRSXy`bD4B5*7N2E-<&-rj-Oa_dk+6{a2=!GlGG^_KfKKU
zMTq+jo9_?kNmmhPoSNI{lDY$`t^{%a^??3JcquV-@Y7=%r5=<&L5`cZ)Gnv$JsNnHSwLl
zEqnf)Rp$2>tg^p9GAh&iJMQx<1qB6_R_aE#$0MSe8sG+lC{jl^k<_;ILHwf{@J+@n
zVn^aeLr3aJ9OyVDu_kdrVs`@Z;jE4`H9amz=6Eorxj2%E9sN7cgWK`EY72UvX5MBU
U%(|F$%DS%q0OA%?C%z>B0J|elC;$Ke
literal 0
HcmV?d00001
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("