<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/scrypted/README.md, branch main</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=main</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-07-18T01:25:24+00:00</updated>
<entry>
<title>docs: sync README + TESTING + scrypted README with v1.4.0 reality</title>
<updated>2026-07-18T01:25:24+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:25:24+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=482d8f45f18c6cc880bf2dfbbb70e3a4d704ac44'/>
<id>urn:sha1:482d8f45f18c6cc880bf2dfbbb70e3a4d704ac44</id>
<content type='text'>
Drift accumulated across the review-fix, mDNS-discovery, and tooling
work:

- Discovery: the design-era "use a Node mDNS library" flow is replaced
  with what shipped — the sandbox has no such libraries; the script does
  a dependency-free legacy-unicast dgram browse (no :5353 bind), host
  dropdowns, blank-name inheritance, and auto-heal on register failure
  (matched by the new mac TXT record) instead of periodic re-browsing.
- /state example: add mac, ota_state, panel dims, tear_guard_engaged,
  temp_c, and the stream stats object; correct `configured` semantics
  (scrypted URL only) and the never-null MAC-derived name in /config.
- /config: document the 54-char name cap (mDNS label limit).
- scrypted/README: add-device + settings tables gain the discovered-host
  dropdown, Wake triggers on create, and the canonical "Viewport name"
  rename field (the Scrypted pencil doesn't propagate — by design);
  trigger-scoped subscriptions; device-wake immediate-204 semantics;
  idle=0 no longer force-slept by the safety timer; "manual IP" dropped
  from limitations; discovery section reframed with the diagnostic.ts
  probe and bridge-networking caveat.
- Source map: dedupe the doubled touch.{h,c} row, add stream_server /
  ota / chip_temp rows, fix nvs_config + endpoint-count drift.
- Build/TESTING: point iteration at `make ota` (USB stays for first
  flash); TXT expectations include mac; DHCP-renumber failure mode now
  documents the verified auto-heal path.
</content>
</entry>
<entry>
<title>docs: bring scrypted/README.md in line with current streaming behavior</title>
<updated>2026-07-01T02:14:43+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T02:14:43+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=05f9e07014c6aa3f0c8e0b23c283ce6d52c383a1'/>
<id>urn:sha1:05f9e07014c6aa3f0c8e0b23c283ce6d52c383a1</id>
<content type='text'>
The v1 README still described the old snapshot-poll path. Update to match the
shipped script: live MJPEG over a TCP data socket (port 81, ~24fps) via ffmpeg,
not ~1fps snapshots. Fixes:
- intro + v1 limitations: MJPEG streaming is done, not a v2 "POST /stream" TODO
- Settings: replace the removed "Frame push interval" with the real Display
  fields (JPEG quality, Max Scrypted-side buffer, Stream prebuffer); add the
  Actions group; correct brightness default (100, was 80)
- Wake triggers default is person+doorbell (was "all three"), doorbell only for
  doorbell-capable cameras
- drop the stale snapshot-source / snapshot-interval / POST /frame 409 mentions
- host field: no mDNS auto-resolve (was contradictory)
- smoke test: Loading -&gt; live video, not "snapshots flowing"
</content>
</entry>
<entry>
<title>docs: document required Medium-stream prebuffer for fast wake</title>
<updated>2026-07-01T02:11:17+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T02:11:17+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=0fd9ac83342451efdf0c7b0a1911037a426c6236'/>
<id>urn:sha1:0fd9ac83342451efdf0c7b0a1911037a426c6236</id>
<content type='text'>
Wake-to-live is ~0.7s only when the streamed substream keeps a rebroadcast
prebuffer. Add a "Fast wake — camera prebuffer (required)" section to
scrypted/README.md: enable Prebuffer on the STREAM: MEDIUM tab (default only
prebuffers High), keep duration &gt;= the detected keyframe interval (~5s), leave
the viewport's Stream prebuffer (ms) at 6000, and how to verify via the log.
</content>
</entry>
<entry>
<title>scrypted: per-viewport frame interval + trigger picker + live status; drop mDNS auto-resolve</title>
<updated>2026-06-15T01:48:05+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T01:48:05+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=79ac384dbb6177d2d828d6ddc2db516888038ae8'/>
<id>urn:sha1:79ac384dbb6177d2d828d6ddc2db516888038ae8</id>
<content type='text'>
- Move "frame push interval" from the parent's global setting to a
  per-viewport child setting (different cameras want different rates).
  Clamped to ≥33 ms (~30 fps max). Default still 1000 ms (1 fps).
- New "Wake triggers" multi-select on each viewport: doorbell, motion,
  person. Default = all three. Clear all of them for tap-only mode.
  handleCameraEvent now gates by this set; clearing it cleanly turns a
  viewport into a tap-only display with no auto-wake.
- Add a "Status (live)" group at the bottom of each viewport's Settings
  page that fetches /state + /config in parallel (1.5 s timeout) and
  surfaces every field — name, mac, ip, awake/asleep, configured,
  uptime, frame + error counters, resolution, free heap/psram, firmware
  version, registered scrypted URL. Offline devices say so cleanly.
- Drop mDNS auto-resolve: no more lookupMdns / refreshHostFromMdns,
  no per-viewport "auto-resolve via mDNS" toggle, no `dns` require.
  The host field is just an operator-set string now. The README
  documents the manual lookup commands (dns-sd, avahi-resolve) so
  finding the device IP stays a one-liner.
- Parent's Settings page is now informational-only (viewport count +
  callback base URL); per-viewport tuning lives on each child.
