@camstack/system 1.2.256 → 1.2.258

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.
@@ -1,9 +1,33 @@
1
1
  import { FirstLevelMacroClass, PtzAutotrackSettings, PtzAutotrackStatus } from '@camstack/types';
2
2
  import { AutotrackLoopConfig } from './autotrack-config.js';
3
+ /**
4
+ * How much of the frame the subject SHOULD occupy, and nothing else about zoom.
5
+ *
6
+ * Hidden on purpose (operator, 2026-09-18: *"rendiamo lo zoom configurabile,
7
+ * nascosto per ora, ci lavoriamo poi quando ne vedo il bisogno"*). It is not in
8
+ * `PtzAutotrackSettings` — that schema is the cross-vendor surface the device
9
+ * page renders, and putting it there would SHOW a control nothing implements.
10
+ * It lives in this addon's own record instead: settable, persisted, invisible.
11
+ *
12
+ * `null` means the loop does not touch zoom at all, which is what every camera
13
+ * gets until somebody decides otherwise.
14
+ */
15
+ export interface AutotrackZoomSetting {
16
+ /**
17
+ * Target share of the frame for the tracked subject's box, `0..1`.
18
+ *
19
+ * The operator's original ask was "keep the target at about 50% of frame",
20
+ * so 0.5 is the number this exists to hold. It is a FRACTION OF AREA on the
21
+ * frame the loop measures in, not a zoom level: the loop is declaredly
22
+ * visual and never learns where the optics are (D520).
23
+ */
24
+ readonly targetSubjectFraction: number | null;
25
+ }
3
26
  /** One camera's record. */
4
27
  export interface AutotrackCameraRecord {
5
28
  readonly enabled: boolean;
6
29
  readonly settings: PtzAutotrackSettings;
30
+ readonly zoom: AutotrackZoomSetting;
7
31
  }
8
32
  /** deviceId → record. */
9
33
  export type AutotrackCameraRecords = Readonly<Record<number, AutotrackCameraRecord>>;
@@ -25,18 +49,20 @@ export declare const DEFAULT_CAMERA_RECORD: AutotrackCameraRecord;
25
49
  */
26
50
  export declare const EMULATED_TARGET_OPTIONS: PtzAutotrackStatus['supportedTargetTypes'];
27
51
  /**
28
- * Read the persisted blob.
52
+ * Why this camera's zoom request is not being honoured, or `null` when it asked
53
+ * for nothing.
29
54
  *
30
- * A malformed ENTRY falls back to the defaults and keeps its `enabled` flag —
31
- * it is never dropped. Dropping it would disarm a function the operator armed,
32
- * which is the one direction this must not fail in; a camera that is not in
33
- * the blob at all is simply a camera nobody has switched on.
55
+ * A setting that is read and ignored in silence is indistinguishable from a
56
+ * broken one, and this one WILL be read by a loop that cannot act on it yet.
57
+ * So asking for it produces a sentence naming the number and what is missing.
34
58
  */
59
+ export declare function zoomRefusalFor(record: AutotrackCameraRecord): string | null;
35
60
  export declare function parseCameraRecords(raw: unknown): AutotrackCameraRecords;
36
61
  /** Immutable per-camera patch — a NEW map, the previous one untouched. */
37
62
  export declare function withCameraPatch(records: AutotrackCameraRecords, deviceId: number, patch: {
38
63
  enabled?: boolean;
39
64
  settings?: Partial<PtzAutotrackSettings>;
65
+ zoom?: Partial<AutotrackZoomSetting>;
40
66
  }): AutotrackCameraRecords;
41
67
  /**
42
68
  * The detection macro class this camera follows.
@@ -52,7 +52,8 @@ var DEFAULT_CAMERA_RECORD = {
52
52
  targetType: "",
53
53
  stopDelaySeconds: 30,
54
54
  disappearDelaySeconds: DEFAULT_AUTOTRACK_LOOP_CONFIG.disappearDelayMs / 1e3
55
- }
55
+ },
56
+ zoom: { targetSubjectFraction: null }
56
57
  };
57
58
  /**
58
59
  * What this implementation can follow, as the cap's target options.
@@ -88,6 +89,32 @@ function parseSettings(raw) {
88
89
  * which is the one direction this must not fail in; a camera that is not in
89
90
  * the blob at all is simply a camera nobody has switched on.
90
91
  */
