@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
|
-
*
|
|
52
|
+
* Why this camera's zoom request is not being honoured, or `null` when it asked
|
|
53
|
+
* for nothing.
|
|
29
54
|
*
|
|
30
|
-
* A
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
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
|
|
1586
|
+
fflags: ["+discardcorrupt"],
|
|
1587
1587
|
lowDelay: true,
|
|
1588
1588
|
analyzeDurationUs: ANALYZE_DURATION_US,
|
|
1589
1589
|
probeSizeBytes: PROBE_SIZE_BYTES
|