@camstack/addon-provider-dreame 0.2.90 → 0.2.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +287 -24
  2. package/dist/addon.mjs +287 -24
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -52672,7 +52672,7 @@ var DreameCameraStream = class extends TypedEmitter {
52672
52672
  }
52673
52673
  };
52674
52674
  //#endregion
52675
- //#region ../types/dist/event-category-CnLqLOKs.mjs
52675
+ //#region ../types/dist/event-category-DAXzJeTX.mjs
52676
52676
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52677
52677
  EventCategory["SystemBoot"] = "system.boot";
52678
52678
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53264,7 +53264,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53264
53264
  * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
53265
53265
  */
53266
53266
  EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
53267
- EventCategory["FrigateLiveEvent"] = "frigate.live-event";
53268
53267
  EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
53269
53268
  /**
53270
53269
  * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
@@ -65298,7 +65297,7 @@ DeviceType.Camera;
65298
65297
  var SourceInfoSchema = object({
65299
65298
  /** Live dispatch key — mutable when the source system allows rename. */
65300
65299
  id: string(),
65301
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'frigate'. */
65300
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
65302
65301
  system: string(),
65303
65302
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
65304
65303
  * Used to detect rename when `id` changes. */
@@ -65311,7 +65310,7 @@ var SourceInfoSchema = object({
65311
65310
  /**
65312
65311
  * Build the synthetic SourceInfo every existing provider falls back to
65313
65312
  * when no upstream value has been persisted. Keeps non-migrated providers
65314
- * (Reolink / Hikvision / ONVIF / Frigate / RTSP) functional without code
65313
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
65315
65314
  * changes — `id` reuses the CamStack stableId, `system` reuses the addon
65316
65315
  * id. Real upstream identity replaces this once a provider migrates and
65317
65316
  * calls `updateSourceInfo()` with concrete values.
@@ -65990,7 +65989,7 @@ var deviceProviderCapability = {
65990
65989
  * - `device-management.router.ts` (deleted in Phase 2)
65991
65990
  * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
65992
65991
  *
65993
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
65992
+ * All device provider addons (rtsp, onvif, …) are hub-local: they may
65994
65993
  * fork into separate processes but never run on remote cluster agents. Therefore:
65995
65994
  * - No nodeId routing needed — this is a pure hub singleton.
65996
65995
  * - The hub's DeviceRegistry is the single source of truth for all live devices.
@@ -70038,7 +70037,7 @@ var NcRuleInputSchema = object({
70038
70037
  * The measured seven-person arrival on device 590 spans 110 s with every
70039
70038
  * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
70040
70039
  * idle cutoff holds it as one and ends it when the arrival actually ends.
70041
- * 30 is Frigate's shipped value for the same decision.
70040
+ * 30 s is a widely shipped value for the same decision.
70042
70041
  *
70043
70042
  * ### What it replaces
70044
70043
  *
@@ -72120,6 +72119,13 @@ var ZoneCrossingSchema = object({
72120
72119
  /** Zone display name at crossing time (falls back to the id). */
72121
72120
  zoneName: string().optional()
72122
72121
  });
72122
+ /** One zone a box was stamped with, and how much of the BOX fell inside it. */
72123
+ var ZoneOverlapSchema = object({
72124
+ zoneId: string(),
72125
+ zoneName: string().optional(),
72126
+ /** Percentage of the BOX area inside the zone, 0–100. */
72127
+ overlapPct: number()
72128
+ });
72123
72129
  var ObjectEventSchema = object({
72124
72130
  ...BaseEventFields,
72125
72131
  kind: literal("object"),
@@ -72144,6 +72150,35 @@ var ObjectEventSchema = object({
72144
72150
  bbox: BoundingBoxSchema.optional(),
72145
72151
  /** Heavy JSON — omitted in slim projection. */
72146
72152
  zones: array(string()).readonly().optional(),
72153
+ /**
72154
+ * How far inside each stamped zone the box actually was, as a percentage of
72155
+ * the BOX area.
72156
+ *
72157
+ * Membership alone cannot express "at least a third of it inside", which is
72158
+ * the bar an operator argues with — and the number already existed: the
72159
+ * engine computed it, the pipeline logged it, and then discarded it. It
72160
+ * matters most on the events that carry no track, where a rule has nothing
72161
+ * else to judge by.
72162
+ */
72163
+ zoneOverlaps: array(ZoneOverlapSchema).readonly().optional(),
72164
+ /**
72165
+ * Present when this event describes a detection the TRACKER refused, so it
72166
+ * has no track and never will.
72167
+ *
72168
+ * A zone-scoped immediate rule asks "is there an animal in the flowerbed
72169
+ * NOW?"; tracking answers a different question ("is this the same subject as
72170
+ * before?") whose confirmation threshold presumes continuity. On 2026-09-09 a
72171
+ * dog at 0.7729, 100% inside a watched zone, was refused three times in six
72172
+ * seconds with three different candidate ids — seven analysed frames in the
72173
+ * window, the subject in a handful of them, metres apart — and produced no
72174
+ * event at all.
72175
+ *
72176
+ * It is NAMED rather than merely implied by a missing `trackId`: an event
72177
+ * with no track and no reason is indistinguishable from a track whose media
72178
+ * went missing, and a consumer that asks this one for a best shot or a
72179
+ * history is asking for something that never existed.
72180
+ */
72181
+ spawnRefused: string().optional(),
72147
72182
  /** Omitted in slim projection. */
72148
72183
  state: TrackStateSchema.optional(),
72149
72184
  /**
@@ -73217,6 +73252,60 @@ var NativeCropResultSchema = object({
73217
73252
  */
73218
73253
  tier: _enum(["native", "ram-fullframe"]).optional()
73219
73254
  });
73255
+ /**
73256
+ * What a PARKED frame is for — the `kind` half of the `(deviceId, trackId,
73257
+ * kind)` key ([D422](../../../docs/decisions/adr-0422-a-relevant-frame-is-parked-when-it-becomes-relevant.md)).
73258
+ *
73259
+ * - `keyFrame` — the track's clean best FULL frame at native width (capped by
73260
+ * the caller's `maxWidth`). What `keyFrame` + `keyFrameSmall` are written
73261
+ * from, and what the detail plane's full-frame rung cuts from.
73262
+ * - `thumbnail` — the best-shot subject slab: the caller's 16:9 central-square
73263
+ * window, cut by the runner at uncapped native resolution.
73264
+ *
73265
+ * The runner never interprets `trackId`; it is an opaque key the hub owns.
73266
+ */
73267
+ var ParkedFrameKindSchema = _enum(["keyFrame", "thumbnail"]);
73268
+ /**
73269
+ * Why a park did NOT happen. Every one is logged on the runner with
73270
+ * `tags: { deviceId }` and the handle's measured age, because a handle already
73271
+ * gone at park time IS the defect the 150–500 ms window exists to prevent —
73272
+ * never a reason to retry in a loop.
73273
+ *
73274
+ * - `handle-gone` — the native surface no longer resolves this handle (never
73275
+ * registered, past the lease bound, or the worker released it). `detail`
73276
+ * carries the runner's own miss reason.
73277
+ * - `encode-failed` — pixels resolved and the JPEG encode threw.
73278
+ */
73279
+ var ParkRefusalSchema = _enum(["handle-gone", "encode-failed"]);
73280
+ /** Answer to `parkTrackFrame` — a discriminated union, so "not parked" always names why. */
73281
+ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
73282
+ parked: literal(true),
73283
+ /** The node holding the parcel — what a later retrieval must be pinned to. */
73284
+ nodeId: string(),
73285
+ width: number().int().positive(),
73286
+ height: number().int().positive(),
73287
+ /** JPEG bytes the parcel costs on the node. */
73288
+ bytes: number().int().nonnegative(),
73289
+ /** Whether this park replaced a parcel already held for the key. */
73290
+ replaced: boolean()
73291
+ }), object({
73292
+ parked: literal(false),
73293
+ nodeId: string(),
73294
+ reason: ParkRefusalSchema,
73295
+ /** Age of the handle at the park, on the runner's clock; null if unknown. */
73296
+ handleAgeMs: number().nullable(),
73297
+ detail: string().optional()
73298
+ })]);
73299
+ /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
73300
+ var ParkedTrackFrameSchema = object({
73301
+ jpeg: string(),
73302
+ width: number().int().positive(),
73303
+ height: number().int().positive(),
73304
+ /** The frame instant the parcel shows (the caller's clock, echoed back). */
73305
+ timestamp: number(),
73306
+ /** When the node stored it (the node's clock). */
73307
+ parkedAt: number()
73308
+ });
73220
73309
  /** Parent detection context passed to `runDetailSubtree` — the crop's
73221
73310
  * originating detection, in FRAME-space coordinates. Reuses
73222
73311
  * `NativeCropBboxSchema`'s `{x,y,w,h}` shape (same numeric fields; here
@@ -73606,7 +73695,7 @@ var RunnerCameraConfigSchema = object({
73606
73695
  * - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
73607
73696
  * analyzer providers) running on decoded frames in this runner.
73608
73697
  * - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
73609
- * analytics, Frigate motion). The provider emits ProviderMotion
73698
+ * analytics). The provider emits ProviderMotion
73610
73699
  * events; the orchestrator forwards to `reportMotion`.
73611
73700
  */
