@camstack/addon-pipeline 1.2.276 → 1.2.279
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/dist/recorder/index.js +50 -8
- package/dist/recorder/index.mjs +50 -8
- package/package.json +1 -1
package/dist/recorder/index.js
CHANGED
|
@@ -13763,6 +13763,35 @@ var PlacementService = class {
|
|
|
13763
13763
|
});
|
|
13764
13764
|
}
|
|
13765
13765
|
};
|
|
13766
|
+
/** How long each kind stays quiet after it has been listed. Absent = no bound. */
|
|
13767
|
+
var QUIET_BY_KIND = {
|
|
13768
|
+
audio: 5 * 6e4,
|
|
13769
|
+
motion: 2 * 6e4
|
|
13770
|
+
};
|
|
13771
|
+
/**
|
|
13772
|
+
* Thin a run of same-kind rows to one per cooldown.
|
|
13773
|
+
*
|
|
13774
|
+
* The DECISION is made oldest-first, because a cooldown read off an unsorted
|
|
13775
|
+
* list keeps an arbitrary subset. The ANSWER keeps the caller's own order:
|
|
13776
|
+
* this provider lists newest-first, and quietly reversing it here would have
|
|
13777
|
+
* turned a density filter into a reordering — caught by the spec that pins
|
|
13778
|
+
* `clips[0].kind`, which is the only reason it is not shipping.
|
|
13779
|
+
*/
|
|
13780
|
+
function applyQuietKinds(clips) {
|
|
13781
|
+
const lastEmitted = /* @__PURE__ */ new Map();
|
|
13782
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
13783
|
+
for (const clip of [...clips].toSorted((a, b) => a.timeRange.startMs - b.timeRange.startMs)) {
|
|
13784
|
+
const quietMs = QUIET_BY_KIND[clip.kind];
|
|
13785
|
+
if (quietMs === void 0) continue;
|
|
13786
|
+
const previous = lastEmitted.get(clip.kind);
|
|
13787
|
+
if (previous !== void 0 && clip.timeRange.startMs - previous < quietMs) {
|
|
13788
|
+
dropped.add(clip.id);
|
|
13789
|
+
continue;
|
|
13790
|
+
}
|
|
13791
|
+
lastEmitted.set(clip.kind, clip.timeRange.startMs);
|
|
13792
|
+
}
|
|
13793
|
+
return clips.filter((clip) => !dropped.has(clip.id));
|
|
13794
|
+
}
|
|
13766
13795
|
//#endregion
|
|
13767
13796
|
//#region src/recorder/addon/videoclip-visits.ts
|
|
13768
13797
|
/**
|
|
@@ -13811,10 +13840,22 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
|
|
|
13811
13840
|
* so the visit became the recording session.
|
|
13812
13841
|
*
|
|
13813
13842
|
* Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
|
|
13814
|
-
* them 7 h 58 m.
|
|
13815
|
-
*
|
|
13816
|
-
*
|
|
13817
|
-
*
|
|
13843
|
+
* them 7 h 58 m.
|
|
13844
|
+
*
|
|
13845
|
+
* ## And the grain is the PADDING, not a constant
|
|
13846
|
+
*
|
|
13847
|
+
* `VISIT_MERGE_GAP_MS` (30 s) was arbitrary here and sat on a cliff. 592's
|
|
13848
|
+
* worst hour holds 492 events — 150 object, 98 motion, **244 audio** — whose
|
|
13849
|
+
* LARGEST gap is 25 s, so nothing ever reached 30 and the hour was one row.
|
|
13850
|
+
* Measured on that hour:
|
|
13851
|
+
*
|
|
13852
|
+
* | rule | clips | longest |
|
|
13853
|
+
* | --- | --- | --- |
|
|
13854
|
+
* | 30 s | 1 | 66 min |
|
|
13855
|
+
* | `preMs + postMs` (15 s) | 105 | 3.5 min |
|
|
13856
|
+
*
|
|
13857
|
+
* So two clips merge exactly when they would otherwise OVERLAP. Nothing to
|
|
13858
|
+
* tune, nothing to disagree with the pad.
|
|
13818
13859
|
*
|
|
13819
13860
|
* ## What the footage is still for
|
|
13820
13861
|
*
|
|
@@ -13825,7 +13866,8 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
|
|
|
13825
13866
|
* recorder never kept is not a clip, and listing it is a row that opens onto
|
|
13826
13867
|
* nothing.
|
|
13827
13868
|
*/
|
|
13828
|
-
function clusterEventVisits(events, ranges,
|
|
13869
|
+
function clusterEventVisits(events, ranges, pad) {
|
|
13870
|
+
const gapMs = pad.preMs + pad.postMs;
|
|
13829
13871
|
if (events.length === 0 || ranges.length === 0) return [];
|
|
13830
13872
|
const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
|
|
13831
13873
|
const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
@@ -14111,7 +14153,7 @@ function intersectsWindow(clip, since, until) {
|
|
|
14111
14153
|
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
14112
14154
|
*/
|
|
14113
14155
|
function leftOverhangMs(pad) {
|
|
14114
|
-
return
|
|
14156
|
+
return pad.preMs + pad.postMs + pad.preMs;
|
|
14115
14157
|
}
|
|
14116
14158
|
/** No events at all — what the degraded branch annotates its visits with. */
|
|
14117
14159
|
var NO_EVENTS = {
|
|
@@ -14171,14 +14213,14 @@ function createVideoclipsProvider(deps) {
|
|
|
14171
14213
|
since: readSince,
|
|
14172
14214
|
until
|
|
14173
14215
|
});
|
|
14174
|
-
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges,
|
|
14216
|
+
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
|
|
14175
14217
|
} catch (err) {
|
|
14176
14218
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
14177
14219
|
deviceId,
|
|
14178
14220
|
error: err instanceof Error ? err.message : String(err)
|
|
14179
14221
|
});
|
|
14180
14222
|
}
|
|
14181
|
-
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
14223
|
+
const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
|
|
14182
14224
|
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
14183
14225
|
reportUnvouchedClips(deps, deviceId, answer);
|
|
14184
14226
|
return answer;
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -13756,6 +13756,35 @@ var PlacementService = class {
|
|
|
13756
13756
|
});
|
|
13757
13757
|
}
|
|
13758
13758
|
};
|
|
13759
|
+
/** How long each kind stays quiet after it has been listed. Absent = no bound. */
|
|
13760
|
+
var QUIET_BY_KIND = {
|
|
13761
|
+
audio: 5 * 6e4,
|
|
13762
|
+
motion: 2 * 6e4
|
|
13763
|
+
};
|
|
13764
|
+
/**
|
|
13765
|
+
* Thin a run of same-kind rows to one per cooldown.
|
|
13766
|
+
*
|
|
13767
|
+
* The DECISION is made oldest-first, because a cooldown read off an unsorted
|
|
13768
|
+
* list keeps an arbitrary subset. The ANSWER keeps the caller's own order:
|
|
13769
|
+
* this provider lists newest-first, and quietly reversing it here would have
|
|
13770
|
+
* turned a density filter into a reordering — caught by the spec that pins
|
|
13771
|
+
* `clips[0].kind`, which is the only reason it is not shipping.
|
|
13772
|
+
*/
|
|
13773
|
+
function applyQuietKinds(clips) {
|
|
13774
|
+
const lastEmitted = /* @__PURE__ */ new Map();
|
|
13775
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
13776
|
+
for (const clip of [...clips].toSorted((a, b) => a.timeRange.startMs - b.timeRange.startMs)) {
|
|
13777
|
+
const quietMs = QUIET_BY_KIND[clip.kind];
|
|
13778
|
+
if (quietMs === void 0) continue;
|
|
13779
|
+
const previous = lastEmitted.get(clip.kind);
|
|
13780
|
+
if (previous !== void 0 && clip.timeRange.startMs - previous < quietMs) {
|
|
13781
|
+
dropped.add(clip.id);
|
|
13782
|
+
continue;
|
|
13783
|
+
}
|
|
13784
|
+
lastEmitted.set(clip.kind, clip.timeRange.startMs);
|
|
13785
|
+
}
|
|
13786
|
+
return clips.filter((clip) => !dropped.has(clip.id));
|
|
13787
|
+
}
|
|
13759
13788
|
//#endregion
|
|
13760
13789
|
//#region src/recorder/addon/videoclip-visits.ts
|
|
13761
13790
|
/**
|
|
@@ -13804,10 +13833,22 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
|
|
|
13804
13833
|
* so the visit became the recording session.
|
|
13805
13834
|
*
|
|
13806
13835
|
* Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
|
|
13807
|
-
* them 7 h 58 m.
|
|
13808
|
-
*
|
|
13809
|
-
*
|
|
13810
|
-
*
|
|
13836
|
+
* them 7 h 58 m.
|
|
13837
|
+
*
|
|
13838
|
+
* ## And the grain is the PADDING, not a constant
|
|
13839
|
+
*
|
|
13840
|
+
* `VISIT_MERGE_GAP_MS` (30 s) was arbitrary here and sat on a cliff. 592's
|
|
13841
|
+
* worst hour holds 492 events — 150 object, 98 motion, **244 audio** — whose
|
|
13842
|
+
* LARGEST gap is 25 s, so nothing ever reached 30 and the hour was one row.
|
|
13843
|
+
* Measured on that hour:
|
|
13844
|
+
*
|
|
13845
|
+
* | rule | clips | longest |
|
|
13846
|
+
* | --- | --- | --- |
|
|
13847
|
+
* | 30 s | 1 | 66 min |
|
|
13848
|
+
* | `preMs + postMs` (15 s) | 105 | 3.5 min |
|
|
13849
|
+
*
|
|
13850
|
+
* So two clips merge exactly when they would otherwise OVERLAP. Nothing to
|
|
13851
|
+
* tune, nothing to disagree with the pad.
|
|
13811
13852
|
*
|
|
13812
13853
|
* ## What the footage is still for
|
|
13813
13854
|
*
|
|
@@ -13818,7 +13859,8 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
|
|
|
13818
13859
|
* recorder never kept is not a clip, and listing it is a row that opens onto
|
|
13819
13860
|
* nothing.
|
|
13820
13861
|
*/
|
|
13821
|
-
function clusterEventVisits(events, ranges,
|
|
13862
|
+
function clusterEventVisits(events, ranges, pad) {
|
|
13863
|
+
const gapMs = pad.preMs + pad.postMs;
|
|
13822
13864
|
if (events.length === 0 || ranges.length === 0) return [];
|
|
13823
13865
|
const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
|
|
13824
13866
|
const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
@@ -14104,7 +14146,7 @@ function intersectsWindow(clip, since, until) {
|
|
|
14104
14146
|
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
14105
14147
|
*/
|
|
14106
14148
|
function leftOverhangMs(pad) {
|
|
14107
|
-
return
|
|
14149
|
+
return pad.preMs + pad.postMs + pad.preMs;
|
|
14108
14150
|
}
|
|
14109
14151
|
/** No events at all — what the degraded branch annotates its visits with. */
|
|
14110
14152
|
var NO_EVENTS = {
|
|
@@ -14164,14 +14206,14 @@ function createVideoclipsProvider(deps) {
|
|
|
14164
14206
|
since: readSince,
|
|
14165
14207
|
until
|
|
14166
14208
|
});
|
|
14167
|
-
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges,
|
|
14209
|
+
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
|
|
14168
14210
|
} catch (err) {
|
|
14169
14211
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
14170
14212
|
deviceId,
|
|
14171
14213
|
error: err instanceof Error ? err.message : String(err)
|
|
14172
14214
|
});
|
|
14173
14215
|
}
|
|
14174
|
-
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
14216
|
+
const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
|
|
14175
14217
|
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
14176
14218
|
reportUnvouchedClips(deps, deviceId, answer);
|
|
14177
14219
|
return answer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.279",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|