<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/scrypted/tsconfig.json, branch v1.4.0</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.4.0</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.4.0'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-07-17T23:56:17+00:00</updated>
<entry>
<title>scrypted: code-review fixes — reload drain order, reconnect storm, wake guards</title>
<updated>2026-07-17T23:56:17+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-17T23:56:17+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=0ac000e0db3feb235fae37389d0af18609e91c50'/>
<id>urn:sha1:0ac000e0db3feb235fae37389d0af18609e91c50</id>
<content type='text'>
- bootstrap: drain the shutdown-cleaner array FIRST, not after the child
  re-discovery loop. The old order tore down the listeners the loop had
  just attached, and since the instance maps still referenced them the
  attachListener fast-path blocked the 5-min register-cycle self-heal
  from ever re-attaching after a warm re-paste. The attach cleaner now
  also invalidates listeners/attachedCameraId (one cleaner per attach),
  so a drain by another instance can't strand dead registrations either.
- stream socket: single-flight reconnect scheduled from 'close' only.
  A failed connect emits 'error' then 'close'; scheduling from both
  doubled outstanding attempts every 500ms against a rebooting device.
  openStreamSocket also destroys the previous socket first.
- onRequest wake: same streams/streamStarting guard as every other start
  path (a concurrent second startStream overwrote the streams entry and
  orphaned the first ffmpeg), respond 204 immediately (the device's POST
  times out at 1s — awaiting startStream turned every tap-wake into a
  firmware state_post_failure), and skip the redundant wake POST back.
- startStream: bail-out paths after the wake POST (camera missing, no
  usable stream, ffmpeg-input conversion failure) now send a
  compensating sleep instead of stranding the panel on Loading; the
  last-ditch getVideoStream fallback no longer rejects out of the call.
- key streams/streamStarting/stopStream by nativeId — v.name drifts to
  the nativeId on reload, stranding or duplicating streams keyed under
  the drifted value; findByName matches display_name first for the same
  reason.
- idle_timeout_ms=0: the Scrypted-side safety timeout still reclaims
  ffmpeg/socket but no longer POSTs sleep over the always-on setting.
- pending bindingDebounce timers cleared across re-paste (they fired
  against the dead instance and attached duplicate listeners);
  releaseDevice drops lastRegisterSig/streamStarting/debounce entries.
- start() single-flights bootstrap; createDevice drops its redundant
  second registerViewport; streamLogger rolls its window on quiet ticks
  (post-lull rates were diluted) and lastLogUs -&gt; lastLogMs; hoist the
  JPEG EOI needle + resume-scan offset in the demux loop; postJSON drops
  the dead 204 check; remove unused sandbox declares and the legacy
  streams.interval field.
- tsconfig: moduleResolution Node -&gt; Bundler (removed in TS 6).
</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>
</feed>