92
+ /**
93
+ * Read the hidden zoom setting.
94
+ *
95
+ * Out-of-range is CLAMPED into the unit interval rather than carried: a control
96
+ * loop that eventually reads this must never be handed a 4, and a silently
97
+ * dropped value would look like "the operator never set it".
98
+ */
99
+ function parseZoom(raw) {
100
+ if (!isRecordObject(raw)) return { targetSubjectFraction: null };
101
+ const value = raw["targetSubjectFraction"];
102
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return { targetSubjectFraction: null };
103
+ return { targetSubjectFraction: Math.min(1, value) };
104
+ }
105
+ /**
106
+ * Why this camera's zoom request is not being honoured, or `null` when it asked
107
+ * for nothing.
108
+ *
109
+ * A setting that is read and ignored in silence is indistinguishable from a
110
+ * broken one, and this one WILL be read by a loop that cannot act on it yet.
111
+ * So asking for it produces a sentence naming the number and what is missing.
112
+ */
113
+ function zoomRefusalFor(record) {
114
+ const target = record.zoom.targetSubjectFraction;
115
+ if (target === null) return null;
116
+ return `autotrack zoom is configured (target subject fraction ${String(target)}) but NOT IMPLEMENTED: the loop drives pan and tilt only. Subject-size control needs a third axis with its own dead band, a rule that it may act only while pan/tilt are settled, and the two D520 measurements first — a stale error costs more the further the optics are zoomed in.`;
117
+ }
91
118
  function parseCameraRecords(raw) {
92
119
  if (!isRecordObject(raw)) return {};
93
120
  const out = {};
@@ -97,7 +124,8 @@ function parseCameraRecords(raw) {
97
124
  if (!isRecordObject(value)) continue;
98
125
  out[deviceId] = {
99
126
  enabled: value["enabled"] === true,
100
- settings: parseSettings(value["settings"])
127
+ settings: parseSettings(value["settings"]),
128
+ zoom: parseZoom(value["zoom"])
101
129
  };
102
130
  }
103
131
  return out;
@@ -112,6 +140,10 @@ function withCameraPatch(records, deviceId, patch) {
112
140
  settings: {
113
141
  ...current.settings,
114
142
  ...patch.settings
143
+ },
144
+ zoom: {
145
+ ...current.zoom,
146
+ ...patch.zoom
115
147
  }
116
148
  }
117
149
  };
@@ -921,6 +953,11 @@ var AutotrackAddon = class extends require_dist.BaseAddon {
921
953
  }
922
954
  });
923
955
  this.ctx.logger.warn("autotrack ON: this camera must not be driven by anyone else", { tags: { deviceId } });
956
+ const zoomRefusal = zoomRefusalFor(record);
957
+ if (zoomRefusal !== null) this.ctx.logger.warn(zoomRefusal, {
958
+ tags: { deviceId },
959
+ meta: { targetSubjectFraction: record.zoom.targetSubjectFraction }
960
+ });
924
961
  }
925
962
  stopSession(deviceId, reason) {
926
963
  const session = this.sessions.get(deviceId);
@@ -47,7 +47,8 @@ var DEFAULT_CAMERA_RECORD = {
47
47
  targetType: "",
48
48
  stopDelaySeconds: 30,
49
49
  disappearDelaySeconds: DEFAULT_AUTOTRACK_LOOP_CONFIG.disappearDelayMs / 1e3
50
- }
50
+ },
51
+ zoom: { targetSubjectFraction: null }
51
52
  };
52
53
  /**
53
54
  * What this implementation can follow, as the cap's target options.
@@ -83,6 +84,32 @@ function parseSettings(raw) {
83
84
  * which is the one direction this must not fail in; a camera that is not in
84
85
  * the blob at all is simply a camera nobody has switched on.
85
86
  */
