@camstack/addon-pipeline 1.2.275 → 1.2.276

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.
@@ -13798,6 +13798,73 @@ function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
13798
13798
  visits.push(finishVisit(pieces, endMs));
13799
13799
  return visits;
13800
13800
  }
13801
+ /**
13802
+ * A visit per BURST OF EVENTS, clamped to the footage that exists behind it.
13803
+ *
13804
+ * ## Why this replaced clustering the availability
13805
+ *
13806
+ * {@link VISIT_MERGE_GAP_MS}'s own docblock states the claim: 30 s "glues
13807
+ * those intra-visit holes without joining visits hours apart". It was applied
13808
+ * to the wrong input. {@link clusterFootageVisits} clusters AVAILABILITY, and
13809
+ * availability only has gaps on an EVENTS-MODE camera, where the writer opens
13810
+ * and closes around activity. A camera that records continuously has none —
13811
+ * so the visit became the recording session.
13812
+ *
13813
+ * Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
13814
+ * them 7 h 58 m. Clustering the same day's EVENTS at the same 30 s gives 46
13815
+ * visits overnight and 36 in the day, median 17 s, longest 99 s. The constant
13816
+ * was right; the input was not. No duration cap: it would split by the clock
13817
+ * rather than by what happened, and nothing here needs splitting that way.
13818
+ *
13819
+ * ## What the footage is still for
13820
+ *
13821
+ * It CLAMPS, it does not define. A visit keeps only the pieces it actually
13822
+ * covers, because {@link stillInstantMs} takes the midpoint of the longest
13823
+ * piece — handed the whole session's range it would put the still hours from
13824
+ * the event. A burst with no footage behind it is dropped: an event the
13825
+ * recorder never kept is not a clip, and listing it is a row that opens onto
13826
+ * nothing.
13827
+ */
13828
+ function clusterEventVisits(events, ranges, gapMs, pad) {
13829
+ if (events.length === 0 || ranges.length === 0) return [];
13830
+ const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
13831
+ const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
13832
+ const bursts = [];
13833
+ let first = times[0];
13834
+ let last = times[0];
13835
+ for (let i = 1; i < times.length; i++) {
13836
+ const t = times[i];
13837
+ if (t - last <= gapMs) {
13838
+ last = t;
13839
+ continue;
13840
+ }
13841
+ bursts.push({
13842
+ startMs: first - pad.preMs,
13843
+ endMs: last + pad.postMs
13844
+ });
13845
+ first = t;
13846
+ last = t;
13847
+ }
13848
+ bursts.push({
13849
+ startMs: first - pad.preMs,
13850
+ endMs: last + pad.postMs
13851
+ });
13852
+ const visits = [];
13853
+ for (const burst of bursts) {
13854
+ const pieces = [];
13855
+ for (const range of sortedRanges) {
13856
+ const startMs = Math.max(range.startMs, burst.startMs);
13857
+ const endMs = Math.min(range.endMs, burst.endMs);
13858
+ if (endMs > startMs) pieces.push({
13859
+ startMs,
13860
+ endMs
13861
+ });
13862
+ }
13863
+ if (pieces.length === 0) continue;
13864
+ visits.push(finishVisit(pieces, pieces[pieces.length - 1].endMs));
13865
+ }
13866
+ return visits;
13867
+ }
13801
13868
  function finishVisit(pieces, endMs) {
13802
13869
  const holes = [];
13803
13870
  for (let i = 1; i < pieces.length; i++) {
@@ -13972,13 +14039,17 @@ function eventsToClips(events, pad, vouched) {
13972
14039
  ...events.audio.map((e) => make(e, "audio"))
13973
14040
  ].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
13974
14041
  }
13975
- /** Every id in the three buckets, in the order the composers tag them. */
13976
- function allEventIds(events) {
14042
+ /** Every event in the three buckets, in the order the composers tag them. */
14043
+ function allEvents(events) {
13977
14044
  return [
13978
14045
  ...events.motion,
13979
14046
  ...events.object,
13980
14047
  ...events.audio
13981
- ].map((e) => e.id);
14048
+ ];
14049
+ }
14050
+ /** Every id in the three buckets, in the order the composers tag them. */
14051
+ function allEventIds(events) {
14052
+ return allEvents(events).map((e) => e.id);
13982
14053
  }
13983
14054
  /**
13984
14055
  * Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
@@ -14095,11 +14166,12 @@ function createVideoclipsProvider(deps) {
14095
14166
  }
14096
14167
  let clips = eventsToClips(events, deps.defaultPad, vouched);
14097
14168
  if (deps.fetchAvailability) try {
14098
- clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
14169
+ const ranges = await deps.fetchAvailability({
14099
14170
  deviceId,
14100
14171
  since: readSince,
14101
14172
  until
14102
- }), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
14173
+ });
14174
+ clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, require_dist.VISIT_MERGE_GAP_MS, deps.defaultPad), events, deps.defaultPad, vouched);
14103
14175
  } catch (err) {
14104
14176
  deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
14105
14177
  deviceId,
@@ -13791,6 +13791,73 @@ function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
13791
13791
  visits.push(finishVisit(pieces, endMs));
13792
13792
  return visits;
13793
13793
  }
13794
+ /**
13795
+ * A visit per BURST OF EVENTS, clamped to the footage that exists behind it.
13796
+ *
13797
+ * ## Why this replaced clustering the availability
13798
+ *
13799
+ * {@link VISIT_MERGE_GAP_MS}'s own docblock states the claim: 30 s "glues
13800
+ * those intra-visit holes without joining visits hours apart". It was applied
13801
+ * to the wrong input. {@link clusterFootageVisits} clusters AVAILABILITY, and
13802
+ * availability only has gaps on an EVENTS-MODE camera, where the writer opens
13803
+ * and closes around activity. A camera that records continuously has none —
13804
+ * so the visit became the recording session.
13805
+ *
13806
+ * Measured on 592, 2026-09-21, 24 h: seven rows covering the whole day, one of
13807
+ * them 7 h 58 m. Clustering the same day's EVENTS at the same 30 s gives 46
13808
+ * visits overnight and 36 in the day, median 17 s, longest 99 s. The constant
13809
+ * was right; the input was not. No duration cap: it would split by the clock
13810
+ * rather than by what happened, and nothing here needs splitting that way.
13811
+ *
13812
+ * ## What the footage is still for
13813
+ *
13814
+ * It CLAMPS, it does not define. A visit keeps only the pieces it actually
13815
+ * covers, because {@link stillInstantMs} takes the midpoint of the longest
13816
+ * piece — handed the whole session's range it would put the still hours from
13817
+ * the event. A burst with no footage behind it is dropped: an event the
13818
+ * recorder never kept is not a clip, and listing it is a row that opens onto
13819
+ * nothing.
13820
+ */
13821
+ function clusterEventVisits(events, ranges, gapMs, pad) {
13822
+ if (events.length === 0 || ranges.length === 0) return [];
13823
+ const times = [...events].map((e) => e.timestamp).toSorted((a, b) => a - b);
13824
+ const sortedRanges = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
13825
+ const bursts = [];
13826
+ let first = times[0];
13827
+ let last = times[0];
13828
+ for (let i = 1; i < times.length; i++) {
13829
+ const t = times[i];
13830
+ if (t - last <= gapMs) {
13831
+ last = t;
13832
+ continue;
13833
+ }
13834
+ bursts.push({
13835
+ startMs: first - pad.preMs,
13836
+ endMs: last + pad.postMs
13837
+ });
13838
+ first = t;
13839
+ last = t;
13840
+ }
13841
+ bursts.push({
13842
+ startMs: first - pad.preMs,
13843
+ endMs: last + pad.postMs
13844
+ });
13845
+ const visits = [];
13846
+ for (const burst of bursts) {
13847
+ const pieces = [];
13848
+ for (const range of sortedRanges) {
13849
+ const startMs = Math.max(range.startMs, burst.startMs);
13850
+ const endMs = Math.min(range.endMs, burst.endMs);
13851
+ if (endMs > startMs) pieces.push({
13852
+ startMs,
13853
+ endMs
13854
+ });
13855
+ }
13856
+ if (pieces.length === 0) continue;
13857
+ visits.push(finishVisit(pieces, pieces[pieces.length - 1].endMs));
13858
+ }
13859
+ return visits;
13860
+ }
13794
13861
  function finishVisit(pieces, endMs) {
13795
13862
  const holes = [];
13796
13863
  for (let i = 1; i < pieces.length; i++) {
@@ -13965,13 +14032,17 @@ function eventsToClips(events, pad, vouched) {
13965
14032
  ...events.audio.map((e) => make(e, "audio"))
13966
14033
  ].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
13967
14034
  }
13968
- /** Every id in the three buckets, in the order the composers tag them. */
13969
- function allEventIds(events) {
14035
+ /** Every event in the three buckets, in the order the composers tag them. */
14036
+ function allEvents(events) {
13970
14037
  return [
13971
14038
  ...events.motion,
13972
14039
  ...events.object,
13973
14040
  ...events.audio
13974
- ].map((e) => e.id);
14041
+ ];
14042
+ }
14043
+ /** Every id in the three buckets, in the order the composers tag them. */
14044
+ function allEventIds(events) {
14045
+ return allEvents(events).map((e) => e.id);
13975
14046
  }
13976
14047
  /**
13977
14048
  * Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
@@ -14088,11 +14159,12 @@ function createVideoclipsProvider(deps) {
14088
14159
  }
14089
14160
  let clips = eventsToClips(events, deps.defaultPad, vouched);
14090
14161
  if (deps.fetchAvailability) try {
14091
- clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
14162
+ const ranges = await deps.fetchAvailability({
14092
14163
  deviceId,
14093
14164
  since: readSince,
14094
14165
  until
14095
- }), VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
14166
+ });
14167
+ clips = visitsToClips(clusterEventVisits(allEvents(events), ranges, VISIT_MERGE_GAP_MS, deps.defaultPad), events, deps.defaultPad, vouched);
14096
14168
  } catch (err) {
14097
14169
  deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
14098
14170
  deviceId,
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.194",
36
+ version: "1.2.195",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.194",
84
+ version: "1.2.195",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-BrNGbvkl.mjs")).catch((e) => {
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.275",
3
+ "version": "1.2.276",
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",