- Watch each camera's ONVIF VideoSource/MotionAlarm; record while it reports movement and stop after a 10s post-roll, through a per-stream MediaMTX path so viewers are never interrupted. - Clips go to RECORDINGS_DIR (default .data/recordings), one folder per recorded stream; RECORD_STREAM, RECORD_KEEP_DAYS and MOTION_RECORDING configure the rest. An hourly sweep deletes clips past retention. - Add allCameraRecords(): listCameras() calls connection(), which never resolves outside a request and silently stalled startup. Background work is no longer awaited in instrumentation either. - Document the settings in the README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
6.9 KiB
Markdown
174 lines
6.9 KiB
Markdown
# Cameras
|
||
|
||
A web interface for discovering and managing Hikvision and Annke IP cameras on your local network.
|
||
|
||
## Securing the web interface
|
||
|
||
The dashboard and its API are protected by a single admin login. The login is stored in a
|
||
file, **never** in plain text: the password is hashed with [scrypt](https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback)
|
||
(random salt, N=65536, r=8, p=1), so it can't be recovered from the file.
|
||
|
||
| Setting | Default |
|
||
| --- | --- |
|
||
| `ADMIN_AUTH_FILE` (environment variable, e.g. in `.env.local`) | `./.data/admin.json` |
|
||
|
||
If the file doesn't exist, the app starts **unsecured**: anyone who can reach it can view your
|
||
cameras and change their logins. To avoid ever running it that way, create the admin file
|
||
before the first start (below).
|
||
|
||
### Setting it up from the browser instead
|
||
|
||
With no admin file, the server prints a **one-time setup code** in its console at startup:
|
||
|
||
```
|
||
====================================================================
|
||
No admin login is set up: the camera dashboard is NOT secured.
|
||
|
||
To secure it from a browser, open /setup and enter this one-time code:
|
||
|
||
XLZ7-Q4MB
|
||
...
|
||
```
|
||
|
||
Opening the app sends you to `/setup`, which asks for that code plus the new admin username
|
||
and password. Only someone who can see the server console can claim the login. After 5 wrong
|
||
codes a new one is printed.
|
||
|
||
You can also choose **Skip for now and run unsecured**. Every page then shows a red warning
|
||
banner, the API stays open to anyone on the network, and the setup prompt returns the next
|
||
time the browser is restarted.
|
||
|
||
### Signing in
|
||
|
||
Once an admin exists, every page and API call needs a session: pages redirect to `/login`,
|
||
and the API answers `401`. A session lasts **12 hours from your last activity**. After 10
|
||
wrong passwords from one address (or 100 from all addresses) within 15 minutes, logins are
|
||
paused for up to 15 minutes. **Sign out** (top right) ends the session in that browser; to
|
||
sign out everywhere, change the password.
|
||
|
||
### Create the admin login (recommended)
|
||
|
||
```bash
|
||
npm run admin:create
|
||
```
|
||
|
||
It asks for a username and a password (at least 12 characters; typing is hidden, and you
|
||
confirm it), then writes the file with owner-only permissions (`0600`). Options:
|
||
|
||
```bash
|
||
npm run admin:create -- --username admin # only ask for the password
|
||
npm run admin:create -- --force # replace the existing admin (reset the password)
|
||
printf '%s\n' "$PW" | npm run admin:create -- --username admin # non-interactive, e.g. provisioning
|
||
```
|
||
|
||
### Without npm: a Node one-liner
|
||
|
||
Any Node 24+ can produce the same file. The password is read from the terminal without
|
||
echoing, so it never appears in your shell history or the process list:
|
||
|
||
```bash
|
||
(read -rs PW && export PW && umask 077 && mkdir -p .data && node -e '
|
||
const c = require("node:crypto"), N = 65536, r = 8, p = 1, salt = c.randomBytes(16);
|
||
const key = c.scryptSync(process.env.PW, salt, 64, { N, r, p, maxmem: 256 * N * r });
|
||
const passwordHash = ["scrypt", N, r, p, salt.toString("base64"), key.toString("base64")].join("$");
|
||
console.log(JSON.stringify({ version: 1, username: "admin", passwordHash }, null, 2));
|
||
' > .data/admin.json)
|
||
```
|
||
|
||
The parentheses run it in a subshell, so the password variable and the stricter `umask` end
|
||
with it.
|
||
|
||
Change `username: "admin"` to taste. Usernames are 1–64 letters, digits, or `. _ @ -`.
|
||
|
||
### File format
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"username": "admin",
|
||
"passwordHash": "scrypt$65536$8$1$<salt, base64>$<64-byte key, base64>"
|
||
}
|
||
```
|
||
|
||
A file that exists but is malformed stops the app from authenticating anyone rather than
|
||
quietly turning security off; fix it or delete it.
|
||
|
||
### Forgot the password?
|
||
|
||
Run `npm run admin:create -- --force` (or delete the file and create it again). Changing the
|
||
password signs out every existing session.
|
||
|
||
## Live video (MediaMTX)
|
||
|
||
Browsers can't play the cameras' RTSP streams, so live video goes through
|
||
[MediaMTX](https://github.com/bluenviron/mediamtx), a single MIT-licensed binary that
|
||
the app will run on this machine only (localhost). Install the pinned build with:
|
||
|
||
```bash
|
||
npm run video:install
|
||
```
|
||
|
||
This downloads the MediaMTX release pinned in `src/lib/mediamtx-install.ts` for your
|
||
platform (macOS or Linux, Intel or ARM), checks its SHA-256 against the value committed
|
||
there, and puts the binary in `./bin/` (not checked in). A download that doesn't match
|
||
is refused and nothing is written. Re-running it does nothing if that build is already
|
||
installed.
|
||
|
||
To upgrade, change `version` in `MEDIAMTX_RELEASE` and replace every `sha256` with the
|
||
values from that release's `checksums.sha256` on GitHub (cross-check them against the
|
||
per-file digests on the release page), then run `npm run video:install` again.
|
||
|
||
## Motion recording
|
||
|
||
When a camera reports motion over ONVIF, the app records a clip and stops a few seconds
|
||
after the motion ends. Clips are fragmented MP4 and play in the browser.
|
||
|
||
| Variable | Default | Meaning |
|
||
| --- | --- | --- |
|
||
| `RECORDINGS_DIR` | `.data/recordings` | Where clips are written; point it at an external drive or NAS if you like. One folder per camera. |
|
||
| `RECORD_STREAM` | `sub` | Which stream is recorded. `sub` is about 5 MB per minute of motion, `main` about 38 MB. |
|
||
| `RECORD_KEEP_DAYS` | `7` | Clips older than this are deleted by an hourly sweep. |
|
||
| `MOTION_RECORDING` | on | Set to `off` to record nothing. |
|
||
|
||
There is no pre-roll: a clip starts when the camera reports motion, which is a second or
|
||
two after movement begins. Recording pulls the camera only while motion lasts.
|
||
|
||
---
|
||
|
||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||
|
||
## Getting Started
|
||
|
||
First, run the development server:
|
||
|
||
```bash
|
||
npm run dev
|
||
# or
|
||
yarn dev
|
||
# or
|
||
pnpm dev
|
||
# or
|
||
bun dev
|
||
```
|
||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||
|
||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||
|
||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||
|
||
## Learn More
|
||
|
||
To learn more about Next.js, take a look at the following resources:
|
||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||
|
||
## Deploy on Vercel
|
||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|