</content>
</entry>
<entry>
<title>scrypted/README: drop stale BOOT-factory-reset + IP-screen refs</title>
<updated>2026-06-14T23:34:03+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T23:34:03+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=d2608f76820eee161622faee764c81e87c40d2da'/>
<id>urn:sha1:d2608f76820eee161622faee764c81e87c40d2da</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted/script: auto-populate viewport host via mDNS</title>
<updated>2026-06-14T15:57:01+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T15:57:01+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=1ba5250c96644440226ad71922f40c7eaa4ef649'/>
<id>urn:sha1:1ba5250c96644440226ad71922f40c7eaa4ef649</id>
<content type='text'>
The host field is now optional in "+ Add Device". The script tries
dns.promises.lookup("viewport-&lt;name&gt;.local") via the OS resolver
(Bonjour on macOS, nss-mdns on Linux, host networking on Docker) on:

- every registerViewport call (plugin start, child instantiation,
  settings change, periodic 5-min refresh),
- right after createDevice so a fresh viewport's host field is
  populated by the time the operator opens its settings page.

On a successful lookup that differs from the stored host, the
resolved IP is written back to the child's storage. POST /config and
POST /frame then use the resolved value.

Falls back gracefully to the operator-entered host (with a one-line
warning if both are empty). A per-viewport "Auto-resolve via mDNS"
toggle (default on) opts out — useful for cross-VLAN setups or hosts
where mDNS doesn't reach.

scrypted/README.md adds a "How mDNS auto-resolve works" section
covering the per-OS resolver requirements and the Docker host-
networking note.
</content>
</entry>
<entry>
<title>Path B: per-viewport Scrypted devices with UI-driven settings</title>
<updated>2026-06-14T15:53:37+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T15:53:37+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=52f4105aa059b9d7310d80cfb86abe844f7be05a'/>
<id>urn:sha1:52f4105aa059b9d7310d80cfb86abe844f7be05a</id>
<content type='text'>
Convert the Scripts-plugin script from a hardcoded BINDINGS constant
into a DeviceProvider + DeviceCreator + HttpRequestHandler. Each
viewport is now a child Scrypted device under the parent script with
its own Settings page; operators add, edit, and delete viewports
entirely through the Scrypted UI.

Parent (ScryptedViewportProvider):
- DeviceProvider: getDevice(nativeId) instantiates a Viewport, attaches
  its camera event listener, and posts /config. releaseDevice tears
  down stream + listener + child storage entry.
- DeviceCreator: "+ Add Device" on the parent's page shows a small form
  (name / host / camera picker filtered to Camera interfaces /
  orientation choice). createDevice() pre-populates the child's storage
  via deviceManager.getDeviceStorage(nativeId) and registers it under
  the parent.
- Tracks known child nativeIds in its own storage as a JSON array so it
  can eagerly instantiate every child on plugin start (each
  registration + camera subscription happens at load time, not lazily).
- 5-min re-register loop catches devices that rebooted or got new DHCP
  leases.
- HttpRequestHandler routes POST &lt;base&gt;/state on the parent's endpoint;
  body {viewport, state} is matched against child names. Honors the
  spec race rules: every callback cancels any prior stream + safety
  timer for that viewport before applying the new state, and a /frame
  409 stops the stream without echoing sleep back.
- Global tuning (frame_interval_ms) lives on the parent's Settings.

Child (Viewport):
- Settings: host (string), camera (type=device with deviceFilter for
  the Camera interface — the UX win), orientation (choices), idle
  timeout, brightness. All persisted via this.storage.
- putSetting fires onBindingChanged() on the parent so re-register +
  re-subscribe happen immediately when any field changes.

scrypted/README.md rewritten for the UI-driven flow — install + add
device + edit + remove + global tuning + smoke test — no more "edit
BINDINGS and re-save."

scrypted/package.json + tsconfig.json: optional `npm install` so
editors can resolve @scrypted/sdk types. Nothing here ships — install
remains "paste into Scripts plugin." node_modules ignored.
</content>
</entry>
<entry>
<title>M9-precursor: v1 Scrypted Script (snapshot-rate end-to-end)</title>
<updated>2026-06-14T15:45:30+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T15:45:30+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=334455c8ab793e2303e5e3669fa6deba179412be'/>
<id>urn:sha1:334455c8ab793e2303e5e3669fa6deba179412be</id>
<content type='text'>
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 -&gt; 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 -&gt; Scrypted POST &lt;base&gt;/state):
    - {state:wake} -&gt; startStream() (cancels pending sleep timer + any
      prior interval, then begins pushing frames).
    - {state:sleep} -&gt; stopStream(sendSleep=false) (no echo back).
    - Unknown viewport -&gt; 404. Bad body -&gt; 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.
</content>
</entry>
</feed>
