@camstack/addon-pipeline 1.2.115 → 1.2.119

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 (32) hide show
  1. package/dist/{addon-utils-C0ht2KPO.js → addon-utils-RAHF2hmW.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +171 -167
  5. package/dist/detection-pipeline/index.mjs +169 -165
  6. package/dist/{dist-BqIbYW1A.js → dist-DIsM42Ex.js} +121 -11
  7. package/dist/{dist-DGQkkWc6.mjs → dist-Db664g2O.mjs} +116 -12
  8. package/dist/{event-loop-stall-monitor-ylMGpz48.js → event-loop-stall-monitor-CSpdFZb6.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-D5jK5v4Z.mjs → event-loop-stall-monitor-DY6339bX.mjs} +1 -1
  10. package/dist/{lazy-sharp-cOsy7l_P.js → lazy-sharp-LSlEXQe_.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +12 -14
  14. package/dist/pipeline-runner/index.mjs +11 -13
  15. package/dist/{process-memory-BtT7WfbJ.mjs → process-memory-CPm5TMBl.mjs} +1 -1
  16. package/dist/{process-memory-45juuHpN.js → process-memory-DpSJQF-p.js} +1 -1
  17. package/dist/recorder/index.js +261 -32
  18. package/dist/recorder/index.mjs +260 -31
  19. package/dist/session-decode/decode-worker-child.js +34 -26
  20. package/dist/session-decode/decode-worker-child.mjs +33 -25
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_SFFNGT.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CstvrzmK.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BiNja8Hw.mjs +26 -0
  24. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BAMyzCWw.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D-NFPV_g.mjs} +1 -1
  25. package/dist/stream-broker/{hostInit-CG28NvlV.mjs → hostInit-kHeLqEod.mjs} +2 -2
  26. package/dist/stream-broker/index.js +8 -6
  27. package/dist/stream-broker/index.mjs +8 -6
  28. package/dist/stream-broker/remoteEntry.js +1 -1
  29. package/dist/{worker-protocol-CIrqGbpG.js → worker-protocol-C1tr8WP3.js} +1 -1
  30. package/dist/{worker-protocol-DyocTIx_.mjs → worker-protocol-_zMPqSas.mjs} +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B1LDFHY9.mjs +0 -26
@@ -1,7 +1,7 @@
1
- const require_dist = require("../dist-BqIbYW1A.js");
1
+ const require_dist = require("../dist-DIsM42Ex.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
3
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
4
- const require_addon_utils = require("../addon-utils-C0ht2KPO.js");
4
+ const require_addon_utils = require("../addon-utils-RAHF2hmW.js");
5
5
  const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
@@ -2984,11 +2984,23 @@ function concatFileDirective(uri) {
2984
2984
  function buildExportConcatList(segments) {
2985
2985
  return `ffconcat version 1.0\n${segments.map((s) => `${concatFileDirective(s.uri)}\nduration ${sec(s.durMs / 1e3)}`).map((l) => `${l}\n`).join("")}`;
2986
2986
  }
2987
+ /**
2988
+ * Concat list for already-finished MP4s. A `duration` line here would
2989
+ * TRUNCATE each hour to whatever dummy we wrote (the stream clock is
2990
+ * already inside the file).
2991
+ */
2992
+ function buildAssembleConcatList(uris) {
2993
+ return `ffconcat version 1.0\n${uris.map((u) => `${concatFileDirective(u)}\n`).join("")}`;
2994
+ }
2987
2995
  /** Pass 1 of a timelapse: remux the ffconcat list into one continuous file.
2988
2996
  * `-safe 0` because the list's paths are absolute — which is exactly what the
2989
2997
  * per-export source list contains (`export-source-playlist.ts`). */
2990
2998
  function buildConcatPassArgs(input) {
2991
2999
  return [
3000
+ "-fflags",
3001
+ "+discardcorrupt+genpts",
3002
+ "-err_detect",
3003
+ "ignore_err",
2992
3004
  "-f",
2993
3005
  "concat",
2994
3006
  "-safe",
@@ -3003,6 +3015,21 @@ function buildConcatPassArgs(input) {
3003
3015
  input.outPath
3004
3016
  ];
3005
3017
  }
3018
+ /** Final stitch of the surviving per-hour MP4s. Already selected + encoded. */
3019
+ function buildAssembleArgs(input) {
3020
+ return [
3021
+ "-f",
3022
+ "concat",
3023
+ "-safe",
3024
+ "0",
3025
+ "-i",
3026
+ input.inputConcatList,
3027
+ "-c",
3028
+ "copy",
3029
+ ...FASTSTART,
3030
+ input.outPath
3031
+ ];
3032
+ }
3006
3033
  /** Build the ffmpeg argv for one export render. */
3007
3034
  function buildExportArgs(input) {
3008
3035
  const { inputPlaylist, outPath, options } = input;
@@ -3048,6 +3075,65 @@ function buildExportArgs(input) {
3048
3075
  ];
3049
3076
  }
3050
3077
  //#endregion
3078
+ //#region src/recorder/addon/export-timelapse-chunks.ts
3079
+ /** One hour of concatenated footage — not one hour of wall clock. */
3080
+ var TIMELAPSE_CHUNK_MS = 36e5;
3081
+ /**
3082
+ * Split a concat list into hour-sized chunks. A single file longer than the
3083
+ * chunk stays whole — it is already one open, and cutting it would need a
3084
+ * second demux we do not have.
3085
+ */
3086
+ function splitTimelapseChunks(entries, chunkMs = TIMELAPSE_CHUNK_MS) {
3087
+ if (entries.length === 0) return [];
3088
+ const chunks = [];
3089
+ let current = [];
3090
+ let currentDur = 0;
3091
+ let streamStartMs = 0;
3092
+ for (const entry of entries) {
3093
+ if (current.length > 0 && currentDur + entry.durMs > chunkMs) {
3094
+ chunks.push({
3095
+ entries: current,
3096
+ streamStartMs
3097
+ });
3098
+ streamStartMs += currentDur;
3099
+ current = [];
3100
+ currentDur = 0;
3101
+ }
3102
+ current.push(entry);
3103
+ currentDur += entry.durMs;
3104
+ }
3105
+ if (current.length > 0) chunks.push({
3106
+ entries: current,
3107
+ streamStartMs
3108
+ });
3109
+ return chunks;
3110
+ }
3111
+ /**
3112
+ * Move stream-time dense ranges onto a chunk's own `t=0`. Ranges that miss
3113
+ * the chunk disappear; a range that straddles the edge is clipped. Empty →
3114
+ * `undefined` so the select stays the single-rate form.
3115
+ */
3116
+ function denseRangesForChunk(dense, chunkStreamStartMs, chunkDurMs) {
3117
+ if (dense === void 0 || dense.ranges.length === 0) return void 0;
3118
+ const startSec = chunkStreamStartMs / 1e3;
3119
+ const endSec = (chunkStreamStartMs + chunkDurMs) / 1e3;
3120
+ const ranges = [];
3121
+ for (const r of dense.ranges) {
3122
+ const from = Math.max(r.fromSec, startSec);
3123
+ const to = Math.min(r.toSec, endSec);
3124
+ if (to <= from) continue;
3125
+ ranges.push({
3126
+ fromSec: from - startSec,
3127
+ toSec: to - startSec
3128
+ });
3129
+ }
3130
+ if (ranges.length === 0) return void 0;
3131
+ return {
3132
+ everyMs: dense.everyMs,
3133
+ ranges
3134
+ };
3135
+ }
3136
+ //#endregion
3051
3137
  //#region src/recorder/addon/export-store.ts
3052
3138
  /**
3053
3139
  * Recording-export persistence for the recorder addon.
@@ -3311,15 +3397,15 @@ function expectedOutSeconds(rec) {
3311
3397
  var ExportEngine = class {
3312
3398
  deps;
3313
3399
  queue = [];
3314
- activeId = null;
3315
- activeProc = null;
3400
+ /** In-flight jobs → their current ffmpeg (null between the two timelapse passes). */
3401
+ active = /* @__PURE__ */ new Map();
3316
3402
  cancelled = /* @__PURE__ */ new Set();
3317
3403
  constructor(deps) {
3318
3404
  this.deps = deps;
3319
3405
  }
3320
3406
  /** Queue a persisted `queued` record for rendering. */
3321
3407
  enqueue(id) {
3322
- if (this.queue.includes(id) || this.activeId === id) return;
3408
+ if (this.queue.includes(id) || this.active.has(id)) return;
3323
3409
  this.queue.push(id);
3324
3410
  this.pump();
3325
3411
  }
@@ -3328,29 +3414,32 @@ var ExportEngine = class {
3328
3414
  this.cancelled.add(id);
3329
3415
  const idx = this.queue.indexOf(id);
3330
3416
  if (idx >= 0) this.queue.splice(idx, 1);
3331
- if (this.activeId === id && this.activeProc) try {
3332
- this.activeProc.kill("SIGKILL");
3417
+ const proc = this.active.get(id);
3418
+ if (proc) try {
3419
+ proc.kill("SIGKILL");
3333
3420
  } catch {}
3334
3421
  else if (idx >= 0) await this.fail(id, "cancelled");
3335
3422
  }
3336
- async pump() {
3337
- if (this.activeId) return;
3338
- const id = this.queue.shift();
3339
- if (!id) return;
3340
- if (this.cancelled.has(id)) {
3341
- this.cancelled.delete(id);
3342
- await this.fail(id, "cancelled");
3343
- this.pump();
3344
- return;
3423
+ pump() {
3424
+ while (this.active.size < 2 && this.queue.length > 0) {
3425
+ const id = this.queue.shift();
3426
+ if (!id) return;
3427
+ if (this.cancelled.has(id)) {
3428
+ this.cancelled.delete(id);
3429
+ this.fail(id, "cancelled");
3430
+ continue;
3431
+ }
3432
+ this.active.set(id, null);
3433
+ this.runOne(id);
3345
3434
  }
3346
- this.activeId = id;
3435
+ }
3436
+ async runOne(id) {
3347
3437
  try {
3348
3438
  await this.render(id);
3349
3439
  } catch (err) {
3350
3440
  await this.fail(id, err instanceof Error ? err.message : String(err));
3351
3441
  } finally {
3352
- this.activeId = null;
3353
- this.activeProc = null;
3442
+ this.active.delete(id);
3354
3443
  this.pump();
3355
3444
  }
3356
3445
  }
@@ -3368,7 +3457,7 @@ var ExportEngine = class {
3368
3457
  try {
3369
3458
  await this.spawnRender(id, rec, playlist, this.streamTimeOptions(rec, playlist.segments));
3370
3459
  } finally {
3371
- if (this.deps.cleanupPlaylist) for (const file of [playlist.path, playlist.concatPath]) try {
3460
+ if (this.deps.cleanupPlaylist) for (const file of [playlist.path, playlist.concatPath].filter((p) => typeof p === "string" && p.length > 0)) try {
3372
3461
  await this.deps.cleanupPlaylist(file);
3373
3462
  } catch (err) {
3374
3463
  this.deps.logger.warn("export: source playlist cleanup failed", {
@@ -3431,7 +3520,7 @@ var ExportEngine = class {
3431
3520
  }
3432
3521
  });
3433
3522
  if (options.timelapse) {
3434
- await this.spawnTimelapse(id, rec, playlist.concatPath, outPath, options);
3523
+ await this.spawnTimelapse(id, rec, playlist, outPath, options);
3435
3524
  return;
3436
3525
  }
3437
3526
  const args = buildExportArgs({
@@ -3455,8 +3544,118 @@ var ExportEngine = class {
3455
3544
  * timestamp line runs on the SAME clock the dense map summed — see
3456
3545
  * `export-ffmpeg-args.ts` for what the HLS demuxer's content-stitching cost.
3457
3546
  */
3458
- async spawnTimelapse(id, rec, concatList, outPath, options) {
3547
+ async spawnTimelapse(id, rec, playlist, outPath, options) {
3548
+ const chunks = splitTimelapseChunks(playlist.concatEntries ?? []);
3549
+ if (chunks.length <= 1) {
3550
+ const concatList = playlist.concatPath;
3551
+ if (concatList === void 0) {
3552
+ await this.fail(id, "no concat list for timelapse");
3553
+ return;
3554
+ }
3555
+ await this.spawnTimelapsePair(id, rec, concatList, outPath, options, {
3556
+ floor: 0,
3557
+ ceil: 99
3558
+ });
3559
+ return;
3560
+ }
3561
+ if (this.deps.writeTextFile === void 0) {
3562
+ const concatList = playlist.concatPath;
3563
+ if (concatList === void 0) {
3564
+ await this.fail(id, "no concat list for timelapse");
3565
+ return;
3566
+ }
3567
+ await this.spawnTimelapsePair(id, rec, concatList, outPath, options, {
3568
+ floor: 0,
3569
+ ceil: 99
3570
+ });
3571
+ return;
3572
+ }
3573
+ const survivors = [];
3574
+ const sidecars = [];
3575
+ try {
3576
+ for (let i = 0; i < chunks.length; i++) {
3577
+ if (this.cancelled.has(id)) {
3578
+ await this.fail(id, "cancelled");
3579
+ return;
3580
+ }
3581
+ const chunk = chunks[i];
3582
+ const chunkDur = chunk.entries.reduce((s, e) => s + e.durMs, 0);
3583
+ const listPath = `${outPath}.chunk${i}.ffconcat`;
3584
+ const concatPath = `${outPath}.chunk${i}.concat.ts`;
3585
+ const chunkMp4 = `${outPath}.chunk${i}.mp4`;
3586
+ sidecars.push(listPath, concatPath, chunkMp4);
3587
+ await this.deps.writeTextFile(listPath, buildExportConcatList(chunk.entries));
3588
+ const span = 90 / chunks.length;
3589
+ const floor = Math.round(i * span);
3590
+ if (await this.spawnTimelapsePair(id, rec, listPath, chunkMp4, {
3591
+ ...options,
3592
+ timelapse: options.timelapse ? {
3593
+ ...options.timelapse,
3594
+ dense: denseRangesForChunk(options.timelapse.dense, chunk.streamStartMs, chunkDur)
3595
+ } : options.timelapse
3596
+ }, {
3597
+ floor,
3598
+ ceil: floor + Math.round(span * .95),
3599
+ finish: false
3600
+ }) === "ok") survivors.push(chunkMp4);
3601
+ else this.deps.logger.warn("export timelapse hour skipped", {
3602
+ tags: { deviceId: rec.deviceId },
3603
+ meta: {
3604
+ exportId: id,
3605
+ chunk: i,
3606
+ streamStartMs: chunk.streamStartMs
3607
+ }
3608
+ });
3609
+ }
3610
+ if (survivors.length === 0) {
3611
+ await this.fail(id, "every timelapse hour failed");
3612
+ return;
3613
+ }
3614
+ if (survivors.length === 1) {
3615
+ const only = survivors[0];
3616
+ const copyArgs = buildExportArgs({
3617
+ inputPlaylist: only,
3618
+ outPath,
3619
+ options: {
3620
+ includeAudio: false,
3621
+ maxLifeMs: 1,
3622
+ deleteAfterDownload: false,
3623
+ speed: 1
3624
+ }
3625
+ });
3626
+ const copied = await this.runFfmpeg(id, rec.deviceId, copyArgs, {
3627
+ totalSec: expectedOutSeconds(rec),
3628
+ floor: 95,
3629
+ ceil: 99
3630
+ });
3631
+ await this.finish(id, copied.code, outPath, copied.stderrTail);
3632
+ return;
3633
+ }
3634
+ const assembleList = `${outPath}.assemble.ffconcat`;
3635
+ sidecars.push(assembleList);
3636
+ await this.deps.writeTextFile(assembleList, buildAssembleConcatList(survivors));
3637
+ const assembled = await this.runFfmpeg(id, rec.deviceId, buildAssembleArgs({
3638
+ inputConcatList: assembleList,
3639
+ outPath
3640
+ }), {
3641
+ totalSec: expectedOutSeconds(rec),
3642
+ floor: 95,
3643
+ ceil: 99
3644
+ });
3645
+ await this.finish(id, assembled.code, outPath, assembled.stderrTail);
3646
+ } finally {
3647
+ for (const file of sidecars) try {
3648
+ await this.deps.removeFile(file);
3649
+ } catch {}
3650
+ }
3651
+ }
3652
+ /**
3653
+ * Concat remux then select. `finish: false` lets the chunk walker skip a
3654
+ * dead hour without failing the whole night.
3655
+ */
3656
+ async spawnTimelapsePair(id, rec, concatList, outPath, options, progress) {
3459
3657
  const concatPath = concatPassOutPath(outPath);
3658
+ const mid = progress.floor + Math.round((progress.ceil - progress.floor) / 2);
3460
3659
  try {
3461
3660
  const concatArgs = buildConcatPassArgs({
3462
3661
  inputConcatList: concatList,
@@ -3464,12 +3663,12 @@ var ExportEngine = class {
3464
3663
  });
3465
3664
  const concatRan = await this.runFfmpeg(id, rec.deviceId, concatArgs, {
3466
3665
  totalSec: Math.max(.001, (rec.toMs - rec.fromMs) / 1e3),
3467
- floor: 0,
3468
- ceil: 49
3666
+ floor: progress.floor,
3667
+ ceil: mid
3469
3668
  });
3470
3669
  if (this.cancelled.has(id) || concatRan.code !== 0) {
3471
- await this.finish(id, concatRan.code, concatPath, concatRan.stderrTail);
3472
- return;
3670
+ if (progress.finish !== false) await this.finish(id, concatRan.code, concatPath, concatRan.stderrTail);
3671
+ return "fail";
3473
3672
  }
3474
3673
  const selectArgs = buildExportArgs({
3475
3674
  inputPlaylist: concatPath,
@@ -3478,10 +3677,11 @@ var ExportEngine = class {
3478
3677
  });
3479
3678
  const selectRan = await this.runFfmpeg(id, rec.deviceId, selectArgs, {
3480
3679
  totalSec: expectedOutSeconds(rec),
3481
- floor: 50,
3482
- ceil: 99
3680
+ floor: mid,
3681
+ ceil: progress.ceil
3483
3682
  });
3484
- await this.finish(id, selectRan.code, outPath, selectRan.stderrTail);
3683
+ if (progress.finish !== false) await this.finish(id, selectRan.code, outPath, selectRan.stderrTail);
3684
+ return selectRan.code === 0 ? "ok" : "fail";
3485
3685
  } finally {
3486
3686
  try {
3487
3687
  await this.deps.removeFile(concatPath);
@@ -3493,10 +3693,28 @@ var ExportEngine = class {
3493
3693
  exportId: id,
3494
3694
  args
3495
3695
  } });
3696
+ const stallMs = this.deps.stallMs ?? 9e4;
3496
3697
  return new Promise((resolve) => {
3497
3698
  const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
3498
- this.activeProc = proc;
3699
+ this.active.set(id, proc);
3499
3700
  const stderrTail = [];
3701
+ let stallTimer = null;
3702
+ const clearStall = () => {
3703
+ if (stallTimer !== null) {
3704
+ clearTimeout(stallTimer);
3705
+ stallTimer = null;
3706
+ }
3707
+ };
3708
+ const armStall = () => {
3709
+ clearStall();
3710
+ stallTimer = setTimeout(() => {
3711
+ stderrTail.push(`ffmpeg stalled: no progress for ${stallMs}ms`);
3712
+ try {
3713
+ proc.kill("SIGKILL");
3714
+ } catch {}
3715
+ }, stallMs);
3716
+ };
3717
+ armStall();
3500
3718
  proc.stderr?.on("data", (chunk) => {
3501
3719
  const text = chunk instanceof Buffer ? chunk.toString("utf8") : String(chunk);
3502
3720
  for (const line of text.split("\n")) {
@@ -3507,6 +3725,7 @@ var ExportEngine = class {
3507
3725
  }
3508
3726
  const m = TIME_RE.exec(line);
3509
3727
  if (m) {
3728
+ armStall();
3510
3729
  const sec = Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]);
3511
3730
  const span = progress.ceil - progress.floor;
3512
3731
  const pct = Math.min(progress.ceil, progress.floor + Math.round(sec / progress.totalSec * span));
@@ -3519,14 +3738,16 @@ var ExportEngine = class {
3519
3738
  }
3520
3739
  });
3521
3740
  proc.on("exit", (code) => {
3522
- this.activeProc = null;
3741
+ clearStall();
3742
+ this.active.set(id, null);
3523
3743
  resolve({
3524
3744
  code: typeof code === "number" ? code : null,
3525
3745
  stderrTail
3526
3746
  });
3527
3747
  });
3528
3748
  proc.on("error", (raw) => {
3529
- this.activeProc = null;
3749
+ clearStall();
3750
+ this.active.set(id, null);
3530
3751
  const failure = readSpawnFailure(raw);
3531
3752
  this.deps.logger.error("export ffmpeg could not be spawned", {
3532
3753
  tags: { deviceId },
@@ -4811,6 +5032,10 @@ async function writeExportSourcePlaylist(input) {
4811
5032
  segments: segments.map((s) => ({
4812
5033
  startMs: s.startMs,
4813
5034
  durMs: s.durMs
5035
+ })),
5036
+ concatEntries: segments.map((s) => ({
5037
+ uri: s.uri,
5038
+ durMs: s.durMs
4814
5039
  }))
4815
5040
  };
4816
5041
  }
@@ -10764,6 +10989,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
10764
10989
  });
10765
10990
  },
10766
10991
  cleanupPlaylist: removeExportFile,
10992
+ writeTextFile: async (p, contents) => {
10993
+ await node_fs.promises.mkdir(node_path.default.dirname(p), { recursive: true });
10994
+ await node_fs.promises.writeFile(p, contents, "utf8");
10995
+ },
10767
10996
  outPath: exportOutPath,
10768
10997
  statBytes: async (p) => (await node_fs.promises.stat(p)).size,
10769
10998
  removeFile: removeExportFile,