@camstack/addon-benchmark 1.2.32 → 1.2.34
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/{MotionZonesSettings-FXWWaTYb.mjs → MotionZonesSettings-CJQ1qIkl.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-BpMptp4x.mjs → PrivacyMaskSettings-DqyCPYRr.mjs} +4 -4
- package/dist/{SceneMonitorEditor-Cf3XyH9c.mjs → SceneMonitorEditor-CN-XsRrp.mjs} +3 -3
- package/dist/_stub.js +13 -13
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-NljcdjRg.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-BWwUyiel.mjs} +4 -4
- package/dist/{_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-__ozgT-Z.mjs → _virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-HpVJnRY1.mjs} +1 -1
- package/dist/{hostInit-bULgLWrs.mjs → hostInit-CLhuyXAy.mjs} +3 -3
- package/dist/index.js +800 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +800 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{player-overlays-D5oVSKal.mjs → player-overlays-0rmH0H8O.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-Cf5Bnr8W.mjs → responsive-CUJot3AN.mjs} +1 -1
- package/dist/{square-D6nXF0kO.mjs → square-9qrfIVAE.mjs} +1 -1
- package/dist/{trash-2-C-_6tBsE.mjs → trash-2-DXimPeDh.mjs} +1 -1
- package/dist/{use-device-snapshot-BexLgV9y.mjs → use-device-snapshot-CWhgQbX2.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-BgULD979.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-oZkDdD9C.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14846,6 +14846,28 @@ var SustainedThroughputResultSchema = external_exports.object({
|
|
|
14846
14846
|
batchSize: external_exports.number(),
|
|
14847
14847
|
concurrency: external_exports.number()
|
|
14848
14848
|
});
|
|
14849
|
+
var ReplaySourceSchema = external_exports.custom();
|
|
14850
|
+
var ReplayOverlaySchema = external_exports.custom();
|
|
14851
|
+
var OrchestrateReplayRunResultSchema = external_exports.custom();
|
|
14852
|
+
var StoredReplayRunSchema = external_exports.custom();
|
|
14853
|
+
var ReplayDiffSchema = external_exports.custom();
|
|
14854
|
+
var RunReplayClipInputSchema = external_exports.object({
|
|
14855
|
+
deviceId: external_exports.number(),
|
|
14856
|
+
nodeId: external_exports.string(),
|
|
14857
|
+
source: ReplaySourceSchema,
|
|
14858
|
+
overlay: ReplayOverlaySchema,
|
|
14859
|
+
fps: external_exports.number().positive(),
|
|
14860
|
+
maxFrames: external_exports.number().int().positive(),
|
|
14861
|
+
maxWidth: external_exports.number().int().positive().default(1920),
|
|
14862
|
+
deviceKey: external_exports.string().optional()
|
|
14863
|
+
});
|
|
14864
|
+
var UploadReplayClipInputSchema = external_exports.object({
|
|
14865
|
+
dataBase64: external_exports.string(),
|
|
14866
|
+
durationMs: external_exports.number().positive()
|
|
14867
|
+
});
|
|
14868
|
+
var UploadReplayClipResultSchema = external_exports.object({ clipId: external_exports.string() });
|
|
14869
|
+
var ListReplayRunsInputSchema = external_exports.object({ deviceId: external_exports.number() });
|
|
14870
|
+
var DiffReplayRunsInputSchema = external_exports.object({ runIdA: external_exports.string(), runIdB: external_exports.string() });
|
|
14849
14871
|
|
|
14850
14872
|
// src/benchmark-actions.ts
|
|
14851
14873
|
var benchmarkActions = (0, import_types2.defineCustomActions)({
|
|
@@ -14948,6 +14970,20 @@ var benchmarkActions = (0, import_types2.defineCustomActions)({
|
|
|
14948
14970
|
clearBenchmarkHistory: (0, import_types2.customAction)(external_exports.object({}), external_exports.object({ success: external_exports.literal(true) }), {
|
|
14949
14971
|
kind: "mutation"
|
|
14950
14972
|
}),
|
|
14973
|
+
// Replay — rigiocare una pipeline vera su una clip fissa. Regia qui,
|
|
14974
|
+
// FrameProcessor pass in `addon-post-analysis` via
|
|
14975
|
+
// `pipelineAnalytics.runReplayFrameProcessor` (see `replay/replay-wiring.ts`).
|
|
14976
|
+
uploadReplayClip: (0, import_types2.customAction)(UploadReplayClipInputSchema, UploadReplayClipResultSchema, {
|
|
14977
|
+
kind: "mutation"
|
|
14978
|
+
}),
|
|
14979
|
+
runReplayClip: (0, import_types2.customAction)(RunReplayClipInputSchema, OrchestrateReplayRunResultSchema, {
|
|
14980
|
+
kind: "mutation"
|
|
14981
|
+
}),
|
|
14982
|
+
listReplayRuns: (0, import_types2.customAction)(
|
|
14983
|
+
ListReplayRunsInputSchema,
|
|
14984
|
+
external_exports.array(StoredReplayRunSchema).readonly()
|
|
14985
|
+
),
|
|
14986
|
+
diffReplayRuns: (0, import_types2.customAction)(DiffReplayRunsInputSchema, ReplayDiffSchema),
|
|
14951
14987
|
// Decoder perf — fixture discovery + run control.
|
|
14952
14988
|
// `z.object({})` for the same reason as `listBenchmarkHistory` above.
|
|
14953
14989
|
listDecoderFixtures: (0, import_types2.customAction)(external_exports.object({}), external_exports.array(DecoderFixtureSchema).readonly()),
|
|
@@ -14961,6 +14997,9 @@ var benchmarkActions = (0, import_types2.defineCustomActions)({
|
|
|
14961
14997
|
)
|
|
14962
14998
|
});
|
|
14963
14999
|
|
|
15000
|
+
// src/benchmark-addon.ts
|
|
15001
|
+
var import_node_crypto = require("crypto");
|
|
15002
|
+
|
|
14964
15003
|
// src/decoder-perf-runner.ts
|
|
14965
15004
|
var fs = __toESM(require("fs"));
|
|
14966
15005
|
var path = __toESM(require("path"));
|
|
@@ -15259,6 +15298,557 @@ async function runDecoderPerfTest(input, deps) {
|
|
|
15259
15298
|
};
|
|
15260
15299
|
}
|
|
15261
15300
|
|
|
15301
|
+
// src/replay/replay-decode.ts
|
|
15302
|
+
var import_node_child_process = require("child_process");
|
|
15303
|
+
function buildReplaySequenceArgs(opts) {
|
|
15304
|
+
const { startSec, durationSec, fps, maxWidth } = opts;
|
|
15305
|
+
return [
|
|
15306
|
+
"-hide_banner",
|
|
15307
|
+
"-loglevel",
|
|
15308
|
+
"error",
|
|
15309
|
+
"-nostdin",
|
|
15310
|
+
"-ss",
|
|
15311
|
+
String(Math.max(0, startSec)),
|
|
15312
|
+
"-i",
|
|
15313
|
+
"pipe:0",
|
|
15314
|
+
"-t",
|
|
15315
|
+
String(Math.max(0, durationSec)),
|
|
15316
|
+
"-vf",
|
|
15317
|
+
`fps=${fps},scale='min(${maxWidth},iw)':-2`,
|
|
15318
|
+
"-f",
|
|
15319
|
+
"mjpeg",
|
|
15320
|
+
"pipe:1"
|
|
15321
|
+
];
|
|
15322
|
+
}
|
|
15323
|
+
var REPLAY_DECODE_MAX_CONCURRENT = 2;
|
|
15324
|
+
var REPLAY_DECODE_TIMEOUT_MS = 15e3;
|
|
15325
|
+
var KILL_GRACE_MS = 500;
|
|
15326
|
+
var ReplayDecoder = class {
|
|
15327
|
+
ffmpegPath;
|
|
15328
|
+
spawnFn;
|
|
15329
|
+
maxConcurrent;
|
|
15330
|
+
timeoutMs;
|
|
15331
|
+
inFlight = 0;
|
|
15332
|
+
constructor(deps) {
|
|
15333
|
+
this.ffmpegPath = deps.ffmpegPath;
|
|
15334
|
+
this.spawnFn = deps.spawnFn ?? ((bin, args) => (0, import_node_child_process.spawn)(bin, [...args]));
|
|
15335
|
+
this.maxConcurrent = deps.maxConcurrent ?? REPLAY_DECODE_MAX_CONCURRENT;
|
|
15336
|
+
this.timeoutMs = deps.timeoutMs ?? REPLAY_DECODE_TIMEOUT_MS;
|
|
15337
|
+
}
|
|
15338
|
+
/** Decodes currently running (tests / metrics). */
|
|
15339
|
+
get running() {
|
|
15340
|
+
return this.inFlight;
|
|
15341
|
+
}
|
|
15342
|
+
/**
|
|
15343
|
+
* Run the sequence once, feeding `input` on stdin (`-i pipe:0` in
|
|
15344
|
+
* {@link buildReplaySequenceArgs}) — the same shape as
|
|
15345
|
+
* `RecordedStillService`, which pipes a GOP rather than pointing ffmpeg at
|
|
15346
|
+
* a file path, because the source bytes (a recorder GOP window, or an
|
|
15347
|
+
* uploaded clip) are fetched through a capability call, not a local path.
|
|
15348
|
+
* Resolves with the whole MJPEG stdout stream.
|
|
15349
|
+
*/
|
|
15350
|
+
async decodeSequence(args, input) {
|
|
15351
|
+
if (this.inFlight >= this.maxConcurrent) {
|
|
15352
|
+
throw new Error(
|
|
15353
|
+
`replay decode busy: node already running ${this.inFlight}/${this.maxConcurrent} decodes`
|
|
15354
|
+
);
|
|
15355
|
+
}
|
|
15356
|
+
this.inFlight += 1;
|
|
15357
|
+
try {
|
|
15358
|
+
return await this.runOnce(args, input);
|
|
15359
|
+
} finally {
|
|
15360
|
+
this.inFlight -= 1;
|
|
15361
|
+
}
|
|
15362
|
+
}
|
|
15363
|
+
runOnce(args, input) {
|
|
15364
|
+
return new Promise((resolve2, reject) => {
|
|
15365
|
+
const chunks = [];
|
|
15366
|
+
const stderrTail = [];
|
|
15367
|
+
let settled = false;
|
|
15368
|
+
const child = (() => {
|
|
15369
|
+
try {
|
|
15370
|
+
return this.spawnFn(this.ffmpegPath, args);
|
|
15371
|
+
} catch (err) {
|
|
15372
|
+
reject(new Error(`replay decode: ffmpeg spawn failed: ${String(err)}`));
|
|
15373
|
+
return null;
|
|
15374
|
+
}
|
|
15375
|
+
})();
|
|
15376
|
+
if (child === null) return;
|
|
15377
|
+
const settle = (fn) => {
|
|
15378
|
+
if (settled) return;
|
|
15379
|
+
settled = true;
|
|
15380
|
+
clearTimeout(timer);
|
|
15381
|
+
fn();
|
|
15382
|
+
};
|
|
15383
|
+
const kill = () => {
|
|
15384
|
+
try {
|
|
15385
|
+
child.kill("SIGTERM");
|
|
15386
|
+
} catch {
|
|
15387
|
+
}
|
|
15388
|
+
const t = setTimeout(() => {
|
|
15389
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
15390
|
+
try {
|
|
15391
|
+
child.kill("SIGKILL");
|
|
15392
|
+
} catch {
|
|
15393
|
+
}
|
|
15394
|
+
}
|
|
15395
|
+
}, KILL_GRACE_MS);
|
|
15396
|
+
t.unref?.();
|
|
15397
|
+
};
|
|
15398
|
+
const timer = setTimeout(() => {
|
|
15399
|
+
settle(() => reject(new Error("replay decode: timed out")));
|
|
15400
|
+
kill();
|
|
15401
|
+
}, this.timeoutMs);
|
|
15402
|
+
timer.unref?.();
|
|
15403
|
+
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
15404
|
+
child.stderr?.on("data", (data) => {
|
|
15405
|
+
const line = data.toString().trim();
|
|
15406
|
+
if (line) {
|
|
15407
|
+
stderrTail.push(line);
|
|
15408
|
+
if (stderrTail.length > 8) stderrTail.shift();
|
|
15409
|
+
}
|
|
15410
|
+
});
|
|
15411
|
+
child.on("error", (err) => {
|
|
15412
|
+
settle(() => reject(new Error(`replay decode: ffmpeg process error: ${err.message}`)));
|
|
15413
|
+
});
|
|
15414
|
+
child.on("exit", (code, signal) => {
|
|
15415
|
+
const stdout = Buffer.concat(chunks);
|
|
15416
|
+
if (code === 0) {
|
|
15417
|
+
settle(() => resolve2(stdout));
|
|
15418
|
+
return;
|
|
15419
|
+
}
|
|
15420
|
+
settle(
|
|
15421
|
+
() => reject(
|
|
15422
|
+
new Error(
|
|
15423
|
+
`replay decode: ffmpeg exited code=${String(code)} signal=${String(signal)}: ${stderrTail.join(" | ") || "(no stderr)"}`
|
|
15424
|
+
)
|
|
15425
|
+
)
|
|
15426
|
+
);
|
|
15427
|
+
});
|
|
15428
|
+
try {
|
|
15429
|
+
child.stdin?.write(Buffer.from(input.buffer, input.byteOffset, input.byteLength));
|
|
15430
|
+
child.stdin?.end();
|
|
15431
|
+
} catch {
|
|
15432
|
+
}
|
|
15433
|
+
});
|
|
15434
|
+
}
|
|
15435
|
+
};
|
|
15436
|
+
|
|
15437
|
+
// src/replay/replay-store.ts
|
|
15438
|
+
var REPLAY_RUNS_COLLECTION = "benchmark:replay-runs";
|
|
15439
|
+
var REPLAY_RUNS_COLUMNS = [
|
|
15440
|
+
{ name: "id", type: "TEXT", primaryKey: true, notNull: true },
|
|
15441
|
+
{ name: "deviceId", type: "INTEGER", notNull: true },
|
|
15442
|
+
{ name: "nodeId", type: "TEXT", notNull: true },
|
|
15443
|
+
{ name: "clipId", type: "TEXT", notNull: true },
|
|
15444
|
+
{ name: "createdAtMs", type: "INTEGER", notNull: true },
|
|
15445
|
+
{ name: "source", type: "JSON", notNull: true },
|
|
15446
|
+
{ name: "plan", type: "JSON", notNull: true },
|
|
15447
|
+
{ name: "verdict", type: "JSON", notNull: true }
|
|
15448
|
+
];
|
|
15449
|
+
var REPLAY_RUNS_INDEXES = [
|
|
15450
|
+
{ name: "idx_replay_runs_device_created", columns: ["deviceId", "createdAtMs"] },
|
|
15451
|
+
{ name: "idx_replay_runs_created", columns: ["createdAtMs"] }
|
|
15452
|
+
];
|
|
15453
|
+
var ReplayStore = class {
|
|
15454
|
+
constructor(store) {
|
|
15455
|
+
this.store = store;
|
|
15456
|
+
}
|
|
15457
|
+
store;
|
|
15458
|
+
/** Declare the collection. Call in `onInitialize`, before the first
|
|
15459
|
+
* `save` — an undeclared collection crash-loops the runner. */
|
|
15460
|
+
static async declare(store) {
|
|
15461
|
+
await store.declareCollection.mutate({
|
|
15462
|
+
collection: REPLAY_RUNS_COLLECTION,
|
|
15463
|
+
columns: [...REPLAY_RUNS_COLUMNS],
|
|
15464
|
+
indexes: [...REPLAY_RUNS_INDEXES]
|
|
15465
|
+
});
|
|
15466
|
+
}
|
|
15467
|
+
async save(run) {
|
|
15468
|
+
const { id, ...rest } = run;
|
|
15469
|
+
await this.store.insert.mutate({
|
|
15470
|
+
collection: REPLAY_RUNS_COLLECTION,
|
|
15471
|
+
record: { id, data: rest }
|
|
15472
|
+
// TECH-DEBT: settings-store boundary, see the comment above.
|
|
15473
|
+
});
|
|
15474
|
+
}
|
|
15475
|
+
/** Runs newest-first, optionally scoped to one device. */
|
|
15476
|
+
async list(query) {
|
|
15477
|
+
const filter = { orderBy: { field: "createdAtMs", direction: "desc" } };
|
|
15478
|
+
if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
|
|
15479
|
+
if (query.limit !== void 0) filter.limit = query.limit;
|
|
15480
|
+
const rows = await this.store.query.query({ collection: REPLAY_RUNS_COLLECTION, filter });
|
|
15481
|
+
return rows.map((r) => ({ id: r.id, ...r.data }));
|
|
15482
|
+
}
|
|
15483
|
+
/** Delete every run created before `cutoffMs`. This store's OWN retention
|
|
15484
|
+
* clock — see the file header for why that matters. Returns the count so
|
|
15485
|
+
* a caller can log it, the way `OpsLogStore`-adjacent sweeps do. */
|
|
15486
|
+
async pruneBefore(cutoffMs) {
|
|
15487
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
15488
|
+
collection: REPLAY_RUNS_COLLECTION,
|
|
15489
|
+
filter: { whereBetween: { createdAtMs: [0, cutoffMs] } }
|
|
15490
|
+
});
|
|
15491
|
+
return deleted;
|
|
15492
|
+
}
|
|
15493
|
+
};
|
|
15494
|
+
|
|
15495
|
+
// src/replay/replay-plan.ts
|
|
15496
|
+
function readPreflight(result) {
|
|
15497
|
+
if (!result.ok) {
|
|
15498
|
+
const detail = result.issues.map((i) => `${i.addonId}: ${i.detail}`).join("; ");
|
|
15499
|
+
return { kind: "refused", reason: "step-unrunnable", detail };
|
|
15500
|
+
}
|
|
15501
|
+
if (result.substitutions.length > 0) {
|
|
15502
|
+
const detail = result.substitutions.map((s) => `${s.addonId}: chose \`${s.chosen}\`, running \`${s.running}\` (${s.format})`).join("; ");
|
|
15503
|
+
return { kind: "refused", reason: "model-substituted", detail };
|
|
15504
|
+
}
|
|
15505
|
+
return { kind: "runnable", format: result.format };
|
|
15506
|
+
}
|
|
15507
|
+
var PACKAGE_DETECTION_STEP_ID = "package-detection";
|
|
15508
|
+
function hasPackageZone(overlay) {
|
|
15509
|
+
return overlay.packageZone !== void 0;
|
|
15510
|
+
}
|
|
15511
|
+
function applyPackageZoneOverlay(steps, overlay) {
|
|
15512
|
+
const existing = steps.find((s) => s.addonId === PACKAGE_DETECTION_STEP_ID);
|
|
15513
|
+
if (!hasPackageZone(overlay)) {
|
|
15514
|
+
if (!existing) return steps;
|
|
15515
|
+
return steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
|
|
15516
|
+
}
|
|
15517
|
+
if (existing) {
|
|
15518
|
+
if (existing.enabled === true) return steps;
|
|
15519
|
+
return steps.map((s) => s.addonId === PACKAGE_DETECTION_STEP_ID ? { ...s, enabled: true } : s);
|
|
15520
|
+
}
|
|
15521
|
+
const node = {
|
|
15522
|
+
addonId: PACKAGE_DETECTION_STEP_ID,
|
|
15523
|
+
enabled: true,
|
|
15524
|
+
children: []
|
|
15525
|
+
};
|
|
15526
|
+
return [...steps, node];
|
|
15527
|
+
}
|
|
15528
|
+
function applySettingsAndModels(steps, overlay, ignored) {
|
|
15529
|
+
const settingsKeys = new Set(Object.keys(overlay.stepSettings ?? {}));
|
|
15530
|
+
const modelKeys = new Set(Object.keys(overlay.stepModels ?? {}));
|
|
15531
|
+
const presentIds = new Set(steps.map((s) => s.addonId));
|
|
15532
|
+
for (const id of settingsKeys) if (!presentIds.has(id)) ignored.add(id);
|
|
15533
|
+
for (const id of modelKeys) if (!presentIds.has(id)) ignored.add(id);
|
|
15534
|
+
return steps.map((s) => {
|
|
15535
|
+
const settingsPatch = overlay.stepSettings?.[s.addonId];
|
|
15536
|
+
const modelOverride = overlay.stepModels?.[s.addonId];
|
|
15537
|
+
if (settingsPatch === void 0 && modelOverride === void 0) return s;
|
|
15538
|
+
return {
|
|
15539
|
+
...s,
|
|
15540
|
+
...modelOverride !== void 0 ? { modelId: modelOverride } : {},
|
|
15541
|
+
...settingsPatch !== void 0 ? { settings: { ...s.settings, ...settingsPatch } } : {}
|
|
15542
|
+
};
|
|
15543
|
+
});
|
|
15544
|
+
}
|
|
15545
|
+
function buildReplayPlan(base, overlay) {
|
|
15546
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
15547
|
+
const withPackageZone = applyPackageZoneOverlay(base.steps, overlay);
|
|
15548
|
+
if (!hasPackageZone(overlay) && (overlay.stepSettings?.[PACKAGE_DETECTION_STEP_ID] !== void 0 || overlay.stepModels?.[PACKAGE_DETECTION_STEP_ID] !== void 0)) {
|
|
15549
|
+
ignored.add(PACKAGE_DETECTION_STEP_ID);
|
|
15550
|
+
}
|
|
15551
|
+
const steps = applySettingsAndModels(withPackageZone, overlay, ignored);
|
|
15552
|
+
return { steps, ignoredOverlays: [...ignored] };
|
|
15553
|
+
}
|
|
15554
|
+
|
|
15555
|
+
// src/replay/replay-source.ts
|
|
15556
|
+
function planReplayFrames(source, fps, maxFrames) {
|
|
15557
|
+
const durationMs = source.kind === "recording" ? source.toMs - source.fromMs : source.durationMs;
|
|
15558
|
+
if (durationMs <= 0) return { kind: "refused", reason: "empty-window" };
|
|
15559
|
+
const stepMs = 1e3 / fps;
|
|
15560
|
+
const startAt = source.kind === "recording" ? source.fromMs : 0;
|
|
15561
|
+
const rawCount = Math.max(1, Math.floor(durationMs / stepMs));
|
|
15562
|
+
const truncated = rawCount > maxFrames;
|
|
15563
|
+
const count = Math.min(rawCount, maxFrames);
|
|
15564
|
+
const instants = [];
|
|
15565
|
+
for (let i = 0; i < count; i++) instants.push(startAt + Math.round(i * stepMs));
|
|
15566
|
+
return { kind: "planned", deviceId: source.deviceId, instants, truncated };
|
|
15567
|
+
}
|
|
15568
|
+
|
|
15569
|
+
// src/replay/mjpeg-split.ts
|
|
15570
|
+
var SOI = Buffer.from([255, 216]);
|
|
15571
|
+
var EOI = Buffer.from([255, 217]);
|
|
15572
|
+
function splitMjpegStream(bytes) {
|
|
15573
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
15574
|
+
const out = [];
|
|
15575
|
+
let pos = 0;
|
|
15576
|
+
for (; ; ) {
|
|
15577
|
+
const soi = buf.indexOf(SOI, pos);
|
|
15578
|
+
if (soi === -1) break;
|
|
15579
|
+
const eoi = buf.indexOf(EOI, soi + SOI.length);
|
|
15580
|
+
if (eoi === -1) break;
|
|
15581
|
+
out.push(buf.subarray(soi, eoi + EOI.length));
|
|
15582
|
+
pos = eoi + EOI.length;
|
|
15583
|
+
}
|
|
15584
|
+
return out;
|
|
15585
|
+
}
|
|
15586
|
+
|
|
15587
|
+
// src/replay/replay-run.ts
|
|
15588
|
+
var DEFAULT_REPLAY_BUDGET_MS = 12e4;
|
|
15589
|
+
var DEFAULT_REPLAY_PACING_MS = 50;
|
|
15590
|
+
function sleep(ms) {
|
|
15591
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
15592
|
+
}
|
|
15593
|
+
async function runReplay(deps, frames) {
|
|
15594
|
+
const now = deps.now ?? Date.now;
|
|
15595
|
+
const budgetMs = deps.budgetMs ?? DEFAULT_REPLAY_BUDGET_MS;
|
|
15596
|
+
const pacingMs = deps.pacingMs ?? DEFAULT_REPLAY_PACING_MS;
|
|
15597
|
+
const startedAt = now();
|
|
15598
|
+
const tags = { deviceId: deps.deviceId };
|
|
15599
|
+
deps.logger.info("replay: run starting", {
|
|
15600
|
+
tags,
|
|
15601
|
+
meta: { framesPlanned: frames.length, nodeId: deps.nodeId, pacingMs, budgetMs }
|
|
15602
|
+
});
|
|
15603
|
+
const succeeded = [];
|
|
15604
|
+
const discarded = [];
|
|
15605
|
+
let framesFailed = 0;
|
|
15606
|
+
let stoppedOnBudget = false;
|
|
15607
|
+
for (let i = 0; i < frames.length; i++) {
|
|
15608
|
+
if (now() - startedAt >= budgetMs) {
|
|
15609
|
+
stoppedOnBudget = true;
|
|
15610
|
+
deps.logger.warn("replay: run stopped on budget \u2014 a partial census, reported as such", {
|
|
15611
|
+
tags,
|
|
15612
|
+
meta: {
|
|
15613
|
+
framesPlanned: frames.length,
|
|
15614
|
+
framesRan: succeeded.length,
|
|
15615
|
+
framesFailed,
|
|
15616
|
+
budgetMs
|
|
15617
|
+
}
|
|
15618
|
+
});
|
|
15619
|
+
break;
|
|
15620
|
+
}
|
|
15621
|
+
const f = frames[i];
|
|
15622
|
+
if (f === void 0) continue;
|
|
15623
|
+
try {
|
|
15624
|
+
const frame = await deps.runPipeline({
|
|
15625
|
+
timestamp: f.timestamp,
|
|
15626
|
+
image: f.image,
|
|
15627
|
+
nodeId: deps.nodeId
|
|
15628
|
+
});
|
|
15629
|
+
succeeded.push({ timestamp: f.timestamp, frame });
|
|
15630
|
+
if (frame.discarded !== void 0) discarded.push(...frame.discarded);
|
|
15631
|
+
} catch (err) {
|
|
15632
|
+
framesFailed += 1;
|
|
15633
|
+
deps.logger.warn("replay: frame failed on the node \u2014 counted, run continues", {
|
|
15634
|
+
tags,
|
|
15635
|
+
meta: { timestamp: f.timestamp, error: err instanceof Error ? err.message : String(err) }
|
|
15636
|
+
});
|
|
15637
|
+
}
|
|
15638
|
+
if (pacingMs > 0 && i < frames.length - 1) await sleep(pacingMs);
|
|
15639
|
+
}
|
|
15640
|
+
const tracks = succeeded.length > 0 ? (await deps.runTracking(succeeded)).tracks : [];
|
|
15641
|
+
const complete = !stoppedOnBudget && framesFailed === 0;
|
|
15642
|
+
deps.logger.info("replay: run finished", {
|
|
15643
|
+
tags,
|
|
15644
|
+
meta: {
|
|
15645
|
+
framesPlanned: frames.length,
|
|
15646
|
+
framesRan: succeeded.length,
|
|
15647
|
+
framesFailed,
|
|
15648
|
+
complete,
|
|
15649
|
+
tracks: tracks.length
|
|
15650
|
+
}
|
|
15651
|
+
});
|
|
15652
|
+
return {
|
|
15653
|
+
kind: succeeded.length === 0 ? "no-frame-ran" : "ran",
|
|
15654
|
+
deviceId: deps.deviceId,
|
|
15655
|
+
nodeId: deps.nodeId,
|
|
15656
|
+
complete,
|
|
15657
|
+
tracks,
|
|
15658
|
+
discarded,
|
|
15659
|
+
counters: { framesPlanned: frames.length, framesRan: succeeded.length, framesFailed }
|
|
15660
|
+
};
|
|
15661
|
+
}
|
|
15662
|
+
|
|
15663
|
+
// src/replay/replay-wiring.ts
|
|
15664
|
+
function clipIdOf(source) {
|
|
15665
|
+
return source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`;
|
|
15666
|
+
}
|
|
15667
|
+
async function orchestrateReplayRun(deps, input) {
|
|
15668
|
+
const tags = { deviceId: input.deviceId };
|
|
15669
|
+
const base = await deps.resolveBaseTree(input.deviceId);
|
|
15670
|
+
const plan = buildReplayPlan(base, input.overlay);
|
|
15671
|
+
if (plan.ignoredOverlays.length > 0) {
|
|
15672
|
+
deps.logger.warn("replay: overlay named a step the resolved tree does not run", {
|
|
15673
|
+
tags,
|
|
15674
|
+
meta: { ignoredOverlays: plan.ignoredOverlays }
|
|
15675
|
+
});
|
|
15676
|
+
}
|
|
15677
|
+
const preflight = await deps.api.pipelineExecutor.validatePipeline.query({
|
|
15678
|
+
steps: [...plan.steps]
|
|
15679
|
+
});
|
|
15680
|
+
const preflightVerdict = readPreflight(preflight);
|
|
15681
|
+
if (preflightVerdict.kind === "refused") {
|
|
15682
|
+
deps.logger.warn("replay: run refused at preflight", {
|
|
15683
|
+
tags,
|
|
15684
|
+
meta: { reason: preflightVerdict.reason, detail: preflightVerdict.detail }
|
|
15685
|
+
});
|
|
15686
|
+
return { kind: "refused", reason: `${preflightVerdict.reason}: ${preflightVerdict.detail}` };
|
|
15687
|
+
}
|
|
15688
|
+
const framePlan = planReplayFrames(input.source, input.fps, input.maxFrames);
|
|
15689
|
+
if (framePlan.kind === "refused") {
|
|
15690
|
+
deps.logger.warn("replay: run refused \u2014 empty window", { tags });
|
|
15691
|
+
return { kind: "refused", reason: framePlan.reason };
|
|
15692
|
+
}
|
|
15693
|
+
if (framePlan.truncated) {
|
|
15694
|
+
deps.logger.warn("replay: frame plan truncated by maxFrames", {
|
|
15695
|
+
tags,
|
|
15696
|
+
meta: { maxFrames: input.maxFrames, planned: framePlan.instants.length }
|
|
15697
|
+
});
|
|
15698
|
+
}
|
|
15699
|
+
const sourceFetch = await deps.fetchSourceBytes(input.source);
|
|
15700
|
+
if (sourceFetch.kind === "refused") {
|
|
15701
|
+
deps.logger.warn("replay: run refused \u2014 could not fetch source bytes", {
|
|
15702
|
+
tags,
|
|
15703
|
+
meta: { reason: sourceFetch.reason }
|
|
15704
|
+
});
|
|
15705
|
+
return { kind: "refused", reason: sourceFetch.reason };
|
|
15706
|
+
}
|
|
15707
|
+
const sourceBytes = sourceFetch.bytes;
|
|
15708
|
+
const first = framePlan.instants[0] ?? 0;
|
|
15709
|
+
const last = framePlan.instants[framePlan.instants.length - 1] ?? first;
|
|
15710
|
+
const startSec = (first - sourceFetch.startMs) / 1e3;
|
|
15711
|
+
const durationSec = Math.max(1 / input.fps, (last - first) / 1e3 + 1 / input.fps);
|
|
15712
|
+
const args = buildReplaySequenceArgs({
|
|
15713
|
+
startSec,
|
|
15714
|
+
durationSec,
|
|
15715
|
+
fps: input.fps,
|
|
15716
|
+
maxWidth: input.maxWidth
|
|
15717
|
+
});
|
|
15718
|
+
const mjpeg = await deps.decoder.decodeSequence(args, sourceBytes);
|
|
15719
|
+
const jpegFrames = splitMjpegStream(mjpeg);
|
|
15720
|
+
const frames = [];
|
|
15721
|
+
for (let i = 0; i < framePlan.instants.length; i++) {
|
|
15722
|
+
const timestamp = framePlan.instants[i];
|
|
15723
|
+
const image = jpegFrames[i];
|
|
15724
|
+
if (timestamp === void 0 || image === void 0) continue;
|
|
15725
|
+
frames.push({ timestamp, image });
|
|
15726
|
+
}
|
|
15727
|
+
if (frames.length < framePlan.instants.length) {
|
|
15728
|
+
deps.logger.warn("replay: decode produced fewer frames than planned", {
|
|
15729
|
+
tags,
|
|
15730
|
+
meta: { planned: framePlan.instants.length, decoded: frames.length }
|
|
15731
|
+
});
|
|
15732
|
+
}
|
|
15733
|
+
const verdict = await runReplay(
|
|
15734
|
+
{
|
|
15735
|
+
deviceId: input.deviceId,
|
|
15736
|
+
nodeId: input.nodeId,
|
|
15737
|
+
runPipeline: async (call) => deps.api.pipelineExecutor.runPipeline.mutate({
|
|
15738
|
+
nodeId: call.nodeId,
|
|
15739
|
+
steps: [...plan.steps],
|
|
15740
|
+
// Copy into a fresh `Uint8Array<ArrayBuffer>` — `mjpeg-split.ts`
|
|
15741
|
+
// hands back a `Buffer` view (`Uint8Array<ArrayBufferLike>`), and
|
|
15742
|
+
// the generated cap input is typed against the narrower
|
|
15743
|
+
// `ArrayBuffer` backing.
|
|
15744
|
+
image: new Uint8Array(call.image),
|
|
15745
|
+
deviceId: input.deviceId,
|
|
15746
|
+
...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
|
|
15747
|
+
}),
|
|
15748
|
+
// THE cross-addon call this whole track exists for: the FrameProcessor
|
|
15749
|
+
// pass lives in `addon-post-analysis`, reached only through this cap
|
|
15750
|
+
// method — see the file header.
|
|
15751
|
+
runTracking: async (tracked) => deps.api.pipelineAnalytics.runReplayFrameProcessor.mutate({
|
|
15752
|
+
deviceId: input.deviceId,
|
|
15753
|
+
source: "pipeline",
|
|
15754
|
+
frames: [...tracked]
|
|
15755
|
+
}),
|
|
15756
|
+
logger: deps.logger
|
|
15757
|
+
},
|
|
15758
|
+
frames
|
|
15759
|
+
);
|
|
15760
|
+
const now = deps.now ?? Date.now;
|
|
15761
|
+
const newId = deps.newId ?? (() => `replay-${now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
15762
|
+
const replayRunId = newId();
|
|
15763
|
+
const record2 = {
|
|
15764
|
+
id: replayRunId,
|
|
15765
|
+
deviceId: input.deviceId,
|
|
15766
|
+
nodeId: input.nodeId,
|
|
15767
|
+
clipId: clipIdOf(input.source),
|
|
15768
|
+
createdAtMs: now(),
|
|
15769
|
+
source: input.source,
|
|
15770
|
+
plan,
|
|
15771
|
+
verdict
|
|
15772
|
+
};
|
|
15773
|
+
await deps.store.save(record2);
|
|
15774
|
+
return { kind: "ran", replayRunId, verdict };
|
|
15775
|
+
}
|
|
15776
|
+
|
|
15777
|
+
// src/replay/replay-diff.ts
|
|
15778
|
+
function iou(a, b) {
|
|
15779
|
+
const x1 = Math.max(a.x, b.x);
|
|
15780
|
+
const y1 = Math.max(a.y, b.y);
|
|
15781
|
+
const x2 = Math.min(a.x + a.w, b.x + b.w);
|
|
15782
|
+
const y2 = Math.min(a.y + a.h, b.y + b.h);
|
|
15783
|
+
const interW = Math.max(0, x2 - x1);
|
|
15784
|
+
const interH = Math.max(0, y2 - y1);
|
|
15785
|
+
const inter = interW * interH;
|
|
15786
|
+
if (inter <= 0) return 0;
|
|
15787
|
+
const union2 = a.w * a.h + b.w * b.h - inter;
|
|
15788
|
+
return union2 <= 0 ? 0 : inter / union2;
|
|
15789
|
+
}
|
|
15790
|
+
function windowsOverlap(a, b) {
|
|
15791
|
+
return a.firstSeenMs <= b.lastSeenMs && b.firstSeenMs <= a.lastSeenMs;
|
|
15792
|
+
}
|
|
15793
|
+
var TRACK_PAIR_MIN_IOU = 0.3;
|
|
15794
|
+
function diffTracks(a, b) {
|
|
15795
|
+
const bMatched = /* @__PURE__ */ new Set();
|
|
15796
|
+
const lost = [];
|
|
15797
|
+
for (const ta of a) {
|
|
15798
|
+
let bestIdx = -1;
|
|
15799
|
+
let bestIou = 0;
|
|
15800
|
+
for (let i = 0; i < b.length; i++) {
|
|
15801
|
+
if (bMatched.has(i)) continue;
|
|
15802
|
+
const tb = b[i];
|
|
15803
|
+
if (tb === void 0 || tb.className !== ta.className) continue;
|
|
15804
|
+
if (!windowsOverlap(ta, tb)) continue;
|
|
15805
|
+
const score = iou(ta.bbox, tb.bbox);
|
|
15806
|
+
if (score >= TRACK_PAIR_MIN_IOU && score > bestIou) {
|
|
15807
|
+
bestIou = score;
|
|
15808
|
+
bestIdx = i;
|
|
15809
|
+
}
|
|
15810
|
+
}
|
|
15811
|
+
if (bestIdx === -1) lost.push(ta);
|
|
15812
|
+
else bMatched.add(bestIdx);
|
|
15813
|
+
}
|
|
15814
|
+
const gained = b.filter((_, i) => !bMatched.has(i));
|
|
15815
|
+
return { gained, lost };
|
|
15816
|
+
}
|
|
15817
|
+
function diffKnobs(a, b) {
|
|
15818
|
+
const stepIds = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
15819
|
+
const changed = [];
|
|
15820
|
+
for (const stepId of stepIds) {
|
|
15821
|
+
const aStep = a[stepId] ?? {};
|
|
15822
|
+
const bStep = b[stepId] ?? {};
|
|
15823
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(aStep), ...Object.keys(bStep)]);
|
|
15824
|
+
for (const key of keys) {
|
|
15825
|
+
const from = aStep[key];
|
|
15826
|
+
const to = bStep[key];
|
|
15827
|
+
if (from !== to) changed.push({ stepId, key, from, to });
|
|
15828
|
+
}
|
|
15829
|
+
}
|
|
15830
|
+
return changed;
|
|
15831
|
+
}
|
|
15832
|
+
function diffPromoted(discardedA, tracksB) {
|
|
15833
|
+
const promoted = [];
|
|
15834
|
+
for (const d of discardedA) {
|
|
15835
|
+
const nowTracked = tracksB.some((t) => t.className === d.macroClass);
|
|
15836
|
+
if (nowTracked)
|
|
15837
|
+
promoted.push({ macroClass: d.macroClass, score: d.score, wasDiscardedBy: d.reason });
|
|
15838
|
+
}
|
|
15839
|
+
return promoted;
|
|
15840
|
+
}
|
|
15841
|
+
function diffReplayRuns(a, b) {
|
|
15842
|
+
if (a.clipId !== b.clipId) return { kind: "incomparable", reason: "different-input" };
|
|
15843
|
+
if (a.nodeId !== b.nodeId) return { kind: "incomparable", reason: "different-node" };
|
|
15844
|
+
return {
|
|
15845
|
+
kind: "compared",
|
|
15846
|
+
changedKnobs: diffKnobs(a.settings, b.settings),
|
|
15847
|
+
tracks: diffTracks(a.tracks, b.tracks),
|
|
15848
|
+
promoted: diffPromoted(a.discarded, b.tracks)
|
|
15849
|
+
};
|
|
15850
|
+
}
|
|
15851
|
+
|
|
15262
15852
|
// src/throughput/sustained-runner.ts
|
|
15263
15853
|
async function runSustainedLoop(opts) {
|
|
15264
15854
|
const concurrency = Math.max(1, Math.floor(opts.concurrency));
|
|
@@ -15386,7 +15976,7 @@ function resolveFixturesDir() {
|
|
|
15386
15976
|
const pkgRoot = path2.resolve(path2.dirname(here), "..");
|
|
15387
15977
|
return path2.join(pkgRoot, "assets", "fixtures");
|
|
15388
15978
|
}
|
|
15389
|
-
var BenchmarkAddon = class extends import_types4.BaseAddon {
|
|
15979
|
+
var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
|
|
15390
15980
|
id = "benchmark";
|
|
15391
15981
|
/** Per-sessionId abort flags for in-flight decoder perf runs. */
|
|
15392
15982
|
decoderPerfAbortFlags = /* @__PURE__ */ new Map();
|
|
@@ -15407,6 +15997,17 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
|
|
|
15407
15997
|
frameCache = /* @__PURE__ */ new Map();
|
|
15408
15998
|
/** Periodic GC handle — clears expired frames every 60s. */
|
|
15409
15999
|
frameCacheSweeper = null;
|
|
16000
|
+
/** The replay run collection — declared in `onInitialize`, before any
|
|
16001
|
+
* `runReplayClip` call can save into it. See `replay/replay-store.ts`. */
|
|
16002
|
+
replayStore = null;
|
|
16003
|
+
/** The replay's one ffmpeg spawn, bounded — see `replay/replay-decode.ts`. */
|
|
16004
|
+
replayDecoder = new ReplayDecoder({
|
|
16005
|
+
ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg"
|
|
16006
|
+
});
|
|
16007
|
+
/** `replay:<StoredReplayRun>` — an in-memory index would drift from the
|
|
16008
|
+
* store; this is a thin cache over `replayStore.list`, keyed for the
|
|
16009
|
+
* diff action's two-id lookup. */
|
|
16010
|
+
replayRunCache = /* @__PURE__ */ new Map();
|
|
15410
16011
|
pages = [
|
|
15411
16012
|
{
|
|
15412
16013
|
id: "benchmark",
|
|
@@ -15427,6 +16028,8 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
|
|
|
15427
16028
|
id: this.id,
|
|
15428
16029
|
listPages: () => this.pages
|
|
15429
16030
|
};
|
|
16031
|
+
await ReplayStore.declare(this.api.settingsStore);
|
|
16032
|
+
this.replayStore = new ReplayStore(this.api.settingsStore);
|
|
15430
16033
|
return {
|
|
15431
16034
|
providers: [{ capability: import_types4.addonPagesSourceCapability, provider }],
|
|
15432
16035
|
customActions: benchmarkActions,
|
|
@@ -15445,7 +16048,11 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
|
|
|
15445
16048
|
clearBenchmarkHistory: async () => this.clearBenchmarkHistory(),
|
|
15446
16049
|
listDecoderFixtures: async () => this.listDecoderFixtures(),
|
|
15447
16050
|
runDecoderPerformanceTest: async (input) => this.runDecoderPerformanceTest(input),
|
|
15448
|
-
abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input)
|
|
16051
|
+
abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input),
|
|
16052
|
+
uploadReplayClip: async (input) => this.uploadReplayClip(input),
|
|
16053
|
+
runReplayClip: async (input) => this.runReplayClip(input),
|
|
16054
|
+
listReplayRuns: async (input) => this.listReplayRuns(input),
|
|
16055
|
+
diffReplayRuns: async (input) => this.diffReplayRunsAction(input)
|
|
15449
16056
|
}
|
|
15450
16057
|
};
|
|
15451
16058
|
}
|
|
@@ -15548,6 +16155,197 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
|
|
|
15548
16155
|
"runPipelineTest"
|
|
15549
16156
|
);
|
|
15550
16157
|
}
|
|
16158
|
+
// ── Replay ────────────────────────────────────────────────────────────
|
|
16159
|
+
/**
|
|
16160
|
+
* Storage location a replay upload lands on. `replayClips` is a NEW
|
|
16161
|
+
* `StorageLocationType` (an open string, no schema change needed — see
|
|
16162
|
+
* `storage-location.ts:19`) but it still needs an actual location
|
|
16163
|
+
* REGISTERED for that type before `storage.write`/`.read` can resolve it
|
|
16164
|
+
* (`storage.upsertLocation`, `cardinality: 'single'`, its own retention).
|
|
16165
|
+
* That registration is an operator/boot-time provisioning step this
|
|
16166
|
+
* change does not perform — see the report's open items.
|
|
16167
|
+
*/
|
|
16168
|
+
static REPLAY_CLIPS_LOCATION = "replayClips";
|
|
16169
|
+
async uploadReplayClip(input) {
|
|
16170
|
+
const bytes = Buffer.from(input.dataBase64, "base64");
|
|
16171
|
+
const clipId = `clip:${(0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex").slice(0, 32)}`;
|
|
16172
|
+
await this.api.storage.write.mutate({
|
|
16173
|
+
location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
|
|
16174
|
+
relativePath: `${clipId}.bin`,
|
|
16175
|
+
data: new Uint8Array(bytes)
|
|
16176
|
+
});
|
|
16177
|
+
this.ctx.logger.info("replay: clip uploaded", { meta: { clipId, bytes: bytes.length } });
|
|
16178
|
+
return { clipId };
|
|
16179
|
+
}
|
|
16180
|
+
async runReplayClip(input) {
|
|
16181
|
+
if (this.replayStore === null) throw new Error("replay: store not initialized");
|
|
16182
|
+
const store = this.replayStore;
|
|
16183
|
+
const source = input.source;
|
|
16184
|
+
const result = await orchestrateReplayRun(
|
|
16185
|
+
{
|
|
16186
|
+
api: this.api,
|
|
16187
|
+
logger: this.ctx.logger,
|
|
16188
|
+
decoder: this.replayDecoder,
|
|
16189
|
+
store,
|
|
16190
|
+
ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg",
|
|
16191
|
+
resolveBaseTree: (deviceId) => this.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }),
|
|
16192
|
+
fetchSourceBytes: (src) => this.fetchReplaySourceBytes(src)
|
|
16193
|
+
},
|
|
16194
|
+
{
|
|
16195
|
+
deviceId: input.deviceId,
|
|
16196
|
+
nodeId: input.nodeId,
|
|
16197
|
+
source,
|
|
16198
|
+
overlay: input.overlay,
|
|
16199
|
+
fps: input.fps,
|
|
16200
|
+
maxFrames: input.maxFrames,
|
|
16201
|
+
maxWidth: input.maxWidth,
|
|
16202
|
+
...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
|
|
16203
|
+
}
|
|
16204
|
+
);
|
|
16205
|
+
if (result.kind === "ran")
|
|
16206
|
+
this.replayRunCache.set(result.replayRunId, {
|
|
16207
|
+
id: result.replayRunId,
|
|
16208
|
+
deviceId: input.deviceId,
|
|
16209
|
+
nodeId: input.nodeId,
|
|
16210
|
+
clipId: source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`,
|
|
16211
|
+
createdAtMs: Date.now(),
|
|
16212
|
+
source,
|
|
16213
|
+
plan: null,
|
|
16214
|
+
verdict: result.verdict
|
|
16215
|
+
});
|
|
16216
|
+
return result;
|
|
16217
|
+
}
|
|
16218
|
+
/**
|
|
16219
|
+
* The profile a replay window is read from. `high` — same choice
|
|
16220
|
+
* `RECORDED_STILL_PROFILE` makes in `addon-post-analysis` — because the
|
|
16221
|
+
* whole point of `recording` over `upload` is NATIVE pixels (`-c:v copy`
|
|
16222
|
+
* means the recorded frame IS the live frame); a window cut from a
|
|
16223
|
+
* lower-rate rendition would be a worse version of the thing this source
|
|
16224
|
+
* exists to avoid.
|
|
16225
|
+
*/
|
|
16226
|
+
static REPLAY_RECORDING_PROFILE = "high";
|
|
16227
|
+
/**
|
|
16228
|
+
* Byte source for the replay decode step.
|
|
16229
|
+
*
|
|
16230
|
+
* `upload` is `storage.read` against the `replayClips` location. `recording`
|
|
16231
|
+
* is `recording.getDeviceConfig` → `recording.locateSegment` →
|
|
16232
|
+
* `recording.readWindowBytes` — the same "cheap question first" ordering
|
|
16233
|
+
* `RecordedStillService` uses (a camera that is not recording costs a
|
|
16234
|
+
* settings read, never a disk read), through `ctx.api` because addons never
|
|
16235
|
+
* import each other.
|
|
16236
|
+
*
|
|
16237
|
+
* Every expected failure — not recording, no footage covering `fromMs`, a
|
|
16238
|
+
* window that runs past its covering segment — returns a NAMED `refused`,
|
|
16239
|
+
* never a thrown error: "the window has no segments" is a RESULT this
|
|
16240
|
+
* whole feature exists to make legible (see the plan's stated unknown about
|
|
16241
|
+
* whether device 615's footage covers 2026-08-26 18:12), not a failure to
|
|
16242
|
+
* hide behind a generic exception.
|
|
16243
|
+
*/
|
|
16244
|
+
async fetchReplaySourceBytes(source) {
|
|
16245
|
+
if (source.kind === "upload") {
|
|
16246
|
+
try {
|
|
16247
|
+
const bytes = await this.api.storage.read.query({
|
|
16248
|
+
location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
|
|
16249
|
+
relativePath: `${source.clipId}.bin`
|
|
16250
|
+
});
|
|
16251
|
+
return { kind: "ok", bytes, startMs: 0 };
|
|
16252
|
+
} catch (err) {
|
|
16253
|
+
return { kind: "refused", reason: `clip-not-found: ${(0, import_types4.errMsg)(err)}` };
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
const profile = _BenchmarkAddon.REPLAY_RECORDING_PROFILE;
|
|
16257
|
+
const tags = { deviceId: source.deviceId };
|
|
16258
|
+
const config2 = await this.api.recording.getDeviceConfig.query({ deviceId: source.deviceId });
|
|
16259
|
+
if (config2.enabled === false) {
|
|
16260
|
+
this.ctx.logger.warn("replay: recording source refused \u2014 camera is not recording", {
|
|
16261
|
+
tags
|
|
16262
|
+
});
|
|
16263
|
+
return {
|
|
16264
|
+
kind: "refused",
|
|
16265
|
+
reason: `no-recording: device ${source.deviceId} does not have recording enabled`
|
|
16266
|
+
};
|
|
16267
|
+
}
|
|
16268
|
+
const located = await this.api.recording.locateSegment.query({
|
|
16269
|
+
deviceId: source.deviceId,
|
|
16270
|
+
profile,
|
|
16271
|
+
epochMs: source.fromMs
|
|
16272
|
+
});
|
|
16273
|
+
if (located.kind !== "segment") {
|
|
16274
|
+
this.ctx.logger.warn(
|
|
16275
|
+
"replay: recording source refused \u2014 no footage covers the window start",
|
|
16276
|
+
{ tags, meta: { fromMs: source.fromMs, toMs: source.toMs, profile } }
|
|
16277
|
+
);
|
|
16278
|
+
return {
|
|
16279
|
+
kind: "refused",
|
|
16280
|
+
reason: `no-footage: device ${source.deviceId} has no ${profile} recording covering ${new Date(source.fromMs).toISOString()}`
|
|
16281
|
+
};
|
|
16282
|
+
}
|
|
16283
|
+
const window = await this.api.recording.readWindowBytes.query({
|
|
16284
|
+
deviceId: source.deviceId,
|
|
16285
|
+
profile,
|
|
16286
|
+
startMs: located.startMs,
|
|
16287
|
+
fromMs: source.fromMs,
|
|
16288
|
+
toMs: source.toMs
|
|
16289
|
+
});
|
|
16290
|
+
if (window.kind !== "ok") {
|
|
16291
|
+
this.ctx.logger.warn(
|
|
16292
|
+
"replay: recording source refused \u2014 window runs past the covering segment",
|
|
16293
|
+
{
|
|
16294
|
+
tags,
|
|
16295
|
+
meta: { fromMs: source.fromMs, toMs: source.toMs, segmentEndMs: window.segmentEndMs }
|
|
16296
|
+
}
|
|
16297
|
+
);
|
|
16298
|
+
return {
|
|
16299
|
+
kind: "refused",
|
|
16300
|
+
reason: `window-spans-multiple-segments: footage for device ${source.deviceId} changes segment at ${window.segmentEndMs}, before the requested end ${source.toMs} \u2014 shorten the window or align it to one segment`
|
|
16301
|
+
};
|
|
16302
|
+
}
|
|
16303
|
+
if (!window.reachesRequestedEnd) {
|
|
16304
|
+
this.ctx.logger.warn(
|
|
16305
|
+
"replay: recording window bytes truncated by the recorder read safety cap",
|
|
16306
|
+
{ tags, meta: { fromMs: source.fromMs, toMs: source.toMs, gopDurMs: window.gopDurMs } }
|
|
16307
|
+
);
|
|
16308
|
+
}
|
|
16309
|
+
return { kind: "ok", bytes: window.data, startMs: window.gopStartMs };
|
|
16310
|
+
}
|
|
16311
|
+
async listReplayRuns(input) {
|
|
16312
|
+
if (this.replayStore === null) return [];
|
|
16313
|
+
const rows = await this.replayStore.list({ deviceId: input.deviceId });
|
|
16314
|
+
for (const row of rows) this.replayRunCache.set(row.id, row);
|
|
16315
|
+
return rows;
|
|
16316
|
+
}
|
|
16317
|
+
/** `verdict` round-trips through the store as opaque JSON
|
|
16318
|
+
* (`StoredReplayRun.verdict: unknown`) — this is the ONE narrowing back
|
|
16319
|
+
* to the shape `diffReplayRuns` needs, at the ONE place that reads it
|
|
16320
|
+
* for that purpose. Same documented boundary `ReplayStore.list` itself
|
|
16321
|
+
* crosses (`replay-store.ts`): this addon is the only reader that knows
|
|
16322
|
+
* what it wrote. */
|
|
16323
|
+
static verdictTracksAndDiscarded(verdict) {
|
|
16324
|
+
return verdict;
|
|
16325
|
+
}
|
|
16326
|
+
async diffReplayRunsAction(input) {
|
|
16327
|
+
const a = this.replayRunCache.get(input.runIdA);
|
|
16328
|
+
const b = this.replayRunCache.get(input.runIdB);
|
|
16329
|
+
if (a === void 0 || b === void 0) {
|
|
16330
|
+
throw new Error(
|
|
16331
|
+
"replay: diff \u2014 one or both run ids are not loaded (call listReplayRuns first)"
|
|
16332
|
+
);
|
|
16333
|
+
}
|
|
16334
|
+
return diffReplayRuns(
|
|
16335
|
+
{
|
|
16336
|
+
clipId: a.clipId,
|
|
16337
|
+
nodeId: a.nodeId,
|
|
16338
|
+
settings: {},
|
|
16339
|
+
..._BenchmarkAddon.verdictTracksAndDiscarded(a.verdict)
|
|
16340
|
+
},
|
|
16341
|
+
{
|
|
16342
|
+
clipId: b.clipId,
|
|
16343
|
+
nodeId: b.nodeId,
|
|
16344
|
+
settings: {},
|
|
16345
|
+
..._BenchmarkAddon.verdictTracksAndDiscarded(b.verdict)
|
|
16346
|
+
}
|
|
16347
|
+
);
|
|
16348
|
+
}
|
|
15551
16349
|
/**
|
|
15552
16350
|
* Batched pipeline tester.
|
|
15553
16351
|
*
|