@camstack/addon-decoder-nodeav 1.2.96 → 1.2.98

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/index.js +177 -11
  2. package/dist/index.mjs +177 -11
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7303,6 +7303,23 @@ function errMsg(err) {
7303
7303
  if (typeof err === "string") return err;
7304
7304
  return String(err);
7305
7305
  }
7306
+ new Set([
7307
+ "track",
7308
+ "summary",
7309
+ "face",
7310
+ "identity",
7311
+ "plate",
7312
+ "vehicle",
7313
+ "scene",
7314
+ "motion",
7315
+ "object",
7316
+ "audio"
7317
+ ]);
7318
+ new Set([
7319
+ "motion",
7320
+ "object",
7321
+ "audio"
7322
+ ]);
7306
7323
  var EncodeProfileSchema = object({
7307
7324
  video: object({
7308
7325
  codec: _enum([
@@ -12438,8 +12455,17 @@ method(object({
12438
12455
  }), array(SettingsRecordSchema).readonly()), method(object({
12439
12456
  namespace: string().optional(),
12440
12457
  collection: string(),
12441
- record: SettingsRecordSchema
12442
- }), _void(), { kind: "mutation" }), method(object({
12458
+ record: object({
12459
+ id: string().optional(),
12460
+ data: record(string(), unknown())
12461
+ })
12462
+ }), object({
12463
+ /**
12464
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12465
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12466
+ * primary key — which is the only place an auto key is knowable.
12467
+ */
12468
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12443
12469
  namespace: string().optional(),
12444
12470
  collection: string(),
12445
12471
  records: array(BulkRecordSchema).readonly()
@@ -12548,8 +12574,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12548
12574
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12549
12575
  namespace: string().optional(),
12550
12576
  collection: string(),
12551
- record: SettingsRecordSchema
12552
- }), _void(), {
12577
+ record: object({
12578
+ id: string().optional(),
12579
+ data: record(string(), unknown())
12580
+ })
12581
+ }), object({
12582
+ /**
12583
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12584
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12585
+ * primary key — which is the only place an auto key is knowable.
12586
+ */
12587
+ id: union([string(), number()]) }), {
12553
12588
  kind: "mutation",
12554
12589
  auth: "admin"
12555
12590
  }), method(object({
@@ -19284,7 +19319,20 @@ var TrackSchema = object({
19284
19319
  ...TrackRetrainFields
19285
19320
  });
19286
19321
  var BaseEventFields = {
19287
- id: string(),
19322
+ /**
19323
+ * A SQLite ROWID, assigned by the database (D474).
19324
+ *
19325
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19326
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19327
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19328
+ * the rowid: the table itself is that B-tree, so the index stops existing
19329
+ * rather than getting smaller. No shorter string does that.
19330
+ *
19331
+ * Defined once here for all three event kinds, which is why they move
19332
+ * together: a per-table migration would have forked this and
19333
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19334
+ */
19335
+ id: number().int(),
19288
19336
  deviceId: number(),
19289
19337
  timestamp: number()
19290
19338
  };
@@ -19303,7 +19351,34 @@ var MotionEventSchema = object({
19303
19351
  /** Omitted in slim projection. */
19304
19352
  frameHeight: number().optional(),
19305
19353
  /** Populated by B5 (recording playback URL for this event). */
19306
- mediaUrl: string().optional()
19354
+ mediaUrl: string().optional(),
19355
+ /**
19356
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19357
+ * episode is still open — a further rising edge extends it in place rather
19358
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19359
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19360
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19361
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19362
+ * motion for an instantaneous trigger).
19363
+ *
19364
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19365
+ * "closed the old way, before this column existed", never "still open".
19366
+ * Nothing in this codebase may read an absent `durationMs` as an open
19367
+ * episode; only `null` means open.
19368
+ */
19369
+ durationMs: number().nullable().optional(),
19370
+ /**
19371
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19372
+ * first entry is always `0`) of every genuine off→on transition the
19373
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19374
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19375
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19376
+ * explicit `false` mid-episode and then resumes before the quiet window
19377
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19378
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19379
+ * which must never be read as "no episode happened here".
19380
+ */
19381
+ edges: array(number()).readonly().optional()
19307
19382
  });
19308
19383
  /**
19309
19384
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19358,6 +19433,23 @@ var ObjectEventSchema = object({
19358
19433
  * includes it (it is light). Absent on rows written before this field.
19359
19434
  */
19360
19435
  frameId: string().optional(),
19436
+ /**
19437
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19438
+ * (D474).
19439
+ *
19440
+ * Only the package detector writes it, and it exists because the event id
19441
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19442
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19443
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19444
+ * assigned by SQLite, so that key had to move off the primary key rather
19445
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19446
+ * every parcel on every boot.
19447
+ *
19448
+ * Absent on every other object event, and on every row written before this
19449
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19450
+ * not per row, and nothing addresses a row by it.
19451
+ */
19452
+ idempotencyKey: string().optional(),
19361
19453
  /** Omitted in slim projection. */
19362
19454
  trackId: string().optional(),
19363
19455
  className: string(),
@@ -20460,6 +20552,14 @@ var NativeCropResultSchema = object({
20460
20552
  * set `encodeJpeg: true`; `bytes` is then absent.
20461
20553
  */
20462
20554
  jpeg: string().optional(),
20555
+ /**
20556
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20557
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20558
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20559
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20560
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20561
+ */
20562
+ jpegBytes: _instanceof(Uint8Array).optional(),
20463
20563
  width: number().int().positive(),
20464
20564
  height: number().int().positive(),
20465
20565
  /**
@@ -20526,7 +20626,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20526
20626
  })]);
20527
20627
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20528
20628
  var ParkedTrackFrameSchema = object({
20529
- jpeg: string(),
20629
+ /**
20630
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20631
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20632
+ * Exactly one of the two is present.
20633
+ */
20634
+ jpeg: string().optional(),
20635
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20636
+ jpegBytes: _instanceof(Uint8Array).optional(),
20530
20637
  width: number().int().positive(),
20531
20638
  height: number().int().positive(),
20532
20639
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21113,6 +21220,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21113
21220
  bbox: NativeCropBboxSchema,
21114
21221
  maxWidth: number().int().positive().optional(),
21115
21222
  /**
21223
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21224
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21225
+ * wire — never assume consent: a pre-D462 caller parses the field as
21226
+ * base64 and bytes would decode to garbage rather than fail.
21227
+ */
21228
+ acceptJpegBytes: boolean().optional(),
21229
+ /**
21116
21230
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21117
21231
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21118
21232
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21180,7 +21294,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21180
21294
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21181
21295
  deviceId: number(),
21182
21296
  trackId: string(),
21183
- kind: ParkedFrameKindSchema
21297
+ kind: ParkedFrameKindSchema,
21298
+ /**
21299
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21300
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21301
+ * wire — never assume consent: a pre-D462 caller parses the field as
21302
+ * base64 and bytes would decode to garbage rather than fail.
21303
+ */
21304
+ acceptJpegBytes: boolean().optional()
21184
21305
  }), ParkedTrackFrameSchema.nullable()), method(object({
21185
21306
  deviceId: number(),
21186
21307
  trackId: string()
@@ -26395,9 +26516,54 @@ object({
26395
26516
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26396
26517
  lastDetectedAt: number().nullable(),
26397
26518
  /**
26398
- * Ms after which `detected` auto-reverts to false if no fresh push
26399
- * arrives. Null means the provider leaves detected state until a
26400
- * native "clear" event.
26519
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26520
+ * `null` while false and on every `Sensor` device (D475) — see that
26521
+ * constant's doc for the one-authority rule.
26522
+ *
26523
+ * ## Reading this field still arms nothing
26524
+ *
26525
+ * It reads like an instruction to the consumer ("revert after N ms if
26526
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26527
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26528
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26529
+ * imported constant, not as a read of `device.state.motion.value`, so this
26530
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26531
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26532
+ * field would add a second falling-edge authority beside whichever one
26533
+ * already owns the device, and two that can disagree are worse than one.
26534
+ * Consumers that need a falling edge SHAPED differently — held open across
26535
+ * a flapping source — debounce on their own side and say so, as
26536
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26537
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26538
+ *
26539
+ * ## Who writes it
26540
+ *
26541
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26542
+ * `cooldown_expired`, which then writes this slice with
26543
+ * `detected: false` (`handlePhaseChanged` in
26544
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26545
+ * matters most for the sources that only ever push a rising one:
26546
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26547
+ * a false.
26548
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26549
+ * provider pushes the false itself, from the upstream system's own
26550
+ * state change. No phase machine is involved.
26551
+ *
26552
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26553
+ *
26554
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26555
+ * phase machine is the sole writer for a camera. It is not.
26556
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26557
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26558
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26559
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26560
+ * two authorities can disagree about one slice, which this repo
26561
+ * forbids, and the doc said otherwise — which is worse than saying
26562
+ * nothing, because it reads as verification.
26563
+ *
26564
+ * This predates D475 and is not fixed there: the fix touches every
26565
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26566
+ * "sole writer" wording without also removing the other writers.
26401
26567
  */
26402
26568
  autoClearAfterMs: number().nullable()
26403
26569
  });
package/dist/index.mjs CHANGED
@@ -7299,6 +7299,23 @@ function errMsg(err) {
7299
7299
  if (typeof err === "string") return err;
7300
7300
  return String(err);
7301
7301
  }
7302
+ new Set([
7303
+ "track",
7304
+ "summary",
7305
+ "face",
7306
+ "identity",
7307
+ "plate",
7308
+ "vehicle",
7309
+ "scene",
7310
+ "motion",
7311
+ "object",
7312
+ "audio"
7313
+ ]);
7314
+ new Set([
7315
+ "motion",
7316
+ "object",
7317
+ "audio"
7318
+ ]);
7302
7319
  var EncodeProfileSchema = object({
7303
7320
  video: object({
7304
7321
  codec: _enum([
@@ -12434,8 +12451,17 @@ method(object({
12434
12451
  }), array(SettingsRecordSchema).readonly()), method(object({
12435
12452
  namespace: string().optional(),
12436
12453
  collection: string(),
12437
- record: SettingsRecordSchema
12438
- }), _void(), { kind: "mutation" }), method(object({
12454
+ record: object({
12455
+ id: string().optional(),
12456
+ data: record(string(), unknown())
12457
+ })
12458
+ }), object({
12459
+ /**
12460
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12461
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12462
+ * primary key — which is the only place an auto key is knowable.
12463
+ */
12464
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12439
12465
  namespace: string().optional(),
12440
12466
  collection: string(),
12441
12467
  records: array(BulkRecordSchema).readonly()
@@ -12544,8 +12570,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12544
12570
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12545
12571
  namespace: string().optional(),
12546
12572
  collection: string(),
12547
- record: SettingsRecordSchema
12548
- }), _void(), {
12573
+ record: object({
12574
+ id: string().optional(),
12575
+ data: record(string(), unknown())
12576
+ })
12577
+ }), object({
12578
+ /**
12579
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12580
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12581
+ * primary key — which is the only place an auto key is knowable.
12582
+ */
12583
+ id: union([string(), number()]) }), {
12549
12584
  kind: "mutation",
12550
12585
  auth: "admin"
12551
12586
  }), method(object({
@@ -19280,7 +19315,20 @@ var TrackSchema = object({
19280
19315
  ...TrackRetrainFields
19281
19316
  });
19282
19317
  var BaseEventFields = {
19283
- id: string(),
19318
+ /**
19319
+ * A SQLite ROWID, assigned by the database (D474).
19320
+ *
19321
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19322
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19323
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19324
+ * the rowid: the table itself is that B-tree, so the index stops existing
19325
+ * rather than getting smaller. No shorter string does that.
19326
+ *
19327
+ * Defined once here for all three event kinds, which is why they move
19328
+ * together: a per-table migration would have forked this and
19329
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19330
+ */
19331
+ id: number().int(),
19284
19332
  deviceId: number(),
19285
19333
  timestamp: number()
19286
19334
  };
@@ -19299,7 +19347,34 @@ var MotionEventSchema = object({
19299
19347
  /** Omitted in slim projection. */
19300
19348
  frameHeight: number().optional(),
19301
19349
  /** Populated by B5 (recording playback URL for this event). */
19302
- mediaUrl: string().optional()
19350
+ mediaUrl: string().optional(),
19351
+ /**
19352
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19353
+ * episode is still open — a further rising edge extends it in place rather
19354
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19355
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19356
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19357
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19358
+ * motion for an instantaneous trigger).
19359
+ *
19360
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19361
+ * "closed the old way, before this column existed", never "still open".
19362
+ * Nothing in this codebase may read an absent `durationMs` as an open
19363
+ * episode; only `null` means open.
19364
+ */
19365
+ durationMs: number().nullable().optional(),
19366
+ /**
19367
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19368
+ * first entry is always `0`) of every genuine off→on transition the
19369
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19370
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19371
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19372
+ * explicit `false` mid-episode and then resumes before the quiet window
19373
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19374
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19375
+ * which must never be read as "no episode happened here".
19376
+ */
19377
+ edges: array(number()).readonly().optional()
19303
19378
  });
19304
19379
  /**
19305
19380
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19354,6 +19429,23 @@ var ObjectEventSchema = object({
19354
19429
  * includes it (it is light). Absent on rows written before this field.
19355
19430
  */
19356
19431
  frameId: string().optional(),
19432
+ /**
19433
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19434
+ * (D474).
19435
+ *
19436
+ * Only the package detector writes it, and it exists because the event id
19437
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19438
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19439
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19440
+ * assigned by SQLite, so that key had to move off the primary key rather
19441
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19442
+ * every parcel on every boot.
19443
+ *
19444
+ * Absent on every other object event, and on every row written before this
19445
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19446
+ * not per row, and nothing addresses a row by it.
19447
+ */
19448
+ idempotencyKey: string().optional(),
19357
19449
  /** Omitted in slim projection. */
19358
19450
  trackId: string().optional(),
19359
19451
  className: string(),
@@ -20456,6 +20548,14 @@ var NativeCropResultSchema = object({
20456
20548
  * set `encodeJpeg: true`; `bytes` is then absent.
20457
20549
  */
20458
20550
  jpeg: string().optional(),
20551
+ /**
20552
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20553
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20554
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20555
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20556
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20557
+ */
20558
+ jpegBytes: _instanceof(Uint8Array).optional(),
20459
20559
  width: number().int().positive(),
20460
20560
  height: number().int().positive(),
20461
20561
  /**
@@ -20522,7 +20622,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20522
20622
  })]);
20523
20623
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20524
20624
  var ParkedTrackFrameSchema = object({
20525
- jpeg: string(),
20625
+ /**
20626
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20627
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20628
+ * Exactly one of the two is present.
20629
+ */
20630
+ jpeg: string().optional(),
20631
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20632
+ jpegBytes: _instanceof(Uint8Array).optional(),
20526
20633
  width: number().int().positive(),
20527
20634
  height: number().int().positive(),
20528
20635
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21109,6 +21216,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21109
21216
  bbox: NativeCropBboxSchema,
21110
21217
  maxWidth: number().int().positive().optional(),
21111
21218
  /**
21219
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21220
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21221
+ * wire — never assume consent: a pre-D462 caller parses the field as
21222
+ * base64 and bytes would decode to garbage rather than fail.
21223
+ */
21224
+ acceptJpegBytes: boolean().optional(),
21225
+ /**
21112
21226
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21113
21227
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21114
21228
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21176,7 +21290,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21176
21290
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21177
21291
  deviceId: number(),
21178
21292
  trackId: string(),
21179
- kind: ParkedFrameKindSchema
21293
+ kind: ParkedFrameKindSchema,
21294
+ /**
21295
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21296
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21297
+ * wire — never assume consent: a pre-D462 caller parses the field as
21298
+ * base64 and bytes would decode to garbage rather than fail.
21299
+ */
21300
+ acceptJpegBytes: boolean().optional()
21180
21301
  }), ParkedTrackFrameSchema.nullable()), method(object({
21181
21302
  deviceId: number(),
21182
21303
  trackId: string()
@@ -26391,9 +26512,54 @@ object({
26391
26512
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26392
26513
  lastDetectedAt: number().nullable(),
26393
26514
  /**
26394
- * Ms after which `detected` auto-reverts to false if no fresh push
26395
- * arrives. Null means the provider leaves detected state until a
26396
- * native "clear" event.
26515
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26516
+ * `null` while false and on every `Sensor` device (D475) — see that
26517
+ * constant's doc for the one-authority rule.
26518
+ *
26519
+ * ## Reading this field still arms nothing
26520
+ *
26521
+ * It reads like an instruction to the consumer ("revert after N ms if
26522
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26523
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26524
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26525
+ * imported constant, not as a read of `device.state.motion.value`, so this
26526
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26527
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26528
+ * field would add a second falling-edge authority beside whichever one
26529
+ * already owns the device, and two that can disagree are worse than one.
26530
+ * Consumers that need a falling edge SHAPED differently — held open across
26531
+ * a flapping source — debounce on their own side and say so, as
26532
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26533
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26534
+ *
26535
+ * ## Who writes it
26536
+ *
26537
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26538
+ * `cooldown_expired`, which then writes this slice with
26539
+ * `detected: false` (`handlePhaseChanged` in
26540
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26541
+ * matters most for the sources that only ever push a rising one:
26542
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26543
+ * a false.
26544
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26545
+ * provider pushes the false itself, from the upstream system's own
26546
+ * state change. No phase machine is involved.
26547
+ *
26548
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26549
+ *
26550
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26551
+ * phase machine is the sole writer for a camera. It is not.
26552
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26553
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26554
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26555
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26556
+ * two authorities can disagree about one slice, which this repo
26557
+ * forbids, and the doc said otherwise — which is worse than saying
26558
+ * nothing, because it reads as verification.
26559
+ *
26560
+ * This predates D475 and is not fixed there: the fix touches every
26561
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26562
+ * "sole writer" wording without also removing the other writers.
26397
26563
  */
26398
26564
  autoClearAfterMs: number().nullable()
26399
26565
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.96",
3
+ "version": "1.2.98",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",