73612
73701
  motionSources: MotionSourcesSchema.default(["analyzer"]),
@@ -73848,7 +73937,26 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
73848
73937
  className: string(),
73849
73938
  /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
73850
73939
  sourceDeviceId: number()
73851
- }), RunStatelessStepResultSchema, { kind: "mutation" });
73940
+ }), RunStatelessStepResultSchema, { kind: "mutation" }), method(object({
73941
+ deviceId: number(),
73942
+ /** Opaque to the runner — the hub's track id. */
73943
+ trackId: string(),
73944
+ kind: ParkedFrameKindSchema,
73945
+ handle: FrameHandleSchema,
73946
+ /** Normalized [0,1] window to cut. `{0,0,1,1}` for a full frame. */
73947
+ bbox: NativeCropBboxSchema,
73948
+ /** Width cap for a full-frame park (the `keyFrame` native cap). */
73949
+ maxWidth: number().int().positive().optional(),
73950
+ /** The frame instant (caller's clock) — orders parks for one key. */
73951
+ timestamp: number()
73952
+ }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
73953
+ deviceId: number(),
73954
+ trackId: string(),
73955
+ kind: ParkedFrameKindSchema
73956
+ }), ParkedTrackFrameSchema.nullable()), method(object({
73957
+ deviceId: number(),
73958
+ trackId: string()
73959
+ }), object({ released: number().int().nonnegative() }), { kind: "mutation" });
73852
73960
  var CameraPipelineConfigSchema = object({
73853
73961
  engine: PipelineEngineChoiceSchema.optional(),
73854
73962
  steps: array(PipelineStepInputSchema).readonly(),
@@ -76918,8 +77026,8 @@ onStatusChanged: { data: object({
76918
77026
  /**
76919
77027
  * Runtime-state slice — every provider that registers this cap
76920
77028
  * stores the same shape under `device.runtimeState[battery]`.
76921
- * Cross-provider uniformity: a Reolink Argus, a Frigate sensor
76922
- * proxy, an ONVIF battery cam all read/write the same keys.
77029
+ * Cross-provider uniformity: a Reolink Argus, a sensor proxy,
77030
+ * an ONVIF battery cam all read/write the same keys.
76923
77031
  * Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
76924
77032
  * via `device.runtimeState.getCapState('battery')` regardless of
76925
77033
  * the underlying driver.
@@ -77069,7 +77177,7 @@ var buttonCapability = {
77069
77177
  *
77070
77178
  * Implementations:
77071
77179
  * - Each camera-providing addon registers this cap natively per
77072
- * device (Reolink, Hikvision, RTSP, Frigate, ONVIF self).
77180
+ * device (Reolink, Hikvision, RTSP, ONVIF self).
77073
77181
  * - Returns a snapshot from the persisted device config blob — no
77074
77182
  * extra round-trip to the camera, no probing.
77075
77183
  * - The cap is admin-only (`auth: 'admin'`) — credentials must not
@@ -79169,7 +79277,7 @@ var AvailableIntegrationTypeSchema = object({
79169
79277
  * Which integration-marker cap the addon declared, so the wizard can
79170
79278
  * branch on CAP — never on addon name. `device-adoption` integrations
79171
79279
  * (Home Assistant, …) route through the broker step (Approach A);
79172
- * `device-provider` integrations (Reolink/Frigate/ONVIF) keep the
79280
+ * `device-provider` integrations (Reolink/ONVIF) keep the
79173
79281
  * legacy config → discovery flow.
79174
79282
  */
79175
79283
  kind: _enum(["device-adoption", "device-provider"]),
@@ -82741,13 +82849,31 @@ var RecordingManifestSchema = object({
82741
82849
  */
82742
82850
  playbackEndpoints: array(string())
82743
82851
  });
82744
- /** Recording storage usage for one camera (bytes on disk across all its
82745
- * profiles/subtrees/locations on this node). */
82852
+ /**
82853
+ * Recording storage usage for one camera — what the ARCHIVE holds for it,
82854
+ * across every profile and every resolvable location on this node.
82855
+ *
82856
+ * ## Archive vs residency, which is the whole point of this shape (D418)
82857
+ *
82858
+ * `usedBytes` / `oldestMs` / `segments` are the durable hour ledger's numbers:
82859
+ * the same quantity `RecordingLocationUsage.usedBytes` reports for the volume
82860
+ * this camera writes to, so the two can be added up and compared.
82861
+ *
82862
+ * They were not always. Until D418 this row came from the recorder's in-RAM
82863
+ * `RecordingIndex`, which since D287 holds only the live tail plus the hours
82864
+ * somebody hydrated — so a hub with 1.9 TB and 3.3 days of footage reported
82865
+ * 60.5 GB and "oldest: three hours ago" on the same page as the volume row
82866
+ * that said otherwise, and two people read it as retention eating recent
82867
+ * footage. Residency is still reported, under `residentBytes` /
82868
+ * `residentOldestMs`, which answer a different question and say so.
82869
+ */
82746
82870
  var RecordingDeviceUsageSchema = object({
82747
82871
  deviceId: number(),
82872
+ /** Bytes the ARCHIVE holds for this camera on this node's resolvable
82873
+ * recordings locations. */
82748
82874
  usedBytes: number(),
82749
82875
  /**
82750
- * Start of this camera's OLDEST indexed segment, across every profile and
82876
+ * Start of this camera's OLDEST ARCHIVED segment, across every profile and
82751
82877
  * location — the "Oldest footage" column in Recordings → Storage, and the
82752
82878
  * only honest answer to "is retention actually holding?" per camera.
82753
82879
  *
@@ -82757,7 +82883,24 @@ var RecordingDeviceUsageSchema = object({
82757
82883
  * (types) and the addon ship on different trains, and the addon is usually
82758
82884
  * the later of the two.
82759
82885
  */
82760
- oldestMs: number().nullable().optional()
82886
+ oldestMs: number().nullable().optional(),
82887
+ /** Segments the archive holds for this camera — the "M" beside `usedBytes`.
82888
+ * OPTIONAL for the same train-skew reason as `oldestMs`. */
82889
+ segments: number().optional(),
82890
+ /**
82891
+ * RESIDENCY, not the archive: bytes the recorder process currently holds in
82892
+ * its RAM index — the live tail plus the hours a reader hydrated.
82893
+ *
82894
+ * It answers "what can be served without touching the disk", never "how much
82895
+ * footage this camera has". A UI that renders it must label it as such, and
82896
+ * must never total it as storage. OPTIONAL: a recorder predating D418 omits
82897
+ * it, and so does a node whose ledger could not answer.
82898
+ */
82899
+ residentBytes: number().optional(),
82900
+ /** The residency counterpart of `oldestMs` — the oldest segment currently in
82901
+ * RAM, which on a healthy recorder is minutes ago and means nothing about
82902
+ * retention. `null` = nothing resident. */
82903
+ residentOldestMs: number().nullable().optional()
82761
82904
  });
82762
82905
  /** Recording storage usage + capacity for one storage location. */
82763
82906
  var RecordingLocationUsageSchema = object({
@@ -82795,6 +82938,17 @@ var RecordingStorageUsageSchema = object({
82795
82938
  totalUsedBytes: number(),
82796
82939
  devices: array(RecordingDeviceUsageSchema),
82797
82940
  /**
82941
+ * WHERE `devices[]` came from, so an empty table is never mistaken for empty
82942
+ * disks (D418).
82943
+ *
82944
+ * `archive` = the durable hour ledger answered. `unavailable` = this node
82945
+ * declared no ledger and the recorder REFUSED rather than answering the
82946
+ * question with the resident index (D295 rule 3) — the rows are UNKNOWN, not
82947
+ * zero, and a UI must say so. OPTIONAL: a recorder predating D418 omits it,
82948
+ * and its rows are residency.
82949
+ */
82950
+ devicesRead: _enum(["archive", "unavailable"]).optional(),
82951
+ /**
82798
82952
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
82799
82953
  * of the payload without it: the hub serves this method through a BAKED
82800
82954
  * router, so a hub whose framework train predates a change to this array
@@ -86674,7 +86828,7 @@ function toDeviceSummary(device, addonId) {
86674
86828
  };
86675
86829
  }
86676
86830
  /**
86677
- * Base class for device-provider addons (rtsp, onvif, frigate).
86831
+ * Base class for device-provider addons (rtsp, onvif).
86678
86832
  *
86679
86833
  * Provides default implementations for the common device-provider cap
86680
86834
  * methods (`start`, `stop`, `getStatus`, `getDevices`, `supportsDiscovery`,
@@ -91330,6 +91484,24 @@ Object.freeze({
91330
91484
  addonId: null,
91331
91485
  access: "view"
91332
91486
  },
91487
+ "pipelineRunner.getParkedTrackFrame": {
91488
+ capName: "pipeline-runner",
91489
+ capScope: "system",
91490
+ addonId: null,
91491
+ access: "view"
91492
+ },
91493
+ "pipelineRunner.parkTrackFrame": {
91494
+ capName: "pipeline-runner",
91495
+ capScope: "system",
91496
+ addonId: null,
91497
+ access: "create"
91498
+ },
91499
+ "pipelineRunner.releaseParkedTrackFrames": {
91500
+ capName: "pipeline-runner",
91501
+ capScope: "system",
91502
+ addonId: null,
91503
+ access: "create"
91504
+ },
91333
91505
  "pipelineRunner.reportMotion": {
91334
91506
  capName: "pipeline-runner",
91335
91507
  capScope: "system",
@@ -94607,6 +94779,21 @@ Object.freeze({
94607
94779
  form: "single",
94608
94780
  optional: false
94609
94781
  }],
94782
+ "pipelineRunner.getParkedTrackFrame": [{
94783
+ name: "deviceId",
94784
+ form: "single",
94785
+ optional: false
94786
+ }],
94787
+ "pipelineRunner.parkTrackFrame": [{
94788
+ name: "deviceId",
94789
+ form: "single",
94790
+ optional: false
94791
+ }],
94792
+ "pipelineRunner.releaseParkedTrackFrames": [{
94793
+ name: "deviceId",
94794
+ form: "single",
94795
+ optional: false
94796
+ }],
94610
94797
  "pipelineRunner.reportMotion": [{
94611
94798
  name: "deviceId",
94612
94799
  form: "single",
@@ -95298,7 +95485,7 @@ var PreviewTextField = string().max(200);
95298
95485
  * Whether the notification's preview is a STILL or a short animation.
95299
95486
  *
95300
95487
  * The operator's ask, verbatim: *"inviato come gif o video (come per le altre
95301
- * rule)"* — his Scrypted advanced-notifier has a `gifRule`, and a ten-hour
95488
+ * rule)"* — the other rules already offer an animated preview, and a ten-hour
95302
95489
  * night reads better as three seconds of motion than as one frame of it. Both
95303
95490
  * modes get the SAME treatment (blurred frame, large centred title); `'gif'`
95304
95491
  * simply applies it to a dozen frames sampled across the render and assembles
@@ -143943,9 +144130,9 @@ var { mulaw, alaw } = resolveAlawmulaw();
143943
144130
  //#endregion
143944
144131
  //#region src/devices/dreame-rfc4571-server.ts
143945
144132
  /**
143946
- * RFC 4571 (framed RTP over TCP) output for the robot camera — the shape the
143947
- * Scrypted plugin (`scrypted-dreame-native/src/rfc4571.ts`) proved on this
143948
- * robot, ported: nodedreame's `DreameCameraStream` speaks the LinkVisual relay
144133
+ * RFC 4571 (framed RTP over TCP) output for the robot camera — the shape
144134
+ * already proven on this robot: nodedreame's `DreameCameraStream` speaks the
144135
+ * LinkVisual relay
143949
144136
  * and emits H.264 access units + AAC (ADTS); nodelink's `Rfc4571Muxer` frames
143950
144137
  * them as RTP on a local TCP server the stream-broker dials as a
143951
144138
  * `pull-rfc4571` source, exactly like a Reolink Baichuan camera.
@@ -143954,9 +144141,28 @@ var { mulaw, alaw } = resolveAlawmulaw();
143954
144141
  * name, and WE own the TCP socket — when the broker's reader detaches we know
143955
144142
  * the viewer is gone and release the monitor at once, instead of polling the
143956
144143
  * broker for consumers or waiting on a duration cap.
144144
+ *
144145
+ * And we own the FEED: the relay dies in three ways nodedreame reports and one
144146
+ * it cannot — the socket errors (`error`; with no listener an EventEmitter
144147
+ * throws, and on 2026-09-09 a `read ETIMEDOUT` took the whole provider down),
144148
+ * the relay closes and the stream stops itself (`stopped`), or the socket goes
144149
+ * silent for minutes before the kernel gives up (nothing at all: 16 min of a
144150
+ * broker dialling a port that accepted and sent nothing). The server reports
144151
+ * every one of them ONCE through `onFeedLost`, and never its own close, so the
144152
+ * device can put the lazy sentinel back and let the next dial be a wake (D424).
143957
144153
  */
143958
144154
  var VIDEO_PT = 96;
143959
144155
  var AUDIO_PT = 97;
144156
+ /**
144157
+ * How long the feed may go without a video access unit before the relay is
144158
+ * declared dead. The broker gives a source 8 s (`RTP_ACTIVITY_TIMEOUT_MS`);
144159
+ * this sits just past it so a stall is judged by the side that can act on it.
144160
+ */
144161
+ var FEED_STALL_MS = 1e4;
144162
+ var FEED_STALL_SWEEP_MS = 1e3;
144163
+ function describeLoss(loss) {
144164
+ return loss.error === null ? loss.reason : `${loss.reason}: ${loss.error.message}`;
144165
+ }
143960
144166
  function deferred() {
143961
144167
  let resolve = () => void 0;
143962
144168
  return {
@@ -143989,11 +144195,33 @@ function consoleInto(logger) {
143989
144195
  * server calls `onConsumerDisconnect` when the last reader detaches.
143990
144196
  */
143991
144197
  async function startRfc4571Server(params) {
143992
- const { stream, logger, onConsumerDisconnect, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3 } = params;
144198
+ const { stream, logger, onConsumerDisconnect, onFeedLost, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3, stallTimeoutMs = FEED_STALL_MS } = params;
143993
144199
  const videoRef = { current: null };
143994
144200
  const audioRef = { current: null };
143995
144201
  const firstKeyframe = deferred();
143996
144202
  const firstAudio = deferred();
144203
+ const firstLoss = deferred();
144204
+ let pendingLoss = null;
144205
+ let toreDown = false;
144206
+ let serving = false;
144207
+ let lastVideoAt = 0;
144208
+ const reportLoss = (loss) => {
144209
+ if (toreDown || pendingLoss !== null) return;
144210
+ pendingLoss = loss;
144211
+ firstLoss.resolve(loss);
144212
+ if (serving) onFeedLost(loss);
144213
+ };
144214
+ stream.on("error", (err) => reportLoss({
144215
+ reason: "error",
144216
+ error: err
144217
+ }));
144218
+ stream.on("stopped", () => reportLoss({
144219
+ reason: "stopped",
144220
+ error: null
144221
+ }));
144222
+ stream.on("videoAccessUnit", () => {
144223
+ lastVideoAt = Date.now();
144224
+ });
143997
144225
  stream.on("videoAccessUnit", (e) => {
143998
144226
  if (videoRef.current !== null) return;
143999
144227
  const p = extractH264ParamSetsFromAccessUnit(e.data);
@@ -144019,8 +144247,11 @@ async function startRfc4571Server(params) {
144019
144247
  }
144020
144248
  });
144021
144249
  await stream.start();
144250
+ const lostBeforeServing = firstLoss.promise.then((loss) => {
144251
+ throw new Error(`relay lost before the first keyframe (${describeLoss(loss)})`);
144252
+ });
144022
144253
  try {
144023
- await withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe");
144254
+ await Promise.race([withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe"), lostBeforeServing]);
144024
144255
  } catch (err) {
144025
144256
  await stream.stop().catch(() => void 0);
144026
144257
  throw err;
@@ -144028,6 +144259,10 @@ async function startRfc4571Server(params) {
144028
144259
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144029
144260
  setTimeout(resolve, audioGraceMs).unref?.();
144030
144261
  })]);
144262
+ if (pendingLoss !== null) {
144263
+ await stream.stop().catch(() => void 0);
144264
+ throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
144265
+ }
144031
144266
  const paramSets = videoRef.current;
144032
144267
  if (paramSets === null) throw new Error("no parameter sets after the first keyframe");
144033
144268
  const audioFacts = audioRef.current;
@@ -144047,7 +144282,6 @@ async function startRfc4571Server(params) {
144047
144282
  if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144048
144283
  let liveClients = 0;
144049
144284
  let idleTimer = null;
144050
- let toreDown = false;
144051
144285
  const server = node_net.default.createServer((socket) => {
144052
144286
  liveClients += 1;
144053
144287
  if (idleTimer !== null) clearTimeout(idleTimer);
@@ -144074,6 +144308,15 @@ async function startRfc4571Server(params) {
144074
144308
  });
144075
144309
  const addr = server.address();
144076
144310
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144311
+ const stallSweep = setInterval(() => {
144312
+ if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144313
+ reportLoss({
144314
+ reason: "stalled",
144315
+ error: null
144316
+ });
144317
+ }, Math.max(5, Math.min(FEED_STALL_SWEEP_MS, Math.floor(stallTimeoutMs / 2))));
144318
+ stallSweep.unref?.();
144319
+ serving = true;
144077
144320
  return {
144078
144321
  host: bindHost,
144079
144322
  port: addr.port,
@@ -144085,6 +144328,7 @@ async function startRfc4571Server(params) {
144085
144328
  } : null,
144086
144329
  close: async () => {
144087
144330
  toreDown = true;
144331
+ clearInterval(stallSweep);
144088
144332
  if (idleTimer !== null) clearTimeout(idleTimer);
144089
144333
  try {
144090
144334
  muxer.close();
@@ -144364,6 +144608,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
144364
144608
  keyframeTimeoutMs: WAKE_TIMEOUT_MS,
144365
144609
  onConsumerDisconnect: () => {
144366
144610
  this.closeStream("idle");
144611
+ },
144612
+ onFeedLost: (loss) => {
144613
+ this.onFeedLost(loss);
144367
144614
  }
144368
144615
  });
144369
144616
  this.live = {
@@ -144403,6 +144650,22 @@ var DreameCameraDevice = class extends DreameChildDevice {
144403
144650
  });
144404
144651
  return this.opening;
144405
144652
  }
144653
+ /**
144654
+ * The feed died under an open monitor. The session is closed — the lazy
144655
+ * sentinel goes back on the broker — so the broker's next dial asks for a
144656
+ * materialize, which is the wake. Logged: this branch drops a live stream.
144657
+ */
144658
+ onFeedLost(loss) {
144659
+ this.ctx.logger.warn("robot camera: relay feed lost — closing the monitor; the next dial re-opens it", {
144660
+ tags: { deviceId: this.id },
144661
+ meta: {
144662
+ reason: loss.reason,
144663
+ error: loss.error === null ? null : errMsg(loss.error),
144664
+ openMs: Date.now() - this.sessionOpenedAt
144665
+ }
144666
+ });
144667
+ this.closeStream("feed-lost");
144668
+ }
144406
144669
  armMaxDuration() {
144407
144670
  if (this.maxTimer !== null) clearTimeout(this.maxTimer);
144408
144671
  this.maxTimer = setTimeout(() => {
package/dist/addon.mjs CHANGED
@@ -52669,7 +52669,7 @@ var DreameCameraStream = class extends TypedEmitter {
52669
52669
  }
52670
52670
  };
52671
52671
  //#endregion
52672
- //#region ../types/dist/event-category-CnLqLOKs.mjs
52672
+ //#region ../types/dist/event-category-DAXzJeTX.mjs
52673
52673
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52674
52674
  EventCategory["SystemBoot"] = "system.boot";
52675
52675
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53261,7 +53261,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53261
53261
  * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
53262
53262
  */
53263
53263
  EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
53264
- EventCategory["FrigateLiveEvent"] = "frigate.live-event";
53265
53264
  EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
53266
53265
  /**
53267
53266
  * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
@@ -65295,7 +65294,7 @@ DeviceType.Camera;
65295
65294
  var SourceInfoSchema = object({
65296
65295
  /** Live dispatch key — mutable when the source system allows rename. */
65297
65296
  id: string(),
65298
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'frigate'. */
65297
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
65299
65298
  system: string(),
65300
65299
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
65301
65300
  * Used to detect rename when `id` changes. */
@@ -65308,7 +65307,7 @@ var SourceInfoSchema = object({
65308
65307
  /**
65309
65308
  * Build the synthetic SourceInfo every existing provider falls back to
65310
65309
  * when no upstream value has been persisted. Keeps non-migrated providers
65311
- * (Reolink / Hikvision / ONVIF / Frigate / RTSP) functional without code
65310
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
65312
65311
  * changes — `id` reuses the CamStack stableId, `system` reuses the addon
65313
65312
  * id. Real upstream identity replaces this once a provider migrates and
65314
65313
  * calls `updateSourceInfo()` with concrete values.
@@ -65987,7 +65986,7 @@ var deviceProviderCapability = {
65987
65986
  * - `device-management.router.ts` (deleted in Phase 2)
65988
65987
  * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
65989
65988
  *
65990
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
65989
+ * All device provider addons (rtsp, onvif, …) are hub-local: they may
65991
65990
  * fork into separate processes but never run on remote cluster agents. Therefore:
65992
65991
  * - No nodeId routing needed — this is a pure hub singleton.
65993
65992
  * - The hub's DeviceRegistry is the single source of truth for all live devices.
@@ -70035,7 +70034,7 @@ var NcRuleInputSchema = object({
70035
70034
  * The measured seven-person arrival on device 590 spans 110 s with every
70036
70035
  * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
70037
70036
  * idle cutoff holds it as one and ends it when the arrival actually ends.
70038
- * 30 is Frigate's shipped value for the same decision.
70037
+ * 30 s is a widely shipped value for the same decision.
70039
70038
  *
70040
70039
  * ### What it replaces
70041
70040
  *
@@ -72117,6 +72116,13 @@ var ZoneCrossingSchema = object({
72117
72116
  /** Zone display name at crossing time (falls back to the id). */
72118
72117
  zoneName: string().optional()
72119
72118
  });
72119
+ /** One zone a box was stamped with, and how much of the BOX fell inside it. */
72120
+ var ZoneOverlapSchema = object({
72121
+ zoneId: string(),
72122
+ zoneName: string().optional(),
72123
+ /** Percentage of the BOX area inside the zone, 0–100. */
72124
+ overlapPct: number()
72125
+ });
72120
72126
  var ObjectEventSchema = object({
72121
72127
  ...BaseEventFields,
72122
72128
  kind: literal("object"),
@@ -72141,6 +72147,35 @@ var ObjectEventSchema = object({
72141
72147
  bbox: BoundingBoxSchema.optional(),
72142
72148
  /** Heavy JSON — omitted in slim projection. */
72143
72149
  zones: array(string()).readonly().optional(),
72150
+ /**
72151
+ * How far inside each stamped zone the box actually was, as a percentage of
72152
+ * the BOX area.
72153
+ *
72154
+ * Membership alone cannot express "at least a third of it inside", which is
72155
+ * the bar an operator argues with — and the number already existed: the
72156
+ * engine computed it, the pipeline logged it, and then discarded it. It
72157
+ * matters most on the events that carry no track, where a rule has nothing
72158
+ * else to judge by.
72159
+ */
72160
+ zoneOverlaps: array(ZoneOverlapSchema).readonly().optional(),
72161
+ /**
72162
+ * Present when this event describes a detection the TRACKER refused, so it
72163
+ * has no track and never will.
72164
+ *
72165
+ * A zone-scoped immediate rule asks "is there an animal in the flowerbed
72166
+ * NOW?"; tracking answers a different question ("is this the same subject as
72167
+ * before?") whose confirmation threshold presumes continuity. On 2026-09-09 a
72168
+ * dog at 0.7729, 100% inside a watched zone, was refused three times in six
72169
+ * seconds with three different candidate ids — seven analysed frames in the
72170
+ * window, the subject in a handful of them, metres apart — and produced no
72171
+ * event at all.
72172
+ *
72173
+ * It is NAMED rather than merely implied by a missing `trackId`: an event
72174
+ * with no track and no reason is indistinguishable from a track whose media
72175
+ * went missing, and a consumer that asks this one for a best shot or a
72176
+ * history is asking for something that never existed.
72177
+ */
72178
+ spawnRefused: string().optional(),
72144
72179
  /** Omitted in slim projection. */
72145
72180
  state: TrackStateSchema.optional(),
72146
72181
  /**
@@ -73214,6 +73249,60 @@ var NativeCropResultSchema = object({
73214
73249
  */
73215
73250
  tier: _enum(["native", "ram-fullframe"]).optional()
73216
73251
  });
73252
+ /**
73253
+ * What a PARKED frame is for — the `kind` half of the `(deviceId, trackId,
73254
+ * kind)` key ([D422](../../../docs/decisions/adr-0422-a-relevant-frame-is-parked-when-it-becomes-relevant.md)).
73255
+ *
73256
+ * - `keyFrame` — the track's clean best FULL frame at native width (capped by
73257
+ * the caller's `maxWidth`). What `keyFrame` + `keyFrameSmall` are written
73258
+ * from, and what the detail plane's full-frame rung cuts from.
73259
+ * - `thumbnail` — the best-shot subject slab: the caller's 16:9 central-square
73260
+ * window, cut by the runner at uncapped native resolution.
73261
+ *
73262
+ * The runner never interprets `trackId`; it is an opaque key the hub owns.
73263
+ */
73264
+ var ParkedFrameKindSchema = _enum(["keyFrame", "thumbnail"]);
73265
+ /**
73266
+ * Why a park did NOT happen. Every one is logged on the runner with
73267
+ * `tags: { deviceId }` and the handle's measured age, because a handle already
73268
+ * gone at park time IS the defect the 150–500 ms window exists to prevent —
73269
+ * never a reason to retry in a loop.
73270
+ *
73271
+ * - `handle-gone` — the native surface no longer resolves this handle (never
73272
+ * registered, past the lease bound, or the worker released it). `detail`
73273
+ * carries the runner's own miss reason.
73274
+ * - `encode-failed` — pixels resolved and the JPEG encode threw.
73275
+ */
73276
+ var ParkRefusalSchema = _enum(["handle-gone", "encode-failed"]);
73277
+ /** Answer to `parkTrackFrame` — a discriminated union, so "not parked" always names why. */
73278
+ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
73279
+ parked: literal(true),
73280
+ /** The node holding the parcel — what a later retrieval must be pinned to. */
73281
+ nodeId: string(),
73282
+ width: number().int().positive(),
73283
+ height: number().int().positive(),
73284
+ /** JPEG bytes the parcel costs on the node. */
73285
+ bytes: number().int().nonnegative(),
73286
+ /** Whether this park replaced a parcel already held for the key. */
73287
+ replaced: boolean()
73288
+ }), object({
73289
+ parked: literal(false),
73290
+ nodeId: string(),
73291
+ reason: ParkRefusalSchema,
73292
+ /** Age of the handle at the park, on the runner's clock; null if unknown. */
73293
+ handleAgeMs: number().nullable(),
73294
+ detail: string().optional()
73295
+ })]);
73296
+ /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
73297
+ var ParkedTrackFrameSchema = object({
73298
+ jpeg: string(),
73299
+ width: number().int().positive(),
73300
+ height: number().int().positive(),
73301
+ /** The frame instant the parcel shows (the caller's clock, echoed back). */
73302
+ timestamp: number(),
73303
+ /** When the node stored it (the node's clock). */
73304
+ parkedAt: number()
73305
+ });
73217
73306
  /** Parent detection context passed to `runDetailSubtree` — the crop's
73218
73307
  * originating detection, in FRAME-space coordinates. Reuses
73219
73308
  * `NativeCropBboxSchema`'s `{x,y,w,h}` shape (same numeric fields; here
@@ -73603,7 +73692,7 @@ var RunnerCameraConfigSchema = object({
73603
73692
  * - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
73604
73693
  * analyzer providers) running on decoded frames in this runner.
73605
73694
  * - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
73606
- * analytics, Frigate motion). The provider emits ProviderMotion
73695
+ * analytics). The provider emits ProviderMotion
73607
73696
  * events; the orchestrator forwards to `reportMotion`.
73608
73697
  */
73609
73698
  motionSources: MotionSourcesSchema.default(["analyzer"]),
@@ -73845,7 +73934,26 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
73845
73934
  className: string(),
73846
73935
  /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
73847
73936
  sourceDeviceId: number()
73848
- }), RunStatelessStepResultSchema, { kind: "mutation" });
73937
+ }), RunStatelessStepResultSchema, { kind: "mutation" }), method(object({
73938
+ deviceId: number(),
73939
+ /** Opaque to the runner — the hub's track id. */
73940
+ trackId: string(),
73941
+ kind: ParkedFrameKindSchema,
73942
+ handle: FrameHandleSchema,
73943
+ /** Normalized [0,1] window to cut. `{0,0,1,1}` for a full frame. */
73944
+ bbox: NativeCropBboxSchema,
73945
+ /** Width cap for a full-frame park (the `keyFrame` native cap). */
73946
+ maxWidth: number().int().positive().optional(),
73947
+ /** The frame instant (caller's clock) — orders parks for one key. */
73948
+ timestamp: number()
73949
+ }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
73950
+ deviceId: number(),
73951
+ trackId: string(),
73952
+ kind: ParkedFrameKindSchema
73953
+ }), ParkedTrackFrameSchema.nullable()), method(object({
73954
+ deviceId: number(),
73955
+ trackId: string()
73956
+ }), object({ released: number().int().nonnegative() }), { kind: "mutation" });
73849
73957
  var CameraPipelineConfigSchema = object({
73850
73958
  engine: PipelineEngineChoiceSchema.optional(),
73851
73959
  steps: array(PipelineStepInputSchema).readonly(),
@@ -76915,8 +77023,8 @@ onStatusChanged: { data: object({
76915
77023
  /**
76916
77024
  * Runtime-state slice — every provider that registers this cap
76917
77025
  * stores the same shape under `device.runtimeState[battery]`.
76918
- * Cross-provider uniformity: a Reolink Argus, a Frigate sensor
76919
- * proxy, an ONVIF battery cam all read/write the same keys.
77026
+ * Cross-provider uniformity: a Reolink Argus, a sensor proxy,
77027
+ * an ONVIF battery cam all read/write the same keys.
76920
77028
  * Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
76921
77029
  * via `device.runtimeState.getCapState('battery')` regardless of
76922
77030
  * the underlying driver.
@@ -77066,7 +77174,7 @@ var buttonCapability = {
77066
77174
  *
77067
77175
  * Implementations:
77068
77176
  * - Each camera-providing addon registers this cap natively per
77069
- * device (Reolink, Hikvision, RTSP, Frigate, ONVIF self).
77177
+ * device (Reolink, Hikvision, RTSP, ONVIF self).
77070
77178
  * - Returns a snapshot from the persisted device config blob — no
77071
77179
  * extra round-trip to the camera, no probing.
77072
77180
  * - The cap is admin-only (`auth: 'admin'`) — credentials must not
@@ -79166,7 +79274,7 @@ var AvailableIntegrationTypeSchema = object({
79166
79274
  * Which integration-marker cap the addon declared, so the wizard can
79167
79275
  * branch on CAP — never on addon name. `device-adoption` integrations
79168
79276
  * (Home Assistant, …) route through the broker step (Approach A);
79169
- * `device-provider` integrations (Reolink/Frigate/ONVIF) keep the
79277
+ * `device-provider` integrations (Reolink/ONVIF) keep the
79170
79278
  * legacy config → discovery flow.
79171
79279
  */
79172
79280
  kind: _enum(["device-adoption", "device-provider"]),
@@ -82738,13 +82846,31 @@ var RecordingManifestSchema = object({
82738
82846
  */
82739
82847
  playbackEndpoints: array(string())
82740
82848
  });
82741
- /** Recording storage usage for one camera (bytes on disk across all its
82742
- * profiles/subtrees/locations on this node). */
82849
+ /**
82850
+ * Recording storage usage for one camera — what the ARCHIVE holds for it,
82851
+ * across every profile and every resolvable location on this node.
82852
+ *
82853
+ * ## Archive vs residency, which is the whole point of this shape (D418)
82854
+ *
82855
+ * `usedBytes` / `oldestMs` / `segments` are the durable hour ledger's numbers:
82856
+ * the same quantity `RecordingLocationUsage.usedBytes` reports for the volume
82857
+ * this camera writes to, so the two can be added up and compared.
82858
+ *
82859
+ * They were not always. Until D418 this row came from the recorder's in-RAM
82860
+ * `RecordingIndex`, which since D287 holds only the live tail plus the hours
82861
+ * somebody hydrated — so a hub with 1.9 TB and 3.3 days of footage reported
82862
+ * 60.5 GB and "oldest: three hours ago" on the same page as the volume row
82863
+ * that said otherwise, and two people read it as retention eating recent
82864
+ * footage. Residency is still reported, under `residentBytes` /
82865
+ * `residentOldestMs`, which answer a different question and say so.
82866
+ */
82743
82867
  var RecordingDeviceUsageSchema = object({
82744
82868
  deviceId: number(),
82869
+ /** Bytes the ARCHIVE holds for this camera on this node's resolvable
82870
+ * recordings locations. */
82745
82871
  usedBytes: number(),
82746
82872
  /**
82747
- * Start of this camera's OLDEST indexed segment, across every profile and
82873
+ * Start of this camera's OLDEST ARCHIVED segment, across every profile and
82748
82874
  * location — the "Oldest footage" column in Recordings → Storage, and the
82749
82875
  * only honest answer to "is retention actually holding?" per camera.
82750
82876
  *
@@ -82754,7 +82880,24 @@ var RecordingDeviceUsageSchema = object({
82754
82880
  * (types) and the addon ship on different trains, and the addon is usually
82755
82881
  * the later of the two.
82756
82882
  */
82757
- oldestMs: number().nullable().optional()
82883
+ oldestMs: number().nullable().optional(),
82884
+ /** Segments the archive holds for this camera — the "M" beside `usedBytes`.
82885
+ * OPTIONAL for the same train-skew reason as `oldestMs`. */
82886
+ segments: number().optional(),
82887
+ /**
82888
+ * RESIDENCY, not the archive: bytes the recorder process currently holds in
82889
+ * its RAM index — the live tail plus the hours a reader hydrated.
82890
+ *
82891
+ * It answers "what can be served without touching the disk", never "how much
82892
+ * footage this camera has". A UI that renders it must label it as such, and
82893
+ * must never total it as storage. OPTIONAL: a recorder predating D418 omits
82894
+ * it, and so does a node whose ledger could not answer.
82895
+ */
82896
+ residentBytes: number().optional(),
82897
+ /** The residency counterpart of `oldestMs` — the oldest segment currently in
82898
+ * RAM, which on a healthy recorder is minutes ago and means nothing about
82899
+ * retention. `null` = nothing resident. */
82900
+ residentOldestMs: number().nullable().optional()
82758
82901
  });
82759
82902
  /** Recording storage usage + capacity for one storage location. */
82760
82903
  var RecordingLocationUsageSchema = object({
@@ -82792,6 +82935,17 @@ var RecordingStorageUsageSchema = object({
82792
82935
  totalUsedBytes: number(),
82793
82936
  devices: array(RecordingDeviceUsageSchema),
82794
82937
  /**
82938
+ * WHERE `devices[]` came from, so an empty table is never mistaken for empty
82939
+ * disks (D418).
82940
+ *
82941
+ * `archive` = the durable hour ledger answered. `unavailable` = this node
82942
+ * declared no ledger and the recorder REFUSED rather than answering the
82943
+ * question with the resident index (D295 rule 3) — the rows are UNKNOWN, not
82944
+ * zero, and a UI must say so. OPTIONAL: a recorder predating D418 omits it,
82945
+ * and its rows are residency.
82946
+ */
82947
+ devicesRead: _enum(["archive", "unavailable"]).optional(),
82948
+ /**
82795
82949
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
82796
82950
  * of the payload without it: the hub serves this method through a BAKED
82797
82951
  * router, so a hub whose framework train predates a change to this array
@@ -86671,7 +86825,7 @@ function toDeviceSummary(device, addonId) {
86671
86825
  };
86672
86826
  }
86673
86827
  /**
86674
- * Base class for device-provider addons (rtsp, onvif, frigate).
86828
+ * Base class for device-provider addons (rtsp, onvif).
86675
86829
  *
86676
86830
  * Provides default implementations for the common device-provider cap
86677
86831
  * methods (`start`, `stop`, `getStatus`, `getDevices`, `supportsDiscovery`,
@@ -91327,6 +91481,24 @@ Object.freeze({
91327
91481
  addonId: null,
91328
91482
  access: "view"
91329
91483
  },
91484
+ "pipelineRunner.getParkedTrackFrame": {
91485
+ capName: "pipeline-runner",
91486
+ capScope: "system",
91487
+ addonId: null,
91488
+ access: "view"
91489
+ },
91490
+ "pipelineRunner.parkTrackFrame": {
91491
+ capName: "pipeline-runner",
91492
+ capScope: "system",
91493
+ addonId: null,
91494
+ access: "create"
91495
+ },
91496
+ "pipelineRunner.releaseParkedTrackFrames": {
91497
+ capName: "pipeline-runner",
91498
+ capScope: "system",
91499
+ addonId: null,
91500
+ access: "create"
91501
+ },
91330
91502
  "pipelineRunner.reportMotion": {
91331
91503
  capName: "pipeline-runner",
91332
91504
  capScope: "system",
@@ -94604,6 +94776,21 @@ Object.freeze({
94604
94776
  form: "single",
94605
94777
  optional: false
94606
94778
  }],
94779
+ "pipelineRunner.getParkedTrackFrame": [{
94780
+ name: "deviceId",
94781
+ form: "single",
94782
+ optional: false
94783
+ }],
94784
+ "pipelineRunner.parkTrackFrame": [{
94785
+ name: "deviceId",
94786
+ form: "single",
94787
+ optional: false
94788
+ }],
94789
+ "pipelineRunner.releaseParkedTrackFrames": [{
94790
+ name: "deviceId",
94791
+ form: "single",
94792
+ optional: false
94793
+ }],
94607
94794
  "pipelineRunner.reportMotion": [{
94608
94795
  name: "deviceId",
94609
94796
  form: "single",
@@ -95295,7 +95482,7 @@ var PreviewTextField = string().max(200);
95295
95482
  * Whether the notification's preview is a STILL or a short animation.
95296
95483
  *
95297
95484
  * The operator's ask, verbatim: *"inviato come gif o video (come per le altre
95298
- * rule)"* — his Scrypted advanced-notifier has a `gifRule`, and a ten-hour
95485
+ * rule)"* — the other rules already offer an animated preview, and a ten-hour
95299
95486
  * night reads better as three seconds of motion than as one frame of it. Both
95300
95487
  * modes get the SAME treatment (blurred frame, large centred title); `'gif'`
95301
95488
  * simply applies it to a dozen frames sampled across the render and assembles
@@ -143940,9 +144127,9 @@ var { mulaw, alaw } = resolveAlawmulaw();
143940
144127
  //#endregion
143941
144128
  //#region src/devices/dreame-rfc4571-server.ts
143942
144129
  /**
143943
- * RFC 4571 (framed RTP over TCP) output for the robot camera — the shape the
143944
- * Scrypted plugin (`scrypted-dreame-native/src/rfc4571.ts`) proved on this
143945
- * robot, ported: nodedreame's `DreameCameraStream` speaks the LinkVisual relay
144130
+ * RFC 4571 (framed RTP over TCP) output for the robot camera — the shape
144131
+ * already proven on this robot: nodedreame's `DreameCameraStream` speaks the
144132
+ * LinkVisual relay
143946
144133
  * and emits H.264 access units + AAC (ADTS); nodelink's `Rfc4571Muxer` frames
143947
144134
  * them as RTP on a local TCP server the stream-broker dials as a
143948
144135
  * `pull-rfc4571` source, exactly like a Reolink Baichuan camera.
@@ -143951,9 +144138,28 @@ var { mulaw, alaw } = resolveAlawmulaw();
143951
144138
  * name, and WE own the TCP socket — when the broker's reader detaches we know
143952
144139
  * the viewer is gone and release the monitor at once, instead of polling the
143953
144140
  * broker for consumers or waiting on a duration cap.
144141
+ *
144142
+ * And we own the FEED: the relay dies in three ways nodedreame reports and one
144143
+ * it cannot — the socket errors (`error`; with no listener an EventEmitter
144144
+ * throws, and on 2026-09-09 a `read ETIMEDOUT` took the whole provider down),
144145
+ * the relay closes and the stream stops itself (`stopped`), or the socket goes
144146
+ * silent for minutes before the kernel gives up (nothing at all: 16 min of a
144147
+ * broker dialling a port that accepted and sent nothing). The server reports
144148
+ * every one of them ONCE through `onFeedLost`, and never its own close, so the
144149
+ * device can put the lazy sentinel back and let the next dial be a wake (D424).
143954
144150
  */
143955
144151
  var VIDEO_PT = 96;
143956
144152
  var AUDIO_PT = 97;
144153
+ /**
144154
+ * How long the feed may go without a video access unit before the relay is
144155
+ * declared dead. The broker gives a source 8 s (`RTP_ACTIVITY_TIMEOUT_MS`);
144156
+ * this sits just past it so a stall is judged by the side that can act on it.
144157
+ */
144158
+ var FEED_STALL_MS = 1e4;
144159
+ var FEED_STALL_SWEEP_MS = 1e3;
144160
+ function describeLoss(loss) {
144161
+ return loss.error === null ? loss.reason : `${loss.reason}: ${loss.error.message}`;
144162
+ }
143957
144163
  function deferred() {
143958
144164
  let resolve = () => void 0;
143959
144165
  return {
@@ -143986,11 +144192,33 @@ function consoleInto(logger) {
143986
144192
  * server calls `onConsumerDisconnect` when the last reader detaches.
143987
144193
  */
143988
144194
  async function startRfc4571Server(params) {
143989
- const { stream, logger, onConsumerDisconnect, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3 } = params;
144195
+ const { stream, logger, onConsumerDisconnect, onFeedLost, bindHost = "0.0.0.0", keyframeTimeoutMs = 2e4, audioGraceMs = 1500, idleGraceMs = 4e3, stallTimeoutMs = FEED_STALL_MS } = params;
143990
144196
  const videoRef = { current: null };
143991
144197
  const audioRef = { current: null };
143992
144198
  const firstKeyframe = deferred();
143993
144199
  const firstAudio = deferred();
144200
+ const firstLoss = deferred();
144201
+ let pendingLoss = null;
144202
+ let toreDown = false;
144203
+ let serving = false;
144204
+ let lastVideoAt = 0;
144205
+ const reportLoss = (loss) => {
144206
+ if (toreDown || pendingLoss !== null) return;
144207
+ pendingLoss = loss;
144208
+ firstLoss.resolve(loss);
144209
+ if (serving) onFeedLost(loss);
144210
+ };
144211
+ stream.on("error", (err) => reportLoss({
144212
+ reason: "error",
144213
+ error: err
144214
+ }));
144215
+ stream.on("stopped", () => reportLoss({
144216
+ reason: "stopped",
144217
+ error: null
144218
+ }));
144219
+ stream.on("videoAccessUnit", () => {
144220
+ lastVideoAt = Date.now();
144221
+ });
143994
144222
  stream.on("videoAccessUnit", (e) => {
143995
144223
  if (videoRef.current !== null) return;
143996
144224
  const p = extractH264ParamSetsFromAccessUnit(e.data);
@@ -144016,8 +144244,11 @@ async function startRfc4571Server(params) {
144016
144244
  }
144017
144245
  });
144018
144246
  await stream.start();
144247
+ const lostBeforeServing = firstLoss.promise.then((loss) => {
144248
+ throw new Error(`relay lost before the first keyframe (${describeLoss(loss)})`);
144249
+ });
144019
144250
  try {
144020
- await withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe");
144251
+ await Promise.race([withTimeout(firstKeyframe.promise, keyframeTimeoutMs, "timed out waiting for the first keyframe"), lostBeforeServing]);
144021
144252
  } catch (err) {
144022
144253
  await stream.stop().catch(() => void 0);
144023
144254
  throw err;
@@ -144025,6 +144256,10 @@ async function startRfc4571Server(params) {
144025
144256
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144026
144257
  setTimeout(resolve, audioGraceMs).unref?.();
144027
144258
  })]);
144259
+ if (pendingLoss !== null) {
144260
+ await stream.stop().catch(() => void 0);
144261
+ throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
144262
+ }
144028
144263
  const paramSets = videoRef.current;
144029
144264
  if (paramSets === null) throw new Error("no parameter sets after the first keyframe");
144030
144265
  const audioFacts = audioRef.current;
@@ -144044,7 +144279,6 @@ async function startRfc4571Server(params) {
144044
144279
  if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144045
144280
  let liveClients = 0;
144046
144281
  let idleTimer = null;
144047
- let toreDown = false;
144048
144282
  const server = net.createServer((socket) => {
144049
144283
  liveClients += 1;
144050
144284
  if (idleTimer !== null) clearTimeout(idleTimer);
@@ -144071,6 +144305,15 @@ async function startRfc4571Server(params) {
144071
144305
  });
144072
144306
  const addr = server.address();
144073
144307
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144308
+ const stallSweep = setInterval(() => {
144309
+ if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144310
+ reportLoss({
144311
+ reason: "stalled",
144312
+ error: null
144313
+ });
144314
+ }, Math.max(5, Math.min(FEED_STALL_SWEEP_MS, Math.floor(stallTimeoutMs / 2))));
144315
+ stallSweep.unref?.();
144316
+ serving = true;
144074
144317
  return {
144075
144318
  host: bindHost,
144076
144319
  port: addr.port,
@@ -144082,6 +144325,7 @@ async function startRfc4571Server(params) {
144082
144325
  } : null,
144083
144326
  close: async () => {
144084
144327
  toreDown = true;
144328
+ clearInterval(stallSweep);
144085
144329
  if (idleTimer !== null) clearTimeout(idleTimer);
144086
144330
  try {
144087
144331
  muxer.close();
@@ -144361,6 +144605,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
144361
144605
  keyframeTimeoutMs: WAKE_TIMEOUT_MS,
144362
144606
  onConsumerDisconnect: () => {
144363
144607
  this.closeStream("idle");
144608
+ },
144609
+ onFeedLost: (loss) => {
144610
+ this.onFeedLost(loss);
144364
144611
  }
144365
144612
  });
144366
144613
  this.live = {
@@ -144400,6 +144647,22 @@ var DreameCameraDevice = class extends DreameChildDevice {
144400
144647
  });
144401
144648
  return this.opening;
144402
144649
  }
144650
+ /**
144651
+ * The feed died under an open monitor. The session is closed — the lazy
144652
+ * sentinel goes back on the broker — so the broker's next dial asks for a
144653
+ * materialize, which is the wake. Logged: this branch drops a live stream.
144654
+ */
144655
+ onFeedLost(loss) {
144656
+ this.ctx.logger.warn("robot camera: relay feed lost — closing the monitor; the next dial re-opens it", {
144657
+ tags: { deviceId: this.id },
144658
+ meta: {
144659
+ reason: loss.reason,
144660
+ error: loss.error === null ? null : errMsg(loss.error),
144661
+ openMs: Date.now() - this.sessionOpenedAt
144662
+ }
144663
+ });
144664
+ this.closeStream("feed-lost");
144665
+ }
144403
144666
  armMaxDuration() {
144404
144667
  if (this.maxTimer !== null) clearTimeout(this.maxTimer);
144405
144668
  this.maxTimer = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.90",
3
+ "version": "0.2.93",
4
4
  "description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
5
5
  "keywords": [
6
6
  "camstack",