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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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 { serveHttp } from "../http_server.js";
|
||||
import { opt } from "../options.js";
|
||||
import { pair } from "../pair.js";
|
||||
import { loadConfig, config } from "../settings.js";
|
||||
|
||||
import { buildLogger, logger } from "../logger.js";
|
||||
|
||||
@@ -16,23 +16,38 @@ yargs(hideBin(process.argv))
|
||||
"start http server",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("audio", { describe: "Also stream audio from camera", default: true })
|
||||
.option("color", { describe: "Use color in logs", default: undefined })
|
||||
.option("color", { describe: "Use color in logs" })
|
||||
.boolean(["audio", "color"])
|
||||
.option("log_level", { describe: "Set log level", default: "info" })
|
||||
.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 })
|
||||
.option("config_file", { describe: "Specify config file" })
|
||||
.option("log_level", { describe: "Set log level" })
|
||||
.option("discovery_ip", { describe: "Camera discovery IP address" })
|
||||
.option("port", { describe: "HTTP Port to listen on" })
|
||||
.string(["log_level", "discovery_ip", "config_file"])
|
||||
.number(["port"])
|
||||
.strict();
|
||||
},
|
||||
(argv) => {
|
||||
const opts: opt = argv as opt;
|
||||
buildLogger(argv.log_level, argv.color);
|
||||
if (argv.config_file !== undefined) {
|
||||
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) {
|
||||
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||
}
|
||||
serveHttp(opts, argv.port, argv.audio || false);
|
||||
serveHttp(config.http_server.port);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
@@ -40,23 +55,19 @@ yargs(hideBin(process.argv))
|
||||
"configure a camera",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("color", { describe: "Use color in logs", default: undefined })
|
||||
.boolean(["color"])
|
||||
.option("log_level", { describe: "Set log level", default: "info" })
|
||||
.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("password", { describe: "Wifi network password" })
|
||||
.demandOption(["ssid", "password"])
|
||||
.string(["ssid", "password"]);
|
||||
},
|
||||
(argv) => {
|
||||
const opts: opt = argv as unknown as opt;
|
||||
buildLogger(argv.log_level, argv.color);
|
||||
buildLogger(argv.log_level, undefined);
|
||||
if (majorVersion < 16) {
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
+17
-10
@@ -4,6 +4,7 @@ import EventEmitter from "node:events";
|
||||
import { Commands } from "./datatypes.js";
|
||||
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { config } from "./settings.js";
|
||||
|
||||
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
||||
const ab = new Uint8Array(msg).buffer;
|
||||
@@ -12,11 +13,15 @@ const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) =
|
||||
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_ip: string): EventEmitter => {
|
||||
export const discoverDevices = (discovery_ips: string[]): EventEmitter => {
|
||||
const sock = createSocket("udp4");
|
||||
const SEND_PORT = 32108;
|
||||
const ee = new EventEmitter();
|
||||
@@ -30,17 +35,19 @@ export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
||||
|
||||
let timers = [];
|
||||
sock.on("listening", () => {
|
||||
sock.setBroadcast(true);
|
||||
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||
|
||||
let ls_buf = create_LanSearch();
|
||||
let int = setInterval(() => {
|
||||
logger.log("trace", `>> 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);
|
||||
}, 3000);
|
||||
timers.push(int);
|
||||
logger.log("trace", `>> LanSearch`);
|
||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||
});
|
||||
});
|
||||
|
||||
sock.bind();
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { Session } from "./session.js";
|
||||
import { u16_swap, u32_swap } from "./utils.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { hexdump } from "./hexdump.js";
|
||||
import { config } from "./settings.js";
|
||||
|
||||
export const notImpl = (session: Session, dv: DataView) => {
|
||||
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
|
||||
// than just missing some frames
|
||||
if (!session.options.attempt_to_fix_packet_loss) {
|
||||
if (!config.cameras[session.devName].fix_packet_loss) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+13
-33
@@ -1,9 +1,8 @@
|
||||
import { RemoteInfo } from "dgram";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import http from "node:http";
|
||||
|
||||
import { logger } from "./logger.js";
|
||||
import { opt } from "./options.js";
|
||||
import { config } from "./settings.js";
|
||||
import { discoverDevices } from "./discovery.js";
|
||||
import { DevSerial, SendDevStatus } from "./impl.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 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/
|
||||
const oMap = [1, 8, 3, 6];
|
||||
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 };
|
||||
}, {});
|
||||
|
||||
let camSettings: { [key: string]: { orientation: number; mirror: boolean } } = {};
|
||||
|
||||
// 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).
|
||||
const cameraName = (id: string): string => cameraNames[id] || id;
|
||||
const cameraName = (id: string): string => config.cameras[id].alias || id;
|
||||
|
||||
// The HTTP server.
|
||||
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
logger.info(`Mapping camera names: ${JSON.stringify(cameraNames)}`);
|
||||
export const serveHttp = (port: number) => {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url.startsWith("/ui/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
@@ -70,7 +50,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
.toString()
|
||||
.replace(/\${id}/g, devId)
|
||||
.replace(/\${name}/g, cameraName(devId))
|
||||
.replace(/\${audio}/g, with_audio.toString());
|
||||
.replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false");
|
||||
res.end(ui);
|
||||
return;
|
||||
}
|
||||
@@ -102,17 +82,17 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
|
||||
if (req.url.startsWith("/rotate/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
let curPos = camSettings[devId]?.orientation || 0;
|
||||
let curPos = config.cameras[devId]?.rotate || 0;
|
||||
let nextPos = (curPos + 1) % 4;
|
||||
logger.debug(`Rotating ${devId} to ${nextPos}`);
|
||||
camSettings[devId].orientation = nextPos;
|
||||
config.cameras[devId].rotate = nextPos;
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
} else if (req.url.startsWith("/mirror/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
logger.debug(`Mirroring ${devId}`);
|
||||
camSettings[devId].mirror = !camSettings[devId].mirror;
|
||||
config.cameras[devId].mirror = !config.cameras[devId].mirror;
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
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) => {
|
||||
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}`);
|
||||
responses[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`);
|
||||
|
||||
s.eventEmitter.on("frame", () => {
|
||||
// Add an EXIF header to indicate if the image should be rotated or mirrored
|
||||
let orientation = camSettings[dev.devId].orientation;
|
||||
orientation = camSettings[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
||||
let orientation = config.cameras[dev.devId].rotate;
|
||||
orientation = config.cameras[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
||||
const exifSegment = orientations[orientation];
|
||||
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
|
||||
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`);
|
||||
delete sessions[dev.devId];
|
||||
});
|
||||
if (with_audio) {
|
||||
if (config.cameras[dev.devId].audio) {
|
||||
s.eventEmitter.on("audio", ({ gap, data }) => {
|
||||
// ew, maybe WS?
|
||||
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;
|
||||
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}`);
|
||||
|
||||
Generated
+15
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -14,6 +15,9 @@
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
@@ -1711,6 +1715,17 @@
|
||||
"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": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
|
||||
+2
-1
@@ -14,9 +14,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
"node": ">=16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RemoteInfo } from "dgram";
|
||||
|
||||
import { opt } from "./options.js";
|
||||
import { config } from "./settings.js";
|
||||
import { discoverDevices } from "./discovery.js";
|
||||
import { DevSerial, SendReboot, SendWifiSettings } from "./impl.js";
|
||||
import { Handlers, makeSession, Session, configureWifi } from "./session.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}`);
|
||||
let sessions: Record<string, Session> = {};
|
||||
|
||||
let devEv = discoverDevices(opts.discovery_ip);
|
||||
let devEv = discoverDevices(config.discovery_ips);
|
||||
if (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;
|
||||
}
|
||||
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", () => {
|
||||
logger.info(`Camera ${dev.devId} disconnected`);
|
||||
|
||||
@@ -4,7 +4,6 @@ import EventEmitter from "node:events";
|
||||
import { Commands, CommandsByValue } from "./datatypes.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 { opt } from "./options.js";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
export type Session = {
|
||||
@@ -24,7 +23,6 @@ export type Session = {
|
||||
frame_is_bad: boolean;
|
||||
frame_was_fixed: boolean;
|
||||
started: boolean;
|
||||
options: opt;
|
||||
};
|
||||
|
||||
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
||||
@@ -53,7 +51,6 @@ export const makeSession = (
|
||||
dev: DevSerial,
|
||||
ra: RemoteInfo,
|
||||
onLogin: (s: Session) => void,
|
||||
options: opt,
|
||||
): Session => {
|
||||
let unackedDrw = {};
|
||||
const sock = createSocket("udp4");
|
||||
@@ -130,7 +127,6 @@ export const makeSession = (
|
||||
rcvSeqId: 0,
|
||||
frame_is_bad: false,
|
||||
frame_was_fixed: false,
|
||||
options: options,
|
||||
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 [];
|
||||
});
|
||||
const ev = discoverDevices("127.0.0.1");
|
||||
const ev = discoverDevices(["127.0.0.1"]);
|
||||
ev.on("discover", (rinfo, dev) => {
|
||||
assert.deepEqual(dev.devId, EXPECTED_SERIAL);
|
||||
ev.emit("close");
|
||||
|
||||
Reference in New Issue
Block a user