@camstack/addon-benchmark 1.2.31 → 1.2.33

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.
Files changed (20) hide show
  1. package/dist/{MotionZonesSettings-D8Q3Jow5.mjs → MotionZonesSettings-FXWWaTYb.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-FT3D4GPd.mjs → PrivacyMaskSettings-BpMptp4x.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-TM3H84Dx.mjs → SceneMonitorEditor-Cf3XyH9c.mjs} +3 -3
  4. package/dist/_stub.js +11 -11
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-BN4YPOoK.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-DepVjAhb.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-__ozgT-Z.mjs +26 -0
  7. package/dist/{hostInit-Bs2l05CN.mjs → hostInit-xoO6xcwl.mjs} +3 -3
  8. package/dist/index.js +721 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +721 -2
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{player-overlays-DxgnPpH_.mjs → player-overlays-D5oVSKal.mjs} +1 -1
  13. package/dist/remoteEntry.js +1 -1
  14. package/dist/{responsive-iH7_dun5.mjs → responsive-Cf5Bnr8W.mjs} +1 -1
  15. package/dist/{square-CEIlw-nt.mjs → square-D6nXF0kO.mjs} +1 -1
  16. package/dist/{trash-2-Dei7ydBK.mjs → trash-2-C-_6tBsE.mjs} +1 -1
  17. package/dist/{use-device-snapshot-DkUKbAdb.mjs → use-device-snapshot-BexLgV9y.mjs} +1 -1
  18. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-DGLXaAWy.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-CSmRZSn7.mjs} +1 -1
  19. package/package.json +1 -1
  20. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DwzvxLG9.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -14811,6 +14811,28 @@ var SustainedThroughputResultSchema = external_exports.object({
14811
14811
  batchSize: external_exports.number(),
14812
14812
  concurrency: external_exports.number()
14813
14813
  });
14814
+ var ReplaySourceSchema = external_exports.custom();
14815
+ var ReplayOverlaySchema = external_exports.custom();
14816
+ var OrchestrateReplayRunResultSchema = external_exports.custom();
14817
+ var StoredReplayRunSchema = external_exports.custom();
14818
+ var ReplayDiffSchema = external_exports.custom();
14819
+ var RunReplayClipInputSchema = external_exports.object({
14820
+ deviceId: external_exports.number(),
14821
+ nodeId: external_exports.string(),
14822
+ source: ReplaySourceSchema,
14823
+ overlay: ReplayOverlaySchema,
14824
+ fps: external_exports.number().positive(),
14825
+ maxFrames: external_exports.number().int().positive(),
14826
+ maxWidth: external_exports.number().int().positive().default(1920),
14827
+ deviceKey: external_exports.string().optional()
14828
+ });
14829
+ var UploadReplayClipInputSchema = external_exports.object({
14830
+ dataBase64: external_exports.string(),
14831
+ durationMs: external_exports.number().positive()
14832
+ });
14833
+ var UploadReplayClipResultSchema = external_exports.object({ clipId: external_exports.string() });
14834
+ var ListReplayRunsInputSchema = external_exports.object({ deviceId: external_exports.number() });
14835
+ var DiffReplayRunsInputSchema = external_exports.object({ runIdA: external_exports.string(), runIdB: external_exports.string() });
14814
14836
 
14815
14837
  // src/benchmark-actions.ts
