feat: expand config with playback, rendering, and connection limits

This commit is contained in:
2026-07-13 17:00:15 +07:00
parent 8a013b0e62
commit b705b02084
2 changed files with 119 additions and 26 deletions
+57 -16
View File
@@ -3,43 +3,84 @@ import { loadConfig } from "./config";
describe("loadConfig", () => { describe("loadConfig", () => {
test("returns defaults when env vars are unset", () => { test("returns defaults when env vars are unset", () => {
const config = loadConfig({}); expect(loadConfig({})).toEqual({
expect(config).toEqual({
host: "0.0.0.0", host: "0.0.0.0",
port: 22, port: 22,
maxLoop: 5, maxLoop: 5,
playbackMode: "loop",
allowUserControl: true,
switchDebounceMs: 120,
loginDelay: 1500, loginDelay: 1500,
maxConnections: 10, maxConnections: 10,
maxTotalConnections: 1000,
maxAuthAttempts: 6,
handshakeTimeout: 10000,
maxDimension: 512,
frameResolution: 360,
brightnessThreshold: 40, brightnessThreshold: 40,
charset: "detailed",
invert: false,
logCredentials: false, logCredentials: false,
videoPath: undefined,
}); });
}); });
test("overrides defaults from env vars", () => { test("overrides defaults from env vars", () => {
const config = loadConfig({ expect(
HOST: "127.0.0.1", loadConfig({
PORT: "2222", HOST: "127.0.0.1",
MAX_LOOP: "3", PORT: "2222",
LOGIN_DELAY: "500", MAX_LOOP: "3",
MAX_CONNECTIONS: "20", PLAYBACK_MODE: "random",
BRIGHTNESS_THRESHOLD: "60", ALLOW_USER_CONTROL: "false",
LOG_CREDENTIALS: "true", SWITCH_DEBOUNCE_MS: "200",
}); LOGIN_DELAY: "500",
expect(config).toEqual({ MAX_CONNECTIONS: "20",
MAX_TOTAL_CONNECTIONS: "50",
MAX_AUTH_ATTEMPTS: "3",
HANDSHAKE_TIMEOUT: "5000",
MAX_DIMENSION: "200",
FRAME_RESOLUTION: "480",
BRIGHTNESS_THRESHOLD: "60",
CHARSET: "blocks",
INVERT: "true",
LOG_CREDENTIALS: "true",
VIDEO_PATH: "/videos/clip.mkv",
})
).toEqual({
host: "127.0.0.1", host: "127.0.0.1",
port: 2222, port: 2222,
maxLoop: 3, maxLoop: 3,
playbackMode: "random",
allowUserControl: false,
switchDebounceMs: 200,
loginDelay: 500, loginDelay: 500,
maxConnections: 20, maxConnections: 20,
maxTotalConnections: 50,
maxAuthAttempts: 3,
handshakeTimeout: 5000,
maxDimension: 200,
frameResolution: 480,
brightnessThreshold: 60, brightnessThreshold: 60,
charset: "blocks",
invert: true,
logCredentials: true, logCredentials: true,
videoPath: "/videos/clip.mkv",
}); });
}); });
test("logCredentials is false for any value other than the string 'true'", () => { test("boolean env vars are true only for the exact string 'true'", () => {
expect(loadConfig({ LOG_CREDENTIALS: "1" }).logCredentials).toBe(false); expect(loadConfig({ LOG_CREDENTIALS: "1" }).logCredentials).toBe(false);
expect(loadConfig({ LOG_CREDENTIALS: "false" }).logCredentials).toBe( expect(loadConfig({ INVERT: "yes" }).invert).toBe(false);
false expect(loadConfig({ INVERT: "true" }).invert).toBe(true);
); });
test("falls back to defaults for non-numeric and out-of-range values", () => {
expect(loadConfig({ PORT: "not-a-number" }).port).toBe(22);
expect(loadConfig({ PORT: "99999" }).port).toBe(65535);
expect(loadConfig({ MAX_DIMENSION: "0" }).maxDimension).toBe(1);
expect(
loadConfig({ BRIGHTNESS_THRESHOLD: "999" }).brightnessThreshold
).toBe(100);
}); });
}); });
+62 -10
View File
@@ -4,25 +4,77 @@ export interface Config {
host: string; host: string;
port: number; port: number;
maxLoop: number; maxLoop: number;
playbackMode: "loop" | "random";
allowUserControl: boolean;
switchDebounceMs: number;
loginDelay: number; loginDelay: number;
maxConnections: number; maxConnections: number;
maxTotalConnections: number;
maxAuthAttempts: number;
handshakeTimeout: number;
maxDimension: number;
frameResolution: number;
brightnessThreshold: number; brightnessThreshold: number;
charset: string;
invert: boolean;
logCredentials: boolean; logCredentials: boolean;
videoPath?: string;
}
function parseIntEnv(
value: string | undefined,
fallback: number,
{ min, max }: { min?: number; max?: number } = {}
): number {
if (value === undefined || value.trim() === "") return fallback;
const parsed = parseInt(value, 10);
if (!Number.isFinite(parsed)) return fallback;
let result = parsed;
if (typeof min === "number") result = Math.max(min, result);
if (typeof max === "number") result = Math.min(max, result);
return result;
}
function parseBoolEnv(value: string | undefined): boolean {
return value === "true";
} }
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
const videoPath = env.VIDEO_PATH?.trim();
return { return {
host: env.HOST ?? "0.0.0.0", host: env.HOST ?? "0.0.0.0",
port: env.PORT ? parseInt(env.PORT, 10) : 22, port: parseIntEnv(env.PORT, 22, { min: 1, max: 65535 }),
maxLoop: env.MAX_LOOP ? parseInt(env.MAX_LOOP, 10) : 5, maxLoop: parseIntEnv(env.MAX_LOOP, 5, { min: 1 }),
loginDelay: env.LOGIN_DELAY ? parseInt(env.LOGIN_DELAY, 10) : 1500, playbackMode:
maxConnections: env.MAX_CONNECTIONS env.PLAYBACK_MODE?.trim().toLowerCase() === "random"
? parseInt(env.MAX_CONNECTIONS, 10) ? "random"
: 10, : "loop",
brightnessThreshold: env.BRIGHTNESS_THRESHOLD allowUserControl: env.ALLOW_USER_CONTROL !== "false",
? parseInt(env.BRIGHTNESS_THRESHOLD, 10) switchDebounceMs: parseIntEnv(env.SWITCH_DEBOUNCE_MS, 120, { min: 0 }),
: 40, loginDelay: parseIntEnv(env.LOGIN_DELAY, 1500, { min: 0 }),
logCredentials: env.LOG_CREDENTIALS === "true", maxConnections: parseIntEnv(env.MAX_CONNECTIONS, 10, { min: 1 }),
maxTotalConnections: parseIntEnv(env.MAX_TOTAL_CONNECTIONS, 1000, {
min: 1,
}),
maxAuthAttempts: parseIntEnv(env.MAX_AUTH_ATTEMPTS, 6, { min: 1 }),
handshakeTimeout: parseIntEnv(env.HANDSHAKE_TIMEOUT, 10000, { min: 0 }),
maxDimension: parseIntEnv(env.MAX_DIMENSION, 512, {
min: 1,
max: 4096,
}),
frameResolution: parseIntEnv(env.FRAME_RESOLUTION, 360, {
min: 16,
max: 1080,
}),
brightnessThreshold: parseIntEnv(env.BRIGHTNESS_THRESHOLD, 40, {
min: 0,
max: 100,
}),
charset: env.CHARSET?.trim() || "detailed",
invert: parseBoolEnv(env.INVERT),
logCredentials: parseBoolEnv(env.LOG_CREDENTIALS),
videoPath: videoPath ? videoPath : undefined,
}; };
} }