@camstack/addon-post-analysis 1.2.0 → 1.2.2
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-CirtGNcr.mjs → dist-Bu9eXObI.mjs} +86 -9
- package/dist/{dist-MgnHaI0L.js → dist-DSP-cVM0.js} +86 -9
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-D8vIkoch.js → node-DtSEvIXU.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dvnr_5XP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-2w2j7Rm4.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-BIaqSUdl.mjs → hostInit-CTQ8-sg-.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +504 -304
- package/dist/pipeline-analytics/index.mjs +503 -303
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-Bu9eXObI.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -3650,6 +3650,10 @@ var TRACKS_COLUMNS = [
|
|
|
3650
3650
|
name: "source",
|
|
3651
3651
|
type: "TEXT"
|
|
3652
3652
|
},
|
|
3653
|
+
{
|
|
3654
|
+
name: "producingDeviceName",
|
|
3655
|
+
type: "TEXT"
|
|
3656
|
+
},
|
|
3653
3657
|
{
|
|
3654
3658
|
name: "firstSeen",
|
|
3655
3659
|
type: "INTEGER",
|
|
@@ -4357,6 +4361,7 @@ var TrackStore = class {
|
|
|
4357
4361
|
className: t.className,
|
|
4358
4362
|
...t.label !== void 0 ? { label: t.label } : {},
|
|
4359
4363
|
source: t.source ?? "sensor",
|
|
4364
|
+
...t.producingDeviceName !== void 0 ? { producingDeviceName: t.producingDeviceName } : {},
|
|
4360
4365
|
firstSeen: t.firstSeen,
|
|
4361
4366
|
lastSeen: t.lastSeen,
|
|
4362
4367
|
positions: [...t.positions],
|
|
@@ -4418,6 +4423,7 @@ var TrackStore = class {
|
|
|
4418
4423
|
const classes = data["classes"];
|
|
4419
4424
|
const label = data["label"];
|
|
4420
4425
|
const source = data["source"];
|
|
4426
|
+
const producingDeviceName = data["producingDeviceName"];
|
|
4421
4427
|
const importance = data["importance"];
|
|
4422
4428
|
const bestEventId = data["bestEventId"];
|
|
4423
4429
|
const importanceReason = data["importanceReason"];
|
|
@@ -4438,6 +4444,7 @@ var TrackStore = class {
|
|
|
4438
4444
|
className: String(data["className"]),
|
|
4439
4445
|
...typeof label === "string" ? { label } : {},
|
|
4440
4446
|
...source === "sensor" || source === "pipeline" ? { source } : {},
|
|
4447
|
+
...typeof producingDeviceName === "string" ? { producingDeviceName } : {},
|
|
4441
4448
|
firstSeen: Number(data["firstSeen"]),
|
|
4442
4449
|
lastSeen: Number(data["lastSeen"]),
|
|
4443
4450
|
positions,
|
|
@@ -4483,6 +4490,7 @@ var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
|
4483
4490
|
*/
|
|
4484
4491
|
var SINGLE_INSTANCE_KINDS = new Set([
|
|
4485
4492
|
"keyFrame",
|
|
4493
|
+
"keyFrameSmall",
|
|
4486
4494
|
"thumbnail",
|
|
4487
4495
|
"firstFrame",
|
|
4488
4496
|
"lastFrame"
|
|
@@ -5239,6 +5247,32 @@ var EventStore = class {
|
|
|
5239
5247
|
});
|
|
5240
5248
|
}
|
|
5241
5249
|
/**
|
|
5250
|
+
* Resolve the OWNING track id for an object event id. Powers the event-media
|
|
5251
|
+
* resolver's track fallback: a new-style object event owns no crop, so its
|
|
5252
|
+
* `mediaUrl` degrades to the track's cadence media, addressed by this id.
|
|
5253
|
+
* Returns `null` when the id is not a persisted object event (e.g. a KeyEvent
|
|
5254
|
+
* whose id is already a track id, or a motion/audio event) — best-effort.
|
|
5255
|
+
*/
|
|
5256
|
+
async getTrackIdForEvent(eventId) {
|
|
5257
|
+
try {
|
|
5258
|
+
const row = await this.store.get.query({
|
|
5259
|
+
collection: OBJECT_EVENTS_COLLECTION,
|
|
5260
|
+
key: eventId
|
|
5261
|
+
});
|
|
5262
|
+
if (row !== null && typeof row === "object" && "trackId" in row) {
|
|
5263
|
+
const trackId = row["trackId"];
|
|
5264
|
+
if (typeof trackId === "string" && trackId.length > 0) return trackId;
|
|
5265
|
+
}
|
|
5266
|
+
return null;
|
|
5267
|
+
} catch (err) {
|
|
5268
|
+
this.logger.debug("getTrackIdForEvent failed", { meta: {
|
|
5269
|
+
eventId,
|
|
5270
|
+
error: String(err)
|
|
5271
|
+
} });
|
|
5272
|
+
return null;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
/**
|
|
5242
5276
|
* Forward-only: set `label` on every already-emitted object event of a track.
|
|
5243
5277
|
* Returns the number of events updated. Best-effort per row. Used when face
|
|
5244
5278
|
* recognition assigns a name to a track after its events were emitted.
|
|
@@ -6145,6 +6179,7 @@ var LinkedCamerasCache = class {
|
|
|
6145
6179
|
for (const cameraId of cameraIds) try {
|
|
6146
6180
|
const { devices } = await this.deps.linkedDevices.getLinkedDevices({ deviceId: cameraId });
|
|
6147
6181
|
for (const d of devices) {
|
|
6182
|
+
if (d.producesTrackedEvents === false) continue;
|
|
6148
6183
|
const list = next.get(d.deviceId);
|
|
6149
6184
|
if (list === void 0) next.set(d.deviceId, [cameraId]);
|
|
6150
6185
|
else if (!list.includes(cameraId)) list.push(cameraId);
|
|
@@ -6211,10 +6246,13 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
|
6211
6246
|
*
|
|
6212
6247
|
* A linked sensor/control state change is projected into the UNIFIED track
|
|
6213
6248
|
* store as a SYNTHETIC track: `className` = the event kind (`contact` /
|
|
6214
|
-
* `lock` / `switch` …), `source: 'sensor'`, media = an on-demand
|
|
6249
|
+
* `lock` / `switch` …), `source: 'sensor'`, media = an on-demand best shot of
|
|
6215
6250
|
* the linked camera in the SAME `getTrackMedia` shape the UIs consume (no
|
|
6216
|
-
* bbox → full frame), `
|
|
6217
|
-
*
|
|
6251
|
+
* bbox → full frame), persisted under `kind: 'keyFrameSmall'` — the kind the
|
|
6252
|
+
* viewer representative-image selectors (hero/clean/reel) actually consume; a
|
|
6253
|
+
* `'snapshot'`-kind blob is ignored by those chains, which is what stuck the
|
|
6254
|
+
* pa-synth reel on "loading". `positions: []`. It joins the timeline via the
|
|
6255
|
+
* existing time-based clustering — no new join mechanism.
|
|
6218
6256
|
*
|
|
6219
6257
|
* The raw `SensorEvent` remains the durable record; this track is its
|
|
6220
6258
|
* projection. Snapshots are debounced per `(camera, kind)` so a chattery
|
|
@@ -6222,6 +6260,28 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
|
6222
6260
|
* `source:'sensor'` tracks (they carry no trajectory).
|
|
6223
6261
|
*/
|
|
6224
6262
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
6263
|
+
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
6264
|
+
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
6265
|
+
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
6266
|
+
var SnapshotTimeoutError = class extends Error {
|
|
6267
|
+
constructor(ms) {
|
|
6268
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
6269
|
+
this.name = "SnapshotTimeoutError";
|
|
6270
|
+
}
|
|
6271
|
+
};
|
|
6272
|
+
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
6273
|
+
function withTimeout$1(promise, ms) {
|
|
6274
|
+
return new Promise((resolve, reject) => {
|
|
6275
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
6276
|
+
promise.then((value) => {
|
|
6277
|
+
clearTimeout(timer);
|
|
6278
|
+
resolve(value);
|
|
6279
|
+
}, (err) => {
|
|
6280
|
+
clearTimeout(timer);
|
|
6281
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
6282
|
+
});
|
|
6283
|
+
});
|
|
6284
|
+
}
|
|
6225
6285
|
/** Full-frame, position-less placeholder (spatial consumers skip these). */
|
|
6226
6286
|
function zeroPosition(timestamp) {
|
|
6227
6287
|
return {
|
|
@@ -6239,6 +6299,7 @@ function zeroPosition(timestamp) {
|
|
|
6239
6299
|
var SyntheticSensorTrackMaterializer = class {
|
|
6240
6300
|
deps;
|
|
6241
6301
|
debounceMs;
|
|
6302
|
+
snapshotTimeoutMs;
|
|
6242
6303
|
makeId;
|
|
6243
6304
|
now;
|
|
6244
6305
|
/** Last snapshot time per `${cameraId}:${kind}`. */
|
|
@@ -6246,10 +6307,29 @@ var SyntheticSensorTrackMaterializer = class {
|
|
|
6246
6307
|
constructor(deps) {
|
|
6247
6308
|
this.deps = deps;
|
|
6248
6309
|
this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6310
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
|
|
6249
6311
|
this.makeId = deps.makeId ?? (() => `pa-synth-${randomUUID()}`);
|
|
6250
6312
|
this.now = deps.now ?? Date.now;
|
|
6251
6313
|
}
|
|
6252
6314
|
/**
|
|
6315
|
+
* Fetch the linked camera's snapshot under a bounded timeout. A hanging
|
|
6316
|
+
* snapshot cap is skipped (returns null + warns) so the track still lands;
|
|
6317
|
+
* a null / failed fetch is likewise reported and skipped.
|
|
6318
|
+
*/
|
|
6319
|
+
async fetchSnapshot(cameraId) {
|
|
6320
|
+
try {
|
|
6321
|
+
const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({
|
|
6322
|
+
deviceId: cameraId,
|
|
6323
|
+
force: true
|
|
6324
|
+
}), this.snapshotTimeoutMs);
|
|
6325
|
+
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
6326
|
+
return snap;
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err);
|
|
6329
|
+
return null;
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
/**
|
|
6253
6333
|
* Materialize a synthetic track for a sensor/control event on `cameraId`.
|
|
6254
6334
|
* Returns the persisted track, or null when debounced. Snapshot failure
|
|
6255
6335
|
* still lands a track (with no media) — it never blocks the sensor record.
|
|
@@ -6262,28 +6342,23 @@ var SyntheticSensorTrackMaterializer = class {
|
|
|
6262
6342
|
const trackId = this.makeId();
|
|
6263
6343
|
const ts = input.timestamp;
|
|
6264
6344
|
let mediaKey = null;
|
|
6265
|
-
|
|
6266
|
-
|
|
6345
|
+
const snap = await this.fetchSnapshot(input.cameraId);
|
|
6346
|
+
if (snap !== null) try {
|
|
6347
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
6348
|
+
let data = raw;
|
|
6349
|
+
try {
|
|
6350
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
6351
|
+
} catch (err) {
|
|
6352
|
+
this.deps.onError?.("downscaleSnapshot", err);
|
|
6353
|
+
}
|
|
6354
|
+
mediaKey = await this.deps.media.put({
|
|
6267
6355
|
deviceId: input.cameraId,
|
|
6268
|
-
|
|
6356
|
+
ownerKind: "track",
|
|
6357
|
+
ownerId: trackId,
|
|
6358
|
+
kind: "keyFrameSmall",
|
|
6359
|
+
timestamp: ts,
|
|
6360
|
+
data
|
|
6269
6361
|
});
|
|
6270
|
-
if (snap !== null) {
|
|
6271
|
-
const raw = Buffer.from(snap.base64, "base64");
|
|
6272
|
-
let data = raw;
|
|
6273
|
-
try {
|
|
6274
|
-
data = await downscaleFullFrameJpeg(raw);
|
|
6275
|
-
} catch (err) {
|
|
6276
|
-
this.deps.onError?.("downscaleSnapshot", err);
|
|
6277
|
-
}
|
|
6278
|
-
mediaKey = await this.deps.media.put({
|
|
6279
|
-
deviceId: input.cameraId,
|
|
6280
|
-
ownerKind: "track",
|
|
6281
|
-
ownerId: trackId,
|
|
6282
|
-
kind: "snapshot",
|
|
6283
|
-
timestamp: ts,
|
|
6284
|
-
data
|
|
6285
|
-
});
|
|
6286
|
-
} else this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
6287
6362
|
} catch (err) {
|
|
6288
6363
|
this.deps.onError?.("snapshotMedia", err);
|
|
6289
6364
|
}
|
|
@@ -6292,6 +6367,7 @@ var SyntheticSensorTrackMaterializer = class {
|
|
|
6292
6367
|
deviceId: input.cameraId,
|
|
6293
6368
|
className: input.kind,
|
|
6294
6369
|
source: "sensor",
|
|
6370
|
+
...input.producingDeviceName !== void 0 ? { producingDeviceName: input.producingDeviceName } : {},
|
|
6295
6371
|
firstSeen: ts,
|
|
6296
6372
|
lastSeen: ts,
|
|
6297
6373
|
positions: [],
|
|
@@ -6324,6 +6400,22 @@ async function resolveFrame(handle, deps) {
|
|
|
6324
6400
|
return deps.getRemoteFrame(handle);
|
|
6325
6401
|
}
|
|
6326
6402
|
//#endregion
|
|
6403
|
+
//#region src/shared/frame/shared-frame-resolver.ts
|
|
6404
|
+
function frameHandleKey(h) {
|
|
6405
|
+
return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
|
|
6406
|
+
}
|
|
6407
|
+
function createSharedFrameResolver(resolve) {
|
|
6408
|
+
let cache = null;
|
|
6409
|
+
return (handle) => {
|
|
6410
|
+
const key = frameHandleKey(handle);
|
|
6411
|
+
if (cache === null || cache.key !== key) cache = {
|
|
6412
|
+
key,
|
|
6413
|
+
value: resolve(handle)
|
|
6414
|
+
};
|
|
6415
|
+
return cache.value;
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
//#endregion
|
|
6327
6419
|
//#region src/shared/frame/square-safe-crop.ts
|
|
6328
6420
|
/**
|
|
6329
6421
|
* Compute a square-safe 16:9 crop region in pixel space.
|
|
@@ -6452,27 +6544,30 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
|
|
|
6452
6544
|
}
|
|
6453
6545
|
//#endregion
|
|
6454
6546
|
//#region src/pipeline-analytics/services/event-media-dispatcher.ts
|
|
6455
|
-
/** All event media is stored at NATIVE resolution (no downscale), high quality.
|
|
6456
|
-
* Small downscaled `thumbnail`s are intentionally left out for now — when we
|
|
6457
|
-
* reintroduce them they'll be a separate small kind. */
|
|
6458
|
-
var MEDIA_QUALITY = 88;
|
|
6459
|
-
/** Output dimensions for the LOCAL-FALLBACK square-safe 16:9 crops
|
|
6460
|
-
* (crop/faceCrop/plateCrop/thumbnail) cut from the resolved ≤640 frame. */
|
|
6461
|
-
var CROP_WIDTH = 640;
|
|
6462
|
-
var CROP_HEIGHT = 360;
|
|
6463
|
-
var CROP_QUALITY = 80;
|
|
6464
6547
|
/**
|
|
6465
|
-
*
|
|
6466
|
-
* `
|
|
6467
|
-
*
|
|
6468
|
-
*
|
|
6469
|
-
*
|
|
6470
|
-
|
|
6548
|
+
* Long-side cap (px) for the boxed timeline tiles — `firstFrame`, `snapshot`,
|
|
6549
|
+
* `lastFrame`. These are the low-res "timeline filmstrip" images: a native full
|
|
6550
|
+
* frame downscaled to 960 with ONLY the track's own box burned in. A native miss
|
|
6551
|
+
* falls back to the ≤640 detection frame (an honest lower-res frame, NEVER an
|
|
6552
|
+
* upscale).
|
|
6553
|
+
*/
|
|
6554
|
+
var SNAPSHOT_MAX_WIDTH = 960;
|
|
6555
|
+
/**
|
|
6556
|
+
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
6557
|
+
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
6558
|
+
* surface). Both frames show the SAME scene at the same aspect ratio, so a
|
|
6559
|
+
* uniform per-axis scale places the box correctly on the higher-res frame.
|
|
6471
6560
|
*/
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
const
|
|
6475
|
-
return
|
|
6561
|
+
function scaleBox(bbox, fromW, fromH, toW, toH, label) {
|
|
6562
|
+
const sx = toW / fromW;
|
|
6563
|
+
const sy = toH / fromH;
|
|
6564
|
+
return {
|
|
6565
|
+
x: bbox.x * sx,
|
|
6566
|
+
y: bbox.y * sy,
|
|
6567
|
+
w: bbox.w * sx,
|
|
6568
|
+
h: bbox.h * sy,
|
|
6569
|
+
...label ? { label } : {}
|
|
6570
|
+
};
|
|
6476
6571
|
}
|
|
6477
6572
|
/**
|
|
6478
6573
|
* True when a packed-RGB frame is (near-)uniform in every channel — the
|
|
@@ -6504,20 +6599,41 @@ function isUniformRgbFrame(data, width, height) {
|
|
|
6504
6599
|
}
|
|
6505
6600
|
/**
|
|
6506
6601
|
* Generates object-event + track media FROM the decoded detection-pipeline
|
|
6507
|
-
* frame (the `frameHandle`/
|
|
6508
|
-
* snapshot cap.
|
|
6509
|
-
*
|
|
6510
|
-
*
|
|
6511
|
-
*
|
|
6512
|
-
*
|
|
6513
|
-
*
|
|
6514
|
-
*
|
|
6515
|
-
*
|
|
6602
|
+
* frame (the `frameHandle`/native surface the detector ran on), NEVER the device
|
|
6603
|
+
* snapshot cap.
|
|
6604
|
+
*
|
|
6605
|
+
* Per object event it writes ONLY the enrichment child crops
|
|
6606
|
+
* (`faceCrop`/`plateCrop`) — the retired `crop`/`fullFrame`/`fullFrameBoxed`
|
|
6607
|
+
* kinds are no longer produced (2026-07-21; old stored tracks keep reading them,
|
|
6608
|
+
* the enum is unchanged). The representative event image is now the track
|
|
6609
|
+
* `thumbnail`/`keyFrame`.
|
|
6610
|
+
*
|
|
6611
|
+
* Per track it writes the boxed timeline tiles — a `firstFrame` on track start,
|
|
6612
|
+
* plus periodic `snapshot`/`lastFrame` — each cut from the runner's NATIVE full
|
|
6613
|
+
* frame downscaled to {@link SNAPSHOT_MAX_WIDTH} (960) with ONLY the track's own
|
|
6614
|
+
* box burned in, falling back to the ≤640 detection frame (honest lower res,
|
|
6615
|
+
* never an upscale) on a native miss. The best `thumbnail` stays a clean,
|
|
6616
|
+
* uncapped native subject crop (native-or-nothing).
|
|
6617
|
+
*
|
|
6618
|
+
* The detection frame is resolved ONCE per call (blank-frame guard); native full
|
|
6619
|
+
* frames + subject crops are fetched per target by handle. Frames recycle in
|
|
6620
|
+
* milliseconds, so the caller must invoke this immediately after producing the
|
|
6621
|
+
* events while the handle is still live.
|
|
6516
6622
|
*/
|
|
6517
6623
|
var EventMediaDispatcher = class {
|
|
6518
6624
|
deps;
|
|
6625
|
+
/**
|
|
6626
|
+
* Per-frame single-slot memo of the native 960 full frame (S3): every track
|
|
6627
|
+
* tile in ONE `captureForFrame` pass shares the SAME `frameHandle`, so multiple
|
|
6628
|
+
* `boxedTimelineFrame` calls would otherwise each round-trip the runner for the
|
|
6629
|
+
* identical native full frame. Keyed by frame identity (`createSharedFrameResolver`)
|
|
6630
|
+
* so a recycled slot (new `seq`) is a fresh fetch. `maxWidth` is always
|
|
6631
|
+
* {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
|
|
6632
|
+
*/
|
|
6633
|
+
sharedNativeFullFrame;
|
|
6519
6634
|
constructor(deps) {
|
|
6520
6635
|
this.deps = deps;
|
|
6636
|
+
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
6521
6637
|
}
|
|
6522
6638
|
async captureForFrame(input) {
|
|
6523
6639
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
@@ -6576,8 +6692,8 @@ var EventMediaDispatcher = class {
|
|
|
6576
6692
|
});
|
|
6577
6693
|
return empty;
|
|
6578
6694
|
}
|
|
6579
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle,
|
|
6580
|
-
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
6695
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6696
|
+
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
|
|
6581
6697
|
const storedSnapshots = [];
|
|
6582
6698
|
const thumbnailTrackIds = [];
|
|
6583
6699
|
for (const sn of snapshots) {
|
|
@@ -6608,21 +6724,7 @@ var EventMediaDispatcher = class {
|
|
|
6608
6724
|
thumbnailWritten: false
|
|
6609
6725
|
};
|
|
6610
6726
|
let boxed = null;
|
|
6611
|
-
if (sn.appendSnapshot || sn.rollingLastFrame)
|
|
6612
|
-
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
6613
|
-
...sn.bbox,
|
|
6614
|
-
...sn.label ? { label: sn.label } : {}
|
|
6615
|
-
}], { quality: MEDIA_QUALITY });
|
|
6616
|
-
} catch (err) {
|
|
6617
|
-
this.deps.logger.warn("event media: track snapshot encode failed", {
|
|
6618
|
-
tags: { deviceId },
|
|
6619
|
-
meta: {
|
|
6620
|
-
deviceId,
|
|
6621
|
-
trackId: sn.trackId,
|
|
6622
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6623
|
-
}
|
|
6624
|
-
});
|
|
6625
|
-
}
|
|
6727
|
+
if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
|
|
6626
6728
|
let stored = null;
|
|
6627
6729
|
if (sn.appendSnapshot && boxed) try {
|
|
6628
6730
|
const mediaKey = await this.deps.mediaStore.put({
|
|
@@ -6642,19 +6744,9 @@ var EventMediaDispatcher = class {
|
|
|
6642
6744
|
} catch {}
|
|
6643
6745
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6644
6746
|
let thumbnailWritten = false;
|
|
6645
|
-
if (sn.bestThumbnail)
|
|
6646
|
-
const crop = await this.cropSubjectRegion(frameHandle,
|
|
6647
|
-
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
|
|
6648
|
-
} catch (err) {
|
|
6649
|
-
this.deps.logger.warn("event media: track thumbnail crop failed", {
|
|
6650
|
-
tags: { deviceId },
|
|
6651
|
-
meta: {
|
|
6652
|
-
deviceId,
|
|
6653
|
-
trackId: sn.trackId,
|
|
6654
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6655
|
-
}
|
|
6656
|
-
});
|
|
6657
|
-
if (boxed) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
6747
|
+
if (sn.bestThumbnail) {
|
|
6748
|
+
const crop = await this.cropSubjectRegion(frameHandle, fw, fh, sn.bbox, cropPadding);
|
|
6749
|
+
if (crop) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
|
|
6658
6750
|
}
|
|
6659
6751
|
return {
|
|
6660
6752
|
storedSnapshot: stored,
|
|
@@ -6666,143 +6758,126 @@ var EventMediaDispatcher = class {
|
|
|
6666
6758
|
* object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
|
|
6667
6759
|
* `thumbnail`: the square-safe 16:9 region around the bbox with NO box drawn.
|
|
6668
6760
|
*
|
|
6669
|
-
* NATIVE-
|
|
6670
|
-
* surface (normalized [0,1] coords map directly onto it)
|
|
6671
|
-
*
|
|
6672
|
-
*
|
|
6673
|
-
*
|
|
6674
|
-
*
|
|
6761
|
+
* NATIVE-OR-NOTHING: the region is requested from the runner's retained native
|
|
6762
|
+
* surface (normalized [0,1] coords map directly onto it) with NO `maxWidth` —
|
|
6763
|
+
* the crop at TRUE native resolution (decision #3). On any miss/error (or a
|
|
6764
|
+
* runner without the method) it returns `null` after a loud `logger.warn`; the
|
|
6765
|
+
* caller SKIPS the write and the per-frame retry lands a real native crop
|
|
6766
|
+
* later. It is NEVER upscaled — an upscaled ≤640 tile is a blurred lie
|
|
6767
|
+
* (case-study lapVar 5–7), so no local resize fallback exists. Runs inside the
|
|
6768
|
+
* live-handle window opened by `captureForFrame`.
|
|
6675
6769
|
*/
|
|
6676
|
-
async cropSubjectRegion(frameHandle,
|
|
6677
|
-
if (this.deps.getNativeCropJpeg)
|
|
6770
|
+
async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
|
|
6771
|
+
if (!this.deps.getNativeCropJpeg) {
|
|
6772
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6773
|
+
shmId: frameHandle.shmId,
|
|
6774
|
+
reason: "no-native-cap"
|
|
6775
|
+
} });
|
|
6776
|
+
return null;
|
|
6777
|
+
}
|
|
6778
|
+
try {
|
|
6678
6779
|
const norm = squareSafeCropRegionNormalized(bbox, {
|
|
6679
6780
|
W: fw,
|
|
6680
6781
|
H: fh
|
|
6681
6782
|
}, cropPadding);
|
|
6682
|
-
const native = await this.deps.getNativeCropJpeg(frameHandle, norm
|
|
6783
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
6683
6784
|
if (native) return native;
|
|
6785
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6786
|
+
shmId: frameHandle.shmId,
|
|
6787
|
+
reason: "native-miss"
|
|
6788
|
+
} });
|
|
6789
|
+
return null;
|
|
6684
6790
|
} catch (err) {
|
|
6685
|
-
this.deps.logger.
|
|
6791
|
+
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6686
6792
|
shmId: frameHandle.shmId,
|
|
6687
6793
|
error: err instanceof Error ? err.message : String(err)
|
|
6688
6794
|
} });
|
|
6795
|
+
return null;
|
|
6689
6796
|
}
|
|
6690
|
-
return this.cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding);
|
|
6691
6797
|
}
|
|
6692
6798
|
/**
|
|
6693
|
-
*
|
|
6694
|
-
*
|
|
6799
|
+
* The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
|
|
6800
|
+
* OWN box burned onto the NATIVE full frame downscaled to
|
|
6801
|
+
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
6802
|
+
* detection frame (`frameData`) downscaled to the same cap with a `logger.warn`
|
|
6803
|
+
* — an honest LOWER-res frame, NEVER an upscale. Returns the JPEG, or `null`
|
|
6804
|
+
* only if the sharp encode itself throws.
|
|
6695
6805
|
*/
|
|
6696
|
-
async
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
H: fh
|
|
6700
|
-
}, cropPadding);
|
|
6701
|
-
const left = Math.max(0, Math.min(region.x, fw - 1));
|
|
6702
|
-
const top = Math.max(0, Math.min(region.y, fh - 1));
|
|
6703
|
-
const width = Math.max(1, Math.min(region.w, fw - left));
|
|
6704
|
-
const height = Math.max(1, Math.min(region.h, fh - top));
|
|
6705
|
-
return await sharp(frameData, { raw: {
|
|
6706
|
-
width: fw,
|
|
6707
|
-
height: fh,
|
|
6708
|
-
channels: 3
|
|
6709
|
-
} }).extract({
|
|
6710
|
-
left,
|
|
6711
|
-
top,
|
|
6712
|
-
width,
|
|
6713
|
-
height
|
|
6714
|
-
}).resize(CROP_WIDTH, CROP_HEIGHT).jpeg({ quality: CROP_QUALITY }).toBuffer();
|
|
6715
|
-
}
|
|
6716
|
-
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
6806
|
+
async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
|
|
6807
|
+
let nf = null;
|
|
6808
|
+
let fetchThrew = false;
|
|
6717
6809
|
try {
|
|
6718
|
-
await this.
|
|
6719
|
-
deviceId,
|
|
6720
|
-
ownerKind: "track",
|
|
6721
|
-
ownerId: trackId,
|
|
6722
|
-
kind,
|
|
6723
|
-
timestamp,
|
|
6724
|
-
data
|
|
6725
|
-
});
|
|
6726
|
-
return true;
|
|
6810
|
+
nf = await this.sharedNativeFullFrame(frameHandle);
|
|
6727
6811
|
} catch (err) {
|
|
6728
|
-
|
|
6812
|
+
fetchThrew = true;
|
|
6813
|
+
this.deps.logger.warn("native full frame fetch threw — 640 boxed fallback", {
|
|
6729
6814
|
tags: { deviceId },
|
|
6730
6815
|
meta: {
|
|
6731
6816
|
deviceId,
|
|
6732
|
-
|
|
6817
|
+
ownerId,
|
|
6733
6818
|
error: err instanceof Error ? err.message : String(err)
|
|
6734
6819
|
}
|
|
6735
6820
|
});
|
|
6736
|
-
return false;
|
|
6737
6821
|
}
|
|
6738
|
-
}
|
|
6739
|
-
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
6740
|
-
const box = {
|
|
6741
|
-
...ev.bbox,
|
|
6742
|
-
label: caption(ev.className, ev.confidence, ev.label)
|
|
6743
|
-
};
|
|
6744
6822
|
try {
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
kind: "crop",
|
|
6751
|
-
timestamp: ev.timestamp,
|
|
6752
|
-
data: crop
|
|
6753
|
-
});
|
|
6754
|
-
} catch (err) {
|
|
6755
|
-
this.deps.logger.warn("event media: crop failed", {
|
|
6823
|
+
if (nf && nf.format === "rgb" && nf.width > 0 && nf.height > 0) {
|
|
6824
|
+
const box = scaleBox(bbox, fw, fh, nf.width, nf.height, label);
|
|
6825
|
+
return await drawBoxedFrame(nf.data, nf.width, nf.height, [box], { quality: 88 });
|
|
6826
|
+
}
|
|
6827
|
+
if (!fetchThrew) this.deps.logger.warn("native full frame miss — 640 boxed fallback (no upscale)", {
|
|
6756
6828
|
tags: { deviceId },
|
|
6757
6829
|
meta: {
|
|
6758
6830
|
deviceId,
|
|
6759
|
-
|
|
6760
|
-
|
|
6831
|
+
ownerId,
|
|
6832
|
+
shmId: frameHandle.shmId
|
|
6761
6833
|
}
|
|
6762
6834
|
});
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
ownerId: ev.eventId,
|
|
6770
|
-
kind: "fullFrame",
|
|
6771
|
-
timestamp: ev.timestamp,
|
|
6772
|
-
data: fullFrame
|
|
6835
|
+
return await drawBoxedFrame(frameData, fw, fh, [{
|
|
6836
|
+
...bbox,
|
|
6837
|
+
...label ? { label } : {}
|
|
6838
|
+
}], {
|
|
6839
|
+
quality: 88,
|
|
6840
|
+
maxWidth: SNAPSHOT_MAX_WIDTH
|
|
6773
6841
|
});
|
|
6774
6842
|
} catch (err) {
|
|
6775
|
-
this.deps.logger.warn("event media:
|
|
6843
|
+
this.deps.logger.warn("event media: timeline frame encode failed", {
|
|
6776
6844
|
tags: { deviceId },
|
|
6777
6845
|
meta: {
|
|
6778
6846
|
deviceId,
|
|
6779
|
-
|
|
6847
|
+
ownerId,
|
|
6780
6848
|
error: err instanceof Error ? err.message : String(err)
|
|
6781
6849
|
}
|
|
6782
6850
|
});
|
|
6851
|
+
return null;
|
|
6783
6852
|
}
|
|
6853
|
+
}
|
|
6854
|
+
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
6784
6855
|
try {
|
|
6785
|
-
|
|
6786
|
-
await this.deps.mediaStore.put({
|
|
6856
|
+
await this.deps.mediaStore.putReplacing({
|
|
6787
6857
|
deviceId,
|
|
6788
|
-
ownerKind: "
|
|
6789
|
-
ownerId:
|
|
6790
|
-
kind
|
|
6791
|
-
timestamp
|
|
6792
|
-
data
|
|
6858
|
+
ownerKind: "track",
|
|
6859
|
+
ownerId: trackId,
|
|
6860
|
+
kind,
|
|
6861
|
+
timestamp,
|
|
6862
|
+
data
|
|
6793
6863
|
});
|
|
6864
|
+
return true;
|
|
6794
6865
|
} catch (err) {
|
|
6795
|
-
this.deps.logger.
|
|
6866
|
+
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
6796
6867
|
tags: { deviceId },
|
|
6797
6868
|
meta: {
|
|
6798
6869
|
deviceId,
|
|
6799
|
-
|
|
6870
|
+
trackId,
|
|
6800
6871
|
error: err instanceof Error ? err.message : String(err)
|
|
6801
6872
|
}
|
|
6802
6873
|
});
|
|
6874
|
+
return false;
|
|
6803
6875
|
}
|
|
6876
|
+
}
|
|
6877
|
+
async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
|
|
6804
6878
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
6805
|
-
const childCropData = await this.cropSubjectRegion(frameHandle,
|
|
6879
|
+
const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
|
|
6880
|
+
if (!childCropData) continue;
|
|
6806
6881
|
await this.deps.mediaStore.put({
|
|
6807
6882
|
deviceId,
|
|
6808
6883
|
ownerKind: "event",
|
|
@@ -6822,13 +6897,10 @@ var EventMediaDispatcher = class {
|
|
|
6822
6897
|
});
|
|
6823
6898
|
}
|
|
6824
6899
|
}
|
|
6825
|
-
async writeTrackFrame(deviceId, frameData, fw, fh, tf) {
|
|
6826
|
-
const
|
|
6827
|
-
|
|
6828
|
-
...tf.label ? { label: tf.label } : {}
|
|
6829
|
-
};
|
|
6900
|
+
async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
|
|
6901
|
+
const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
|
|
6902
|
+
if (!boxed) return;
|
|
6830
6903
|
try {
|
|
6831
|
-
const boxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
|
|
6832
6904
|
await this.deps.mediaStore.put({
|
|
6833
6905
|
deviceId,
|
|
6834
6906
|
ownerKind: "track",
|
|
@@ -8704,34 +8776,13 @@ function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight) {
|
|
|
8704
8776
|
}
|
|
8705
8777
|
//#endregion
|
|
8706
8778
|
//#region src/pipeline-analytics/pipeline/key-frame-capture.ts
|
|
8707
|
-
/**
|
|
8708
|
-
* Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
|
|
8709
|
-
* (Design B — one native full-frame per track at its best-detection moment).
|
|
8710
|
-
*
|
|
8711
|
-
* ## Why this exists (the missing native keyFrame)
|
|
8712
|
-
*
|
|
8713
|
-
* `keyFrame` was historically captured ONLY inside the CLIP object-embedding
|
|
8714
|
-
* best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
|
|
8715
|
-
* Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
|
|
8716
|
-
* a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
|
|
8717
|
-
* wide), so that gate was never satisfied and the native `keyFrame` was NEVER
|
|
8718
|
-
* produced — every stored frame stayed at the ≤640×360 detection resolution.
|
|
8719
|
-
*
|
|
8720
|
-
* The fix decouples the `keyFrame` from the clip path: it is captured on the
|
|
8721
|
-
* GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
|
|
8722
|
-
* `thumbnail`), reusing the WORKING native crop path (`captureCrop` →
|
|
8723
|
-
* `pipelineRunner.getNativeCrop`, which cuts the ROI from the decode worker's
|
|
8724
|
-
* retained NATIVE surface and only falls back to the detection frame on a miss).
|
|
8725
|
-
* A full-frame ROI at {@link KEYFRAME_NATIVE_MAX_WIDTH} therefore yields a frame
|
|
8726
|
-
* LARGER than the detection raster (up to the cap), which is the whole point of
|
|
8727
|
-
* the `keyFrame` kind.
|
|
8728
|
-
*/
|
|
8729
8779
|
/** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
|
|
8730
|
-
* Native resolution is the point
|
|
8731
|
-
*
|
|
8732
|
-
*
|
|
8733
|
-
*
|
|
8734
|
-
|
|
8780
|
+
* Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
|
|
8781
|
+
* resize, 3840 ≤ 4096); an 8K cam is bounded to 4096 (still far past the retired
|
|
8782
|
+
* 1920 cap). This honours "native, no 1920 intermediate" while keeping the raw
|
|
8783
|
+
* rgb transfer wire-safe. Env-overridable (`CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH`)
|
|
8784
|
+
* for a homelab that wants a tighter/looser bound. */
|
|
8785
|
+
var KEYFRAME_NATIVE_MAX_WIDTH = Number.parseInt(process.env.CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH ?? "", 10) || 4096;
|
|
8735
8786
|
/**
|
|
8736
8787
|
* The tracks whose native `keyFrame` should be (re)captured THIS frame: exactly
|
|
8737
8788
|
* the tracks that hit a GENUINE new best-frame moment (`keyFrame`), NOT the
|
|
@@ -8742,31 +8793,39 @@ function selectKeyFrameTrackIds(targets) {
|
|
|
8742
8793
|
return targets.filter((t) => t.keyFrame).map((t) => t.trackId);
|
|
8743
8794
|
}
|
|
8744
8795
|
/**
|
|
8745
|
-
*
|
|
8746
|
-
*
|
|
8747
|
-
*
|
|
8748
|
-
* a
|
|
8796
|
+
* Gate for the clean best-shot native `keyFrame` (design 2026-07-21): the keyFrame
|
|
8797
|
+
* is the track's CLEAN native best frame and MUST NEVER be persisted from the
|
|
8798
|
+
* ≤640 RAM fallback (`tier: 'ram-fullframe'`) — that would pin the stored keyFrame
|
|
8799
|
+
* at detection resolution. On a `ram-fullframe` tier the caller drops the write
|
|
8800
|
+
* and the per-frame re-fire retries a real native surface later.
|
|
8801
|
+
*
|
|
8802
|
+
* An ABSENT tier (`undefined`, from a pre-tier runner on a mixed-version cluster)
|
|
8803
|
+
* is ACCEPTED exactly as before the tier field existed — back-compat, never break
|
|
8804
|
+
* mixed-version clusters. Only an explicit `ram-fullframe` is rejected.
|
|
8805
|
+
*
|
|
8806
|
+
* NOTE: this gates the native `keyFrame` ONLY. The 960 boxed TIMELINE tiles keep
|
|
8807
|
+
* accepting the ≤640 fallback (an honest low-res timeline is acceptable there).
|
|
8749
8808
|
*/
|
|
8750
|
-
function
|
|
8751
|
-
return
|
|
8752
|
-
bbox: {
|
|
8753
|
-
x: 0,
|
|
8754
|
-
y: 0,
|
|
8755
|
-
w: frameWidth,
|
|
8756
|
-
h: frameHeight
|
|
8757
|
-
},
|
|
8758
|
-
padding: 0,
|
|
8759
|
-
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
8760
|
-
};
|
|
8809
|
+
function keyFrameAcceptsTier(tier) {
|
|
8810
|
+
return tier !== "ram-fullframe";
|
|
8761
8811
|
}
|
|
8762
8812
|
/**
|
|
8763
|
-
*
|
|
8764
|
-
*
|
|
8765
|
-
*
|
|
8766
|
-
*
|
|
8813
|
+
* Encode BOTH key-frame variants from a single decoded native full frame. No box
|
|
8814
|
+
* is drawn (the `keyFrame` is the clean best full frame); `drawBoxedFrame([])`
|
|
8815
|
+
* is used purely as the raw-rgb → JPEG (+ optional downscale) encoder so the two
|
|
8816
|
+
* variants share one code path. The caller has ALREADY fetched the native frame
|
|
8817
|
+
* at {@link KEYFRAME_NATIVE_MAX_WIDTH}, so `keyFrame` re-encodes it verbatim
|
|
8818
|
+
* (no resize) and `keyFrameSmall` downscales the same buffer to ≤960 —
|
|
8819
|
+
* `withoutEnlargement` never upscales a sub-960 native frame.
|
|
8767
8820
|
*/
|
|
8768
|
-
function
|
|
8769
|
-
return
|
|
8821
|
+
async function encodeKeyFrameVariants(native) {
|
|
8822
|
+
return {
|
|
8823
|
+
keyFrame: await drawBoxedFrame(native.data, native.width, native.height, [], { quality: 88 }),
|
|
8824
|
+
keyFrameSmall: await drawBoxedFrame(native.data, native.width, native.height, [], {
|
|
8825
|
+
quality: 88,
|
|
8826
|
+
maxWidth: 960
|
|
8827
|
+
})
|
|
8828
|
+
};
|
|
8770
8829
|
}
|
|
8771
8830
|
//#endregion
|
|
8772
8831
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
@@ -11529,22 +11588,6 @@ var PlateRecognizer = class {
|
|
|
11529
11588
|
}
|
|
11530
11589
|
};
|
|
11531
11590
|
//#endregion
|
|
11532
|
-
//#region src/shared/frame/shared-frame-resolver.ts
|
|
11533
|
-
function frameHandleKey(h) {
|
|
11534
|
-
return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
|
|
11535
|
-
}
|
|
11536
|
-
function createSharedFrameResolver(resolve) {
|
|
11537
|
-
let cache = null;
|
|
11538
|
-
return (handle) => {
|
|
11539
|
-
const key = frameHandleKey(handle);
|
|
11540
|
-
if (cache === null || cache.key !== key) cache = {
|
|
11541
|
-
key,
|
|
11542
|
-
value: resolve(handle)
|
|
11543
|
-
};
|
|
11544
|
-
return cache.value;
|
|
11545
|
-
};
|
|
11546
|
-
}
|
|
11547
|
-
//#endregion
|
|
11548
11591
|
//#region src/shared/frame/encode-crop.ts
|
|
11549
11592
|
/**
|
|
11550
11593
|
* JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
|
|
@@ -11560,6 +11603,21 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
|
|
|
11560
11603
|
channels: 3
|
|
11561
11604
|
} }).jpeg({ quality: 90 }).toBuffer();
|
|
11562
11605
|
}
|
|
11606
|
+
/**
|
|
11607
|
+
* Decode a base64 JPEG (the COMPRESSED cross-node crop the owning runner
|
|
11608
|
+
* returns) back into raw RGB (24-bit) pixels, so every downstream consumer
|
|
11609
|
+
* keeps its existing RGB contract regardless of whether the crop was fetched
|
|
11610
|
+
* same-node (raw) or cross-node (JPEG). The decode happens IN-PROCESS on the
|
|
11611
|
+
* post-processing node — no raw pixels ever cross a process boundary.
|
|
11612
|
+
*/
|
|
11613
|
+
async function decodeJpegToRgb(base64Jpeg) {
|
|
11614
|
+
const { data, info } = await sharp(Buffer.from(base64Jpeg, "base64")).raw().toBuffer({ resolveWithObject: true });
|
|
11615
|
+
return {
|
|
11616
|
+
bytes: data,
|
|
11617
|
+
width: info.width,
|
|
11618
|
+
height: info.height
|
|
11619
|
+
};
|
|
11620
|
+
}
|
|
11563
11621
|
//#endregion
|
|
11564
11622
|
//#region src/pipeline-analytics/pipeline/event-child-crops.ts
|
|
11565
11623
|
/**
|
|
@@ -11720,6 +11778,26 @@ function padBbox(bbox, padding) {
|
|
|
11720
11778
|
};
|
|
11721
11779
|
}
|
|
11722
11780
|
//#endregion
|
|
11781
|
+
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
11782
|
+
function createCaptureCrop(deps) {
|
|
11783
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
11784
|
+
const paddedNorm = padBbox({
|
|
11785
|
+
x: bbox.x / frameWidth,
|
|
11786
|
+
y: bbox.y / frameHeight,
|
|
11787
|
+
w: bbox.w / frameWidth,
|
|
11788
|
+
h: bbox.h / frameHeight
|
|
11789
|
+
}, padding);
|
|
11790
|
+
const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
11791
|
+
if (nativeCrop) {
|
|
11792
|
+
deps.bumpCropMetric(true);
|
|
11793
|
+
return nativeCrop;
|
|
11794
|
+
}
|
|
11795
|
+
deps.bumpCropMetric(false);
|
|
11796
|
+
deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", { meta: { nodeId: frameHandle.nodeId } });
|
|
11797
|
+
return null;
|
|
11798
|
+
};
|
|
11799
|
+
}
|
|
11800
|
+
//#endregion
|
|
11723
11801
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
11724
11802
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
11725
11803
|
if (!cfg.enabled) return false;
|
|
@@ -11905,6 +11983,32 @@ async function makeSquareThumb(bytes, size) {
|
|
|
11905
11983
|
}).jpeg({ quality: THUMB_QUALITY }).toBuffer();
|
|
11906
11984
|
}
|
|
11907
11985
|
//#endregion
|
|
11986
|
+
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
11987
|
+
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
11988
|
+
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
11989
|
+
* for new tracks (served only for old stored rows). Undefined when the event
|
|
11990
|
+
* owns no media. */
|
|
11991
|
+
function pickEventOwnedMedia(files) {
|
|
11992
|
+
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
11993
|
+
}
|
|
11994
|
+
/** Pick a track's fallback media in the shared cadence-preference order the
|
|
11995
|
+
* KeyEvent path uses: best `thumbnail` → rolling `lastFrame` → `firstFrame` →
|
|
11996
|
+
* newest `snapshot` → any track blob. Undefined when the track owns no media. */
|
|
11997
|
+
function pickTrackFallbackMedia(files) {
|
|
11998
|
+
return files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
11999
|
+
}
|
|
12000
|
+
/**
|
|
12001
|
+
* Resolve the default-path media for an event id: the event's own media when it
|
|
12002
|
+
* has any, else the owning track's fallback media. Returns `null` when neither
|
|
12003
|
+
* yields a blob (caller 404s → the surface shows an icon).
|
|
12004
|
+
*/
|
|
12005
|
+
async function resolveDefaultEventMedia(deps) {
|
|
12006
|
+
const owned = pickEventOwnedMedia(deps.eventFiles);
|
|
12007
|
+
if (owned) return owned;
|
|
12008
|
+
const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
|
|
12009
|
+
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
12010
|
+
}
|
|
12011
|
+
//#endregion
|
|
11908
12012
|
//#region src/pipeline-analytics/index.ts
|
|
11909
12013
|
/**
|
|
11910
12014
|
* Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
|
|
@@ -11979,12 +12083,20 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
|
11979
12083
|
* without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
|
|
11980
12084
|
* which is how 65% of the timeline tiles rendered as whole scenes. It sits
|
|
11981
12085
|
* right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
|
|
12086
|
+
*
|
|
12087
|
+
* `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
|
|
12088
|
+
* joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
|
|
12089
|
+
* for tracks whose only clean media is the small key frame; it sits last
|
|
12090
|
+
* (fullest-scene, but subject-centered enough to serve as a reasonable fallback
|
|
12091
|
+
* over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
|
|
12092
|
+
* keep resolving.
|
|
11982
12093
|
*/
|
|
11983
12094
|
var CLEAN_MEDIA_KINDS = [
|
|
11984
12095
|
"crop",
|
|
11985
12096
|
"thumbnail",
|
|
11986
12097
|
"fullFrame",
|
|
11987
|
-
"keyFrame"
|
|
12098
|
+
"keyFrame",
|
|
12099
|
+
"keyFrameSmall"
|
|
11988
12100
|
];
|
|
11989
12101
|
/**
|
|
11990
12102
|
* Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
|
|
@@ -12247,6 +12359,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12247
12359
|
* optional `maxWidth` caps the native crop width (used for the full-frame key
|
|
12248
12360
|
* frame so a 4K native surface never floods the transport). */
|
|
12249
12361
|
captureCrop = null;
|
|
12362
|
+
/** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
|
|
12363
|
+
* to the owning runner, PLUS the source `tier`. Captured in the async init
|
|
12364
|
+
* block so `persistKeyFrames` can fetch the native full frame for the
|
|
12365
|
+
* `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
|
|
12366
|
+
* a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
|
|
12367
|
+
getNativeKeyFrameRgb = null;
|
|
12250
12368
|
shuttingDown = false;
|
|
12251
12369
|
/** True only on the cluster's designated post-processing node. When false the
|
|
12252
12370
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -12440,6 +12558,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12440
12558
|
} });
|
|
12441
12559
|
}
|
|
12442
12560
|
const pipelineRunnerApi = api.pipelineRunner;
|
|
12561
|
+
const isRemoteHandle = (handle) => handle.nodeId !== ownNodeId;
|
|
12562
|
+
const cropReplyToRgb = async (reply) => {
|
|
12563
|
+
if (reply.jpeg !== void 0) {
|
|
12564
|
+
const rgb = await decodeJpegToRgb(reply.jpeg);
|
|
12565
|
+
return {
|
|
12566
|
+
bytes: Buffer.from(rgb.bytes),
|
|
12567
|
+
width: rgb.width,
|
|
12568
|
+
height: rgb.height
|
|
12569
|
+
};
|
|
12570
|
+
}
|
|
12571
|
+
if (reply.bytes !== void 0) return {
|
|
12572
|
+
bytes: Buffer.from(reply.bytes),
|
|
12573
|
+
width: reply.width,
|
|
12574
|
+
height: reply.height
|
|
12575
|
+
};
|
|
12576
|
+
return null;
|
|
12577
|
+
};
|
|
12443
12578
|
const getRemoteFrame = async (handle) => {
|
|
12444
12579
|
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
12445
12580
|
const full = await pipelineRunnerApi.getNativeCrop.query({
|
|
@@ -12450,17 +12585,50 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12450
12585
|
w: 1,
|
|
12451
12586
|
h: 1
|
|
12452
12587
|
},
|
|
12453
|
-
maxWidth: handle.width
|
|
12588
|
+
maxWidth: handle.width,
|
|
12589
|
+
encodeJpeg: isRemoteHandle(handle)
|
|
12454
12590
|
}, nodePin(handle.nodeId));
|
|
12455
12591
|
if (!full || full.width <= 0 || full.height <= 0) return null;
|
|
12592
|
+
const rgb = await cropReplyToRgb(full);
|
|
12593
|
+
if (!rgb) return null;
|
|
12456
12594
|
return {
|
|
12457
|
-
data:
|
|
12458
|
-
width:
|
|
12459
|
-
height:
|
|
12595
|
+
data: rgb.bytes,
|
|
12596
|
+
width: rgb.width,
|
|
12597
|
+
height: rgb.height,
|
|
12460
12598
|
format: "rgb",
|
|
12461
12599
|
timestamp: 0
|
|
12462
12600
|
};
|
|
12463
12601
|
};
|
|
12602
|
+
const fetchNativeFullFrameTiered = async (handle, maxWidth) => {
|
|
12603
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
12604
|
+
const full = await pipelineRunnerApi.getNativeCrop.query({
|
|
12605
|
+
handle,
|
|
12606
|
+
bbox: {
|
|
12607
|
+
x: 0,
|
|
12608
|
+
y: 0,
|
|
12609
|
+
w: 1,
|
|
12610
|
+
h: 1
|
|
12611
|
+
},
|
|
12612
|
+
maxWidth,
|
|
12613
|
+
encodeJpeg: isRemoteHandle(handle)
|
|
12614
|
+
}, nodePin(handle.nodeId));
|
|
12615
|
+
if (!full || full.width <= 0 || full.height <= 0) return null;
|
|
12616
|
+
const rgb = await cropReplyToRgb(full);
|
|
12617
|
+
if (!rgb) return null;
|
|
12618
|
+
const tier = full.tier === "ram-fullframe" ? "ram-fullframe" : "native";
|
|
12619
|
+
return {
|
|
12620
|
+
frame: {
|
|
12621
|
+
data: rgb.bytes,
|
|
12622
|
+
width: rgb.width,
|
|
12623
|
+
height: rgb.height,
|
|
12624
|
+
format: "rgb",
|
|
12625
|
+
timestamp: 0
|
|
12626
|
+
},
|
|
12627
|
+
tier
|
|
12628
|
+
};
|
|
12629
|
+
};
|
|
12630
|
+
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
12631
|
+
this.getNativeKeyFrameRgb = fetchNativeFullFrameTiered;
|
|
12464
12632
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
12465
12633
|
let nativeHits = 0;
|
|
12466
12634
|
let nativeFallbacks = 0;
|
|
@@ -12483,14 +12651,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12483
12651
|
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
12484
12652
|
handle: frameHandle,
|
|
12485
12653
|
bbox: paddedNorm,
|
|
12486
|
-
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
12654
|
+
...maxWidth !== void 0 ? { maxWidth } : {},
|
|
12655
|
+
encodeJpeg: isRemoteHandle(frameHandle)
|
|
12487
12656
|
}, nodePin(frameHandle.nodeId));
|
|
12488
12657
|
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
12489
|
-
return
|
|
12490
|
-
bytes: Buffer.from(native.bytes),
|
|
12491
|
-
width: native.width,
|
|
12492
|
-
height: native.height
|
|
12493
|
-
};
|
|
12658
|
+
return await cropReplyToRgb(native);
|
|
12494
12659
|
} catch (err) {
|
|
12495
12660
|
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
|
|
12496
12661
|
return null;
|
|
@@ -12503,12 +12668,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12503
12668
|
};
|
|
12504
12669
|
const getNativeCropJpeg = async (frameHandle, normalizedBbox, maxWidth) => {
|
|
12505
12670
|
const native = await fetchNativeCropRgb(frameHandle, normalizedBbox, maxWidth);
|
|
12506
|
-
if (!native
|
|
12507
|
-
if (native) cropMetricLogger.debug("native subject crop below floor — local fallback", { meta: {
|
|
12508
|
-
width: native.width,
|
|
12509
|
-
height: native.height,
|
|
12510
|
-
maxWidth
|
|
12511
|
-
} });
|
|
12671
|
+
if (!native) {
|
|
12512
12672
|
bumpCropMetric(false);
|
|
12513
12673
|
return null;
|
|
12514
12674
|
}
|
|
@@ -12519,28 +12679,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12519
12679
|
this.eventMediaDispatcher = new EventMediaDispatcher({
|
|
12520
12680
|
getRemoteFrame,
|
|
12521
12681
|
getNativeCropJpeg,
|
|
12682
|
+
getNativeFullFrameRgb,
|
|
12522
12683
|
mediaStore: this.mediaStore,
|
|
12523
12684
|
logger: logger.child("EventMediaDispatcher")
|
|
12524
12685
|
});
|
|
12525
|
-
const
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
w: bbox.w / frameWidth,
|
|
12531
|
-
h: bbox.h / frameHeight
|
|
12532
|
-
}, padding);
|
|
12533
|
-
const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
|
|
12534
|
-
if (nativeCrop) {
|
|
12535
|
-
bumpCropMetric(true);
|
|
12536
|
-
return nativeCrop;
|
|
12537
|
-
}
|
|
12538
|
-
bumpCropMetric(false);
|
|
12539
|
-
const decoded = await resolveFrameShared(frameHandle);
|
|
12540
|
-
if (!decoded || decoded.format !== "rgb") return null;
|
|
12541
|
-
const { crop } = await extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
|
|
12542
|
-
return crop;
|
|
12543
|
-
};
|
|
12686
|
+
const captureCrop = createCaptureCrop({
|
|
12687
|
+
tryNativeCrop,
|
|
12688
|
+
bumpCropMetric,
|
|
12689
|
+
logger: logger.child("CaptureCrop")
|
|
12690
|
+
});
|
|
12544
12691
|
this.captureCrop = captureCrop;
|
|
12545
12692
|
this.faceRecognizer = new FaceRecognizer({
|
|
12546
12693
|
identityStore: this.identityStore,
|
|
@@ -13389,7 +13536,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13389
13536
|
else plateCrops += 1;
|
|
13390
13537
|
const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
|
|
13391
13538
|
const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
|
|
13392
|
-
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle
|
|
13539
|
+
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
|
|
13393
13540
|
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
|
|
13394
13541
|
const captureCounts = {
|
|
13395
13542
|
events: eventTargets.length,
|
|
@@ -14000,30 +14147,54 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14000
14147
|
}));
|
|
14001
14148
|
}
|
|
14002
14149
|
/**
|
|
14003
|
-
* Capture
|
|
14004
|
-
*
|
|
14150
|
+
* Capture the CLEAN best full frame per given track at this best-detection
|
|
14151
|
+
* moment and store BOTH variants (`putReplacing` → one row per track, per
|
|
14152
|
+
* kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
|
|
14153
|
+
* — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
|
|
14154
|
+
* from the SAME native surface (no second fetch). No box is drawn — these are
|
|
14155
|
+
* the clean hero images (`keyFrameSmall` is the web-friendly one).
|
|
14005
14156
|
*
|
|
14006
|
-
*
|
|
14007
|
-
*
|
|
14008
|
-
* `
|
|
14009
|
-
*
|
|
14010
|
-
*
|
|
14011
|
-
*
|
|
14012
|
-
* Issued in the live-frame window so the native lease is still held. Best-
|
|
14013
|
-
* effort (D8) — a per-track failure is logged and never thrown.
|
|
14157
|
+
* NATIVE-OR-NOTHING: a native miss logs `error` and returns (no upscale, no
|
|
14158
|
+
* ≤640 stand-in) — the next genuine new-best frame retries. The `keyFrame`
|
|
14159
|
+
* key is recorded in `keyFrameKeyByTrackId` so the face / plate /
|
|
14160
|
+
* object-embedding rows LINK the SAME native key frame (Design B). Issued in
|
|
14161
|
+
* the live-frame window so the native lease is still held. Best-effort (D8) —
|
|
14162
|
+
* a per-track failure is logged and never thrown.
|
|
14014
14163
|
*/
|
|
14015
|
-
async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle
|
|
14016
|
-
const
|
|
14164
|
+
async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle) {
|
|
14165
|
+
const getNative = this.getNativeKeyFrameRgb;
|
|
14017
14166
|
const mediaStore = this.mediaStore;
|
|
14018
|
-
if (!
|
|
14019
|
-
const req = buildKeyFrameCaptureRequest(frameWidth, frameHeight);
|
|
14167
|
+
if (!getNative || !mediaStore) return;
|
|
14020
14168
|
const pending = trackIds.filter((id) => !this.keyFrameInFlight.has(id));
|
|
14021
14169
|
if (pending.length === 0) return;
|
|
14022
14170
|
for (const id of pending) this.keyFrameInFlight.add(id);
|
|
14023
14171
|
await Promise.all(pending.map(async (trackId) => {
|
|
14024
14172
|
try {
|
|
14025
|
-
const
|
|
14026
|
-
|
|
14173
|
+
const result = await getNative(frameHandle, KEYFRAME_NATIVE_MAX_WIDTH);
|
|
14174
|
+
const native = result?.frame;
|
|
14175
|
+
if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
|
|
14176
|
+
this.ctx.logger.error("key-frame native miss — no upscale, will retry next best", {
|
|
14177
|
+
tags: { deviceId },
|
|
14178
|
+
meta: {
|
|
14179
|
+
trackId,
|
|
14180
|
+
shmId: frameHandle.shmId
|
|
14181
|
+
}
|
|
14182
|
+
});
|
|
14183
|
+
return;
|
|
14184
|
+
}
|
|
14185
|
+
const tier = result.tier;
|
|
14186
|
+
if (!keyFrameAcceptsTier(tier)) {
|
|
14187
|
+
this.ctx.logger.warn("keyFrame native miss — RAM tier rejected, will retry", {
|
|
14188
|
+
tags: { deviceId },
|
|
14189
|
+
meta: {
|
|
14190
|
+
trackId,
|
|
14191
|
+
shmId: frameHandle.shmId,
|
|
14192
|
+
tier
|
|
14193
|
+
}
|
|
14194
|
+
});
|
|
14195
|
+
return;
|
|
14196
|
+
}
|
|
14197
|
+
const { keyFrame, keyFrameSmall } = await encodeKeyFrameVariants(native);
|
|
14027
14198
|
const key = await mediaStore.putReplacing({
|
|
14028
14199
|
deviceId,
|
|
14029
14200
|
ownerKind: "track",
|
|
@@ -14033,6 +14204,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14033
14204
|
data: keyFrame
|
|
14034
14205
|
});
|
|
14035
14206
|
this.keyFrameKeyByTrackId.set(trackId, key);
|
|
14207
|
+
await mediaStore.putReplacing({
|
|
14208
|
+
deviceId,
|
|
14209
|
+
ownerKind: "track",
|
|
14210
|
+
ownerId: trackId,
|
|
14211
|
+
kind: "keyFrameSmall",
|
|
14212
|
+
timestamp,
|
|
14213
|
+
data: keyFrameSmall
|
|
14214
|
+
});
|
|
14036
14215
|
} catch (err) {
|
|
14037
14216
|
this.ctx.logger.debug("key-frame capture failed", {
|
|
14038
14217
|
tags: { deviceId },
|
|
@@ -14980,11 +15159,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14980
15159
|
if (materializer === null || descriptor === void 0) return;
|
|
14981
15160
|
try {
|
|
14982
15161
|
const cameraIds = await cache.camerasFor(data.deviceId);
|
|
15162
|
+
if (cameraIds.length === 0) return;
|
|
15163
|
+
const producingDeviceName = await this.resolveProducingDeviceName(data.deviceId);
|
|
14983
15164
|
for (const cameraId of cameraIds) await materializer.materialize({
|
|
14984
15165
|
cameraId,
|
|
14985
15166
|
sourceDeviceId: data.deviceId,
|
|
14986
15167
|
kind: descriptor.kind,
|
|
14987
|
-
timestamp
|
|
15168
|
+
timestamp,
|
|
15169
|
+
...producingDeviceName !== void 0 ? { producingDeviceName } : {}
|
|
14988
15170
|
});
|
|
14989
15171
|
} catch (err) {
|
|
14990
15172
|
this.ctx.logger.warn("synthetic sensor-track materialize failed", {
|
|
@@ -14996,6 +15178,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14996
15178
|
});
|
|
14997
15179
|
}
|
|
14998
15180
|
}
|
|
15181
|
+
/**
|
|
15182
|
+
* Resolve the NAME of a device (the linked sensor/control that produced a
|
|
15183
|
+
* synthetic event) via the device-manager cap. Best-effort — returns
|
|
15184
|
+
* undefined on any lookup failure or an unknown device so the synthetic
|
|
15185
|
+
* track still materializes without a label.
|
|
15186
|
+
*/
|
|
15187
|
+
async resolveProducingDeviceName(deviceId) {
|
|
15188
|
+
const api = this.ctx?.api;
|
|
15189
|
+
if (!api) return void 0;
|
|
15190
|
+
try {
|
|
15191
|
+
return (await api.deviceManager.getDevice.query({ deviceId }))?.name;
|
|
15192
|
+
} catch (err) {
|
|
15193
|
+
this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
|
|
15194
|
+
tags: { deviceId },
|
|
15195
|
+
meta: { error: errMsg(err) }
|
|
15196
|
+
});
|
|
15197
|
+
return;
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
14999
15200
|
async clearTracks(input) {
|
|
15000
15201
|
this.trackStore?.clearDevice(input.deviceId);
|
|
15001
15202
|
this.stationaryRegistry?.clearDevice(input.deviceId);
|
|
@@ -15511,17 +15712,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15511
15712
|
key: clean.key
|
|
15512
15713
|
};
|
|
15513
15714
|
}
|
|
15514
|
-
const
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
if (!chosenTrack) return null;
|
|
15715
|
+
const chosen = await resolveDefaultEventMedia({
|
|
15716
|
+
eventId: id,
|
|
15717
|
+
eventFiles,
|
|
15718
|
+
listTrackMedia: (trackId) => this.mediaStore?.listByOwner("track", trackId) ?? Promise.resolve([]),
|
|
15719
|
+
getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
|
|
15720
|
+
});
|
|
15721
|
+
if (!chosen) return null;
|
|
15522
15722
|
return {
|
|
15523
|
-
bytes: Buffer.from(
|
|
15524
|
-
key:
|
|
15723
|
+
bytes: Buffer.from(chosen.base64, "base64"),
|
|
15724
|
+
key: chosen.key
|
|
15525
15725
|
};
|
|
15526
15726
|
}
|
|
15527
15727
|
/**
|