diff options
| author | Luke Hoersten <[email protected]> | 2026-06-14 10:45:30 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-14 10:45:30 -0500 |
| commit | 334455c8ab793e2303e5e3669fa6deba179412be (patch) | |
| tree | 02b4511047e311705a182577c836183fa02f7a5f /scrypted/README.md | |
| parent | 0a147e5942a9788cb0802f680d2c57a3da87efab (diff) | |
M9-precursor: v1 Scrypted Script (snapshot-rate end-to-end)
scrypted/scrypted-viewport.ts — single-file TypeScript script for
Scrypted's Scripts plugin. Binds N viewports to N Scrypted cameras and
implements the full Scrypted side of the protocol:
Outbound (Scrypted -> device):
- POST /config on script load + every 5 min (re-syncs after device
reboot or DHCP renumber).
- POST /state {wake} when a bound camera fires an event or when the
device's own POST /state {wake} arrives (operator tap).
- POST /frame at FRAME_INTERVAL_MS (1 fps default) via
camera.takePicture({picture:{width,height}}).
- POST /state {sleep} when the Scrypted-side per-stream timer
expires.
Inbound (device -> Scrypted POST <base>/state):
- {state:wake} -> startStream() (cancels pending sleep timer + any
prior interval, then begins pushing frames).
- {state:sleep} -> stopStream(sendSleep=false) (no echo back).
- Unknown viewport -> 404. Bad body -> 400.
Race rules from the spec are honored:
- Every callback cancels prior pending state for that viewport
before applying the new state (cancelPendingSleep semantics).
- startStream / stopStream are idempotent — re-entering is safe.
- On /frame returning 409, stream stops immediately without
echoing /state {sleep} back (the device already slept).
Configuration is a single BINDINGS array at the top of the file —
edit name/host/cameraId/orientation per viewport and save in the
Scrypted UI. No package install. Tuning constants
(IDLE_TIMEOUT_MS, FRAME_INTERVAL_MS, REREGISTER_INTERVAL_MS, etc.)
are right above the class.
scrypted/README.md walks through Scripts-plugin install, the BINDINGS
schema, what each constant controls, an event-by-event description of
the script's behavior, the v1 limitations (snapshot-rate, manual IP,
camera must respect picture dims), and an end-to-end smoke test.
Top-level README's Scrypted Integration section now links the script
file and the install README directly.
Diffstat (limited to 'scrypted/README.md')
| -rw-r--r-- | scrypted/README.md | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/scrypted/README.md b/scrypted/README.md new file mode 100644 index 0000000..f279ca9 --- /dev/null +++ b/scrypted/README.md @@ -0,0 +1,83 @@ +# Scrypted-side script (v1) + +The ESP32 firmware needs something on the Scrypted side that: +- registers each viewport via `POST /config` on startup, +- receives device-initiated `POST /state` callbacks (tap, idle timeout), +- starts a JPEG stream to a viewport when its bound camera fires an event (doorbell ring, motion, person), and +- stops the stream when the viewport reports `sleep` or its own per-stream timer expires. + +`scrypted-viewport.ts` in this directory does all of that as a single-file TypeScript script for the **Scripts plugin**. v2 will replace this with a packaged plugin doing `POST /stream` over MJPEG; for now this is enough to bring up end-to-end functionality. + +## Install + +1. In Scrypted's web UI: **Plugins → search "Scripts" → install** (if you don't already have it). +2. **+ Add Device → Scripts plugin → New Script**. +3. Paste the entire contents of `scrypted-viewport.ts` into the editor and **Save**. +4. Edit the `BINDINGS` array at the top of the script for your setup: + + ```ts + const BINDINGS: Binding[] = [ + { + name: "mudroom", // matches /config viewport + host: "192.168.1.42", // viewport IP (LAN-resolvable) + cameraId: "abcdef0123456789", // Scrypted device id + orientation: "portrait", // 480x800 + }, + ]; + ``` + + - **`name`**: must match what the script POSTs to the device's `/config` (also drives `viewport-<name>.local`). + - **`host`**: viewport's current IP or hostname. v1 has no mDNS-SD discovery — set it manually and update if DHCP renumbers (or use a DHCP reservation). + - **`cameraId`**: the Scrypted device id (not the human name) of the camera to bind. Grab it from the URL bar on the camera's settings page in Scrypted. + - **`orientation`**: `portrait` (480×800) or `landscape` (800×480). Sent to the device via `/config` and used to size the snapshots. + +5. Save. The script's `start()` runs immediately. Within a few seconds you should see lines in the Scrypted log: + + ``` + Scrypted Viewport script up. Callback URL base: http://scrypted.local:11080/endpoint/<scriptId> + Bindings: 1 viewport(s) + subscribed to "Front Door" events for viewport "mudroom" + ``` + +6. On the viewport, `GET /config` should now show the populated `scrypted` URL and the `viewport` name. `GET /state` should show `configured: true` and `state: "asleep"`. + +## Tuning constants + +Defined right above the class: + +| Constant | Default | What it controls | +| --- | --- | --- | +| `IDLE_TIMEOUT_MS` | 60000 | Sent to the device in `/config`. Both sides use this value independently. | +| `STREAM_TIMEOUT_MS` | == `IDLE_TIMEOUT_MS` | Scrypted-side per-stream cutoff. Keep it the same as the device's idle timeout so both ends agree. | +| `FRAME_INTERVAL_MS` | 1000 | Snapshot push cadence during an active stream. ~1 fps is fine for ambient camera viewing. | +| `REREGISTER_INTERVAL_MS` | 300000 | Re-issue `/config` to every viewport every 5 minutes so a viewport that rebooted or moved IPs re-syncs without manual intervention. | +| `HTTP_TIMEOUT_MS` | 1000 | Per-call timeout for outbound POSTs. Matches the device's own 1 s timeout. | + +## What the script does on each event + +- **Camera event (doorbell ring / motion / person)** → call `startStream(binding.name)`: + - cancel any previous stream for that viewport, + - POST `{state: "wake"}` to the device, + - start the snapshot interval, + - arm a `STREAM_TIMEOUT_MS` safety timer. +- **Device-initiated `{state: "wake"}` callback** (operator tapped the panel) → same `startStream` path. +- **Device-initiated `{state: "sleep"}` callback** → `stopStream` with `sendSleep=false` (the device already knows). +- **Per-stream safety timer fires** → `stopStream` with `sendSleep=true` (tell the device to sleep). +- **`POST /frame` returns 409** → device went to sleep on its own (tap-to-sleep, or its idle timer); `stopStream` with `sendSleep=false`. + +## v1 limitations + +- Snapshot-rate only (~1 fps). Live MJPEG over `POST /stream` is v2. +- Manual IP per viewport (no mDNS-SD discovery yet — that's v1.1). +- Camera must respect `picture.width` / `picture.height` in `PictureOptions` or be paired with a snapshot plugin that resizes. If the camera returns the wrong size, the device rejects `/frame` with 400 and you'll see warning logs. Workaround: configure the camera plugin's snapshot size, or wait for v2 which will resize Scrypted-side via FFmpeg. +- No retry on transport errors. Best-effort matches the device's own semantics; the next event or callback re-syncs. + +## End-to-end smoke test + +Once installed and bindings are filled in: + +1. **Viewport boots** with no `/config` yet → IP screen on panel. +2. Script runs → `POST /config` lands → viewport → `state: asleep`, backlight off. +3. Tap viewport → device POSTs `{viewport: "mudroom", state: "wake"}` → script logs `recv mudroom -> wake` → snapshots start flowing for `STREAM_TIMEOUT_MS`. +4. Tap viewport again → device POSTs `{state: "sleep"}` → script stops streaming. +5. Trigger the bound camera (doorbell, motion sensor, or person detection) → script POSTs `{state: "wake"}` to viewport → snapshots flow for `STREAM_TIMEOUT_MS`, then both sides time out and sleep. |
