src.nth.io/

summaryrefslogtreecommitdiff
path: root/scrypted/PLUGIN-CONVERSION.md
blob: 084b83b06c67dfe468bf1ee9a3fab03f4360ae1f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Plan: convert the Scripts-plugin script into a real Scrypted plugin

Status: **planned, not started.** The script (`scrypted-viewport.ts`, deployed
by pasting into Scrypted's Scripts editor) is fully working as of v1.4.0 —
this conversion is a structural investment, not a bug fix. Written so a
future session can pick it up cold.

## Why

The Scripts sandbox does not tear down a previous load's resources before
constructing a new instance. Most of the script's hardest-won machinery
exists *only* to survive that:

- the `globalThis.__viewportShutdownCleaners` registry + drain-at-bootstrap
  ordering (a re-paste must kill the prior load's ffmpeg children, sockets,
  intervals, and camera listeners)
- listener-map invalidation inside the cleaners (so a drain by another
  instance can't strand `attachedListenerSig`/`listeners` claiming live
  registrations)
- the `display_name`-is-canonical scheme (v.name drifts to the nativeId on
  script reload)
- the storage-attach race on reload (root cause of the historical
  doorbell-wake bug) and the register-cycle self-healing re-attach
- `SCRIPT_VERSION` stamping + manual paste as the whole deploy story

A real plugin runs in its own worker process that Scrypted kills and
restarts on deploy: the cross-reload leak class disappears, and deploys
become scriptable (`npx scrypted deploy`) — no more paste step, and agent
sessions can ship plugin changes end-to-end like they already do firmware.

Secondary wins: real `import sdk from '@scrypted/sdk'` with actual types
(today everything is `declare const` + `type Foo = any`), npm deps become
possible (not currently needed), per-device consoles.

## Shape

- New top-level `plugin/` package (scaffold: `npx scrypted create` or copy
  the @scrypted/plugin template; webpack build → `dist/plugin.zip`).
  Keep `scrypted/scrypted-viewport.ts` frozen and working until cutover is
  verified, then delete it and fold `scrypted/README.md` install docs into
  the plugin's.
- Not published to npm — deployed straight at the LAN server:
  `npx scrypted login <server>` once, then `npx scrypted deploy <server>`.
- `Makefile`: add `make plugin` (build + deploy). Retire the
  `SCRIPT_VERSION` stamp convention; `package.json` version + git replace it.

## Port map (what moves, what changes, what dies)

Moves ~1:1 (the classes already implement the right interfaces):
- `ScryptedViewportProvider` (DeviceProvider, DeviceCreator,
  HttpRequestHandler, Settings, StartStop) and `Viewport` (Settings)
- the whole stream pipeline (ffmpeg spawn via `child_process`, JPEG demux,
  raw TCP :81 framing, skip-oldest backpressure, streamLogger)
- mDNS discovery (`mdnsBrowse` + wire codec — keep the hand-rolled
  dependency-free version; it's proven against real firmware responses),
  host comboboxes, auto-heal, mac seeding
- wake/sleep + trigger-scoped `attachListener`/`handleCameraEvent`

Changes:
- `declare const` globals → `import sdk, { ScryptedDeviceBase,
  ScryptedInterface, ScryptedDeviceType, ... } from '@scrypted/sdk'`;
  `systemManager` etc. become `sdk.systemManager` etc.
- Replace the `type X = any` aliases with real SDK types incrementally —
  start loose, tighten after the port compiles.
- Drop the bootstrap `onDeviceDiscovered` hack that overrides the Scripts
  plugin's hardcoded `Unknown` device type — the plugin's `package.json`
  `scrypted` section declares type/interfaces properly.
- `HttpRequestHandler` endpoint URL changes (new plugin id in the path).
  No firmware change needed: the 5-minute `registerViewport` cycle POSTs
  the new callback base via `/config` automatically.

Dies (the point of the exercise):
- `pushShutdownCleaner`/`drainShutdownCleaners` and everything that exists
  to serve them. KEEP the register-cycle `attachListener` self-heal (cheap
  idempotent belt-and-braces) and `display_name`-as-canonical (harmless,
  and protects against any residual name races).
- `SCRIPT_VERSION`.

## Open questions to verify during the port

1. **Orphaned ffmpeg on plugin restart.** The worker process is killed on
   deploy; spawned ffmpeg children survive parent death but should die on
   the next stdout write (EPIPE) since their pipe consumer is gone. Verify
   with a deploy mid-stream (`ps` for ffmpeg after). If they linger, add a
   `process.on('exit')`-adjacent kill or track children and SIGTERM them
   in a StartStop.stop path.
2. **Storage race**: confirm `deviceManager.getDeviceStorage` is reliably
   attached at plugin startup for pre-existing children (expected: yes —
   this was a Scripts-sandbox artifact). If yes, the "no camera assigned /
   no host" boot warnings disappear.
3. Settings UI parity: `combobox`/`choices`, `deviceFilter`, multi-select
   triggers, one-shot action toggles — all standard Setting fields, expect
   no changes; eyeball each page.
4. `endpointManager.getInsecurePublicLocalEndpoint(nativeId)` behavior in
   a real plugin (should be identical).

## Cutover checklist (order matters)

1. Deploy the plugin; do NOT bind any viewports yet.
2. STOP the old script via its Scripts-UI Stop (drains every listener /
   stream / interval — verified working), then delete the script device.
   Never run both live: two instances = duplicate camera listeners =
   duplicate streams to one panel (known failure mode).
3. Recreate the kitchen viewport in the plugin: "+ Add Device", pick the
   discovered host (name auto-fills from mDNS TXT), pick camera, set
   triggers. (Only one device exists today; storage migration is not worth
   building.)
4. Confirm firmware `/config` shows the NEW callback URL after the first
   register (or force it with a settings save).
5. Run the verification suite below.

## Verification suite (same bar as the script)

- boot log clean, no storage-race warnings
- doorbell press → wake + stream (`event BinarySensor ... (wake)`)
- panel tap → `recv "kitchen" -> wake (device-initiated)` + stream, full
  brightness, 204 on the device side (`state_post_failures` stays 0)
- manual Wake/Sleep toggles from settings
- rename via "Viewport name" → firmware `/config` + mDNS hostname follow
- auto-heal: set host to a wrong IP → `mdns auto-heal` log line corrects it
- deploy mid-stream → old ffmpeg exits (no orphan), next event streams
- steady-state stream health: painted ≈ 24 fps, g2g ~60-100 ms, `/state`
  stream window sane (see TESTING.md for the sampling loop)

## Estimate

Roughly one focused session: scaffold + mechanical port + build tooling in
the first half; cutover + the verification suite in the second. Firmware
is untouched throughout.