87
+ /**
88
+ * Read the hidden zoom setting.
89
+ *
90
+ * Out-of-range is CLAMPED into the unit interval rather than carried: a control
91
+ * loop that eventually reads this must never be handed a 4, and a silently
92
+ * dropped value would look like "the operator never set it".
93
+ */
94
+ function parseZoom(raw) {
95
+ if (!isRecordObject(raw)) return { targetSubjectFraction: null };
96
+ const value = raw["targetSubjectFraction"];
97
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return { targetSubjectFraction: null };
98
+ return { targetSubjectFraction: Math.min(1, value) };
99
+ }
100
+ /**
101
+ * Why this camera's zoom request is not being honoured, or `null` when it asked
102
+ * for nothing.
103
+ *
104
+ * A setting that is read and ignored in silence is indistinguishable from a
105
+ * broken one, and this one WILL be read by a loop that cannot act on it yet.
106
+ * So asking for it produces a sentence naming the number and what is missing.
107
+ */
108
+ function zoomRefusalFor(record) {
109
+ const target = record.zoom.targetSubjectFraction;
110
+ if (target === null) return null;
111
+ return `autotrack zoom is configured (target subject fraction ${String(target)}) but NOT IMPLEMENTED: the loop drives pan and tilt only. Subject-size control needs a third axis with its own dead band, a rule that it may act only while pan/tilt are settled, and the two D520 measurements first — a stale error costs more the further the optics are zoomed in.`;
112
+ }
86
113
  function parseCameraRecords(raw) {
87
114
  if (!isRecordObject(raw)) return {};
88
115
  const out = {};
@@ -92,7 +119,8 @@ function parseCameraRecords(raw) {
92
119
  if (!isRecordObject(value)) continue;
93
120
  out[deviceId] = {
94
121
  enabled: value["enabled"] === true,
95
- settings: parseSettings(value["settings"])
122
+ settings: parseSettings(value["settings"]),
123
+ zoom: parseZoom(value["zoom"])
96
124
  };
97
125
  }
98
126
  return out;
@@ -107,6 +135,10 @@ function withCameraPatch(records, deviceId, patch) {
107
135
  settings: {
108
136
  ...current.settings,
109
137
  ...patch.settings
138
+ },
139
+ zoom: {
140
+ ...current.zoom,
141
+ ...patch.zoom
110
142
  }
111
143
  }
112
144
  };
@@ -916,6 +948,11 @@ var AutotrackAddon = class extends BaseAddon {
916
948
  }
917
949
  });
918
950
  this.ctx.logger.warn("autotrack ON: this camera must not be driven by anyone else", { tags: { deviceId } });
951
+ const zoomRefusal = zoomRefusalFor(record);
952
+ if (zoomRefusal !== null) this.ctx.logger.warn(zoomRefusal, {
953
+ tags: { deviceId },
954
+ meta: { targetSubjectFraction: record.zoom.targetSubjectFraction }
955
+ });
919
956
  }
920
957
  stopSession(deviceId, reason) {
921
958
  const session = this.sessions.get(deviceId);
@@ -1588,7 +1588,7 @@ function inputPlanFor(source) {
1588
1588
  return {
1589
1589
  url: source.url,
1590
1590
  rtspTransport: "tcp",
1591
- fflags: ["+discardcorrupt+nobuffer"],
1591
+ fflags: ["+discardcorrupt"],
1592
1592
  lowDelay: true,
1593
1593
  analyzeDurationUs: ANALYZE_DURATION_US,
1594
1594
  probeSizeBytes: PROBE_SIZE_BYTES
@@ -1583,7 +1583,7 @@ function inputPlanFor(source) {
1583
1583
  return {
1584
1584
  url: source.url,
1585
1585
  rtspTransport: "tcp",
1586
- fflags: ["+discardcorrupt+nobuffer"],
1586
+ fflags: ["+discardcorrupt"],
1587
1587
  lowDelay: true,
1588
1588
  analyzeDurationUs: ANALYZE_DURATION_US,
1589
1589
  probeSizeBytes: PROBE_SIZE_BYTES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.256",
3
+ "version": "1.2.258",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",