14816
14838
  var benchmarkActions = defineCustomActions({
@@ -14913,6 +14935,20 @@ var benchmarkActions = defineCustomActions({
14913
14935
  clearBenchmarkHistory: customAction(external_exports.object({}), external_exports.object({ success: external_exports.literal(true) }), {
14914
14936
  kind: "mutation"
14915
14937
  }),
14938
+ // Replay — rigiocare una pipeline vera su una clip fissa. Regia qui,
14939
+ // FrameProcessor pass in `addon-post-analysis` via
14940
+ // `pipelineAnalytics.runReplayFrameProcessor` (see `replay/replay-wiring.ts`).
14941
+ uploadReplayClip: customAction(UploadReplayClipInputSchema, UploadReplayClipResultSchema, {
14942
+ kind: "mutation"
14943
+ }),
14944
+ runReplayClip: customAction(RunReplayClipInputSchema, OrchestrateReplayRunResultSchema, {
14945
+ kind: "mutation"
14946
+ }),
14947
+ listReplayRuns: customAction(
14948
+ ListReplayRunsInputSchema,
14949
+ external_exports.array(StoredReplayRunSchema).readonly()
14950
+ ),
14951
+ diffReplayRuns: customAction(DiffReplayRunsInputSchema, ReplayDiffSchema),
14916
14952
  // Decoder perf — fixture discovery + run control.
14917
14953
  // `z.object({})` for the same reason as `listBenchmarkHistory` above.
14918
14954
  listDecoderFixtures: customAction(external_exports.object({}), external_exports.array(DecoderFixtureSchema).readonly()),
@@ -14926,6 +14962,9 @@ var benchmarkActions = defineCustomActions({
14926
14962
  )
14927
14963
  });
14928
14964
 
14965
+ // src/benchmark-addon.ts
14966
+ import { createHash } from "crypto";
14967
+
14929
14968
  // src/decoder-perf-runner.ts
14930
14969
  import * as fs from "fs";
14931
14970
  import * as path from "path";
@@ -15224,6 +15263,549 @@ async function runDecoderPerfTest(input, deps) {
15224
15263
  };
15225
15264
  }
15226
15265
 
15266
+ // src/replay/replay-decode.ts
15267
+ import { spawn } from "child_process";
15268
+ function buildReplaySequenceArgs(opts) {
15269
+ const { startSec, durationSec, fps, maxWidth } = opts;
15270
+ return [
15271
+ "-hide_banner",
15272
+ "-loglevel",
15273
+ "error",
15274
+ "-nostdin",
15275
+ "-ss",
15276
+ String(Math.max(0, startSec)),
15277
+ "-i",
15278
+ "pipe:0",
15279
+ "-t",
15280
+ String(Math.max(0, durationSec)),
15281
+ "-vf",
15282
+ `fps=${fps},scale='min(${maxWidth},iw)':-2`,
15283
+ "-f",
15284
+ "mjpeg",
15285
+ "pipe:1"
15286
+ ];
15287
+ }
15288
+ var REPLAY_DECODE_MAX_CONCURRENT = 2;
15289
+ var REPLAY_DECODE_TIMEOUT_MS = 15e3;
15290
+ var KILL_GRACE_MS = 500;
15291
+ var ReplayDecoder = class {
15292
+ ffmpegPath;
15293
+ spawnFn;
15294
+ maxConcurrent;
15295
+ timeoutMs;
15296
+ inFlight = 0;
15297
+ constructor(deps) {
15298
+ this.ffmpegPath = deps.ffmpegPath;
15299
+ this.spawnFn = deps.spawnFn ?? ((bin, args) => spawn(bin, [...args]));
15300
+ this.maxConcurrent = deps.maxConcurrent ?? REPLAY_DECODE_MAX_CONCURRENT;
15301
+ this.timeoutMs = deps.timeoutMs ?? REPLAY_DECODE_TIMEOUT_MS;
15302
+ }
15303
+ /** Decodes currently running (tests / metrics). */
15304
+ get running() {
15305
+ return this.inFlight;
15306
+ }
15307
+ /**
15308
+ * Run the sequence once, feeding `input` on stdin (`-i pipe:0` in
15309
+ * {@link buildReplaySequenceArgs}) — the same shape as
15310
+ * `RecordedStillService`, which pipes a GOP rather than pointing ffmpeg at
15311
+ * a file path, because the source bytes (a recorder GOP window, or an
15312
+ * uploaded clip) are fetched through a capability call, not a local path.
15313
+ * Resolves with the whole MJPEG stdout stream.
15314
+ */
15315
+ async decodeSequence(args, input) {
15316
+ if (this.inFlight >= this.maxConcurrent) {
15317
+ throw new Error(
15318
+ `replay decode busy: node already running ${this.inFlight}/${this.maxConcurrent} decodes`
15319
+ );
15320
+ }
15321
+ this.inFlight += 1;
15322
+ try {
15323
+ return await this.runOnce(args, input);
15324
+ } finally {
15325
+ this.inFlight -= 1;
15326
+ }
15327
+ }
15328
+ runOnce(args, input) {
15329
+ return new Promise((resolve2, reject) => {
15330
+ const chunks = [];
15331
+ const stderrTail = [];
15332
+ let settled = false;
15333
+ const child = (() => {
15334
+ try {
15335
+ return this.spawnFn(this.ffmpegPath, args);
15336
+ } catch (err) {
15337
+ reject(new Error(`replay decode: ffmpeg spawn failed: ${String(err)}`));
15338
+ return null;
15339
+ }
15340
+ })();
15341
+ if (child === null) return;
15342
+ const settle = (fn) => {
15343
+ if (settled) return;
15344
+ settled = true;
15345
+ clearTimeout(timer);
15346
+ fn();
15347
+ };
15348
+ const kill = () => {
15349
+ try {
15350
+ child.kill("SIGTERM");
15351
+ } catch {
15352
+ }
15353
+ const t = setTimeout(() => {
15354
+ if (child.exitCode === null && child.signalCode === null) {
15355
+ try {
15356
+ child.kill("SIGKILL");
15357
+ } catch {
15358
+ }
15359
+ }
15360
+ }, KILL_GRACE_MS);
15361
+ t.unref?.();
15362
+ };
15363
+ const timer = setTimeout(() => {
15364
+ settle(() => reject(new Error("replay decode: timed out")));
15365
+ kill();
15366
+ }, this.timeoutMs);
15367
+ timer.unref?.();
15368
+ child.stdout?.on("data", (chunk) => chunks.push(chunk));
15369
+ child.stderr?.on("data", (data) => {
15370
+ const line = data.toString().trim();
15371
+ if (line) {
15372
+ stderrTail.push(line);
15373
+ if (stderrTail.length > 8) stderrTail.shift();
15374
+ }
15375
+ });
15376
+ child.on("error", (err) => {
15377
+ settle(() => reject(new Error(`replay decode: ffmpeg process error: ${err.message}`)));
15378
+ });
15379
+ child.on("exit", (code, signal) => {
15380
+ const stdout = Buffer.concat(chunks);
15381
+ if (code === 0) {
15382
+ settle(() => resolve2(stdout));
15383
+ return;
15384
+ }
15385
+ settle(
15386
+ () => reject(
15387
+ new Error(
15388
+ `replay decode: ffmpeg exited code=${String(code)} signal=${String(signal)}: ${stderrTail.join(" | ") || "(no stderr)"}`
15389
+ )
15390
+ )
15391
+ );
15392
+ });
15393
+ try {
15394
+ child.stdin?.write(Buffer.from(input.buffer, input.byteOffset, input.byteLength));
15395
+ child.stdin?.end();
15396
+ } catch {
15397
+ }
15398
+ });
15399
+ }
15400
+ };
15401
+
15402
+ // src/replay/replay-store.ts
15403
+ var REPLAY_RUNS_COLLECTION = "benchmark:replay-runs";
15404
+ var REPLAY_RUNS_COLUMNS = [
15405
+ { name: "id", type: "TEXT", primaryKey: true, notNull: true },
15406
+ { name: "deviceId", type: "INTEGER", notNull: true },
15407
+ { name: "nodeId", type: "TEXT", notNull: true },
15408
+ { name: "clipId", type: "TEXT", notNull: true },
15409
+ { name: "createdAtMs", type: "INTEGER", notNull: true },
15410
+ { name: "source", type: "JSON", notNull: true },
15411
+ { name: "plan", type: "JSON", notNull: true },
15412
+ { name: "verdict", type: "JSON", notNull: true }
15413
+ ];
15414
+ var REPLAY_RUNS_INDEXES = [
15415
+ { name: "idx_replay_runs_device_created", columns: ["deviceId", "createdAtMs"] },
15416
+ { name: "idx_replay_runs_created", columns: ["createdAtMs"] }
15417
+ ];
15418
+ var ReplayStore = class {
15419
+ constructor(store) {
15420
+ this.store = store;
15421
+ }
15422
+ store;
15423
+ /** Declare the collection. Call in `onInitialize`, before the first
15424
+ * `save` — an undeclared collection crash-loops the runner. */
15425
+ static async declare(store) {
15426
+ await store.declareCollection.mutate({
15427
+ collection: REPLAY_RUNS_COLLECTION,
15428
+ columns: [...REPLAY_RUNS_COLUMNS],
15429
+ indexes: [...REPLAY_RUNS_INDEXES]
15430
+ });
15431
+ }
15432
+ async save(run) {
15433
+ const { id, ...rest } = run;
15434
+ await this.store.insert.mutate({
15435
+ collection: REPLAY_RUNS_COLLECTION,
15436
+ record: { id, data: rest }
15437
+ // TECH-DEBT: settings-store boundary, see the comment above.
15438
+ });
15439
+ }
15440
+ /** Runs newest-first, optionally scoped to one device. */
15441
+ async list(query) {
15442
+ const filter = { orderBy: { field: "createdAtMs", direction: "desc" } };
15443
+ if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
15444
+ if (query.limit !== void 0) filter.limit = query.limit;
15445
+ const rows = await this.store.query.query({ collection: REPLAY_RUNS_COLLECTION, filter });
15446
+ return rows.map((r) => ({ id: r.id, ...r.data }));
15447
+ }
15448
+ /** Delete every run created before `cutoffMs`. This store's OWN retention
15449
+ * clock — see the file header for why that matters. Returns the count so
15450
+ * a caller can log it, the way `OpsLogStore`-adjacent sweeps do. */
15451
+ async pruneBefore(cutoffMs) {
15452
+ const { deleted } = await this.store.deleteWhere.mutate({
15453
+ collection: REPLAY_RUNS_COLLECTION,
15454
+ filter: { whereBetween: { createdAtMs: [0, cutoffMs] } }
15455
+ });
15456
+ return deleted;
15457
+ }
15458
+ };
15459
+
15460
+ // src/replay/replay-plan.ts
15461
+ function readPreflight(result) {
15462
+ if (!result.ok) {
15463
+ const detail = result.issues.map((i) => `${i.addonId}: ${i.detail}`).join("; ");
15464
+ return { kind: "refused", reason: "step-unrunnable", detail };
15465
+ }
15466
+ if (result.substitutions.length > 0) {
15467
+ const detail = result.substitutions.map((s) => `${s.addonId}: chose \`${s.chosen}\`, running \`${s.running}\` (${s.format})`).join("; ");
15468
+ return { kind: "refused", reason: "model-substituted", detail };
15469
+ }
15470
+ return { kind: "runnable", format: result.format };
15471
+ }
15472
+ var PACKAGE_DETECTION_STEP_ID = "package-detection";
15473
+ function hasPackageZone(overlay) {
15474
+ return overlay.packageZone !== void 0;
15475
+ }
15476
+ function applyPackageZoneOverlay(steps, overlay) {
15477
+ const existing = steps.find((s) => s.addonId === PACKAGE_DETECTION_STEP_ID);
15478
+ if (!hasPackageZone(overlay)) {
15479
+ if (!existing) return steps;
15480
+ return steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
15481
+ }
15482
+ if (existing) {
15483
+ if (existing.enabled === true) return steps;
15484
+ return steps.map((s) => s.addonId === PACKAGE_DETECTION_STEP_ID ? { ...s, enabled: true } : s);
15485
+ }
15486
+ const node = {
15487
+ addonId: PACKAGE_DETECTION_STEP_ID,
15488
+ enabled: true,
15489
+ children: []
15490
+ };
15491
+ return [...steps, node];
15492
+ }
15493
+ function applySettingsAndModels(steps, overlay, ignored) {
15494
+ const settingsKeys = new Set(Object.keys(overlay.stepSettings ?? {}));
15495
+ const modelKeys = new Set(Object.keys(overlay.stepModels ?? {}));
15496
+ const presentIds = new Set(steps.map((s) => s.addonId));
15497
+ for (const id of settingsKeys) if (!presentIds.has(id)) ignored.add(id);
15498
+ for (const id of modelKeys) if (!presentIds.has(id)) ignored.add(id);
15499
+ return steps.map((s) => {
15500
+ const settingsPatch = overlay.stepSettings?.[s.addonId];
15501
+ const modelOverride = overlay.stepModels?.[s.addonId];
15502
+ if (settingsPatch === void 0 && modelOverride === void 0) return s;
15503
+ return {
15504
+ ...s,
15505
+ ...modelOverride !== void 0 ? { modelId: modelOverride } : {},
15506
+ ...settingsPatch !== void 0 ? { settings: { ...s.settings, ...settingsPatch } } : {}
15507
+ };
15508
+ });
15509
+ }
15510
+ function buildReplayPlan(base, overlay) {
15511
+ const ignored = /* @__PURE__ */ new Set();
15512
+ const withPackageZone = applyPackageZoneOverlay(base.steps, overlay);
15513
+ if (!hasPackageZone(overlay) && (overlay.stepSettings?.[PACKAGE_DETECTION_STEP_ID] !== void 0 || overlay.stepModels?.[PACKAGE_DETECTION_STEP_ID] !== void 0)) {
15514
+ ignored.add(PACKAGE_DETECTION_STEP_ID);
15515
+ }
15516
+ const steps = applySettingsAndModels(withPackageZone, overlay, ignored);
15517
+ return { steps, ignoredOverlays: [...ignored] };
15518
+ }
15519
+
15520
+ // src/replay/replay-source.ts
15521
+ function planReplayFrames(source, fps, maxFrames) {
15522
+ const durationMs = source.kind === "recording" ? source.toMs - source.fromMs : source.durationMs;
15523
+ if (durationMs <= 0) return { kind: "refused", reason: "empty-window" };
15524
+ const stepMs = 1e3 / fps;
15525
+ const startAt = source.kind === "recording" ? source.fromMs : 0;
15526
+ const rawCount = Math.max(1, Math.floor(durationMs / stepMs));
15527
+ const truncated = rawCount > maxFrames;
15528
+ const count = Math.min(rawCount, maxFrames);
15529
+ const instants = [];
15530
+ for (let i = 0; i < count; i++) instants.push(startAt + Math.round(i * stepMs));
15531
+ return { kind: "planned", deviceId: source.deviceId, instants, truncated };
15532
+ }
15533
+
15534
+ // src/replay/mjpeg-split.ts
15535
+ var SOI = Buffer.from([255, 216]);
15536
+ var EOI = Buffer.from([255, 217]);
15537
+ function splitMjpegStream(bytes) {
15538
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
15539
+ const out = [];
15540
+ let pos = 0;
15541
+ for (; ; ) {
15542
+ const soi = buf.indexOf(SOI, pos);
15543
+ if (soi === -1) break;
15544
+ const eoi = buf.indexOf(EOI, soi + SOI.length);
15545
+ if (eoi === -1) break;
15546
+ out.push(buf.subarray(soi, eoi + EOI.length));
15547
+ pos = eoi + EOI.length;
15548
+ }
15549
+ return out;
15550
+ }
15551
+
15552
+ // src/replay/replay-run.ts
15553
+ var DEFAULT_REPLAY_BUDGET_MS = 12e4;
15554
+ var DEFAULT_REPLAY_PACING_MS = 50;
15555
+ function sleep(ms) {
15556
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
15557
+ }
15558
+ async function runReplay(deps, frames) {
15559
+ const now = deps.now ?? Date.now;
15560
+ const budgetMs = deps.budgetMs ?? DEFAULT_REPLAY_BUDGET_MS;
15561
+ const pacingMs = deps.pacingMs ?? DEFAULT_REPLAY_PACING_MS;
15562
+ const startedAt = now();
15563
+ const tags = { deviceId: deps.deviceId };
15564
+ deps.logger.info("replay: run starting", {
15565
+ tags,
15566
+ meta: { framesPlanned: frames.length, nodeId: deps.nodeId, pacingMs, budgetMs }
15567
+ });
15568
+ const succeeded = [];
15569
+ const discarded = [];
15570
+ let framesFailed = 0;
15571
+ let stoppedOnBudget = false;
15572
+ for (let i = 0; i < frames.length; i++) {
15573
+ if (now() - startedAt >= budgetMs) {
15574
+ stoppedOnBudget = true;
15575
+ deps.logger.warn("replay: run stopped on budget \u2014 a partial census, reported as such", {
15576
+ tags,
15577
+ meta: {
15578
+ framesPlanned: frames.length,
15579
+ framesRan: succeeded.length,
15580
+ framesFailed,
15581
+ budgetMs
15582
+ }
15583
+ });
15584
+ break;
15585
+ }
15586
+ const f = frames[i];
15587
+ if (f === void 0) continue;
15588
+ try {
15589
+ const frame = await deps.runPipeline({
15590
+ timestamp: f.timestamp,
15591
+ image: f.image,
15592
+ nodeId: deps.nodeId
15593
+ });
15594
+ succeeded.push({ timestamp: f.timestamp, frame });
15595
+ if (frame.discarded !== void 0) discarded.push(...frame.discarded);
15596
+ } catch (err) {
15597
+ framesFailed += 1;
15598
+ deps.logger.warn("replay: frame failed on the node \u2014 counted, run continues", {
15599
+ tags,
15600
+ meta: { timestamp: f.timestamp, error: err instanceof Error ? err.message : String(err) }
15601
+ });
15602
+ }
15603
+ if (pacingMs > 0 && i < frames.length - 1) await sleep(pacingMs);
15604
+ }
15605
+ const tracks = succeeded.length > 0 ? (await deps.runTracking(succeeded)).tracks : [];
15606
+ const complete = !stoppedOnBudget && framesFailed === 0;
15607
+ deps.logger.info("replay: run finished", {
15608
+ tags,
15609
+ meta: {
15610
+ framesPlanned: frames.length,
15611
+ framesRan: succeeded.length,
15612
+ framesFailed,
15613
+ complete,
15614
+ tracks: tracks.length
15615
+ }
15616
+ });
15617
+ return {
15618
+ kind: succeeded.length === 0 ? "no-frame-ran" : "ran",
15619
+ deviceId: deps.deviceId,
15620
+ nodeId: deps.nodeId,
15621
+ complete,
15622
+ tracks,
15623
+ discarded,
15624
+ counters: { framesPlanned: frames.length, framesRan: succeeded.length, framesFailed }
15625
+ };
15626
+ }
15627
+
15628
+ // src/replay/replay-wiring.ts
15629
+ function clipIdOf(source) {
15630
+ return source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`;
15631
+ }
15632
+ async function orchestrateReplayRun(deps, input) {
15633
+ const tags = { deviceId: input.deviceId };
15634
+ const base = await deps.resolveBaseTree(input.deviceId);
15635
+ const plan = buildReplayPlan(base, input.overlay);
15636
+ if (plan.ignoredOverlays.length > 0) {
15637
+ deps.logger.warn("replay: overlay named a step the resolved tree does not run", {
15638
+ tags,
15639
+ meta: { ignoredOverlays: plan.ignoredOverlays }
15640
+ });
15641
+ }
15642
+ const preflight = await deps.api.pipelineExecutor.validatePipeline.query({
15643
+ steps: [...plan.steps]
15644
+ });
15645
+ const preflightVerdict = readPreflight(preflight);
15646
+ if (preflightVerdict.kind === "refused") {
15647
+ deps.logger.warn("replay: run refused at preflight", {
15648
+ tags,
15649
+ meta: { reason: preflightVerdict.reason, detail: preflightVerdict.detail }
15650
+ });
15651
+ return { kind: "refused", reason: `${preflightVerdict.reason}: ${preflightVerdict.detail}` };
15652
+ }
15653
+ const framePlan = planReplayFrames(input.source, input.fps, input.maxFrames);
15654
+ if (framePlan.kind === "refused") {
15655
+ deps.logger.warn("replay: run refused \u2014 empty window", { tags });
15656
+ return { kind: "refused", reason: framePlan.reason };
15657
+ }
15658
+ if (framePlan.truncated) {
15659
+ deps.logger.warn("replay: frame plan truncated by maxFrames", {
15660
+ tags,
15661
+ meta: { maxFrames: input.maxFrames, planned: framePlan.instants.length }
15662
+ });
15663
+ }
15664
+ const sourceBytes = await deps.fetchSourceBytes(input.source);
15665
+ const first = framePlan.instants[0] ?? 0;
15666
+ const last = framePlan.instants[framePlan.instants.length - 1] ?? first;
15667
+ const startSec = input.source.kind === "recording" ? (first - input.source.fromMs) / 1e3 : first / 1e3;
15668
+ const durationSec = Math.max(1 / input.fps, (last - first) / 1e3 + 1 / input.fps);
15669
+ const args = buildReplaySequenceArgs({
15670
+ startSec,
15671
+ durationSec,
15672
+ fps: input.fps,
15673
+ maxWidth: input.maxWidth
15674
+ });
15675
+ const mjpeg = await deps.decoder.decodeSequence(args, sourceBytes);
15676
+ const jpegFrames = splitMjpegStream(mjpeg);
15677
+ const frames = [];
15678
+ for (let i = 0; i < framePlan.instants.length; i++) {
15679
+ const timestamp = framePlan.instants[i];
15680
+ const image = jpegFrames[i];
15681
+ if (timestamp === void 0 || image === void 0) continue;
15682
+ frames.push({ timestamp, image });
15683
+ }
15684
+ if (frames.length < framePlan.instants.length) {
15685
+ deps.logger.warn("replay: decode produced fewer frames than planned", {
15686
+ tags,
15687
+ meta: { planned: framePlan.instants.length, decoded: frames.length }
15688
+ });
15689
+ }
15690
+ const verdict = await runReplay(
15691
+ {
15692
+ deviceId: input.deviceId,
15693
+ nodeId: input.nodeId,
15694
+ runPipeline: async (call) => deps.api.pipelineExecutor.runPipeline.mutate({
15695
+ nodeId: call.nodeId,
15696
+ steps: [...plan.steps],
15697
+ // Copy into a fresh `Uint8Array<ArrayBuffer>` — `mjpeg-split.ts`
15698
+ // hands back a `Buffer` view (`Uint8Array<ArrayBufferLike>`), and
15699
+ // the generated cap input is typed against the narrower
15700
+ // `ArrayBuffer` backing.
15701
+ image: new Uint8Array(call.image),
15702
+ deviceId: input.deviceId,
15703
+ ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
15704
+ }),
15705
+ // THE cross-addon call this whole track exists for: the FrameProcessor
15706
+ // pass lives in `addon-post-analysis`, reached only through this cap
15707
+ // method — see the file header.
15708
+ runTracking: async (tracked) => deps.api.pipelineAnalytics.runReplayFrameProcessor.mutate({
15709
+ deviceId: input.deviceId,
15710
+ source: "pipeline",
15711
+ frames: [...tracked]
15712
+ }),
15713
+ logger: deps.logger
15714
+ },
15715
+ frames
15716
+ );
15717
+ const now = deps.now ?? Date.now;
15718
+ const newId = deps.newId ?? (() => `replay-${now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
15719
+ const replayRunId = newId();
15720
+ const record2 = {
15721
+ id: replayRunId,
15722
+ deviceId: input.deviceId,
15723
+ nodeId: input.nodeId,
15724
+ clipId: clipIdOf(input.source),
15725
+ createdAtMs: now(),
15726
+ source: input.source,
15727
+ plan,
15728
+ verdict
15729
+ };
15730
+ await deps.store.save(record2);
15731
+ return { kind: "ran", replayRunId, verdict };
15732
+ }
15733
+
15734
+ // src/replay/replay-diff.ts
15735
+ function iou(a, b) {
15736
+ const x1 = Math.max(a.x, b.x);
15737
+ const y1 = Math.max(a.y, b.y);
15738
+ const x2 = Math.min(a.x + a.w, b.x + b.w);
15739
+ const y2 = Math.min(a.y + a.h, b.y + b.h);
15740
+ const interW = Math.max(0, x2 - x1);
15741
+ const interH = Math.max(0, y2 - y1);
15742
+ const inter = interW * interH;
15743
+ if (inter <= 0) return 0;
15744
+ const union2 = a.w * a.h + b.w * b.h - inter;
15745
+ return union2 <= 0 ? 0 : inter / union2;
15746
+ }
15747
+ function windowsOverlap(a, b) {
15748
+ return a.firstSeenMs <= b.lastSeenMs && b.firstSeenMs <= a.lastSeenMs;
15749
+ }
15750
+ var TRACK_PAIR_MIN_IOU = 0.3;
15751
+ function diffTracks(a, b) {
15752
+ const bMatched = /* @__PURE__ */ new Set();
15753
+ const lost = [];
15754
+ for (const ta of a) {
15755
+ let bestIdx = -1;
15756
+ let bestIou = 0;
15757
+ for (let i = 0; i < b.length; i++) {
15758
+ if (bMatched.has(i)) continue;
15759
+ const tb = b[i];
15760
+ if (tb === void 0 || tb.className !== ta.className) continue;
15761
+ if (!windowsOverlap(ta, tb)) continue;
15762
+ const score = iou(ta.bbox, tb.bbox);
15763
+ if (score >= TRACK_PAIR_MIN_IOU && score > bestIou) {
15764
+ bestIou = score;
15765
+ bestIdx = i;
15766
+ }
15767
+ }
15768
+ if (bestIdx === -1) lost.push(ta);
15769
+ else bMatched.add(bestIdx);
15770
+ }
15771
+ const gained = b.filter((_, i) => !bMatched.has(i));
15772
+ return { gained, lost };
15773
+ }
15774
+ function diffKnobs(a, b) {
15775
+ const stepIds = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
15776
+ const changed = [];
15777
+ for (const stepId of stepIds) {
15778
+ const aStep = a[stepId] ?? {};
15779
+ const bStep = b[stepId] ?? {};
15780
+ const keys = /* @__PURE__ */ new Set([...Object.keys(aStep), ...Object.keys(bStep)]);
15781
+ for (const key of keys) {
15782
+ const from = aStep[key];
15783
+ const to = bStep[key];
15784
+ if (from !== to) changed.push({ stepId, key, from, to });
15785
+ }
15786
+ }
15787
+ return changed;
15788
+ }
15789
+ function diffPromoted(discardedA, tracksB) {
15790
+ const promoted = [];
15791
+ for (const d of discardedA) {
15792
+ const nowTracked = tracksB.some((t) => t.className === d.macroClass);
15793
+ if (nowTracked)
15794
+ promoted.push({ macroClass: d.macroClass, score: d.score, wasDiscardedBy: d.reason });
15795
+ }
15796
+ return promoted;
15797
+ }
15798
+ function diffReplayRuns(a, b) {
15799
+ if (a.clipId !== b.clipId) return { kind: "incomparable", reason: "different-input" };
15800
+ if (a.nodeId !== b.nodeId) return { kind: "incomparable", reason: "different-node" };
15801
+ return {
15802
+ kind: "compared",
15803
+ changedKnobs: diffKnobs(a.settings, b.settings),
15804
+ tracks: diffTracks(a.tracks, b.tracks),
15805
+ promoted: diffPromoted(a.discarded, b.tracks)
15806
+ };
15807
+ }
15808
+
15227
15809
  // src/throughput/sustained-runner.ts
15228
15810
  async function runSustainedLoop(opts) {
15229
15811
  const concurrency = Math.max(1, Math.floor(opts.concurrency));
@@ -15351,7 +15933,7 @@ function resolveFixturesDir() {
15351
15933
  const pkgRoot = path2.resolve(path2.dirname(here), "..");
15352
15934
  return path2.join(pkgRoot, "assets", "fixtures");
15353
15935
  }
15354
- var BenchmarkAddon = class extends BaseAddon {
15936
+ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
15355
15937
  id = "benchmark";
15356
15938
  /** Per-sessionId abort flags for in-flight decoder perf runs. */
15357
15939
  decoderPerfAbortFlags = /* @__PURE__ */ new Map();
@@ -15372,6 +15954,17 @@ var BenchmarkAddon = class extends BaseAddon {
15372
15954
  frameCache = /* @__PURE__ */ new Map();
15373
15955
  /** Periodic GC handle — clears expired frames every 60s. */
15374
15956
  frameCacheSweeper = null;
15957
+ /** The replay run collection — declared in `onInitialize`, before any
15958
+ * `runReplayClip` call can save into it. See `replay/replay-store.ts`. */
15959
+ replayStore = null;
15960
+ /** The replay's one ffmpeg spawn, bounded — see `replay/replay-decode.ts`. */
15961
+ replayDecoder = new ReplayDecoder({
15962
+ ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg"
15963
+ });
15964
+ /** `replay:<StoredReplayRun>` — an in-memory index would drift from the
15965
+ * store; this is a thin cache over `replayStore.list`, keyed for the
15966
+ * diff action's two-id lookup. */
15967
+ replayRunCache = /* @__PURE__ */ new Map();
15375
15968
  pages = [
15376
15969
  {
15377
15970
  id: "benchmark",
@@ -15392,6 +15985,8 @@ var BenchmarkAddon = class extends BaseAddon {
15392
15985
  id: this.id,
15393
15986
  listPages: () => this.pages
15394
15987
  };
15988
+ await ReplayStore.declare(this.api.settingsStore);
15989
+ this.replayStore = new ReplayStore(this.api.settingsStore);
15395
15990
  return {
15396
15991
  providers: [{ capability: addonPagesSourceCapability, provider }],
15397
15992
  customActions: benchmarkActions,
@@ -15410,7 +16005,11 @@ var BenchmarkAddon = class extends BaseAddon {
15410
16005
  clearBenchmarkHistory: async () => this.clearBenchmarkHistory(),
15411
16006
  listDecoderFixtures: async () => this.listDecoderFixtures(),
15412
16007
  runDecoderPerformanceTest: async (input) => this.runDecoderPerformanceTest(input),
15413
- abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input)
16008
+ abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input),
16009
+ uploadReplayClip: async (input) => this.uploadReplayClip(input),
16010
+ runReplayClip: async (input) => this.runReplayClip(input),
16011
+ listReplayRuns: async (input) => this.listReplayRuns(input),
16012
+ diffReplayRuns: async (input) => this.diffReplayRunsAction(input)
15414
16013
  }
15415
16014
  };
15416
16015
  }
@@ -15513,6 +16112,126 @@ var BenchmarkAddon = class extends BaseAddon {
15513
16112
  "runPipelineTest"
15514
16113
  );
15515
16114
  }
16115
+ // ── Replay ────────────────────────────────────────────────────────────
16116
+ /**
16117
+ * Storage location a replay upload lands on. `replayClips` is a NEW
16118
+ * `StorageLocationType` (an open string, no schema change needed — see
16119
+ * `storage-location.ts:19`) but it still needs an actual location
16120
+ * REGISTERED for that type before `storage.write`/`.read` can resolve it
16121
+ * (`storage.upsertLocation`, `cardinality: 'single'`, its own retention).
16122
+ * That registration is an operator/boot-time provisioning step this
16123
+ * change does not perform — see the report's open items.
16124
+ */
16125
+ static REPLAY_CLIPS_LOCATION = "replayClips";
16126
+ async uploadReplayClip(input) {
16127
+ const bytes = Buffer.from(input.dataBase64, "base64");
16128
+ const clipId = `clip:${createHash("sha256").update(bytes).digest("hex").slice(0, 32)}`;
16129
+ await this.api.storage.write.mutate({
16130
+ location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16131
+ relativePath: `${clipId}.bin`,
16132
+ data: new Uint8Array(bytes)
16133
+ });
16134
+ this.ctx.logger.info("replay: clip uploaded", { meta: { clipId, bytes: bytes.length } });
16135
+ return { clipId };
16136
+ }
16137
+ async runReplayClip(input) {
16138
+ if (this.replayStore === null) throw new Error("replay: store not initialized");
16139
+ const store = this.replayStore;
16140
+ const source = input.source;
16141
+ const result = await orchestrateReplayRun(
16142
+ {
16143
+ api: this.api,
16144
+ logger: this.ctx.logger,
16145
+ decoder: this.replayDecoder,
16146
+ store,
16147
+ ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg",
16148
+ resolveBaseTree: (deviceId) => this.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }),
16149
+ fetchSourceBytes: (src) => this.fetchReplaySourceBytes(src)
16150
+ },
16151
+ {
16152
+ deviceId: input.deviceId,
16153
+ nodeId: input.nodeId,
16154
+ source,
16155
+ overlay: input.overlay,
16156
+ fps: input.fps,
16157
+ maxFrames: input.maxFrames,
16158
+ maxWidth: input.maxWidth,
16159
+ ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
16160
+ }
16161
+ );
16162
+ if (result.kind === "ran")
16163
+ this.replayRunCache.set(result.replayRunId, {
16164
+ id: result.replayRunId,
16165
+ deviceId: input.deviceId,
16166
+ nodeId: input.nodeId,
16167
+ clipId: source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`,
16168
+ createdAtMs: Date.now(),
16169
+ source,
16170
+ plan: null,
16171
+ verdict: result.verdict
16172
+ });
16173
+ return result;
16174
+ }
16175
+ /**
16176
+ * Byte source for the replay decode step.
16177
+ *
16178
+ * `upload` is fully wired: `storage.read` against the `replayClips`
16179
+ * location. `recording` is NOT — it needs a recorder-side read port
16180
+ * spanning a time WINDOW (locate + multi-GOP read, the plan's own
16181
+ * comparison is `RecordedStillPorts`, which reads exactly ONE instant).
16182
+ * Whether device 615's footage even covers 2026-08-26 18:12 was never
16183
+ * verified against the live hub — the plan's Unknown #2. Reporting that
16184
+ * clearly here beats guessing at a port shape nobody has confirmed.
16185
+ */
16186
+ async fetchReplaySourceBytes(source) {
16187
+ if (source.kind === "upload") {
16188
+ return this.api.storage.read.query({
16189
+ location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16190
+ relativePath: `${source.clipId}.bin`
16191
+ });
16192
+ }
16193
+ throw new Error(
16194
+ "replay: 'recording' source is not wired \u2014 needs a recorder read port spanning a time window (locate + multi-GOP read), and whether this camera\u2019s footage covers the requested window was never verified against the live hub. Use an uploaded clip today."
16195
+ );
16196
+ }
16197
+ async listReplayRuns(input) {
16198
+ if (this.replayStore === null) return [];
16199
+ const rows = await this.replayStore.list({ deviceId: input.deviceId });
16200
+ for (const row of rows) this.replayRunCache.set(row.id, row);
16201
+ return rows;
16202
+ }
16203
+ /** `verdict` round-trips through the store as opaque JSON
16204
+ * (`StoredReplayRun.verdict: unknown`) — this is the ONE narrowing back
16205
+ * to the shape `diffReplayRuns` needs, at the ONE place that reads it
16206
+ * for that purpose. Same documented boundary `ReplayStore.list` itself
16207
+ * crosses (`replay-store.ts`): this addon is the only reader that knows
16208
+ * what it wrote. */
16209
+ static verdictTracksAndDiscarded(verdict) {
16210
+ return verdict;
16211
+ }
16212
+ async diffReplayRunsAction(input) {
16213
+ const a = this.replayRunCache.get(input.runIdA);
16214
+ const b = this.replayRunCache.get(input.runIdB);
16215
+ if (a === void 0 || b === void 0) {
16216
+ throw new Error(
16217
+ "replay: diff \u2014 one or both run ids are not loaded (call listReplayRuns first)"
16218
+ );
16219
+ }
16220
+ return diffReplayRuns(
16221
+ {
16222
+ clipId: a.clipId,
16223
+ nodeId: a.nodeId,
16224
+ settings: {},
16225
+ ..._BenchmarkAddon.verdictTracksAndDiscarded(a.verdict)
16226
+ },
16227
+ {
16228
+ clipId: b.clipId,
16229
+ nodeId: b.nodeId,
16230
+ settings: {},
16231
+ ..._BenchmarkAddon.verdictTracksAndDiscarded(b.verdict)
16232
+ }
16233
+ );
16234
+ }
15516
16235
  /**
15517
16236
  * Batched pipeline tester.
15518
16237
  *