@trillboards/edge-sdk 0.2.14 → 0.2.15
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.
- package/deploy/windows/README.md +7 -0
- package/deploy/windows/build-windows.ps1 +4 -0
- package/deploy/windows/ensure-sensing.ps1 +58 -0
- package/deploy/windows/install-update.ps1 +32 -6
- package/deploy/windows/stop-existing.ps1 +51 -1
- package/deploy/windows/trillboards-edge.cmd +8 -2
- package/deploy/windows/trillboards-hidden-run.vbs +47 -0
- package/deploy/windows/trillboards-setup.ps1 +25 -0
- package/dist/AudienceAnalyzeClient.d.ts +16 -2
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
- package/dist/AudienceAnalyzeClient.js.map +1 -1
- package/dist/CloudSensingManager.d.ts +149 -5
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +365 -23
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/SceneChangeDetector.d.ts +39 -3
- package/dist/SceneChangeDetector.d.ts.map +1 -1
- package/dist/SceneChangeDetector.js +84 -16
- package/dist/SceneChangeDetector.js.map +1 -1
- package/dist/VlmAdmissionGate.d.ts +14 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -1
- package/dist/VlmAdmissionGate.js +14 -0
- package/dist/VlmAdmissionGate.js.map +1 -1
- package/dist/VlmCandidateBuffer.d.ts +236 -0
- package/dist/VlmCandidateBuffer.d.ts.map +1 -0
- package/dist/VlmCandidateBuffer.js +347 -0
- package/dist/VlmCandidateBuffer.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +65 -0
- package/dist/discovery.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/recorderRanking.d.ts +71 -0
- package/dist/recorderRanking.d.ts.map +1 -0
- package/dist/recorderRanking.js +230 -0
- package/dist/recorderRanking.js.map +1 -0
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +40 -0
- package/dist/sensingRunMode.js.map +1 -1
- package/dist/setupMode.d.ts +9 -0
- package/dist/setupMode.d.ts.map +1 -1
- package/dist/setupMode.js +23 -1
- package/dist/setupMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VlmAdmissionGate.js","sourceRoot":"","sources":["../src/VlmAdmissionGate.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"VlmAdmissionGate.js","sourceRoot":"","sources":["../src/VlmAdmissionGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VlmCandidateBuffer — the bounded hold between the cascade and the admission
|
|
3
|
+
* gate, for frames the cascade has ALREADY decided are interesting.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS, AND WHY IT IS NOT THE QUEUE THE GATE REFUSES.
|
|
6
|
+
* `VlmAdmissionGate` drops rather than queues, and its reason is correct as
|
|
7
|
+
* stated: "by the time a queued frame reached the model the scene has moved
|
|
8
|
+
* on." That is a statement about AMBIENT SAMPLING, where every frame is
|
|
9
|
+
* interchangeable and the newest one is strictly the best one, so holding an
|
|
10
|
+
* old frame is pure loss.
|
|
11
|
+
*
|
|
12
|
+
* Alerting is the other case, and alerting is the product. By the time a frame
|
|
13
|
+
* reaches this buffer the motion cascade has already spent its decode and its
|
|
14
|
+
* detector and concluded THIS frame is different from the scene it had — a
|
|
15
|
+
* person walked in, something fell, an aisle that was empty is not. Those
|
|
16
|
+
* frames are not interchangeable with the next one: at a 30 s scan cadence the
|
|
17
|
+
* next look is a different event, and on a saturated recorder it may be
|
|
18
|
+
* thirteen minutes away. Dropping the one frame that carried the event and
|
|
19
|
+
* later analysing an empty aisle is not "staying fresh"; it is answering a
|
|
20
|
+
* question nobody asked while the one that mattered went unrecorded.
|
|
21
|
+
*
|
|
22
|
+
* So the buffer holds CANDIDATES ONLY. A frame the cascade classified as
|
|
23
|
+
* no-change must never enter — that would rebuild exactly the undifferentiated
|
|
24
|
+
* backlog the gate exists to prevent, and the caller is responsible for that
|
|
25
|
+
* invariant because only the caller has the cascade verdict.
|
|
26
|
+
*
|
|
27
|
+
* THREE BOUNDS, ALL COUNTED. A hold that is unbounded in size is a memory leak
|
|
28
|
+
* and a hold that is unbounded in age is the gate's original complaint. So:
|
|
29
|
+
*
|
|
30
|
+
* - `maxPerFeed` — one busy camera can never hold more than its share, which
|
|
31
|
+
* is what keeps camera 12 from starving the other 31.
|
|
32
|
+
* - `maxFrames` / `maxBytes` — a hard ceiling on the whole buffer, enforced
|
|
33
|
+
* on the way IN. When it binds, the candidate dropped is the OLDEST of the
|
|
34
|
+
* feed that currently holds the MOST, so pressure is paid by whoever is
|
|
35
|
+
* causing it.
|
|
36
|
+
* - `ttlMs` — past this age the gate's insight takes over and the candidate
|
|
37
|
+
* is dropped as expired rather than analysed.
|
|
38
|
+
*
|
|
39
|
+
* Every one of those three drops is COUNTED. A silent eviction is the same
|
|
40
|
+
* defect as the silent drop this class was built to fix — it would move the
|
|
41
|
+
* lost frame from one invisible place to another.
|
|
42
|
+
*
|
|
43
|
+
* SERVICE ORDER is least-recently-served feed first, oldest candidate within
|
|
44
|
+
* that feed. Across feeds that is round-robin, so service cannot be won by
|
|
45
|
+
* whichever timer happens to fire first. Within a feed it is FIFO, because
|
|
46
|
+
* consecutive candidates from one feed are frames of the SAME event and the
|
|
47
|
+
* earliest one is the one that shows the transition — a person entering a
|
|
48
|
+
* restricted area is visible in the frame where they enter, and by the last
|
|
49
|
+
* frame of the burst they may already be out of view.
|
|
50
|
+
*
|
|
51
|
+
* THREADING. Node is single-threaded and no method here awaits, so `peek()`
|
|
52
|
+
* followed by `take()` is one critical section by construction: they are
|
|
53
|
+
* guaranteed to name the same candidate. `CloudSensingManager.pumpCandidates`
|
|
54
|
+
* depends on that — it peeks to learn the feed, acquires an admission slot for
|
|
55
|
+
* it, and only then commits with `take()`.
|
|
56
|
+
*/
|
|
57
|
+
/** One frame the cascade judged worth an inference, waiting for a slot. */
|
|
58
|
+
export interface VlmCandidate {
|
|
59
|
+
/** Feed the frame came from. */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
/** The encoded frame, exactly as the capture path produced it. */
|
|
62
|
+
readonly jpeg: Buffer;
|
|
63
|
+
/** Clock reading when the cascade admitted it as a candidate. */
|
|
64
|
+
readonly enqueuedAtMs: number;
|
|
65
|
+
/** The capture mode the tick would have POSTed with. */
|
|
66
|
+
readonly captureMode: string;
|
|
67
|
+
/** The cascade reason that made this a candidate (diagnostics). */
|
|
68
|
+
readonly reason: string;
|
|
69
|
+
/**
|
|
70
|
+
* The cascade's stationary-suppression key for this frame. Carried rather
|
|
71
|
+
* than recomputed so that when the frame finally reaches the model, THAT
|
|
72
|
+
* frame's baseline is the one committed — a held candidate can be served long
|
|
73
|
+
* after later frames from the same feed were evaluated.
|
|
74
|
+
*/
|
|
75
|
+
readonly signature: string | null;
|
|
76
|
+
}
|
|
77
|
+
export interface VlmCandidateBufferOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Candidates held across ALL feeds. Default 32 — one per feed on the
|
|
80
|
+
* 32-camera recorder this was sized against, so the common case is "every
|
|
81
|
+
* camera may hold its event" and contention only appears when several fire
|
|
82
|
+
* at once.
|
|
83
|
+
*/
|
|
84
|
+
maxFrames?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Candidates one feed may hold. Default 2 — the onset frame plus one
|
|
87
|
+
* follow-up. Motion is temporally clustered, so a third frame of the same
|
|
88
|
+
* burst is a near-duplicate of the second and buys nothing a slot could not.
|
|
89
|
+
*/
|
|
90
|
+
maxPerFeed?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Total encoded bytes held. Default 16 MiB. This is the bound that stays
|
|
93
|
+
* true when the frame size changes: measured on the production CCTV fixtures
|
|
94
|
+
* at `jpegQuality` 70, a 640x480 frame encodes to ~54 KB and a 1920x1080
|
|
95
|
+
* frame to ~199 KB, so 32 candidates is ~1.7 MB at the default capture size
|
|
96
|
+
* and ~6.2 MB at 1080p — the FRAME cap binds first at every realistic
|
|
97
|
+
* resolution, and the byte cap is what keeps that true if it stops being.
|
|
98
|
+
*/
|
|
99
|
+
maxBytes?: number;
|
|
100
|
+
/**
|
|
101
|
+
* How long a candidate stays worth a slot (ms). Default 90000 — see
|
|
102
|
+
* DEFAULT_CANDIDATE_TTL_MS.
|
|
103
|
+
*/
|
|
104
|
+
ttlMs?: number;
|
|
105
|
+
/** Clock injection for tests. Default `Date.now`. */
|
|
106
|
+
now?: () => number;
|
|
107
|
+
}
|
|
108
|
+
export interface VlmCandidateFeedStats {
|
|
109
|
+
name: string;
|
|
110
|
+
/** Candidates this feed is holding right now. */
|
|
111
|
+
buffered: number;
|
|
112
|
+
enqueued: number;
|
|
113
|
+
served: number;
|
|
114
|
+
evicted: number;
|
|
115
|
+
expired: number;
|
|
116
|
+
}
|
|
117
|
+
export interface VlmCandidateBufferStats {
|
|
118
|
+
/** Candidates held right now, across all feeds. */
|
|
119
|
+
buffered: number;
|
|
120
|
+
/** Encoded bytes held right now. */
|
|
121
|
+
bytes: number;
|
|
122
|
+
maxFrames: number;
|
|
123
|
+
maxPerFeed: number;
|
|
124
|
+
maxBytes: number;
|
|
125
|
+
ttlMs: number;
|
|
126
|
+
/** Cumulative candidates accepted. */
|
|
127
|
+
enqueued: number;
|
|
128
|
+
/** Cumulative candidates handed to the model. */
|
|
129
|
+
served: number;
|
|
130
|
+
/** Cumulative candidates dropped to make room — the fairness cost, visible. */
|
|
131
|
+
evicted: number;
|
|
132
|
+
/** Cumulative candidates dropped for being older than `ttlMs`. */
|
|
133
|
+
expired: number;
|
|
134
|
+
/** Age (ms) of the oldest held candidate, or null when empty. */
|
|
135
|
+
oldestAgeMs: number | null;
|
|
136
|
+
perFeed: VlmCandidateFeedStats[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* DEFAULT_CANDIDATE_TTL_MS — how long an alert is still an alert.
|
|
140
|
+
*
|
|
141
|
+
* The admission gate's sentence is right at a long enough horizon: a frame the
|
|
142
|
+
* model looks at ten minutes late describes a scene that no longer exists, and
|
|
143
|
+
* the slot it burns would have been better spent on something current. The
|
|
144
|
+
* only question is where that horizon actually sits, and the answer comes from
|
|
145
|
+
* what the frame is FOR, not from how fresh it is in the abstract.
|
|
146
|
+
*
|
|
147
|
+
* The events this connector exists to catch — a spill, a fall, someone in a
|
|
148
|
+
* restricted area — persist on the order of a minute or two, and the response
|
|
149
|
+
* to one is a person walking over to look. An alert that lands 90 s late still
|
|
150
|
+
* finds the spill on the floor and the person still down, and is worth every
|
|
151
|
+
* bit of the slot it takes. One that lands ten minutes late is a report rather
|
|
152
|
+
* than an alert: either somebody already noticed or nobody is going to.
|
|
153
|
+
*
|
|
154
|
+
* 90 s is also three full 30 s face-triggered cadences, so a candidate
|
|
155
|
+
* survives a couple of rounds of contention on a saturated recorder, and it is
|
|
156
|
+
* well inside the 5 min still-scene heartbeat, so a held candidate can never
|
|
157
|
+
* outlive the feed's own next guaranteed report. The buffer may delay a frame;
|
|
158
|
+
* it can never stand in for the cadence.
|
|
159
|
+
*/
|
|
160
|
+
export declare const DEFAULT_CANDIDATE_TTL_MS = 90000;
|
|
161
|
+
/** Why an `offer()` did not result in a held candidate. */
|
|
162
|
+
export type VlmCandidateOfferResult =
|
|
163
|
+
/** Held. */
|
|
164
|
+
'buffered'
|
|
165
|
+
/** Could not be held at all (a single frame larger than the whole budget). */
|
|
166
|
+
| 'rejected';
|
|
167
|
+
export declare class VlmCandidateBuffer {
|
|
168
|
+
private readonly maxFrames;
|
|
169
|
+
private readonly maxPerFeed;
|
|
170
|
+
private readonly maxBytes;
|
|
171
|
+
private readonly ttlMs;
|
|
172
|
+
private readonly now;
|
|
173
|
+
private readonly feeds;
|
|
174
|
+
private held;
|
|
175
|
+
private heldBytes;
|
|
176
|
+
private enqueued;
|
|
177
|
+
private served;
|
|
178
|
+
private evicted;
|
|
179
|
+
private expired;
|
|
180
|
+
/** Monotonic tie-break so two feeds served in the same millisecond rotate. */
|
|
181
|
+
private serveTick;
|
|
182
|
+
private readonly serveOrder;
|
|
183
|
+
constructor(options?: VlmCandidateBufferOptions);
|
|
184
|
+
getMaxFrames(): number;
|
|
185
|
+
getMaxPerFeed(): number;
|
|
186
|
+
getTtlMs(): number;
|
|
187
|
+
/** Candidates held right now. */
|
|
188
|
+
size(): number;
|
|
189
|
+
/** Encoded bytes held right now. */
|
|
190
|
+
bytes(): number;
|
|
191
|
+
/**
|
|
192
|
+
* Hold one candidate. The CALLER guarantees the cascade classified it as
|
|
193
|
+
* interesting — a no-change frame must never be offered, because a buffer of
|
|
194
|
+
* undifferentiated frames is the queue the admission gate exists to refuse.
|
|
195
|
+
*
|
|
196
|
+
* Never blocks and never grows past the caps: room is made by evicting, and
|
|
197
|
+
* every eviction is counted.
|
|
198
|
+
*/
|
|
199
|
+
offer(candidate: Omit<VlmCandidate, 'enqueuedAtMs'> & {
|
|
200
|
+
enqueuedAtMs?: number;
|
|
201
|
+
}, nowMs?: number): VlmCandidateOfferResult;
|
|
202
|
+
/**
|
|
203
|
+
* The candidate that would be served next, WITHOUT removing it. Least
|
|
204
|
+
* recently served feed, oldest candidate within it.
|
|
205
|
+
*
|
|
206
|
+
* Nothing here awaits, so a `peek()` immediately followed by `take()` is
|
|
207
|
+
* guaranteed to name the same candidate — which is what lets a caller
|
|
208
|
+
* acquire an admission slot for the right feed before committing.
|
|
209
|
+
*/
|
|
210
|
+
peek(nowMs?: number): VlmCandidate | null;
|
|
211
|
+
/** Remove and return the next candidate (same one `peek()` names). */
|
|
212
|
+
take(nowMs?: number): VlmCandidate | null;
|
|
213
|
+
/**
|
|
214
|
+
* Drop every candidate older than `ttlMs`, counting each one. Idempotent and
|
|
215
|
+
* safe to call as often as convenient — it is a scan over a few dozen items.
|
|
216
|
+
*/
|
|
217
|
+
expire(nowMs?: number): number;
|
|
218
|
+
/** Age (ms) of the oldest held candidate, or null when empty. */
|
|
219
|
+
oldestAgeMs(nowMs?: number): number | null;
|
|
220
|
+
/** Candidates this feed is holding right now. */
|
|
221
|
+
bufferedFor(name: string): number;
|
|
222
|
+
/** Drop everything and zero every counter (a stop()/start() cycle is clean). */
|
|
223
|
+
reset(): void;
|
|
224
|
+
getStats(nowMs?: number): VlmCandidateBufferStats;
|
|
225
|
+
private feedQueue;
|
|
226
|
+
/**
|
|
227
|
+
* Least-recently-served feed that is holding something. A feed the buffer has
|
|
228
|
+
* never served outranks one it has, so a camera whose event has been waiting
|
|
229
|
+
* is not overtaken by the camera that was just served.
|
|
230
|
+
*/
|
|
231
|
+
private nextFeed;
|
|
232
|
+
/** Drop the feed that holds the MOST candidates by one. False when empty. */
|
|
233
|
+
private evictFromFullestFeed;
|
|
234
|
+
private evictOldestOf;
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=VlmCandidateBuffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VlmCandidateBuffer.d.ts","sourceRoot":"","sources":["../src/VlmCandidateBuffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* VlmCandidateBuffer — the bounded hold between the cascade and the admission
|
|
4
|
+
* gate, for frames the cascade has ALREADY decided are interesting.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS, AND WHY IT IS NOT THE QUEUE THE GATE REFUSES.
|
|
7
|
+
* `VlmAdmissionGate` drops rather than queues, and its reason is correct as
|
|
8
|
+
* stated: "by the time a queued frame reached the model the scene has moved
|
|
9
|
+
* on." That is a statement about AMBIENT SAMPLING, where every frame is
|
|
10
|
+
* interchangeable and the newest one is strictly the best one, so holding an
|
|
11
|
+
* old frame is pure loss.
|
|
12
|
+
*
|
|
13
|
+
* Alerting is the other case, and alerting is the product. By the time a frame
|
|
14
|
+
* reaches this buffer the motion cascade has already spent its decode and its
|
|
15
|
+
* detector and concluded THIS frame is different from the scene it had — a
|
|
16
|
+
* person walked in, something fell, an aisle that was empty is not. Those
|
|
17
|
+
* frames are not interchangeable with the next one: at a 30 s scan cadence the
|
|
18
|
+
* next look is a different event, and on a saturated recorder it may be
|
|
19
|
+
* thirteen minutes away. Dropping the one frame that carried the event and
|
|
20
|
+
* later analysing an empty aisle is not "staying fresh"; it is answering a
|
|
21
|
+
* question nobody asked while the one that mattered went unrecorded.
|
|
22
|
+
*
|
|
23
|
+
* So the buffer holds CANDIDATES ONLY. A frame the cascade classified as
|
|
24
|
+
* no-change must never enter — that would rebuild exactly the undifferentiated
|
|
25
|
+
* backlog the gate exists to prevent, and the caller is responsible for that
|
|
26
|
+
* invariant because only the caller has the cascade verdict.
|
|
27
|
+
*
|
|
28
|
+
* THREE BOUNDS, ALL COUNTED. A hold that is unbounded in size is a memory leak
|
|
29
|
+
* and a hold that is unbounded in age is the gate's original complaint. So:
|
|
30
|
+
*
|
|
31
|
+
* - `maxPerFeed` — one busy camera can never hold more than its share, which
|
|
32
|
+
* is what keeps camera 12 from starving the other 31.
|
|
33
|
+
* - `maxFrames` / `maxBytes` — a hard ceiling on the whole buffer, enforced
|
|
34
|
+
* on the way IN. When it binds, the candidate dropped is the OLDEST of the
|
|
35
|
+
* feed that currently holds the MOST, so pressure is paid by whoever is
|
|
36
|
+
* causing it.
|
|
37
|
+
* - `ttlMs` — past this age the gate's insight takes over and the candidate
|
|
38
|
+
* is dropped as expired rather than analysed.
|
|
39
|
+
*
|
|
40
|
+
* Every one of those three drops is COUNTED. A silent eviction is the same
|
|
41
|
+
* defect as the silent drop this class was built to fix — it would move the
|
|
42
|
+
* lost frame from one invisible place to another.
|
|
43
|
+
*
|
|
44
|
+
* SERVICE ORDER is least-recently-served feed first, oldest candidate within
|
|
45
|
+
* that feed. Across feeds that is round-robin, so service cannot be won by
|
|
46
|
+
* whichever timer happens to fire first. Within a feed it is FIFO, because
|
|
47
|
+
* consecutive candidates from one feed are frames of the SAME event and the
|
|
48
|
+
* earliest one is the one that shows the transition — a person entering a
|
|
49
|
+
* restricted area is visible in the frame where they enter, and by the last
|
|
50
|
+
* frame of the burst they may already be out of view.
|
|
51
|
+
*
|
|
52
|
+
* THREADING. Node is single-threaded and no method here awaits, so `peek()`
|
|
53
|
+
* followed by `take()` is one critical section by construction: they are
|
|
54
|
+
* guaranteed to name the same candidate. `CloudSensingManager.pumpCandidates`
|
|
55
|
+
* depends on that — it peeks to learn the feed, acquires an admission slot for
|
|
56
|
+
* it, and only then commits with `take()`.
|
|
57
|
+
*/
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.VlmCandidateBuffer = exports.DEFAULT_CANDIDATE_TTL_MS = void 0;
|
|
60
|
+
/**
|
|
61
|
+
* DEFAULT_CANDIDATE_TTL_MS — how long an alert is still an alert.
|
|
62
|
+
*
|
|
63
|
+
* The admission gate's sentence is right at a long enough horizon: a frame the
|
|
64
|
+
* model looks at ten minutes late describes a scene that no longer exists, and
|
|
65
|
+
* the slot it burns would have been better spent on something current. The
|
|
66
|
+
* only question is where that horizon actually sits, and the answer comes from
|
|
67
|
+
* what the frame is FOR, not from how fresh it is in the abstract.
|
|
68
|
+
*
|
|
69
|
+
* The events this connector exists to catch — a spill, a fall, someone in a
|
|
70
|
+
* restricted area — persist on the order of a minute or two, and the response
|
|
71
|
+
* to one is a person walking over to look. An alert that lands 90 s late still
|
|
72
|
+
* finds the spill on the floor and the person still down, and is worth every
|
|
73
|
+
* bit of the slot it takes. One that lands ten minutes late is a report rather
|
|
74
|
+
* than an alert: either somebody already noticed or nobody is going to.
|
|
75
|
+
*
|
|
76
|
+
* 90 s is also three full 30 s face-triggered cadences, so a candidate
|
|
77
|
+
* survives a couple of rounds of contention on a saturated recorder, and it is
|
|
78
|
+
* well inside the 5 min still-scene heartbeat, so a held candidate can never
|
|
79
|
+
* outlive the feed's own next guaranteed report. The buffer may delay a frame;
|
|
80
|
+
* it can never stand in for the cadence.
|
|
81
|
+
*/
|
|
82
|
+
exports.DEFAULT_CANDIDATE_TTL_MS = 90000;
|
|
83
|
+
const DEFAULT_MAX_FRAMES = 32;
|
|
84
|
+
const DEFAULT_MAX_PER_FEED = 2;
|
|
85
|
+
const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
86
|
+
class VlmCandidateBuffer {
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
this.feeds = new Map();
|
|
89
|
+
this.held = 0;
|
|
90
|
+
this.heldBytes = 0;
|
|
91
|
+
this.enqueued = 0;
|
|
92
|
+
this.served = 0;
|
|
93
|
+
this.evicted = 0;
|
|
94
|
+
this.expired = 0;
|
|
95
|
+
/** Monotonic tie-break so two feeds served in the same millisecond rotate. */
|
|
96
|
+
this.serveTick = 0;
|
|
97
|
+
this.serveOrder = new Map();
|
|
98
|
+
this.maxFrames = Math.max(1, Math.trunc(options.maxFrames ?? DEFAULT_MAX_FRAMES));
|
|
99
|
+
// A per-feed cap above the global cap is not a cap; clamp so the two can
|
|
100
|
+
// never disagree about what "one feed's share" means.
|
|
101
|
+
this.maxPerFeed = Math.min(this.maxFrames, Math.max(1, Math.trunc(options.maxPerFeed ?? DEFAULT_MAX_PER_FEED)));
|
|
102
|
+
this.maxBytes = Math.max(1, Math.trunc(options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
103
|
+
this.ttlMs = Math.max(1, Math.trunc(options.ttlMs ?? exports.DEFAULT_CANDIDATE_TTL_MS));
|
|
104
|
+
this.now = options.now ?? Date.now;
|
|
105
|
+
}
|
|
106
|
+
getMaxFrames() {
|
|
107
|
+
return this.maxFrames;
|
|
108
|
+
}
|
|
109
|
+
getMaxPerFeed() {
|
|
110
|
+
return this.maxPerFeed;
|
|
111
|
+
}
|
|
112
|
+
getTtlMs() {
|
|
113
|
+
return this.ttlMs;
|
|
114
|
+
}
|
|
115
|
+
/** Candidates held right now. */
|
|
116
|
+
size() {
|
|
117
|
+
return this.held;
|
|
118
|
+
}
|
|
119
|
+
/** Encoded bytes held right now. */
|
|
120
|
+
bytes() {
|
|
121
|
+
return this.heldBytes;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Hold one candidate. The CALLER guarantees the cascade classified it as
|
|
125
|
+
* interesting — a no-change frame must never be offered, because a buffer of
|
|
126
|
+
* undifferentiated frames is the queue the admission gate exists to refuse.
|
|
127
|
+
*
|
|
128
|
+
* Never blocks and never grows past the caps: room is made by evicting, and
|
|
129
|
+
* every eviction is counted.
|
|
130
|
+
*/
|
|
131
|
+
offer(candidate, nowMs) {
|
|
132
|
+
const now = nowMs ?? this.now();
|
|
133
|
+
this.expire(now);
|
|
134
|
+
const entry = {
|
|
135
|
+
name: candidate.name,
|
|
136
|
+
jpeg: candidate.jpeg,
|
|
137
|
+
captureMode: candidate.captureMode,
|
|
138
|
+
reason: candidate.reason,
|
|
139
|
+
signature: candidate.signature,
|
|
140
|
+
enqueuedAtMs: candidate.enqueuedAtMs ?? now,
|
|
141
|
+
};
|
|
142
|
+
const incomingBytes = entry.jpeg.length;
|
|
143
|
+
// A single frame larger than the entire budget can never be held, and
|
|
144
|
+
// emptying the buffer for it would trade N real candidates for zero. Say
|
|
145
|
+
// no, and count it — an uncounted refusal is the defect this class fixes.
|
|
146
|
+
if (incomingBytes > this.maxBytes) {
|
|
147
|
+
const feed = this.feedQueue(entry.name);
|
|
148
|
+
feed.evicted++;
|
|
149
|
+
this.evicted++;
|
|
150
|
+
return 'rejected';
|
|
151
|
+
}
|
|
152
|
+
const feed = this.feedQueue(entry.name);
|
|
153
|
+
// 1. This feed's own share first, so the eviction lands on the feed that
|
|
154
|
+
// is over its allowance rather than on a quiet neighbour.
|
|
155
|
+
while (feed.items.length >= this.maxPerFeed) {
|
|
156
|
+
this.evictOldestOf(feed);
|
|
157
|
+
}
|
|
158
|
+
// 2. Then the global ceilings. Pressure is paid by whoever holds the most.
|
|
159
|
+
while (this.held + 1 > this.maxFrames ||
|
|
160
|
+
this.heldBytes + incomingBytes > this.maxBytes) {
|
|
161
|
+
if (!this.evictFromFullestFeed())
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
feed.items.push(entry);
|
|
165
|
+
this.held++;
|
|
166
|
+
this.heldBytes += incomingBytes;
|
|
167
|
+
feed.enqueued++;
|
|
168
|
+
this.enqueued++;
|
|
169
|
+
return 'buffered';
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The candidate that would be served next, WITHOUT removing it. Least
|
|
173
|
+
* recently served feed, oldest candidate within it.
|
|
174
|
+
*
|
|
175
|
+
* Nothing here awaits, so a `peek()` immediately followed by `take()` is
|
|
176
|
+
* guaranteed to name the same candidate — which is what lets a caller
|
|
177
|
+
* acquire an admission slot for the right feed before committing.
|
|
178
|
+
*/
|
|
179
|
+
peek(nowMs) {
|
|
180
|
+
this.expire(nowMs ?? this.now());
|
|
181
|
+
const feed = this.nextFeed();
|
|
182
|
+
return feed ? feed.items[0] : null;
|
|
183
|
+
}
|
|
184
|
+
/** Remove and return the next candidate (same one `peek()` names). */
|
|
185
|
+
take(nowMs) {
|
|
186
|
+
const now = nowMs ?? this.now();
|
|
187
|
+
this.expire(now);
|
|
188
|
+
const feed = this.nextFeed();
|
|
189
|
+
if (!feed)
|
|
190
|
+
return null;
|
|
191
|
+
const item = feed.items.shift();
|
|
192
|
+
if (!item)
|
|
193
|
+
return null;
|
|
194
|
+
this.held--;
|
|
195
|
+
this.heldBytes -= item.jpeg.length;
|
|
196
|
+
feed.served++;
|
|
197
|
+
this.served++;
|
|
198
|
+
feed.lastServedAtMs = now;
|
|
199
|
+
this.serveOrder.set(feed.name, ++this.serveTick);
|
|
200
|
+
return item;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Drop every candidate older than `ttlMs`, counting each one. Idempotent and
|
|
204
|
+
* safe to call as often as convenient — it is a scan over a few dozen items.
|
|
205
|
+
*/
|
|
206
|
+
expire(nowMs) {
|
|
207
|
+
const now = nowMs ?? this.now();
|
|
208
|
+
let dropped = 0;
|
|
209
|
+
for (const feed of this.feeds.values()) {
|
|
210
|
+
while (feed.items.length > 0 && now - feed.items[0].enqueuedAtMs > this.ttlMs) {
|
|
211
|
+
const item = feed.items.shift();
|
|
212
|
+
this.held--;
|
|
213
|
+
this.heldBytes -= item.jpeg.length;
|
|
214
|
+
feed.expired++;
|
|
215
|
+
this.expired++;
|
|
216
|
+
dropped++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return dropped;
|
|
220
|
+
}
|
|
221
|
+
/** Age (ms) of the oldest held candidate, or null when empty. */
|
|
222
|
+
oldestAgeMs(nowMs) {
|
|
223
|
+
const now = nowMs ?? this.now();
|
|
224
|
+
let oldest = null;
|
|
225
|
+
for (const feed of this.feeds.values()) {
|
|
226
|
+
const head = feed.items[0];
|
|
227
|
+
if (!head)
|
|
228
|
+
continue;
|
|
229
|
+
const age = Math.max(0, now - head.enqueuedAtMs);
|
|
230
|
+
if (oldest === null || age > oldest)
|
|
231
|
+
oldest = age;
|
|
232
|
+
}
|
|
233
|
+
return oldest;
|
|
234
|
+
}
|
|
235
|
+
/** Candidates this feed is holding right now. */
|
|
236
|
+
bufferedFor(name) {
|
|
237
|
+
return this.feeds.get(name)?.items.length ?? 0;
|
|
238
|
+
}
|
|
239
|
+
/** Drop everything and zero every counter (a stop()/start() cycle is clean). */
|
|
240
|
+
reset() {
|
|
241
|
+
this.feeds.clear();
|
|
242
|
+
this.serveOrder.clear();
|
|
243
|
+
this.held = 0;
|
|
244
|
+
this.heldBytes = 0;
|
|
245
|
+
this.enqueued = 0;
|
|
246
|
+
this.served = 0;
|
|
247
|
+
this.evicted = 0;
|
|
248
|
+
this.expired = 0;
|
|
249
|
+
this.serveTick = 0;
|
|
250
|
+
}
|
|
251
|
+
getStats(nowMs) {
|
|
252
|
+
const now = nowMs ?? this.now();
|
|
253
|
+
this.expire(now);
|
|
254
|
+
return {
|
|
255
|
+
buffered: this.held,
|
|
256
|
+
bytes: this.heldBytes,
|
|
257
|
+
maxFrames: this.maxFrames,
|
|
258
|
+
maxPerFeed: this.maxPerFeed,
|
|
259
|
+
maxBytes: this.maxBytes,
|
|
260
|
+
ttlMs: this.ttlMs,
|
|
261
|
+
enqueued: this.enqueued,
|
|
262
|
+
served: this.served,
|
|
263
|
+
evicted: this.evicted,
|
|
264
|
+
expired: this.expired,
|
|
265
|
+
oldestAgeMs: this.oldestAgeMs(now),
|
|
266
|
+
perFeed: [...this.feeds.values()].map((f) => ({
|
|
267
|
+
name: f.name,
|
|
268
|
+
buffered: f.items.length,
|
|
269
|
+
enqueued: f.enqueued,
|
|
270
|
+
served: f.served,
|
|
271
|
+
evicted: f.evicted,
|
|
272
|
+
expired: f.expired,
|
|
273
|
+
})),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// ─── Private ────────────────────────────────────────────────────────────────
|
|
277
|
+
feedQueue(name) {
|
|
278
|
+
let feed = this.feeds.get(name);
|
|
279
|
+
if (!feed) {
|
|
280
|
+
feed = {
|
|
281
|
+
name,
|
|
282
|
+
items: [],
|
|
283
|
+
lastServedAtMs: null,
|
|
284
|
+
enqueued: 0,
|
|
285
|
+
served: 0,
|
|
286
|
+
evicted: 0,
|
|
287
|
+
expired: 0,
|
|
288
|
+
};
|
|
289
|
+
this.feeds.set(name, feed);
|
|
290
|
+
}
|
|
291
|
+
return feed;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Least-recently-served feed that is holding something. A feed the buffer has
|
|
295
|
+
* never served outranks one it has, so a camera whose event has been waiting
|
|
296
|
+
* is not overtaken by the camera that was just served.
|
|
297
|
+
*/
|
|
298
|
+
nextFeed() {
|
|
299
|
+
let best = null;
|
|
300
|
+
let bestOrder = Infinity;
|
|
301
|
+
let bestOldest = Infinity;
|
|
302
|
+
for (const feed of this.feeds.values()) {
|
|
303
|
+
const head = feed.items[0];
|
|
304
|
+
if (!head)
|
|
305
|
+
continue;
|
|
306
|
+
// Never served ⇒ order 0, which sorts ahead of every served feed.
|
|
307
|
+
const order = this.serveOrder.get(feed.name) ?? 0;
|
|
308
|
+
if (order < bestOrder || (order === bestOrder && head.enqueuedAtMs < bestOldest)) {
|
|
309
|
+
best = feed;
|
|
310
|
+
bestOrder = order;
|
|
311
|
+
bestOldest = head.enqueuedAtMs;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return best;
|
|
315
|
+
}
|
|
316
|
+
/** Drop the feed that holds the MOST candidates by one. False when empty. */
|
|
317
|
+
evictFromFullestFeed() {
|
|
318
|
+
let victim = null;
|
|
319
|
+
for (const feed of this.feeds.values()) {
|
|
320
|
+
if (feed.items.length === 0)
|
|
321
|
+
continue;
|
|
322
|
+
if (victim === null ||
|
|
323
|
+
feed.items.length > victim.items.length ||
|
|
324
|
+
// Same depth ⇒ the one whose head is oldest, so ties are broken toward
|
|
325
|
+
// the frame closest to expiring anyway.
|
|
326
|
+
(feed.items.length === victim.items.length &&
|
|
327
|
+
feed.items[0].enqueuedAtMs < victim.items[0].enqueuedAtMs)) {
|
|
328
|
+
victim = feed;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!victim)
|
|
332
|
+
return false;
|
|
333
|
+
this.evictOldestOf(victim);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
evictOldestOf(feed) {
|
|
337
|
+
const item = feed.items.shift();
|
|
338
|
+
if (!item)
|
|
339
|
+
return;
|
|
340
|
+
this.held--;
|
|
341
|
+
this.heldBytes -= item.jpeg.length;
|
|
342
|
+
feed.evicted++;
|
|
343
|
+
this.evicted++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
exports.VlmCandidateBuffer = VlmCandidateBuffer;
|
|
347
|
+
//# sourceMappingURL=VlmCandidateBuffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VlmCandidateBuffer.js","sourceRoot":"","sources":["../src/VlmCandidateBuffer.ts"],"names":[],"mappings":"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"}
|