@camstack/addon-post-analysis 1.2.91 → 1.2.92
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/{dist-CTIUuGRB.js → dist-BfrtqGyh.js} +32 -17
- package/dist/{dist-CquggoRC.mjs → dist-VnaTfwAL.mjs} +32 -17
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Q3EjHtct.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-D2EZ3nOf.mjs → hostInit-CpWvyhen.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +358 -160
- package/dist/pipeline-analytics/index.mjs +321 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BfrtqGyh.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -28856,88 +28856,6 @@ var PlateGalleryProvider = class {
|
|
|
28856
28856
|
}
|
|
28857
28857
|
};
|
|
28858
28858
|
//#endregion
|
|
28859
|
-
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
28860
|
-
/**
|
|
28861
|
-
* The operator button for the orphan audit.
|
|
28862
|
-
*
|
|
28863
|
-
* The audit engine, its five bounds and its resumable cursor already existed
|
|
28864
|
-
* and had run in `report` mode for weeks; what it had no way to be was
|
|
28865
|
-
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
28866
|
-
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
28867
|
-
* six hours, and remembering to flip it back — with nothing returned to look
|
|
28868
|
-
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
28869
|
-
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
28870
|
-
* drain by leaving a switch on.
|
|
28871
|
-
*
|
|
28872
|
-
* Three properties this action does NOT leave to the caller's care:
|
|
28873
|
-
*
|
|
28874
|
-
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
28875
|
-
* logs and returns, and deletes nothing — regardless of what the durable
|
|
28876
|
-
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
28877
|
-
* typed. The engine's own default is the same for the same reason.
|
|
28878
|
-
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
28879
|
-
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
28880
|
-
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
28881
|
-
* hub-main with every live frame.
|
|
28882
|
-
* 3. **It answers.** The report comes back per collection with rows AND
|
|
28883
|
-
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
28884
|
-
* from a log line.
|
|
28885
|
-
*/
|
|
28886
|
-
/**
|
|
28887
|
-
* `report` counts; `reclaim` deletes.
|
|
28888
|
-
*
|
|
28889
|
-
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
28890
|
-
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
28891
|
-
* unstated intent here is the one that destroys nothing.
|
|
28892
|
-
*/
|
|
28893
|
-
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
28894
|
-
var OrphanAuditActionInputSchema = require_dist.object({
|
|
28895
|
-
mode: OrphanAuditActionModeSchema,
|
|
28896
|
-
/**
|
|
28897
|
-
* Start every scope from the top instead of resuming its persisted cursor.
|
|
28898
|
-
*
|
|
28899
|
-
* What draining a backlog wants, and never the default — the cursor exists
|
|
28900
|
-
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
28901
|
-
*/
|
|
28902
|
-
restart: require_dist.boolean().optional(),
|
|
28903
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
28904
|
-
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
28905
|
-
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
28906
|
-
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
28907
|
-
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
28908
|
-
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
28909
|
-
/**
|
|
28910
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
28911
|
-
* exclusively save rows, never select them, which is what keeps this off
|
|
28912
|
-
* the one-age-keyed-sweep guard's books.
|
|
28913
|
-
*/
|
|
28914
|
-
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
28915
|
-
});
|
|
28916
|
-
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
28917
|
-
collection: require_dist.string(),
|
|
28918
|
-
examined: require_dist.number(),
|
|
28919
|
-
orphans: require_dist.number(),
|
|
28920
|
-
reclaimed: require_dist.number(),
|
|
28921
|
-
bytesReclaimed: require_dist.number(),
|
|
28922
|
-
exempt: require_dist.number(),
|
|
28923
|
-
withinGrace: require_dist.number(),
|
|
28924
|
-
unresolvable: require_dist.number(),
|
|
28925
|
-
stoppedBy: require_dist.string(),
|
|
28926
|
-
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
28927
|
-
nextCursor: require_dist.number()
|
|
28928
|
-
});
|
|
28929
|
-
var OrphanAuditActionResultSchema = require_dist.object({
|
|
28930
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
28931
|
-
ran: require_dist.boolean(),
|
|
28932
|
-
mode: require_dist._enum(["report", "reclaim"]),
|
|
28933
|
-
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
28934
|
-
totalOrphans: require_dist.number(),
|
|
28935
|
-
totalReclaimed: require_dist.number(),
|
|
28936
|
-
totalBytesReclaimed: require_dist.number(),
|
|
28937
|
-
durationMs: require_dist.number()
|
|
28938
|
-
});
|
|
28939
|
-
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
28940
|
-
//#endregion
|
|
28941
28859
|
//#region src/pipeline-analytics/store/storage-pressure.ts
|
|
28942
28860
|
/**
|
|
28943
28861
|
* storage-pressure — make a full disk something the system REACTS to, not just
|
|
@@ -29104,6 +29022,11 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
29104
29022
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
29105
29023
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
29106
29024
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
29025
|
+
/** Index rows per page inside one presence chunk. Must stay under
|
|
29026
|
+
* `SETTINGS_QUERY_DEFAULT_ROW_CAP` (2 000) so the engine's backstop can never
|
|
29027
|
+
* silently truncate a page — an owner reported absent because a cap bit would
|
|
29028
|
+
* DELETE a live thumbnail from the surface. */
|
|
29029
|
+
var MEDIA_PRESENCE_PAGE_SIZE = 1e3;
|
|
29107
29030
|
/**
|
|
29108
29031
|
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
29109
29032
|
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
@@ -29531,6 +29454,59 @@ var MediaStore = class {
|
|
|
29531
29454
|
});
|
|
29532
29455
|
}
|
|
29533
29456
|
/**
|
|
29457
|
+
* Which of `ownerIds` hold at least ONE media row — index only, no blob is
|
|
29458
|
+
* opened, no base64 is allocated.
|
|
29459
|
+
*
|
|
29460
|
+
* This is the licence `videoclips` needs to publish a `thumbnail`. The
|
|
29461
|
+
* event-media URL is stamped from a template for every event in the system,
|
|
29462
|
+
* so "the plane is up" was being served as "there are bytes": 64 of one
|
|
29463
|
+
* camera's 179 clips pointed at a 404 on 2026-08-17.
|
|
29464
|
+
*
|
|
29465
|
+
* Batched by `whereIn` and paged by offset. NOT `listInfoByOwner` in a loop:
|
|
29466
|
+
* that is one query per owner, which is the N+1 this exists to prevent (and
|
|
29467
|
+
* `queryObjectByTrackIds` already records what believing `whereIn` was
|
|
29468
|
+
* unavailable cost the search path).
|
|
29469
|
+
*
|
|
29470
|
+
* `deviceId` is the AUTHORIZATION filter, exactly as in `listByOwner`.
|
|
29471
|
+
*
|
|
29472
|
+
* THROWS on a query failure rather than answering partially — a probe that
|
|
29473
|
+
* quietly under-reports reads as "these owners have nothing", and the caller
|
|
29474
|
+
* drops thumbnails that exist.
|
|
29475
|
+
*/
|
|
29476
|
+
async ownersWithMedia(ownerKind, ownerIds, deviceId) {
|
|
29477
|
+
const found = /* @__PURE__ */ new Set();
|
|
29478
|
+
const unique = [...new Set(ownerIds)];
|
|
29479
|
+
for (let i = 0; i < unique.length; i += 200) {
|
|
29480
|
+
const chunk = unique.slice(i, i + 200);
|
|
29481
|
+
const answered = /* @__PURE__ */ new Set();
|
|
29482
|
+
for (let offset = 0;; offset += MEDIA_PRESENCE_PAGE_SIZE) {
|
|
29483
|
+
const rows = await this.store.query.query({
|
|
29484
|
+
collection: MEDIA_COLLECTION,
|
|
29485
|
+
filter: {
|
|
29486
|
+
where: {
|
|
29487
|
+
ownerKind,
|
|
29488
|
+
deviceId
|
|
29489
|
+
},
|
|
29490
|
+
whereIn: { ownerId: chunk },
|
|
29491
|
+
orderBy: {
|
|
29492
|
+
field: "ownerId",
|
|
29493
|
+
direction: "asc"
|
|
29494
|
+
},
|
|
29495
|
+
limit: MEDIA_PRESENCE_PAGE_SIZE,
|
|
29496
|
+
offset
|
|
29497
|
+
}
|
|
29498
|
+
});
|
|
29499
|
+
for (const row of rows) {
|
|
29500
|
+
const ownerId = row.data["ownerId"];
|
|
29501
|
+
if (typeof ownerId === "string") answered.add(ownerId);
|
|
29502
|
+
}
|
|
29503
|
+
if (rows.length < 1e3 || answered.size >= chunk.length) break;
|
|
29504
|
+
}
|
|
29505
|
+
for (const ownerId of answered) found.add(ownerId);
|
|
29506
|
+
}
|
|
29507
|
+
return found;
|
|
29508
|
+
}
|
|
29509
|
+
/**
|
|
29534
29510
|
* Delete all media (blobs + index rows) for the given owners of one kind.
|
|
29535
29511
|
* Best-effort per row; returns the number of index rows removed.
|
|
29536
29512
|
*/
|
|
@@ -29878,6 +29854,88 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
29878
29854
|
};
|
|
29879
29855
|
}
|
|
29880
29856
|
//#endregion
|
|
29857
|
+
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
29858
|
+
/**
|
|
29859
|
+
* The operator button for the orphan audit.
|
|
29860
|
+
*
|
|
29861
|
+
* The audit engine, its five bounds and its resumable cursor already existed
|
|
29862
|
+
* and had run in `report` mode for weeks; what it had no way to be was
|
|
29863
|
+
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
29864
|
+
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
29865
|
+
* six hours, and remembering to flip it back — with nothing returned to look
|
|
29866
|
+
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
29867
|
+
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
29868
|
+
* drain by leaving a switch on.
|
|
29869
|
+
*
|
|
29870
|
+
* Three properties this action does NOT leave to the caller's care:
|
|
29871
|
+
*
|
|
29872
|
+
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
29873
|
+
* logs and returns, and deletes nothing — regardless of what the durable
|
|
29874
|
+
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
29875
|
+
* typed. The engine's own default is the same for the same reason.
|
|
29876
|
+
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
29877
|
+
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
29878
|
+
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
29879
|
+
* hub-main with every live frame.
|
|
29880
|
+
* 3. **It answers.** The report comes back per collection with rows AND
|
|
29881
|
+
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
29882
|
+
* from a log line.
|
|
29883
|
+
*/
|
|
29884
|
+
/**
|
|
29885
|
+
* `report` counts; `reclaim` deletes.
|
|
29886
|
+
*
|
|
29887
|
+
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
29888
|
+
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
29889
|
+
* unstated intent here is the one that destroys nothing.
|
|
29890
|
+
*/
|
|
29891
|
+
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
29892
|
+
var OrphanAuditActionInputSchema = require_dist.object({
|
|
29893
|
+
mode: OrphanAuditActionModeSchema,
|
|
29894
|
+
/**
|
|
29895
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
29896
|
+
*
|
|
29897
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
29898
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
29899
|
+
*/
|
|
29900
|
+
restart: require_dist.boolean().optional(),
|
|
29901
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
29902
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
29903
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
29904
|
+
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
29905
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
29906
|
+
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
29907
|
+
/**
|
|
29908
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
29909
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
29910
|
+
* the one-age-keyed-sweep guard's books.
|
|
29911
|
+
*/
|
|
29912
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
29913
|
+
});
|
|
29914
|
+
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
29915
|
+
collection: require_dist.string(),
|
|
29916
|
+
examined: require_dist.number(),
|
|
29917
|
+
orphans: require_dist.number(),
|
|
29918
|
+
reclaimed: require_dist.number(),
|
|
29919
|
+
bytesReclaimed: require_dist.number(),
|
|
29920
|
+
exempt: require_dist.number(),
|
|
29921
|
+
withinGrace: require_dist.number(),
|
|
29922
|
+
unresolvable: require_dist.number(),
|
|
29923
|
+
stoppedBy: require_dist.string(),
|
|
29924
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
29925
|
+
nextCursor: require_dist.number()
|
|
29926
|
+
});
|
|
29927
|
+
var OrphanAuditActionResultSchema = require_dist.object({
|
|
29928
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
29929
|
+
ran: require_dist.boolean(),
|
|
29930
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
29931
|
+
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
29932
|
+
totalOrphans: require_dist.number(),
|
|
29933
|
+
totalReclaimed: require_dist.number(),
|
|
29934
|
+
totalBytesReclaimed: require_dist.number(),
|
|
29935
|
+
durationMs: require_dist.number()
|
|
29936
|
+
});
|
|
29937
|
+
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
29938
|
+
//#endregion
|
|
29881
29939
|
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
29882
29940
|
/**
|
|
29883
29941
|
* Videoclip visits — the grain a clip should have on an events-mode camera.
|
|
@@ -29944,7 +30002,22 @@ function dominantKind(attached) {
|
|
|
29944
30002
|
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
29945
30003
|
return "motion";
|
|
29946
30004
|
}
|
|
29947
|
-
|
|
30005
|
+
/**
|
|
30006
|
+
* The instant a recorded still can actually be decoded inside a visit: the
|
|
30007
|
+
* midpoint of its LONGEST contiguous range.
|
|
30008
|
+
*
|
|
30009
|
+
* NOT the midpoint of the visit. A visit spans its holes by construction (it is
|
|
30010
|
+
* ranges glued across gaps up to `VISIT_MERGE_GAP_MS`), so the naive midpoint
|
|
30011
|
+
* lands in dead air roughly as often as not and the recorder's still route
|
|
30012
|
+
* answers 204 for a visit with plenty of footage.
|
|
30013
|
+
*/
|
|
30014
|
+
function stillInstantMs(ranges) {
|
|
30015
|
+
let best;
|
|
30016
|
+
for (const range of ranges) if (best === void 0 || range.endMs - range.startMs > best.endMs - best.startMs) best = range;
|
|
30017
|
+
if (best === void 0) return void 0;
|
|
30018
|
+
return Math.floor((best.startMs + best.endMs) / 2);
|
|
30019
|
+
}
|
|
30020
|
+
function visitsToClips(visits, events, pad, vouched) {
|
|
29948
30021
|
const tagged = [
|
|
29949
30022
|
...events.object.map((event) => ({
|
|
29950
30023
|
kind: "object",
|
|
@@ -29962,7 +30035,8 @@ function visitsToClips(visits, events, pad) {
|
|
|
29962
30035
|
return visits.map((visit) => {
|
|
29963
30036
|
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
29964
30037
|
const kind = dominantKind(attached);
|
|
29965
|
-
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
30038
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
|
|
30039
|
+
const stillAtMs = stillInstantMs(visit.ranges);
|
|
29966
30040
|
return {
|
|
29967
30041
|
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
29968
30042
|
source: "analytics",
|
|
@@ -29971,8 +30045,10 @@ function visitsToClips(visits, events, pad) {
|
|
|
29971
30045
|
startMs: visit.startMs,
|
|
29972
30046
|
endMs: visit.endMs
|
|
29973
30047
|
},
|
|
29974
|
-
thumbnail: withThumb
|
|
29975
|
-
|
|
30048
|
+
...withThumb?.event.mediaUrl !== void 0 ? { thumbnail: withThumb.event.mediaUrl } : {},
|
|
30049
|
+
...stillAtMs !== void 0 ? { stillAtMs } : {},
|
|
30050
|
+
eventIds: attached.slice(0, 24).map((a) => a.event.id),
|
|
30051
|
+
eventCount: attached.length,
|
|
29976
30052
|
holes: [...visit.holes]
|
|
29977
30053
|
};
|
|
29978
30054
|
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
@@ -29996,6 +30072,17 @@ function visitsToClips(visits, events, pad) {
|
|
|
29996
30072
|
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
29997
30073
|
*/
|
|
29998
30074
|
var SOURCE = "analytics";
|
|
30075
|
+
/**
|
|
30076
|
+
* Backstop on the ANNOTATION read — never on the clip list.
|
|
30077
|
+
*
|
|
30078
|
+
* A visit is annotated by every event inside it, so the clip `limit` must not
|
|
30079
|
+
* reach this fetch (one 615 visit legitimately spans 2 345 motion rows). This
|
|
30080
|
+
* bound exists only so a pathological camera-day cannot pull an unbounded page:
|
|
30081
|
+
* the store orders `timestamp DESC`, so a truncation drops the OLDEST rows and
|
|
30082
|
+
* the EARLIEST visits silently lose their annotations — which is why hitting it
|
|
30083
|
+
* is logged, per camera, rather than absorbed.
|
|
30084
|
+
*/
|
|
30085
|
+
var VISIT_EVENT_FETCH_LIMIT = 5e3;
|
|
29999
30086
|
function clipIdFor(eventId, startMs, endMs) {
|
|
30000
30087
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
30001
30088
|
}
|
|
@@ -30016,7 +30103,7 @@ function parseClipId(clipId) {
|
|
|
30016
30103
|
};
|
|
30017
30104
|
}
|
|
30018
30105
|
/** Map per-kind events into time-ranged clips, newest-first. Pure. */
|
|
30019
|
-
function eventsToClips(events, pad) {
|
|
30106
|
+
function eventsToClips(events, pad, vouched) {
|
|
30020
30107
|
const make = (e, kind) => {
|
|
30021
30108
|
const startMs = e.timestamp - pad.preMs;
|
|
30022
30109
|
const endMs = e.timestamp + pad.postMs;
|
|
@@ -30028,7 +30115,7 @@ function eventsToClips(events, pad) {
|
|
|
30028
30115
|
startMs,
|
|
30029
30116
|
endMs
|
|
30030
30117
|
},
|
|
30031
|
-
thumbnail: e.mediaUrl
|
|
30118
|
+
...e.mediaUrl !== void 0 && vouched.has(e.id) ? { thumbnail: e.mediaUrl } : {}
|
|
30032
30119
|
};
|
|
30033
30120
|
};
|
|
30034
30121
|
return [
|
|
@@ -30037,29 +30124,106 @@ function eventsToClips(events, pad) {
|
|
|
30037
30124
|
...events.audio.map((e) => make(e, "audio"))
|
|
30038
30125
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
30039
30126
|
}
|
|
30127
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
30128
|
+
function allEventIds(events) {
|
|
30129
|
+
return [
|
|
30130
|
+
...events.motion,
|
|
30131
|
+
...events.object,
|
|
30132
|
+
...events.audio
|
|
30133
|
+
].map((e) => e.id);
|
|
30134
|
+
}
|
|
30135
|
+
/**
|
|
30136
|
+
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
30137
|
+
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
30138
|
+
* owning TRACK's. Two batched index reads at most, and the second only over the
|
|
30139
|
+
* object events the first did not already answer for — motion and audio are
|
|
30140
|
+
* trackless roots, so they never need it.
|
|
30141
|
+
*
|
|
30142
|
+
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
30143
|
+
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
30144
|
+
*/
|
|
30145
|
+
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
30146
|
+
const eventIds = allEventIds(events);
|
|
30147
|
+
if (eventIds.length === 0) return /* @__PURE__ */ new Set();
|
|
30148
|
+
const ownMedia = await fetchMediaOwners({
|
|
30149
|
+
deviceId,
|
|
30150
|
+
ownerKind: "event",
|
|
30151
|
+
ownerIds: eventIds
|
|
30152
|
+
});
|
|
30153
|
+
const residualTracks = [];
|
|
30154
|
+
const seenTracks = /* @__PURE__ */ new Set();
|
|
30155
|
+
for (const event of events.object) {
|
|
30156
|
+
const trackId = event.trackId;
|
|
30157
|
+
if (trackId === void 0 || ownMedia.has(event.id) || seenTracks.has(trackId)) continue;
|
|
30158
|
+
seenTracks.add(trackId);
|
|
30159
|
+
residualTracks.push(trackId);
|
|
30160
|
+
}
|
|
30161
|
+
const trackMedia = residualTracks.length === 0 ? /* @__PURE__ */ new Set() : await fetchMediaOwners({
|
|
30162
|
+
deviceId,
|
|
30163
|
+
ownerKind: "track",
|
|
30164
|
+
ownerIds: residualTracks
|
|
30165
|
+
});
|
|
30166
|
+
const vouched = new Set(ownMedia);
|
|
30167
|
+
for (const event of events.object) {
|
|
30168
|
+
const trackId = event.trackId;
|
|
30169
|
+
if (trackId !== void 0 && trackMedia.has(trackId)) vouched.add(event.id);
|
|
30170
|
+
}
|
|
30171
|
+
return vouched;
|
|
30172
|
+
}
|
|
30173
|
+
/** Keep only clips that actually touch the caller's window. The reads are
|
|
30174
|
+
* widened by {@link leftOverhangMs}; the ANSWER is not. */
|
|
30175
|
+
function intersectsWindow(clip, since, until) {
|
|
30176
|
+
return clip.timeRange.startMs < until && clip.timeRange.endMs > since;
|
|
30177
|
+
}
|
|
30178
|
+
/**
|
|
30179
|
+
* How far BEFORE `since` the event + availability reads must reach.
|
|
30180
|
+
*
|
|
30181
|
+
* A visit straddling `since` keeps its heartbeat rows and loses the transition
|
|
30182
|
+
* row — the only one that ever carries a snapshot — so the same visit shows a
|
|
30183
|
+
* picture or doesn't depending on where the operator scrolled. One wider range
|
|
30184
|
+
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
30185
|
+
*/
|
|
30186
|
+
function leftOverhangMs(pad) {
|
|
30187
|
+
return require_dist.VISIT_MERGE_GAP_MS + pad.preMs;
|
|
30188
|
+
}
|
|
30040
30189
|
function createVideoclipsProvider(deps) {
|
|
30041
30190
|
return {
|
|
30042
30191
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
30192
|
+
const readSince = since - leftOverhangMs(deps.defaultPad);
|
|
30043
30193
|
const events = await deps.fetchEvents({
|
|
30044
30194
|
deviceId,
|
|
30045
|
-
since,
|
|
30195
|
+
since: readSince,
|
|
30046
30196
|
until,
|
|
30047
|
-
|
|
30197
|
+
limit: deps.fetchAvailability ? VISIT_EVENT_FETCH_LIMIT : limit
|
|
30048
30198
|
});
|
|
30049
|
-
|
|
30199
|
+
warnOnTruncatedFetch(deps, deviceId, events);
|
|
30200
|
+
let vouched = /* @__PURE__ */ new Set();
|
|
30201
|
+
try {
|
|
30202
|
+
vouched = await resolveVouchedEvents(deviceId, events, deps.fetchMediaOwners);
|
|
30203
|
+
} catch (err) {
|
|
30204
|
+
deps.logWarn?.("videoclips: media presence read failed — listing without thumbnails", {
|
|
30205
|
+
deviceId,
|
|
30206
|
+
events: allEventIds(events).length,
|
|
30207
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30208
|
+
});
|
|
30209
|
+
}
|
|
30210
|
+
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
30050
30211
|
if (deps.fetchAvailability) try {
|
|
30051
30212
|
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
30052
30213
|
deviceId,
|
|
30053
|
-
since,
|
|
30214
|
+
since: readSince,
|
|
30054
30215
|
until
|
|
30055
|
-
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
30216
|
+
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
|
|
30056
30217
|
} catch (err) {
|
|
30057
30218
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
30058
30219
|
deviceId,
|
|
30059
30220
|
error: err instanceof Error ? err.message : String(err)
|
|
30060
30221
|
});
|
|
30061
30222
|
}
|
|
30062
|
-
|
|
30223
|
+
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
30224
|
+
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
30225
|
+
reportUnvouchedClips(deps, deviceId, answer);
|
|
30226
|
+
return answer;
|
|
30063
30227
|
},
|
|
30064
30228
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
30065
30229
|
const parsed = parseClipId(clipId);
|
|
@@ -30072,6 +30236,33 @@ function createVideoclipsProvider(deps) {
|
|
|
30072
30236
|
}
|
|
30073
30237
|
};
|
|
30074
30238
|
}
|
|
30239
|
+
/** The annotation backstop biting is a SILENT loss of the earliest visits'
|
|
30240
|
+
* events — say so, per camera, instead of shipping a thinner list. */
|
|
30241
|
+
function warnOnTruncatedFetch(deps, deviceId, events) {
|
|
30242
|
+
if (!deps.fetchAvailability) return;
|
|
30243
|
+
const truncated = [
|
|
30244
|
+
"motion",
|
|
30245
|
+
"object",
|
|
30246
|
+
"audio"
|
|
30247
|
+
].filter((kind) => events[kind].length >= VISIT_EVENT_FETCH_LIMIT);
|
|
30248
|
+
if (truncated.length === 0) return;
|
|
30249
|
+
deps.logWarn?.("videoclips: annotation fetch truncated — earliest visits lose their events", {
|
|
30250
|
+
deviceId,
|
|
30251
|
+
kinds: truncated,
|
|
30252
|
+
limit: VISIT_EVENT_FETCH_LIMIT
|
|
30253
|
+
});
|
|
30254
|
+
}
|
|
30255
|
+
/** The operator's acceptance criterion as a number: a rising count on one
|
|
30256
|
+
* camera is its motion-snapshot capturer failing, not a quiet day. */
|
|
30257
|
+
function reportUnvouchedClips(deps, deviceId, clips) {
|
|
30258
|
+
const clipsWithoutStill = clips.filter((c) => c.thumbnail === void 0).length;
|
|
30259
|
+
if (clipsWithoutStill === 0) return;
|
|
30260
|
+
deps.logInfo?.("videoclips: visit has no vouched still", {
|
|
30261
|
+
deviceId,
|
|
30262
|
+
clipsWithoutStill,
|
|
30263
|
+
clips: clips.length
|
|
30264
|
+
});
|
|
30265
|
+
}
|
|
30075
30266
|
//#endregion
|
|
30076
30267
|
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
30077
30268
|
/**
|
|
@@ -47262,6 +47453,66 @@ var EventMediaDispatcher = class {
|
|
|
47262
47453
|
}
|
|
47263
47454
|
}
|
|
47264
47455
|
};
|
|
47456
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
47457
|
+
constructor(ms) {
|
|
47458
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
47459
|
+
this.name = "SnapshotTimeoutError";
|
|
47460
|
+
}
|
|
47461
|
+
};
|
|
47462
|
+
function withTimeout$1(promise, ms) {
|
|
47463
|
+
return new Promise((resolve, reject) => {
|
|
47464
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
47465
|
+
promise.then((value) => {
|
|
47466
|
+
clearTimeout(timer);
|
|
47467
|
+
resolve(value);
|
|
47468
|
+
}, (err) => {
|
|
47469
|
+
clearTimeout(timer);
|
|
47470
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47471
|
+
});
|
|
47472
|
+
});
|
|
47473
|
+
}
|
|
47474
|
+
var MotionEventSnapshotCapturer = class {
|
|
47475
|
+
deps;
|
|
47476
|
+
snapshotTimeoutMs;
|
|
47477
|
+
constructor(deps) {
|
|
47478
|
+
this.deps = deps;
|
|
47479
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47480
|
+
}
|
|
47481
|
+
/**
|
|
47482
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47483
|
+
* throws — the durable record is the motion row, already inserted by
|
|
47484
|
+
* the caller.
|
|
47485
|
+
*/
|
|
47486
|
+
async capture(input) {
|
|
47487
|
+
let snap;
|
|
47488
|
+
try {
|
|
47489
|
+
snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47490
|
+
} catch (err) {
|
|
47491
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47492
|
+
return;
|
|
47493
|
+
}
|
|
47494
|
+
if (snap === null) return;
|
|
47495
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
47496
|
+
let data = raw;
|
|
47497
|
+
try {
|
|
47498
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47499
|
+
} catch (err) {
|
|
47500
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47501
|
+
}
|
|
47502
|
+
try {
|
|
47503
|
+
await this.deps.media.put({
|
|
47504
|
+
deviceId: input.deviceId,
|
|
47505
|
+
ownerKind: "event",
|
|
47506
|
+
ownerId: input.eventId,
|
|
47507
|
+
kind: "snapshot",
|
|
47508
|
+
timestamp: input.timestamp,
|
|
47509
|
+
data
|
|
47510
|
+
});
|
|
47511
|
+
} catch (err) {
|
|
47512
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47513
|
+
}
|
|
47514
|
+
}
|
|
47515
|
+
};
|
|
47265
47516
|
/**
|
|
47266
47517
|
* Project one `DeviceStateChanged` of a mapped sensor/control cap into a
|
|
47267
47518
|
* synthetic track per camera the event belongs to. Returns the number of
|
|
@@ -47358,16 +47609,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
47358
47609
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
47359
47610
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
47360
47611
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
47361
|
-
var SnapshotTimeoutError
|
|
47612
|
+
var SnapshotTimeoutError = class extends Error {
|
|
47362
47613
|
constructor(ms) {
|
|
47363
47614
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
47364
47615
|
this.name = "SnapshotTimeoutError";
|
|
47365
47616
|
}
|
|
47366
47617
|
};
|
|
47367
47618
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
47368
|
-
function withTimeout
|
|
47619
|
+
function withTimeout(promise, ms) {
|
|
47369
47620
|
return new Promise((resolve, reject) => {
|
|
47370
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError
|
|
47621
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47371
47622
|
promise.then((value) => {
|
|
47372
47623
|
clearTimeout(timer);
|
|
47373
47624
|
resolve(value);
|
|
@@ -47422,11 +47673,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
47422
47673
|
*/
|
|
47423
47674
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
47424
47675
|
try {
|
|
47425
|
-
const snap = await withTimeout
|
|
47676
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
47426
47677
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
47427
47678
|
return snap;
|
|
47428
47679
|
} catch (err) {
|
|
47429
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError
|
|
47680
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
47430
47681
|
return null;
|
|
47431
47682
|
}
|
|
47432
47683
|
}
|
|
@@ -47492,66 +47743,6 @@ var SyntheticTrackMaterializer = class {
|
|
|
47492
47743
|
return track;
|
|
47493
47744
|
}
|
|
47494
47745
|
};
|
|
47495
|
-
var SnapshotTimeoutError = class extends Error {
|
|
47496
|
-
constructor(ms) {
|
|
47497
|
-
super(`snapshot cap timed out after ${ms}ms`);
|
|
47498
|
-
this.name = "SnapshotTimeoutError";
|
|
47499
|
-
}
|
|
47500
|
-
};
|
|
47501
|
-
function withTimeout(promise, ms) {
|
|
47502
|
-
return new Promise((resolve, reject) => {
|
|
47503
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47504
|
-
promise.then((value) => {
|
|
47505
|
-
clearTimeout(timer);
|
|
47506
|
-
resolve(value);
|
|
47507
|
-
}, (err) => {
|
|
47508
|
-
clearTimeout(timer);
|
|
47509
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47510
|
-
});
|
|
47511
|
-
});
|
|
47512
|
-
}
|
|
47513
|
-
var MotionEventSnapshotCapturer = class {
|
|
47514
|
-
deps;
|
|
47515
|
-
snapshotTimeoutMs;
|
|
47516
|
-
constructor(deps) {
|
|
47517
|
-
this.deps = deps;
|
|
47518
|
-
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47519
|
-
}
|
|
47520
|
-
/**
|
|
47521
|
-
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47522
|
-
* throws — the durable record is the motion row, already inserted by
|
|
47523
|
-
* the caller.
|
|
47524
|
-
*/
|
|
47525
|
-
async capture(input) {
|
|
47526
|
-
let snap;
|
|
47527
|
-
try {
|
|
47528
|
-
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47529
|
-
} catch (err) {
|
|
47530
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47531
|
-
return;
|
|
47532
|
-
}
|
|
47533
|
-
if (snap === null) return;
|
|
47534
|
-
const raw = Buffer.from(snap.base64, "base64");
|
|
47535
|
-
let data = raw;
|
|
47536
|
-
try {
|
|
47537
|
-
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47538
|
-
} catch (err) {
|
|
47539
|
-
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47540
|
-
}
|
|
47541
|
-
try {
|
|
47542
|
-
await this.deps.media.put({
|
|
47543
|
-
deviceId: input.deviceId,
|
|
47544
|
-
ownerKind: "event",
|
|
47545
|
-
ownerId: input.eventId,
|
|
47546
|
-
kind: "snapshot",
|
|
47547
|
-
timestamp: input.timestamp,
|
|
47548
|
-
data
|
|
47549
|
-
});
|
|
47550
|
-
} catch (err) {
|
|
47551
|
-
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47552
|
-
}
|
|
47553
|
-
}
|
|
47554
|
-
};
|
|
47555
47746
|
var TrackingSettingsSchema = require_dist.object({
|
|
47556
47747
|
/**
|
|
47557
47748
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -53091,6 +53282,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53091
53282
|
audio
|
|
53092
53283
|
};
|
|
53093
53284
|
},
|
|
53285
|
+
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
53286
|
+
return stores.mediaStore.ownersWithMedia(ownerKind, ownerIds, deviceId);
|
|
53287
|
+
},
|
|
53094
53288
|
fetchAvailability: async ({ deviceId, since, until }) => {
|
|
53095
53289
|
return (await api.recording.getAvailability.query({
|
|
53096
53290
|
deviceId,
|
|
@@ -53102,6 +53296,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53102
53296
|
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53103
53297
|
meta
|
|
53104
53298
|
}),
|
|
53299
|
+
logInfo: (message, meta) => this.ctx.logger.info(message, {
|
|
53300
|
+
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53301
|
+
meta
|
|
53302
|
+
}),
|
|
53105
53303
|
resolvePlayback: async ({ deviceId, startMs, endMs }) => {
|
|
53106
53304
|
const manifest = await api.recording.getPlaybackManifest.query({
|
|
53107
53305
|
deviceId,
|