@camstack/addon-pipeline 1.2.275 → 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 +121 -7
- package/dist/recorder/index.mjs +121 -7
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BrNGbvkl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D0A4Ra4h.mjs} +1 -1
- package/dist/stream-broker/{hostInit-CSJt3rp0.mjs → hostInit-C8TnCvWj.mjs} +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- 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
|
/**
|
|
@@ -13798,6 +13827,86 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
|
|
|
13798
13827
|
visits.push(finishVisit(pieces, endMs));
|
|
13799
13828
|
return visits;
|
|
13800
13829
|
}
|
|
13830
|
+
/**
|
|
13831
|
+
* A visit per BURST OF EVENTS, clamped to the footage that exists behind it.
|
|
13832
|
+
*
|
|
13833
|
+
* ## Why this replaced clustering the availability
|
|
13834
|
+
*
|
|
13835
|
+
* {@link VISIT_MERGE_GAP_MS}'s own docblock states the claim: 30 s "glues
|
|
13836
|
+
* those intra-visit holes without joining visits hours apart". It was applied
|
|
13837
|
+
* to the wrong input. {@link clusterFootageVisits} clusters AVAILABILITY, and
|
|
13838
|
+
* availability only has gaps on an EVENTS-MODE camera, where the writer opens
|
|
13839
|
+
* and closes around activity. A camera that records continuously has none —
|
|
13840
|
+
* so the visit became the recording session.
|
|
13841
|
+
*
|
|
13842
|
+
* Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
|
|
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.
|
|
13859
|
+
*
|
|
13860
|
+
* ## What the footage is still for
|
|
13861
|
+
*
|
|
13862
|
+
* It CLAMPS, it does not define. A visit keeps only the pieces it actually
|
|
13863
|
+
* covers, because {@link stillInstantMs} takes the midpoint of the longest
|
|
13864
|
+
* piece — handed the whole session's range it would put the still hours from
|
|
13865
|
+
* the event. A burst with no footage behind it is dropped: an event the
|
|
13866
|
+
* recorder never kept is not a clip, and listing it is a row that opens onto
|
|
13867
|
+
* nothing.
|
|
13868
|
+
*/
|
|
13869
|
+
function clusterEventVisits(events, ranges, pad) {
|
|
13870
|
+
const gapMs = pad.preMs + pad.postMs;
|
|
13871
|
+
if (events.length === 0 || ranges.length === 0) return [];
|
|
13872
|
+
const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
|
|
13873
|
+
const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
13874
|
+
const bursts = [];
|
|
13875
|
+
let first = times[0];
|
|
13876
|
+
let last = times[0];
|
|
13877
|
+
for (let i = 1; i < times.length; i++) {
|
|
13878
|
+
const t = times[i];
|
|
13879
|
+
if (t - last <= gapMs) {
|
|
13880
|
+
last = t;
|
|
13881
|
+
continue;
|
|
13882
|
+
}
|
|
13883
|
+
bursts.push({
|
|
13884
|
+
startMs: first - pad.preMs,
|
|
13885
|
+
endMs: last + pad.postMs
|
|
13886
|
+
});
|
|
13887
|
+
first = t;
|
|
13888
|
+
last = t;
|
|
13889
|
+
}
|
|
13890
|
+
bursts.push({
|
|
13891
|
+
startMs: first - pad.preMs,
|
|
13892
|
+
endMs: last + pad.postMs
|
|
13893
|
+
});
|
|
13894
|
+
const visits = [];
|
|
13895
|
+
for (const burst of bursts) {
|
|
13896
|
+
const pieces = [];
|
|
13897
|
+
for (const range of sortedRanges) {
|
|
13898
|
+
const startMs = Math.max(range.startMs, burst.startMs);
|
|
13899
|
+
const endMs = Math.min(range.endMs, burst.endMs);
|
|
13900
|
+
if (endMs > startMs) pieces.push({
|
|
13901
|
+
startMs,
|
|
13902
|
+
endMs
|
|
13903
|
+
});
|
|
13904
|
+
}
|
|
13905
|
+
if (pieces.length === 0) continue;
|
|
13906
|
+
visits.push(finishVisit(pieces, pieces[pieces.length - 1].endMs));
|
|
13907
|
+
}
|
|
13908
|
+
return visits;
|
|
13909
|
+
}
|
|
13801
13910
|
function finishVisit(pieces, endMs) {
|
|
13802
13911
|
const holes = [];
|
|
13803
13912
|
for (let i = 1; i < pieces.length; i++) {
|
|
@@ -13972,13 +14081,17 @@ function eventsToClips(events, pad, vouched) {
|
|
|
13972
14081
|
...events.audio.map((e) => make(e, "audio"))
|
|
13973
14082
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
13974
14083
|
}
|
|
13975
|
-
/** Every
|
|
13976
|
-
function
|
|
14084
|
+
/** Every event in the three buckets, in the order the composers tag them. */
|
|
14085
|
+
function allEvents(events) {
|
|
13977
14086
|
return [
|
|
13978
14087
|
...events.motion,
|
|
13979
14088
|
...events.object,
|
|
13980
14089
|
...events.audio
|
|
13981
|
-
]
|
|
14090
|
+
];
|
|
14091
|
+
}
|
|
14092
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
14093
|
+
function allEventIds(events) {
|
|
14094
|
+
return allEvents(events).map((e) => e.id);
|
|
13982
14095
|
}
|
|
13983
14096
|
/**
|
|
13984
14097
|
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
@@ -14040,7 +14153,7 @@ function intersectsWindow(clip, since, until) {
|
|
|
14040
14153
|
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
14041
14154
|
*/
|
|
14042
14155
|
function leftOverhangMs(pad) {
|
|
14043
|
-
return
|
|
14156
|
+
return pad.preMs + pad.postMs + pad.preMs;
|
|
14044
14157
|
}
|
|
14045
14158
|
/** No events at all — what the degraded branch annotates its visits with. */
|
|
14046
14159
|
var NO_EVENTS = {
|
|
@@ -14095,18 +14208,19 @@ function createVideoclipsProvider(deps) {
|
|
|
14095
14208
|
}
|
|
14096
14209
|
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
14097
14210
|
if (deps.fetchAvailability) try {
|
|
14098
|
-
|
|
14211
|
+
const ranges = await deps.fetchAvailability({
|
|
14099
14212
|
deviceId,
|
|
14100
14213
|
since: readSince,
|
|
14101
14214
|
until
|
|
14102
|
-
})
|
|
14215
|
+
});
|
|
14216
|
+
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
|
|
14103
14217
|
} catch (err) {
|
|
14104
14218
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
14105
14219
|
deviceId,
|
|
14106
14220
|
error: err instanceof Error ? err.message : String(err)
|
|
14107
14221
|
});
|
|
14108
14222
|
}
|
|
14109
|
-
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
14223
|
+
const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
|
|
14110
14224
|
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
14111
14225
|
reportUnvouchedClips(deps, deviceId, answer);
|
|
14112
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
|
/**
|
|
@@ -13791,6 +13820,86 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
|
|
|
13791
13820
|
visits.push(finishVisit(pieces, endMs));
|
|
13792
13821
|
return visits;
|
|
13793
13822
|
}
|
|
13823
|
+
/**
|
|
13824
|
+
* A visit per BURST OF EVENTS, clamped to the footage that exists behind it.
|
|
13825
|
+
*
|
|
13826
|
+
* ## Why this replaced clustering the availability
|
|
13827
|
+
*
|
|
13828
|
+
* {@link VISIT_MERGE_GAP_MS}'s own docblock states the claim: 30 s "glues
|
|
13829
|
+
* those intra-visit holes without joining visits hours apart". It was applied
|
|
13830
|
+
* to the wrong input. {@link clusterFootageVisits} clusters AVAILABILITY, and
|
|
13831
|
+
* availability only has gaps on an EVENTS-MODE camera, where the writer opens
|
|
13832
|
+
* and closes around activity. A camera that records continuously has none —
|
|
13833
|
+
* so the visit became the recording session.
|
|
13834
|
+
*
|
|
13835
|
+
* Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
|
|
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.
|
|
13852
|
+
*
|
|
13853
|
+
* ## What the footage is still for
|
|
13854
|
+
*
|
|
13855
|
+
* It CLAMPS, it does not define. A visit keeps only the pieces it actually
|
|
13856
|
+
* covers, because {@link stillInstantMs} takes the midpoint of the longest
|
|
13857
|
+
* piece — handed the whole session's range it would put the still hours from
|
|
13858
|
+
* the event. A burst with no footage behind it is dropped: an event the
|
|
13859
|
+
* recorder never kept is not a clip, and listing it is a row that opens onto
|
|
13860
|
+
* nothing.
|
|
13861
|
+
*/
|
|
13862
|
+
function clusterEventVisits(events, ranges, pad) {
|
|
13863
|
+
const gapMs = pad.preMs + pad.postMs;
|
|
13864
|
+
if (events.length === 0 || ranges.length === 0) return [];
|
|
13865
|
+
const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
|
|
13866
|
+
const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
13867
|
+
const bursts = [];
|
|
13868
|
+
let first = times[0];
|
|
13869
|
+
let last = times[0];
|
|
13870
|
+
for (let i = 1; i < times.length; i++) {
|
|
13871
|
+
const t = times[i];
|
|
13872
|
+
if (t - last <= gapMs) {
|
|
13873
|
+
last = t;
|
|
13874
|
+
continue;
|
|
13875
|
+
}
|
|
13876
|
+
bursts.push({
|
|
13877
|
+
startMs: first - pad.preMs,
|
|
13878
|
+
endMs: last + pad.postMs
|
|
13879
|
+
});
|
|
13880
|
+
first = t;
|
|
13881
|
+
last = t;
|
|
13882
|
+
}
|
|
13883
|
+
bursts.push({
|
|
13884
|
+
startMs: first - pad.preMs,
|
|
13885
|
+
endMs: last + pad.postMs
|
|
13886
|
+
});
|
|
13887
|
+
const visits = [];
|
|
13888
|
+
for (const burst of bursts) {
|
|
13889
|
+
const pieces = [];
|
|
13890
|
+
for (const range of sortedRanges) {
|
|
13891
|
+
const startMs = Math.max(range.startMs, burst.startMs);
|
|
13892
|
+
const endMs = Math.min(range.endMs, burst.endMs);
|
|
13893
|
+
if (endMs > startMs) pieces.push({
|
|
13894
|
+
startMs,
|
|
13895
|
+
endMs
|
|
13896
|
+
});
|
|
13897
|
+
}
|
|
13898
|
+
if (pieces.length === 0) continue;
|
|
13899
|
+
visits.push(finishVisit(pieces, pieces[pieces.length - 1].endMs));
|
|
13900
|
+
}
|
|
13901
|
+
return visits;
|
|
13902
|
+
}
|
|
13794
13903
|
function finishVisit(pieces, endMs) {
|
|
13795
13904
|
const holes = [];
|
|
13796
13905
|
for (let i = 1; i < pieces.length; i++) {
|
|
@@ -13965,13 +14074,17 @@ function eventsToClips(events, pad, vouched) {
|
|
|
13965
14074
|
...events.audio.map((e) => make(e, "audio"))
|
|
13966
14075
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
13967
14076
|
}
|
|
13968
|
-
/** Every
|
|
13969
|
-
function
|
|
14077
|
+
/** Every event in the three buckets, in the order the composers tag them. */
|
|
14078
|
+
function allEvents(events) {
|
|
13970
14079
|
return [
|
|
13971
14080
|
...events.motion,
|
|
13972
14081
|
...events.object,
|
|
13973
14082
|
...events.audio
|
|
13974
|
-
]
|
|
14083
|
+
];
|
|
14084
|
+
}
|
|
14085
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
14086
|
+
function allEventIds(events) {
|
|
14087
|
+
return allEvents(events).map((e) => e.id);
|
|
13975
14088
|
}
|
|
13976
14089
|
/**
|
|
13977
14090
|
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
@@ -14033,7 +14146,7 @@ function intersectsWindow(clip, since, until) {
|
|
|
14033
14146
|
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
14034
14147
|
*/
|
|
14035
14148
|
function leftOverhangMs(pad) {
|
|
14036
|
-
return
|
|
14149
|
+
return pad.preMs + pad.postMs + pad.preMs;
|
|
14037
14150
|
}
|
|
14038
14151
|
/** No events at all — what the degraded branch annotates its visits with. */
|
|
14039
14152
|
var NO_EVENTS = {
|
|
@@ -14088,18 +14201,19 @@ function createVideoclipsProvider(deps) {
|
|
|
14088
14201
|
}
|
|
14089
14202
|
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
14090
14203
|
if (deps.fetchAvailability) try {
|
|
14091
|
-
|
|
14204
|
+
const ranges = await deps.fetchAvailability({
|
|
14092
14205
|
deviceId,
|
|
14093
14206
|
since: readSince,
|
|
14094
14207
|
until
|
|
14095
|
-
})
|
|
14208
|
+
});
|
|
14209
|
+
clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, deps.defaultPad), events, deps.defaultPad, vouched);
|
|
14096
14210
|
} catch (err) {
|
|
14097
14211
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
14098
14212
|
deviceId,
|
|
14099
14213
|
error: err instanceof Error ? err.message : String(err)
|
|
14100
14214
|
});
|
|
14101
14215
|
}
|
|
14102
|
-
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
14216
|
+
const inWindow = applyQuietKinds(clips).filter((c) => intersectsWindow(c, since, until));
|
|
14103
14217
|
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
14104
14218
|
reportUnvouchedClips(deps, deviceId, answer);
|
|
14105
14219
|
return answer;
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D0A4Ra4h.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
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",
|