src.nth.io/

summaryrefslogtreecommitdiff
path: root/scrypted/README.md
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 10:45:30 -0500
committerLuke Hoersten <[email protected]>2026-06-14 10:45:30 -0500
commit334455c8ab793e2303e5e3669fa6deba179412be (patch)
tree02b4511047e311705a182577c836183fa02f7a5f /scrypted/README.md
parent0a147e5942a9788cb0802f680d2c57a3da87efab (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.md83
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.