- A scan no longer looks hung: the button counts seconds against the chosen timeout, the page says in words that it is listening for cameras and asking every address in turn, the refresh afterwards has its own state, and the result reports how many cameras were found, how long it took and how many addresses were checked. scanNetwork() returns that report; discoverCameras() still exists on top of it. - The config pointer now remembers the binary folder beside the data folder, so install-video and start can't disagree about where MediaMTX lives, whichever directory each is run from. Checked through an installed tarball against the real network: 2 cameras found, 4,093 addresses probed, 3.0 s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
5.4 KiB
JavaScript
Executable File
138 lines
5.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// The command people get from npm (vrek iss-kz5c88a). Node runs the TypeScript in src/lib
|
|
// directly (type stripping, Node 24+), so there is nothing to build for the CLI itself.
|
|
import { spawn } from "node:child_process";
|
|
import { access } from "node:fs/promises";
|
|
import { createInterface } from "node:readline/promises";
|
|
import { parseArgs } from "node:util";
|
|
import path from "node:path";
|
|
import {
|
|
applyEnvironment,
|
|
firstRun,
|
|
rememberBinDir,
|
|
SECRETS_FILE,
|
|
serverEnvironment,
|
|
} from "../dist/lib/first-run.js";
|
|
import { installMediamtx } from "../dist/lib/mediamtx-install.js";
|
|
import { adminFilePath, readAdminFile } from "../dist/lib/admin-file.js";
|
|
|
|
const HELP = `onvif-dashboard — watch and manage ONVIF cameras on your own network
|
|
|
|
Usage:
|
|
onvif-dashboard start run the dashboard (PORT, HOSTNAME)
|
|
onvif-dashboard setup choose where data lives and prepare this machine
|
|
onvif-dashboard admin create or replace the admin login (--force to replace)
|
|
onvif-dashboard install-video download MediaMTX, needed for live video and recording
|
|
onvif-dashboard help this message
|
|
|
|
Environment:
|
|
CAMERAS_DATA_DIR where data lives (asked once, then remembered)
|
|
ADMIN_AUTH_FILE, CAMERA_REGISTRY_FILE, CAMERA_CREDENTIALS_FILE,
|
|
AUDIT_LOG_FILE, RECORDINGS_DIR, MEDIAMTX_BIN override individual paths
|
|
`;
|
|
|
|
/** Asks a question when there's a terminal; returns undefined when there isn't. */
|
|
function prompter() {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return undefined;
|
|
return async (question, fallback) => {
|
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
try {
|
|
return await rl.question(`${question} [${fallback}] `);
|
|
} finally {
|
|
rl.close();
|
|
}
|
|
};
|
|
}
|
|
|
|
async function setup() {
|
|
const ask = prompter();
|
|
const result = await firstRun({ ask });
|
|
console.log(`\nData folder: ${result.dataDir}`);
|
|
console.log(`Key for stored camera logins: ${path.join(result.dataDir, SECRETS_FILE)}`);
|
|
|
|
if (!result.videoReady) {
|
|
const install = ask ? await ask("Download MediaMTX now? Live video and recording need it. (y/n)", "y") : "n";
|
|
if (/^y/i.test(install || "y")) await installVideo();
|
|
else console.log("Skipped. Run onvif-dashboard install-video when you want live video.");
|
|
} else {
|
|
console.log("Live video: MediaMTX is installed.");
|
|
}
|
|
|
|
await applyEnvironment();
|
|
const admin = await readAdminFile(adminFilePath()).catch(() => null);
|
|
console.log(
|
|
admin
|
|
? `\nAdmin login: already set up (${adminFilePath()}).`
|
|
: `\nNo admin login yet. Start the app and open it in a browser: it prints a one-time setup code` +
|
|
` in this console, which you enter to create the login. You can skip that, but then anyone on` +
|
|
` your network can use it.`,
|
|
);
|
|
console.log("\nReady. Start it with: onvif-dashboard start");
|
|
}
|
|
|
|
/** Runs the built server, with the remembered folder and stored key in its environment. */
|
|
async function start() {
|
|
await applyEnvironment();
|
|
if (!process.env.CAMERA_CREDENTIALS_KEY) {
|
|
// Never started before: take the defaults quietly rather than refuse to run.
|
|
await firstRun();
|
|
}
|
|
// Settle every path now: the server runs from inside the package, not from here.
|
|
await serverEnvironment();
|
|
|
|
const server = path.join(import.meta.dirname, "..", ".next", "standalone", "server.js");
|
|
if (!(await access(server).then(() => true, () => false))) {
|
|
console.error("The built server is missing. From a source checkout, run: npm run build");
|
|
process.exit(1);
|
|
}
|
|
|
|
const child = spawn(process.execPath, [server], {
|
|
cwd: path.dirname(server),
|
|
stdio: "inherit",
|
|
env: { ...process.env, PORT: process.env.PORT ?? "3000", HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0" },
|
|
});
|
|
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal));
|
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
}
|
|
|
|
/** Creates the admin login, prompting for it; the script it runs has the interactive bits. */
|
|
async function admin(args) {
|
|
await applyEnvironment();
|
|
const script = path.join(import.meta.dirname, "..", "scripts", "create-admin.mjs");
|
|
const child = spawn(process.execPath, [script, ...args], {
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
}
|
|
|
|
async function installVideo() {
|
|
// Remember where it goes, so starting from another directory still finds it.
|
|
const binDir = await rememberBinDir();
|
|
const result = await installMediamtx({ binDir });
|
|
console.log(
|
|
result.status === "installed"
|
|
? `Installed MediaMTX ${result.version} at ${result.binary} (checksum verified).`
|
|
: `MediaMTX ${result.version} is already installed at ${result.binary}.`,
|
|
);
|
|
}
|
|
|
|
const { positionals } = parseArgs({ allowPositionals: true, strict: false });
|
|
const command = positionals[0] ?? "help";
|
|
|
|
try {
|
|
if (command === "start") await start();
|
|
else if (command === "setup") await setup();
|
|
else if (command === "admin") await admin(process.argv.slice(3));
|
|
else if (command === "install-video") await installVideo();
|
|
else if (command === "help" || command === "--help" || command === "-h") console.log(HELP);
|
|
else {
|
|
console.error(`Unknown command: ${command}\n`);
|
|
console.log(HELP);
|
|
process.exit(2);
|
|
}
|
|
} catch (err) {
|
|
console.error(err instanceof Error ? err.message : err);
|
|
process.exit(1);
|
|
}
|