implement settings support
This commit is contained in:
@@ -35,11 +35,54 @@ 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. 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. Click the button below the image to mute/unmute the audio.
|
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).
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
You can provide a config file in `yml` format, check `config.example.yml` for an updated example:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
http_server:
|
||||||
|
port: 5000
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: debug
|
||||||
|
use_color: true
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
FTYC477360FAWUK:
|
||||||
|
alias: "A9"
|
||||||
|
rotate: 1
|
||||||
|
mirror: false
|
||||||
|
fix_packet_loss: yes
|
||||||
|
audio: true
|
||||||
|
BATC609531EXLVS:
|
||||||
|
alias: "X5"
|
||||||
|
|
||||||
|
# If you are crossing broadcast domains (VLANs) then
|
||||||
|
# you need to specify all IPs as unicast targets
|
||||||
|
discovery_ips:
|
||||||
|
- 192.168.40.101
|
||||||
|
- 192.168.40.102
|
||||||
|
- 192.168.40.103
|
||||||
|
- 192.168.40.104
|
||||||
|
- 192.168.40.105
|
||||||
|
|
||||||
|
# If you are in the same broadcast domain, then
|
||||||
|
# it's easier to just use the broadcast address of your network
|
||||||
|
# discovery_ips:
|
||||||
|
# - 192.168.1.255
|
||||||
|
|
||||||
|
blacklisted_ips:
|
||||||
|
- 192.168.40.102
|
||||||
|
```
|
||||||
|
|
||||||
|
All keys are optional
|
||||||
|
|
||||||
|
You must restart the HTTP server for changes to the settings file to take effect.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# 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
|
|
||||||
+27
-16
@@ -3,8 +3,8 @@ import { hideBin } from "yargs/helpers";
|
|||||||
import yargs from "yargs/yargs";
|
import yargs from "yargs/yargs";
|
||||||
|
|
||||||
import { serveHttp } from "../http_server.js";
|
import { serveHttp } from "../http_server.js";
|
||||||
import { opt } from "../options.js";
|
|
||||||
import { pair } from "../pair.js";
|
import { pair } from "../pair.js";
|
||||||
|
import { loadConfig, config } from "../settings.js";
|
||||||
|
|
||||||
import { buildLogger, logger } from "../logger.js";
|
import { buildLogger, logger } from "../logger.js";
|
||||||
|
|
||||||
@@ -16,23 +16,38 @@ yargs(hideBin(process.argv))
|
|||||||
"start http server",
|
"start http server",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("audio", { describe: "Also stream audio from camera", default: true })
|
.option("color", { describe: "Use color in logs" })
|
||||||
.option("color", { describe: "Use color in logs", default: undefined })
|
|
||||||
.boolean(["audio", "color"])
|
.boolean(["audio", "color"])
|
||||||
.option("log_level", { describe: "Set log level", default: "info" })
|
.option("config_file", { describe: "Specify config file" })
|
||||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
.option("log_level", { describe: "Set log level" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("discovery_ip", { describe: "Camera discovery IP address" })
|
||||||
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
|
.option("port", { describe: "HTTP Port to listen on" })
|
||||||
|
.string(["log_level", "discovery_ip", "config_file"])
|
||||||
.number(["port"])
|
.number(["port"])
|
||||||
.strict();
|
.strict();
|
||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as opt;
|
if (argv.config_file !== undefined) {
|
||||||
buildLogger(argv.log_level, argv.color);
|
loadConfig(argv.config_file);
|
||||||
|
}
|
||||||
|
if (argv.port) {
|
||||||
|
config.http_server.port = argv.port;
|
||||||
|
}
|
||||||
|
if (argv.color !== undefined) {
|
||||||
|
config.logging.use_color = argv.color;
|
||||||
|
}
|
||||||
|
if (argv.log_level !== undefined) {
|
||||||
|
config.logging.level = argv.log_level;
|
||||||
|
}
|
||||||
|
if (argv.discovery_ip !== undefined) {
|
||||||
|
config.discovery_ips = [argv.discovery_ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLogger(config.logging.level, config.logging.use_color);
|
||||||
if (majorVersion < 16) {
|
if (majorVersion < 16) {
|
||||||
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
}
|
}
|
||||||
serveHttp(opts, argv.port, argv.audio || false);
|
serveHttp(config.http_server.port);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.command(
|
.command(
|
||||||
@@ -40,23 +55,19 @@ yargs(hideBin(process.argv))
|
|||||||
"configure a camera",
|
"configure a camera",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("color", { describe: "Use color in logs", default: undefined })
|
|
||||||
.boolean(["color"])
|
|
||||||
.option("log_level", { describe: "Set log level", default: "info" })
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
.option("discovery_ip", { describe: "Camera discovery IP address", 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("ssid", { describe: "Wifi network for the camera to connect to" })
|
.option("ssid", { describe: "Wifi network for the camera to connect to" })
|
||||||
.option("password", { describe: "Wifi network password" })
|
.option("password", { describe: "Wifi network password" })
|
||||||
.demandOption(["ssid", "password"])
|
.demandOption(["ssid", "password"])
|
||||||
.string(["ssid", "password"]);
|
.string(["ssid", "password"]);
|
||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as unknown as opt;
|
buildLogger(argv.log_level, undefined);
|
||||||
buildLogger(argv.log_level, argv.color);
|
|
||||||
if (majorVersion < 16) {
|
if (majorVersion < 16) {
|
||||||
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
}
|
}
|
||||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
pair({ ssid: argv.ssid, password: argv.password });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
http_server:
|
||||||
|
port: 5000
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: debug
|
||||||
|
use_color: true
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
FTYC477360FAWUK:
|
||||||
|
alias: "A9"
|
||||||
|
rotate: 1
|
||||||
|
mirror: false
|
||||||
|
fix_packet_loss: yes
|
||||||
|
audio: true
|
||||||
|
BATC609531EXLVS:
|
||||||
|
alias: "X5"
|
||||||
|
|
||||||
|
# If you are crossing broadcast domains (VLANs) then
|
||||||
|
# you need to specify all IPs as unicast targets
|
||||||
|
discovery_ips:
|
||||||
|
- 192.168.40.101
|
||||||
|
- 192.168.40.102
|
||||||
|
- 192.168.40.103
|
||||||
|
- 192.168.40.104
|
||||||
|
- 192.168.40.105
|
||||||
|
|
||||||
|
# If you are in the same broadcast domain, then
|
||||||
|
# it's easier to just use the broadcast address of your network
|
||||||
|
# discovery_ips:
|
||||||
|
# - 192.168.1.255
|
||||||
|
|
||||||
|
blacklisted_ips:
|
||||||
|
- 192.168.40.102
|
||||||
+11
-4
@@ -4,6 +4,7 @@ import EventEmitter from "node:events";
|
|||||||
import { Commands } from "./datatypes.js";
|
import { Commands } from "./datatypes.js";
|
||||||
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
import { config } from "./settings.js";
|
||||||
|
|
||||||
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
||||||
const ab = new Uint8Array(msg).buffer;
|
const ab = new Uint8Array(msg).buffer;
|
||||||
@@ -12,11 +13,15 @@ const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) =
|
|||||||
if (cmd_id != Commands.PunchPkt) {
|
if (cmd_id != Commands.PunchPkt) {
|
||||||
return;
|
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");
|
logger.debug("Received a PunchPkt message");
|
||||||
ee.emit("discover", rinfo, parse_PunchPkt(dv));
|
ee.emit("discover", rinfo, parse_PunchPkt(dv));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
export const discoverDevices = (discovery_ips: string[]): EventEmitter => {
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
const SEND_PORT = 32108;
|
const SEND_PORT = 32108;
|
||||||
const ee = new EventEmitter();
|
const ee = new EventEmitter();
|
||||||
@@ -30,18 +35,20 @@ export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
|||||||
|
|
||||||
let timers = [];
|
let timers = [];
|
||||||
sock.on("listening", () => {
|
sock.on("listening", () => {
|
||||||
|
let ls_buf = create_LanSearch();
|
||||||
sock.setBroadcast(true);
|
sock.setBroadcast(true);
|
||||||
|
discovery_ips.forEach((discovery_ip) => {
|
||||||
logger.info(`Searching for devices on ${discovery_ip}`);
|
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||||
|
|
||||||
let ls_buf = create_LanSearch();
|
|
||||||
let int = setInterval(() => {
|
let int = setInterval(() => {
|
||||||
logger.log("trace", `>> LanSearch`);
|
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
timers.push(int);
|
timers.push(int);
|
||||||
logger.log("trace", `>> LanSearch`);
|
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
sock.bind();
|
sock.bind();
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -5,6 +5,7 @@ import { Session } from "./session.js";
|
|||||||
import { u16_swap, u32_swap } from "./utils.js";
|
import { u16_swap, u32_swap } from "./utils.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
import { hexdump } from "./hexdump.js";
|
import { hexdump } from "./hexdump.js";
|
||||||
|
import { config } from "./settings.js";
|
||||||
|
|
||||||
export const notImpl = (session: Session, dv: DataView) => {
|
export const notImpl = (session: Session, dv: DataView) => {
|
||||||
const raw = dv.readU16();
|
const raw = dv.readU16();
|
||||||
@@ -207,7 +208,7 @@ const deal_with_data = (session: Session, dv: DataView) => {
|
|||||||
}
|
}
|
||||||
// this should always be enabled but currently it seems to cause more visual distortion
|
// this should always be enabled but currently it seems to cause more visual distortion
|
||||||
// than just missing some frames
|
// than just missing some frames
|
||||||
if (!session.options.attempt_to_fix_packet_loss) {
|
if (!config.cameras[session.devName].fix_packet_loss) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-33
@@ -1,9 +1,8 @@
|
|||||||
import { RemoteInfo } from "dgram";
|
import { RemoteInfo } from "dgram";
|
||||||
import { readFileSync, existsSync } from "node:fs";
|
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
import { opt } from "./options.js";
|
import { config } from "./settings.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial, SendDevStatus } from "./impl.js";
|
import { DevSerial, SendDevStatus } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
||||||
@@ -19,9 +18,6 @@ const responses: Record<string, http.ServerResponse[]> = {};
|
|||||||
const audioResponses: Record<string, http.ServerResponse[]> = {};
|
const audioResponses: Record<string, http.ServerResponse[]> = {};
|
||||||
const sessions: Record<string, Session> = {};
|
const sessions: Record<string, Session> = {};
|
||||||
|
|
||||||
// Text file containing the mapping of camera names.
|
|
||||||
const nameFile = "cameras.txt";
|
|
||||||
|
|
||||||
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
||||||
const oMap = [1, 8, 3, 6];
|
const oMap = [1, 8, 3, 6];
|
||||||
const oMapMirror = [2, 7, 4, 5];
|
const oMapMirror = [2, 7, 4, 5];
|
||||||
@@ -29,29 +25,13 @@ const orientations = [1, 2, 3, 4, 5, 6, 7, 8].reduce((acc, cur) => {
|
|||||||
return { [cur]: createExifOrientation(cur), ...acc };
|
return { [cur]: createExifOrientation(cur), ...acc };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
let camSettings: { [key: string]: { orientation: number; mirror: boolean } } = {};
|
|
||||||
|
|
||||||
// Reads the mapping of serial numbers to camera names from the text file.
|
// Reads the mapping of serial numbers to 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].trim()]: kv[1].trim() };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Returns the camera name (custom name, if it exists, otherwise its ID).
|
// Returns the camera name (custom name, if it exists, otherwise its ID).
|
||||||
const cameraName = (id: string): string => cameraNames[id] || id;
|
const cameraName = (id: string): string => config.cameras[id].alias || id;
|
||||||
|
|
||||||
// The HTTP server.
|
// The HTTP server.
|
||||||
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
export const serveHttp = (port: number) => {
|
||||||
logger.info(`Mapping camera names: ${JSON.stringify(cameraNames)}`);
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.url.startsWith("/ui/")) {
|
if (req.url.startsWith("/ui/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
@@ -70,7 +50,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
.toString()
|
.toString()
|
||||||
.replace(/\${id}/g, devId)
|
.replace(/\${id}/g, devId)
|
||||||
.replace(/\${name}/g, cameraName(devId))
|
.replace(/\${name}/g, cameraName(devId))
|
||||||
.replace(/\${audio}/g, with_audio.toString());
|
.replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false");
|
||||||
res.end(ui);
|
res.end(ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,17 +82,17 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
|
|
||||||
if (req.url.startsWith("/rotate/")) {
|
if (req.url.startsWith("/rotate/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
let curPos = camSettings[devId]?.orientation || 0;
|
let curPos = config.cameras[devId]?.rotate || 0;
|
||||||
let nextPos = (curPos + 1) % 4;
|
let nextPos = (curPos + 1) % 4;
|
||||||
logger.debug(`Rotating ${devId} to ${nextPos}`);
|
logger.debug(`Rotating ${devId} to ${nextPos}`);
|
||||||
camSettings[devId].orientation = nextPos;
|
config.cameras[devId].rotate = nextPos;
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
} else if (req.url.startsWith("/mirror/")) {
|
} else if (req.url.startsWith("/mirror/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
logger.debug(`Mirroring ${devId}`);
|
logger.debug(`Mirroring ${devId}`);
|
||||||
camSettings[devId].mirror = !camSettings[devId].mirror;
|
config.cameras[devId].mirror = !config.cameras[devId].mirror;
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
@@ -155,7 +135,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let devEv = discoverDevices(opts.discovery_ip);
|
let devEv = discoverDevices(config.discovery_ips);
|
||||||
|
|
||||||
const startSession = (s: Session) => {
|
const startSession = (s: Session) => {
|
||||||
s.send(SendDevStatus(s));
|
s.send(SendDevStatus(s));
|
||||||
@@ -172,14 +152,14 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||||
responses[dev.devId] = [];
|
responses[dev.devId] = [];
|
||||||
audioResponses[dev.devId] = [];
|
audioResponses[dev.devId] = [];
|
||||||
const s = makeSession(Handlers, dev, rinfo, startSession, opts);
|
const s = makeSession(Handlers, dev, rinfo, startSession);
|
||||||
|
|
||||||
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
||||||
|
|
||||||
s.eventEmitter.on("frame", () => {
|
s.eventEmitter.on("frame", () => {
|
||||||
// Add an EXIF header to indicate if the image should be rotated or mirrored
|
// Add an EXIF header to indicate if the image should be rotated or mirrored
|
||||||
let orientation = camSettings[dev.devId].orientation;
|
let orientation = config.cameras[dev.devId].rotate;
|
||||||
orientation = camSettings[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
orientation = config.cameras[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
||||||
const exifSegment = orientations[orientation];
|
const exifSegment = orientations[orientation];
|
||||||
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
|
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
|
||||||
const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]);
|
const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]);
|
||||||
@@ -193,7 +173,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
logger.info(`Camera ${dev.devId} disconnected`);
|
logger.info(`Camera ${dev.devId} disconnected`);
|
||||||
delete sessions[dev.devId];
|
delete sessions[dev.devId];
|
||||||
});
|
});
|
||||||
if (with_audio) {
|
if (config.cameras[dev.devId].audio) {
|
||||||
s.eventEmitter.on("audio", ({ gap, data }) => {
|
s.eventEmitter.on("audio", ({ gap, data }) => {
|
||||||
// ew, maybe WS?
|
// ew, maybe WS?
|
||||||
var b64encoded = Buffer.from(data).toString("base64");
|
var b64encoded = Buffer.from(data).toString("base64");
|
||||||
@@ -205,7 +185,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
sessions[dev.devId] = s;
|
sessions[dev.devId] = s;
|
||||||
camSettings[dev.devId] = { orientation: 0, mirror: false };
|
config.cameras[dev.devId] = { rotate: 0, mirror: false, ...(config.cameras[dev.devId] || {}) };
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Starting HTTP server on port ${port}`);
|
logger.info(`Starting HTTP server on port ${port}`);
|
||||||
|
|||||||
Generated
+15
@@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
|
"yaml": "^2.4.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
"mocha": "^10.2.0",
|
"mocha": "^10.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
@@ -1711,6 +1715,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
|
"yaml": "^2.4.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { RemoteInfo } from "dgram";
|
import { RemoteInfo } from "dgram";
|
||||||
|
|
||||||
import { opt } from "./options.js";
|
import { config } from "./settings.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial, SendReboot, SendWifiSettings } from "./impl.js";
|
import { DevSerial, SendReboot, SendWifiSettings } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
|
export const pair = ({ ssid, password }: { ssid: string; password: string }) => {
|
||||||
logger.info(`Will configure any devices found to join ${ssid}`);
|
logger.info(`Will configure any devices found to join ${ssid}`);
|
||||||
let sessions: Record<string, Session> = {};
|
let sessions: Record<string, Session> = {};
|
||||||
|
|
||||||
let devEv = discoverDevices(opts.discovery_ip);
|
let devEv = discoverDevices(config.discovery_ips);
|
||||||
if (password == "") {
|
if (password == "") {
|
||||||
throw new Error("You must set a non-zero-length password");
|
throw new Error("You must set a non-zero-length password");
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; passwo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||||
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
|
const s = makeSession(Handlers, dev, rinfo, onLogin);
|
||||||
|
|
||||||
s.eventEmitter.on("disconnect", () => {
|
s.eventEmitter.on("disconnect", () => {
|
||||||
logger.info(`Camera ${dev.devId} disconnected`);
|
logger.info(`Camera ${dev.devId} disconnected`);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import EventEmitter from "node:events";
|
|||||||
import { Commands, CommandsByValue } from "./datatypes.js";
|
import { Commands, CommandsByValue } from "./datatypes.js";
|
||||||
import { handle_Drw, handle_DrwAck, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, notImpl, noop } from "./handlers.js";
|
import { handle_Drw, handle_DrwAck, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, notImpl, noop } from "./handlers.js";
|
||||||
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
||||||
import { opt } from "./options.js";
|
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
@@ -24,7 +23,6 @@ export type Session = {
|
|||||||
frame_is_bad: boolean;
|
frame_is_bad: boolean;
|
||||||
frame_was_fixed: boolean;
|
frame_was_fixed: boolean;
|
||||||
started: boolean;
|
started: boolean;
|
||||||
options: opt;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
||||||
@@ -53,7 +51,6 @@ export const makeSession = (
|
|||||||
dev: DevSerial,
|
dev: DevSerial,
|
||||||
ra: RemoteInfo,
|
ra: RemoteInfo,
|
||||||
onLogin: (s: Session) => void,
|
onLogin: (s: Session) => void,
|
||||||
options: opt,
|
|
||||||
): Session => {
|
): Session => {
|
||||||
let unackedDrw = {};
|
let unackedDrw = {};
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
@@ -130,7 +127,6 @@ export const makeSession = (
|
|||||||
rcvSeqId: 0,
|
rcvSeqId: 0,
|
||||||
frame_is_bad: false,
|
frame_is_bad: false,
|
||||||
frame_was_fixed: false,
|
frame_was_fixed: false,
|
||||||
options: options,
|
|
||||||
unackedDrw,
|
unackedDrw,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
|
interface HttpServerConfig {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoggingConfig {
|
||||||
|
level: string;
|
||||||
|
use_color?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraConfig {
|
||||||
|
alias?: string;
|
||||||
|
rotate?: number;
|
||||||
|
mirror?: boolean;
|
||||||
|
audio?: boolean;
|
||||||
|
fix_packet_loss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
http_server: HttpServerConfig;
|
||||||
|
logging: LoggingConfig;
|
||||||
|
cameras: Record<string, CameraConfig>;
|
||||||
|
discovery_ips: string[];
|
||||||
|
blacklisted_ips: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultConfig: AppConfig = {
|
||||||
|
http_server: { port: 5000 },
|
||||||
|
logging: { level: "info" },
|
||||||
|
cameras: {},
|
||||||
|
discovery_ips: ["192.168.1.255"],
|
||||||
|
blacklisted_ips: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = DefaultConfig;
|
||||||
|
|
||||||
|
export const loadConfig = (path: string) => {
|
||||||
|
const data = fs.readFileSync(path, { encoding: "utf-8" });
|
||||||
|
config = parse(data) as AppConfig;
|
||||||
|
config = { ...DefaultConfig, ...config };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { config };
|
||||||
@@ -20,7 +20,7 @@ describe("integration", () => {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
const ev = discoverDevices("127.0.0.1");
|
const ev = discoverDevices(["127.0.0.1"]);
|
||||||
ev.on("discover", (rinfo, dev) => {
|
ev.on("discover", (rinfo, dev) => {
|
||||||
assert.deepEqual(dev.devId, EXPECTED_SERIAL);
|
assert.deepEqual(dev.devId, EXPECTED_SERIAL);
|
||||||
ev.emit("close");
|
ev.emit("close");
|
||||||
|
|||||||
Reference in New Issue
Block a user