| Age | Commit message (Collapse) | Author | Files | Lines |
|
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.
|
|
PLUGIN-CONVERSION.md captures the full plan so a future session can pick
it up cold: why (the Scripts sandbox's reload-leak machinery — cleaner
registry, drain ordering, name-drift workarounds — exists only because
of the paste-deploy model), the port map (what moves 1:1, what changes,
what dies), open questions to verify (orphaned ffmpeg on worker restart,
storage attach at startup), the cutover order (never run script + plugin
together), and the verification suite. Linked from the README's Related
docs and Status table.
|
|
|
|
|
|
attachListener listened on BinarySensor+MotionSensor+ObjectDetector
unconditionally and relied on handleCameraEvent to filter by trigger —
correct, but a doorbell-only viewport still took a callback per motion
re-assert on a chatty camera, and the "subscribed to [...]" log implied
all three were wake sources. The listener set now follows the selected
triggers (zero triggers = tap-only: no listeners at all), and the
idempotency key becomes a "cameraId|triggers" signature
(attachedCameraId -> attachedListenerSig) so a trigger change forces a
re-attach through the existing rebind path. Event-time filtering stays
as defense in depth (ObjectDetector still needs the person-class check).
|
|
Creating a viewport with the name field blank fell back to "viewport"
with no way to change it afterward — display_name is deliberately the
canonical name (v.name drifts on reload), so the Scrypted device-name
pencil never propagated. Two fixes:
- createDevice: an empty name now inherits the discovered device's
advertised TXT name, so picking "10.0.13.83 — kitchen (…)" from the
host dropdown names the viewport "kitchen" without retyping.
- child settings gain a "Viewport name" field: updates display_name,
mirrors the name onto the Scrypted device record, and re-registers so
the firmware name + mDNS hostname follow.
|
|
|
|
Browse for _scrypted-viewport._tcp with a plain dgram socket on an
EPHEMERAL port: per RFC 6762 §6.7 a query from a non-5353 source port is
a legacy unicast query and responders reply unicast to that port —
verified in the ESP-IDF responder (mdns_send.c answers to the querier's
addr/port whenever src_port != 5353). No :5353 bind means no conflict
with Scrypted's own HomeKit mDNS or a host avahi-daemon, and it works
under Docker host-networking or native alike. One response packet
carries PTR+SRV+TXT+A (RFC 6763 §12.1), so parsing is per-packet with
no follow-up queries; the wire codec (name compression included) is
~150 lines, no dependencies.
Surfaced three ways:
- add-device form: host field is a combobox listing discovered
viewports as "ip — name (vX, WxH)"; manual entry still works and
only the address token is stored.
- child settings page: same choices on the existing host field.
- auto-heal: when a /config register fails, re-browse and match by MAC
(seeded from /state and discovery; survives renames) then by name;
on a hit at a new address, rewrite host and retry once. Runs only on
register failure, so it's rate-limited to the 5-min cycle and removes
the need for a DHCP reservation.
Browses are best-effort ([] on any failure) and cached 30s so settings
re-renders don't spam the LAN. diagnostic.ts gains the same browse as a
paste-and-run probe that validates dgram + multicast reachability from
inside the real Scrypted sandbox before trusting the feature.
|
|
|
|
- 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 -> 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 -> Bundler (removed in TS 6).
|
|
Tear-free display path and thermal visibility:
- display: triple buffering + scan tracking via on_refresh_done —
decode target is never the scanning or pending fb, so the
flip-vs-scan tear (measured on ~6% of painted frames at full
stream rate) is impossible by construction, with zero added
latency. /state tear_guard_engaged counts averted frames.
- temp: on-die TSENS reported as /state temp_c, on the info
overlay, and on the Scrypted per-stream stats line.
- screens: INFO_MAX_LINES 16 -> 20 (temp line was silently capped).
- docs: README Display strategy rewritten for the triple-buffer
model; new TCP window + EMAC tuning section.
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.2.
|
|
|
|
- Scrypted per-stream stats line gains temp=<c>C from /state temp_c —
free thermal trending under streaming load.
- README Display strategy rewritten for the tear-free triple-buffer
model: why the deferred fb-index reload tears under double
buffering, why scan-tracked buffer roles beat vsync-waiting, and
the tear_guard_engaged counter.
- New 'TCP window + EMAC tuning' section documenting the measured
window raise, the EMAC-RX-pool-below-window RTO regression, and
the pool >= TCP_WND invariant; stale 'window bump is safe but
won't help' backlog text updated.
- Memory strategy updated (3 fbs, decoder writes into them directly,
stream body ring).
|
|
Stream throughput tuning, measured on hardware (kitchen panel):
wire 53 -> 74 Mbps, per-frame recv 30.5 -> 21.4 ms, painted fps
19.8 -> 23.6, g2g 73 -> 62 ms, sender backpressure 61% -> 47%.
- lwip: TCP_WND 5760 -> 23040 (16 x MSS), RECVMBOX 6 -> 32
- eth: EMAC RX DMA pool sized above the TCP window (1600B x 24);
below-window pool caused silent burst tail-drop + ~200-400ms
sender RTO stalls
- stream: TCP-window decomposition instrumentation (wire kbps,
hdr_gap, pend_age) in the window log and /state; connect log
stamps TCP_WND/MSS/RECVMBOX
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.1.
|
|
|
|
skip-oldest)
|
|
Unify firmware and plugin versions for the production release. Highlights
since v1.1.0/1.2.0:
- event wake fixed (self-healing camera-listener re-attach after the reload/
add storage race); doorbell/motion/person confirmed on hardware
- stop() fully tears down (stop+start == fresh load); stale childId pruning;
live-stream events ignored (no queue/relaunch)
- triggers default to person+doorbell; doorbell hidden for non-doorbell cams
- cold-start (wake -> live video) ~6s -> ~0.7s: prebuffered substream by id +
request >= GOP + burst-friendly ffmpeg flags; snapshot removed
- firmware: Loading screen on new stream connection (no stale-frame flash)
- logging trimmed for chatty cameras
- README/protocol docs rewritten for the TCP-socket streaming model
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.0.
|
|
|
|
A chatty camera keeps a viewport streaming for long stretches, and the old
logging emitted ~5 cold-start stamps + a 10s window line per ~10s per stream.
Cut it down:
- keep a single cold-start stamp (first ffmpeg frame); drop spawned / first
stdout byte / first socket.write.
- gate the 10s window line on noteworthy windows only (socket-not-ready drops,
a buffer-cap flush this window, firmware shedding >=2fps, or painted <20fps);
healthy 24fps windows print nothing — the per-wake lines already show liveness.
- log "registered" only when the pushed config actually changes, not on every
5-minute reregister cycle. lastRegisterSig cleared on stop() for fresh-load
parity.
No behavior change beyond logging.
|
|
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 -> live video, not "snapshots flowing"
|
|
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 >= the detected keyframe interval (~5s), leave
the viewport's Stream prebuffer (ms) at 6000, and how to verify via the log.
|
|
|
|
The prebuffered stream now paints in ~0.7s, so the takePicture→transform→POST
first-paint bridge was consistently slower than the video it was meant to
cover, while adding camera load and a stale-overpaint risk. Drop it: remove
pushSnapshot and its call, the shouldPaint gate, and the firstStreamFrameSeen
plumbing. On the slow paths (no/cold prebuffer) the panel holds its prior frame
until the stream's first frame lands. buildVf stays (used by the live path).
|
|
|
|
Now that the prebuffered stream paints in ~0.7s, the parallel snapshot usually
loses the race and, if slow (a Unifi takePicture cache-miss can be several
seconds), would land after live video and overpaint it with a staler still.
pushSnapshot takes a shouldPaint predicate; startStream passes
() => !firstStreamFrameSeen (flipped by the ffmpeg first-frame handler), so a
snapshot that finishes after the stream is already painting is dropped before
the POST. Preserves the snapshot as a gap-filler for the slow paths (live-edge
fallback / cold prebuffer) where the stream hasn't painted yet.
|
|
|
|
Remove the temporary instrumentation used to diagnose the event-wake and
cold-start work: the evtscan system-wide listener, the attach/trace/streamopt/
src/inputArgs logs, the StartStop entry logs, and ffmpeg -loglevel info/-nostats
(back to error). Keep the lightweight one-shot cold-start stamps (spawned →
first byte → first frame → first socket.write) and usingPrebuffer in the config
line — cheap regression signals — plus all the actual fixes and the prebuffered-
stream selection logic.
|
|
|
|
Event/lifecycle fixes:
- attachListener now self-heals: it's idempotent (tracks the bound camera
per viewport) and re-runs on registerViewport success, so the camera
subscription survives the reload/add storage-attach race that previously
left a bound viewport with no listener until a manual re-save.
- stop() always drains the global cleaner array (never gates on this.running)
and also cancels bindingDebounce timers + clears the stream idle timeout on
abort, so StartStop.stop()+start() == a fresh load with no orphaned ffmpeg,
sockets, listeners, or intervals.
- ignore all camera events while a stream is live or starting (guard moved to
the top of handleCameraEvent) — the wake window is anchored to the first
event; later events don't queue, relaunch, or extend it.
- prune stale childIds whose storage container is gone (deleted device).
Triggers:
- default to person + doorbell (motion opt-in; doorbell cameras are chatty);
doorbell only offered for doorbell-capable cameras. Reconcile stored
triggers + re-render settings live when the camera binding changes.
Cold-start (wake -> first live frame): ~6s -> ~0.7s.
- Root cause: Unifi GOP is ~5s and the rebroadcast RTSP serves live-edge, so
ffmpeg waited for the next keyframe.
- Fix: select the prebuffered substream by id (smallest that covers the panel,
not the 5MP High), request prebuffer >= GOP, and drop -avioflags direct /
-fflags nobuffer on the prebuffer path so ffmpeg gulps the buffered-keyframe
burst instead of trickling it. Live-edge fallback keeps the low-latency flags.
Steady-state g2g unchanged (~50-100ms).
Includes temporary diagnostics (evtscan, attach/trace/streamopt/src logs,
ffmpeg -loglevel info) to be stripped in the follow-up commit.
|
|
|
|
|
|
|
|
broken)
ScryptedDevice.listen() takes a single ScryptedInterface | string |
EventListenerOptions, not an array. We were passing
[BinarySensor, MotionSensor, ObjectDetector] which stringifies to a
comma-joined garbage filter, leaking unrelated events (Settings, ...)
through and apparently dropping BinarySensor entirely — bell rings
never surfaced. Also drops the child-device traversal: confirmed via
unifi-protect/src/main.ts that the bell BinarySensor lives on the
camera device itself for doorbells.
|
|
|
|
|
|
|
|
|
|
|
|
diagnostic
|
|
|
|
Scripts plugin)
The 'Unknown' text the user saw above the Status and Controls panel
is the device TYPE, not the lifecycle state. Scripts plugin
(plugins/core/src/script.ts:65) hardcodes type=ScryptedDeviceType.Unknown
when it registers any script device, regardless of what interfaces
the loaded class actually implements.
Override it from inside the script: call deviceManager.onDeviceDiscovered
ourselves with type=Bridge, which semantically fits our DeviceProvider
that bridges multiple child viewport devices. The override runs after
Scripts plugin's postRunScript-driven discovery so the later call wins.
Pass the full interface set explicitly (auto-detection found from
method names: Settings/DeviceProvider/DeviceCreator/HttpRequestHandler/
StartStop, plus Scripts base Scriptable+Program) — partial lists drop
interfaces.
The override is wrapped in try/catch — if the device record's
provider mapping changes in a future Scrypted version, we degrade
to the old Unknown label rather than the script failing to boot.
|
|
|
|
In v101fb3e we shipped StartStop + OnOff side-by-side to see which
the 'Status and Controls' panel actually wired to. The v44c7a63
diagnostic session confirmed: clicking STOP fires stop() (StartStop),
not turnOff() (OnOff). User's console showed:
lifecycle: start() called (running=false)
...
lifecycle: stop() called (running=true)
stop: tearing down 1 resources
So StartStop alone is the right interface. OnOff just produced a
duplicate 'Status and Controls' panel for the same lifecycle. Drop it.
Also dropping the verbose lifecycle: ... logging — we know the
binding now. The cleanup mechanism is the load-bearing observable
('stop: tearing down N resources' from drainShutdownCleaners).
The status text still rendering 'Unknown' (rather than Running/Stopped)
is a separate cosmetic concern — possibly the duplicate panels were
confusing the UI; with a single StartStop panel left, the displayed
status may now reflect this.running correctly. To be verified.
|
|
|
|
start() previously logged only at entry, so a silent throw between
'Scrypted Viewport up' and 'this.running = true' would leave us
guessing. Add explicit end-of-method logs with the post-write state,
plus a try/catch around bootstrap that surfaces the exception
message before re-throwing.
This will tell us whether start() reaches its set this.running=true
on script load (i.e. whether the prototype state-proxy write fires)
or whether bootstrap is silently failing somewhere mid-way.
|
|
|
|
panel binding
The 'Status and Controls' panel in @scrypted/core 0.3.147 was rendering
Unknown despite the previous commit adding StartStop. Reading the
SDK (sdk/src/index.ts:195 + plugins/core/src/script.ts:47) confirmed
how it should work:
- ScryptedDeviceBase installs Object.defineProperty getters/setters
for every interface property at runtime, so this.running = true
propagates through _lazyLoadDeviceState → getDeviceState proxy →
system state.
- Scripts plugin's mergeHandler auto-detects interfaces by mapping
method names: start/stop → StartStop, turnOn/turnOff → OnOff,
putSetting/getSettings → Settings, etc.
In theory the previous commit was sufficient. To pin down whether the
panel is calling something different (older Scrypted UIs lean toward
OnOff), this commit:
- Implements OnOff alongside StartStop. Both pairs are bound to the
same drain/bootstrap logic; whichever the panel calls, the user's
intent succeeds.
- Initialises this.running = false and this.on = false synchronously
in the constructor so the device state record carries a defined
value at registration time, instead of Scrypted defaulting to
Unknown on undefined.
- Logs 'lifecycle: X() called (...)' on every method entry. After
the user reloads + clicks STOP / START, the console will reveal
exactly which method Scrypted is invoking — or whether neither is.
If one of the methods fires, the other is dead code we can drop. If
neither fires on the panel's STOP/START button, the panel is a
Scripts-runtime control unrelated to device interfaces and we need
to look for a different lifecycle hook.
|
|
|
|
Silent early-return at 'if (!v.cameraId) return;' makes a brand-new
viewport with no camera selected look identical (in the console) to
one that subscribed successfully — there's no positive or negative
signal until you try to fire a camera event. After observing a fresh
viewport produce zero output on a doorbell press, switching the
early-return to a warning that says 'no camera assigned — open
Settings and pick a camera; subscription skipped' so the missing
configuration becomes self-evident.
|
|
|