@camstack/addon-pipeline 1.2.367 → 1.2.368

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-CESZ9DM5.js → custom-model-catalog-BY-aEI2s.js} +2 -2
  4. package/dist/{custom-model-catalog-BqKEZYFI.mjs → custom-model-catalog-Bnel86hH.mjs} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-ClKIJKn0.mjs → dist-5AhGOWWu.mjs} +381 -0
  8. package/dist/{dist-BmQm6_nm.js → dist-DDXsZaRH.js} +381 -0
  9. package/dist/{model-catalogs-B3lwxS5R.js → model-catalogs-DJN58c_U.js} +1 -1
  10. package/dist/{model-catalogs-DKHNVZC0.mjs → model-catalogs-DWR9EsGI.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-BqTmCMlP.js → node-CiciZWts.js} +1 -1
  14. package/dist/{node-DlpWXqvO.mjs → node-DqZJFOzR.mjs} +1 -1
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-DHF3ynR3.js → process-memory-BRu19Dgr.js} +1 -1
  18. package/dist/{process-memory-BjIiiXGZ.mjs → process-memory-Batknjjt.mjs} +1 -1
  19. package/dist/recorder/index.js +3 -2
  20. package/dist/recorder/index.mjs +3 -2
  21. package/dist/{segment-demux-js-D1Up0mTP.js → segment-demux-js-0CRf7GAb.js} +2 -2
  22. package/dist/{segment-demux-js-Du_eAog1.mjs → segment-demux-js-dlhO3Q8W.mjs} +2 -2
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CRxJwkgE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CSajlHKC.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BfVREmhg.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-5KL24-a_.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-DEnYuDjZ.mjs → hostInit-KJ1eka58.mjs} +2 -2
  30. package/dist/stream-broker/index.js +123 -57
  31. package/dist/stream-broker/index.mjs +123 -57
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CIBBQAJc.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DNGQ-Tic.mjs +0 -26
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-BmQm6_nm.js");
7
- const require_node = require("../node-BqTmCMlP.js");
6
+ const require_dist = require("../dist-DDXsZaRH.js");
7
+ const require_node = require("../node-CiciZWts.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-D1Up0mTP.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-0CRf7GAb.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -33941,23 +33941,37 @@ function createHttpClipStreamOpener(options = {}) {
33941
33941
  req.end();
33942
33942
  });
33943
33943
  }
33944
- //#endregion
33945
- //#region src/stream-broker/recorded/demux-off-thread.ts
33946
33944
  /**
33947
- * Off-thread JS demux client — mp4box parse on a `worker_threads` Worker
33948
- * inside the stream-broker runner (D228). Same addon, same WebRTC track;
33949
- * the live restream event loop is not the one walking boxes.
33950
- *
33951
- * The worker file is a separate vite entry (`dist/stream-broker/demux-worker-child.js`).
33952
- * This module is inlined into `dist/stream-broker/index.js`, so
33953
- * `import.meta.url` is THAT file and the child is a sibling — the same
33954
- * resolution trick as `fork-decode-worker.ts` / `decode-worker-main.js`.
33955
- *
33956
- * Missing worker file (vitest from `src/`, a botched deploy) falls back to
33957
- * {@link demuxSegmentFullJs} on this thread. A worker that errors a job
33958
- * returns null so {@link demuxSegmentPreferJs} takes the ffmpeg subprocess
33959
- * (already off-loop) instead of re-running mp4box here.
33960
- */
33945
+ * Heap shaping for the demux isolate. The runner's process-wide V8 flags
33946
+ * (`--initial-old-space-size=512`, `--max-semi-space-size=8`, from the
33947
+ * stream-broker manifest) are sized for the MAIN isolate and leak into every
33948
+ * Worker: with a 512 MB initial old space the worker grew to 144 MB of
33949
+ * garbage without a single major GC. Explicit `resourceLimits` replace the
33950
+ * inherited sizing (measured under those flags with the same allocation
33951
+ * churn: worker heap 474 MB without limits, 88 MB with them; see
33952
+ * docs/history/sessions.md).
33953
+ *
33954
+ * 128 MB old generation: parsing a recorded fragment retains about 1 MB
33955
+ * after GC for an 86 KB input (fixture bench), so even a 10 MB fragment (a
33956
+ * long 4K segment) holds well under 30 MB live; 128 MB is more than 4x that.
33957
+ * A parse that still exceeds it fails the worker cleanly
33958
+ * (`ERR_WORKER_OUT_OF_MEMORY`): in-flight parses resolve null (ffmpeg
33959
+ * fallback) and the next parse spawns a fresh worker.
33960
+ */
33961
+ var DEMUX_WORKER_RESOURCE_LIMITS = {
33962
+ maxOldGenerationSizeMb: 128,
33963
+ maxYoungGenerationSizeMb: 16
33964
+ };
33965
+ var defaultTimers = {
33966
+ set: (fn, ms) => {
33967
+ const t = setTimeout(fn, ms);
33968
+ t.unref();
33969
+ return t;
33970
+ },
33971
+ clear: (handle) => {
33972
+ if (handle !== void 0) clearTimeout(handle);
33973
+ }
33974
+ };
33961
33975
  function isCodec(value) {
33962
33976
  return value === "H264" || value === "H265";
33963
33977
  }
@@ -33995,50 +34009,90 @@ function defaultWorkerFilename() {
33995
34009
  /** {@link createOffThreadJsParse} plus the heap accessor the memory probe reads. */
33996
34010
  function createOffThreadDemuxClient(deps) {
33997
34011
  const Ctor = deps.WorkerCtor;
33998
- let worker = null;
33999
- let spawnedAt = 0;
34012
+ const idleMs = deps.idleMs ?? 6e4;
34013
+ const timers = deps.timers ?? defaultTimers;
34014
+ const now = deps.now ?? Date.now;
34015
+ let current = null;
34000
34016
  let nextId = 1;
34001
- const pending = /* @__PURE__ */ new Map();
34002
- const failAll = () => {
34003
- const waiting = [...pending.values()];
34004
- pending.clear();
34005
- for (const resolve of waiting) resolve(null);
34017
+ const failAll = (slot, why) => {
34018
+ const waiting = [...slot.pending.values()];
34019
+ slot.pending.clear();
34020
+ if (waiting.length > 0 && deps.logger) for (const deviceId of new Set(waiting.map((p) => p.deviceId))) deps.logger.warn(`demux worker lost with parses in flight (${why}) — ffmpeg fallback`, {
34021
+ ...deviceId !== void 0 ? { tags: { deviceId } } : {},
34022
+ meta: { inFlight: waiting.length }
34023
+ });
34024
+ for (const p of waiting) p.resolve(null);
34025
+ };
34026
+ const retire = (slot) => {
34027
+ timers.clear(slot.idleTimer);
34028
+ slot.idleTimer = void 0;
34029
+ if (current === slot) current = null;
34030
+ };
34031
+ const onIdle = async (slot) => {
34032
+ slot.idleTimer = void 0;
34033
+ if (current !== slot || slot.pending.size > 0) return;
34034
+ current = null;
34035
+ const heapBytes = (await slot.worker.getHeapStatistics?.().catch(() => null))?.total_heap_size ?? slot.lastHeapBytes;
34036
+ deps.logger?.info("demux worker idle — terminating", { meta: {
34037
+ ageMs: Math.max(0, now() - slot.spawnedAt),
34038
+ idleMs,
34039
+ lastHeapMb: Math.round(heapBytes / 1048576)
34040
+ } });
34041
+ slot.worker.terminate();
34006
34042
  };
34007
- const attach = (w) => {
34043
+ const armIdle = (slot) => {
34044
+ timers.clear(slot.idleTimer);
34045
+ slot.idleTimer = timers.set(() => void onIdle(slot), idleMs);
34046
+ };
34047
+ const attach = (slot) => {
34048
+ const w = slot.worker;
34008
34049
  w.on("message", (message) => {
34009
34050
  const parsed = parseWorkerMessage(message);
34010
34051
  if (!parsed) return;
34011
- const resolve = pending.get(parsed.id);
34012
- if (!resolve) return;
34013
- pending.delete(parsed.id);
34014
- resolve(parsed.aus);
34052
+ const pending = slot.pending.get(parsed.id);
34053
+ if (!pending) return;
34054
+ slot.pending.delete(parsed.id);
34055
+ pending.resolve(parsed.aus);
34056
+ if (slot.pending.size === 0 && current === slot) armIdle(slot);
34015
34057
  });
34016
- w.on("error", () => {
34017
- worker = null;
34018
- failAll();
34058
+ w.on("error", (err) => {
34059
+ retire(slot);
34060
+ failAll(slot, String(err));
34019
34061
  });
34020
34062
  w.on("exit", () => {
34021
- worker = null;
34022
- failAll();
34063
+ retire(slot);
34064
+ failAll(slot, "exit");
34023
34065
  });
34024
34066
  };
34025
34067
  const ensureWorker = () => {
34026
- if (worker) return worker;
34068
+ if (current) return current;
34027
34069
  if (!Ctor) return null;
34028
34070
  try {
34029
- const spawned = new Ctor(deps.workerFilename);
34030
- attach(spawned);
34031
- worker = spawned;
34032
- spawnedAt = Date.now();
34033
- return spawned;
34034
- } catch {
34071
+ const slot = {
34072
+ worker: new Ctor(deps.workerFilename, { resourceLimits: DEMUX_WORKER_RESOURCE_LIMITS }),
34073
+ pending: /* @__PURE__ */ new Map(),
34074
+ spawnedAt: now(),
34075
+ idleTimer: void 0,
34076
+ lastHeapBytes: 0
34077
+ };
34078
+ attach(slot);
34079
+ current = slot;
34080
+ deps.logger?.info("demux worker spawned", { meta: {
34081
+ resourceLimits: DEMUX_WORKER_RESOURCE_LIMITS,
34082
+ idleMs
34083
+ } });
34084
+ return slot;
34085
+ } catch (err) {
34086
+ deps.logger?.warn(`demux worker spawn failed: ${String(err)} — parsing in-process`);
34035
34087
  return null;
34036
34088
  }
34037
34089
  };
34038
- const parse = async (bytes) => {
34090
+ const parse = async (bytes, ctx) => {
34039
34091
  if (!deps.existsSync(deps.workerFilename)) return deps.parseInProcess(bytes);
34040
- const w = ensureWorker();
34041
- if (!w) return deps.parseInProcess(bytes);
34092
+ const slot = ensureWorker();
34093
+ if (!slot) return deps.parseInProcess(bytes);
34094
+ timers.clear(slot.idleTimer);
34095
+ slot.idleTimer = void 0;
34042
34096
  const id = nextId;
34043
34097
  nextId += 1;
34044
34098
  const copy = new Uint8Array(bytes);
@@ -34047,26 +34101,30 @@ function createOffThreadDemuxClient(deps) {
34047
34101
  bytes: copy
34048
34102
  };
34049
34103
  return new Promise((resolve) => {
34050
- pending.set(id, resolve);
34051
- w.postMessage(request, [copy.buffer]);
34104
+ slot.pending.set(id, {
34105
+ resolve,
34106
+ deviceId: ctx?.deviceId
34107
+ });
34108
+ slot.worker.postMessage(request, [copy.buffer]);
34052
34109
  });
34053
34110
  };
34054
- const readHeaps = async (now = Date.now()) => {
34055
- const live = worker;
34111
+ const readHeaps = async (at = Date.now()) => {
34112
+ const live = current;
34056
34113
  const empty = {
34057
34114
  workers: 0,
34058
34115
  heapBytes: 0,
34059
34116
  physicalBytes: 0,
34060
34117
  usedBytes: 0,
34061
34118
  maxAgeMs: 0,
34062
- idleTerminates: false
34119
+ idleTerminates: true
34063
34120
  };
34064
34121
  if (live === null) return empty;
34065
- const stats = await live.getHeapStatistics?.().catch(() => null);
34122
+ const stats = await live.worker.getHeapStatistics?.().catch(() => null);
34123
+ if (stats) live.lastHeapBytes = stats.total_heap_size;
34066
34124
  return {
34067
34125
  ...empty,
34068
34126
  workers: 1,
34069
- maxAgeMs: Math.max(0, now - spawnedAt),
34127
+ maxAgeMs: Math.max(0, at - live.spawnedAt),
34070
34128
  ...stats ? {
34071
34129
  heapBytes: stats.total_heap_size,
34072
34130
  physicalBytes: stats.total_physical_size,
@@ -34084,15 +34142,21 @@ function defaultOffThreadClient() {
34084
34142
  workerFilename: defaultWorkerFilename(),
34085
34143
  existsSync: node_fs.existsSync,
34086
34144
  parseInProcess: require_segment_demux_js.demuxSegmentFullJs,
34087
- WorkerCtor: node_worker_threads.Worker
34145
+ WorkerCtor: node_worker_threads.Worker,
34146
+ ...sharedLogger ? { logger: sharedLogger } : {}
34088
34147
  });
34089
34148
  }
34090
34149
  var shared;
34091
34150
  /** Process-wide JS parse: one Worker, shared by every WebRTC session. */
34092
- var offThreadJsParse = (bytes) => {
34151
+ var offThreadJsParse = (bytes, ctx) => {
34093
34152
  shared ??= defaultOffThreadClient();
34094
- return shared.parse(bytes);
34153
+ return shared.parse(bytes, ctx);
34095
34154
  };
34155
+ var sharedLogger;
34156
+ /** Give the process-wide client a logger; call before the first parse. */
34157
+ function setDemuxWorkerLogger(logger) {
34158
+ sharedLogger = logger;
34159
+ }
34096
34160
  /** Heap of the shared demux worker, for the `[broker-mem]` probe (tick only). */
34097
34161
  async function readDemuxWorkerHeaps(now) {
34098
34162
  return shared ? shared.readHeaps(now) : {
@@ -34101,7 +34165,7 @@ async function readDemuxWorkerHeaps(now) {
34101
34165
  physicalBytes: 0,
34102
34166
  usedBytes: 0,
34103
34167
  maxAgeMs: 0,
34104
- idleTerminates: false
34168
+ idleTerminates: true
34105
34169
  };
34106
34170
  }
34107
34171
  /**
@@ -35004,6 +35068,7 @@ var BrokerWebrtcServer = class {
35004
35068
  stopped = false;
35005
35069
  constructor(options) {
35006
35070
  this.logger = options.logger;
35071
+ setDemuxWorkerLogger(options.logger.child("demux-worker"));
35007
35072
  this.getIceServersFn = options.getIceServers;
35008
35073
  this.staticIceServers = options.iceServers;
35009
35074
  this.icePortRange = options.icePortRange;
@@ -35669,6 +35734,7 @@ var BrokerWebrtcServer = class {
35669
35734
  if (locateRecorded && readRecorded) {
35670
35735
  const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
35671
35736
  ffmpegPath: resolveFfmpeg(),
35737
+ deviceId,
35672
35738
  onDemuxPath: (outcome) => {
35673
35739
  const report = demuxTally.record(outcome, Date.now());
35674
35740
  if (!report) return;
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-ClKIJKn0.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-DlpWXqvO.mjs";
2
+ import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-5AhGOWWu.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-DqZJFOzR.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-Du_eAog1.mjs";
7
+ import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-dlhO3Q8W.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os$1 from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -33937,23 +33937,37 @@ function createHttpClipStreamOpener(options = {}) {
33937
33937
  req.end();
33938
33938
  });
33939
33939
  }
33940
- //#endregion
33941
- //#region src/stream-broker/recorded/demux-off-thread.ts
33942
33940
  /**
33943
- * Off-thread JS demux client — mp4box parse on a `worker_threads` Worker
33944
- * inside the stream-broker runner (D228). Same addon, same WebRTC track;
33945
- * the live restream event loop is not the one walking boxes.
33946
- *
33947
- * The worker file is a separate vite entry (`dist/stream-broker/demux-worker-child.js`).
33948
- * This module is inlined into `dist/stream-broker/index.js`, so
33949
- * `import.meta.url` is THAT file and the child is a sibling — the same
33950
- * resolution trick as `fork-decode-worker.ts` / `decode-worker-main.js`.
33951
- *
33952
- * Missing worker file (vitest from `src/`, a botched deploy) falls back to
33953
- * {@link demuxSegmentFullJs} on this thread. A worker that errors a job
33954
- * returns null so {@link demuxSegmentPreferJs} takes the ffmpeg subprocess
33955
- * (already off-loop) instead of re-running mp4box here.
33956
- */
33941
+ * Heap shaping for the demux isolate. The runner's process-wide V8 flags
33942
+ * (`--initial-old-space-size=512`, `--max-semi-space-size=8`, from the
33943
+ * stream-broker manifest) are sized for the MAIN isolate and leak into every
33944
+ * Worker: with a 512 MB initial old space the worker grew to 144 MB of
33945
+ * garbage without a single major GC. Explicit `resourceLimits` replace the
33946
+ * inherited sizing (measured under those flags with the same allocation
33947
+ * churn: worker heap 474 MB without limits, 88 MB with them; see
33948
+ * docs/history/sessions.md).
33949
+ *
33950
+ * 128 MB old generation: parsing a recorded fragment retains about 1 MB
33951
+ * after GC for an 86 KB input (fixture bench), so even a 10 MB fragment (a
33952
+ * long 4K segment) holds well under 30 MB live; 128 MB is more than 4x that.
33953
+ * A parse that still exceeds it fails the worker cleanly
33954
+ * (`ERR_WORKER_OUT_OF_MEMORY`): in-flight parses resolve null (ffmpeg
33955
+ * fallback) and the next parse spawns a fresh worker.
33956
+ */
33957
+ var DEMUX_WORKER_RESOURCE_LIMITS = {
33958
+ maxOldGenerationSizeMb: 128,
33959
+ maxYoungGenerationSizeMb: 16
33960
+ };
33961
+ var defaultTimers = {
33962
+ set: (fn, ms) => {
33963
+ const t = setTimeout(fn, ms);
33964
+ t.unref();
33965
+ return t;
33966
+ },
33967
+ clear: (handle) => {
33968
+ if (handle !== void 0) clearTimeout(handle);
33969
+ }
33970
+ };
33957
33971
  function isCodec(value) {
33958
33972
  return value === "H264" || value === "H265";
33959
33973
  }
@@ -33991,50 +34005,90 @@ function defaultWorkerFilename() {
33991
34005
  /** {@link createOffThreadJsParse} plus the heap accessor the memory probe reads. */
33992
34006
  function createOffThreadDemuxClient(deps) {
33993
34007
  const Ctor = deps.WorkerCtor;
33994
- let worker = null;
33995
- let spawnedAt = 0;
34008
+ const idleMs = deps.idleMs ?? 6e4;
34009
+ const timers = deps.timers ?? defaultTimers;
34010
+ const now = deps.now ?? Date.now;
34011
+ let current = null;
33996
34012
  let nextId = 1;
33997
- const pending = /* @__PURE__ */ new Map();
33998
- const failAll = () => {
33999
- const waiting = [...pending.values()];
34000
- pending.clear();
34001
- for (const resolve of waiting) resolve(null);
34013
+ const failAll = (slot, why) => {
34014
+ const waiting = [...slot.pending.values()];
34015
+ slot.pending.clear();
34016
+ if (waiting.length > 0 && deps.logger) for (const deviceId of new Set(waiting.map((p) => p.deviceId))) deps.logger.warn(`demux worker lost with parses in flight (${why}) — ffmpeg fallback`, {
34017
+ ...deviceId !== void 0 ? { tags: { deviceId } } : {},
34018
+ meta: { inFlight: waiting.length }
34019
+ });
34020
+ for (const p of waiting) p.resolve(null);
34021
+ };
34022
+ const retire = (slot) => {
34023
+ timers.clear(slot.idleTimer);
34024
+ slot.idleTimer = void 0;
34025
+ if (current === slot) current = null;
34026
+ };
34027
+ const onIdle = async (slot) => {
34028
+ slot.idleTimer = void 0;
34029
+ if (current !== slot || slot.pending.size > 0) return;
34030
+ current = null;
34031
+ const heapBytes = (await slot.worker.getHeapStatistics?.().catch(() => null))?.total_heap_size ?? slot.lastHeapBytes;
34032
+ deps.logger?.info("demux worker idle — terminating", { meta: {
34033
+ ageMs: Math.max(0, now() - slot.spawnedAt),
34034
+ idleMs,
34035
+ lastHeapMb: Math.round(heapBytes / 1048576)
34036
+ } });
34037
+ slot.worker.terminate();
34002
34038
  };
34003
- const attach = (w) => {
34039
+ const armIdle = (slot) => {
34040
+ timers.clear(slot.idleTimer);
34041
+ slot.idleTimer = timers.set(() => void onIdle(slot), idleMs);
34042
+ };
34043
+ const attach = (slot) => {
34044
+ const w = slot.worker;
34004
34045
  w.on("message", (message) => {
34005
34046
  const parsed = parseWorkerMessage(message);
34006
34047
  if (!parsed) return;
34007
- const resolve = pending.get(parsed.id);
34008
- if (!resolve) return;
34009
- pending.delete(parsed.id);
34010
- resolve(parsed.aus);
34048
+ const pending = slot.pending.get(parsed.id);
34049
+ if (!pending) return;
34050
+ slot.pending.delete(parsed.id);
34051
+ pending.resolve(parsed.aus);
34052
+ if (slot.pending.size === 0 && current === slot) armIdle(slot);
34011
34053
  });
34012
- w.on("error", () => {
34013
- worker = null;
34014
- failAll();
34054
+ w.on("error", (err) => {
34055
+ retire(slot);
34056
+ failAll(slot, String(err));
34015
34057
  });
34016
34058
  w.on("exit", () => {
34017
- worker = null;
34018
- failAll();
34059
+ retire(slot);
34060
+ failAll(slot, "exit");
34019
34061
  });
34020
34062
  };
34021
34063
  const ensureWorker = () => {
34022
- if (worker) return worker;
34064
+ if (current) return current;
34023
34065
  if (!Ctor) return null;
34024
34066
  try {
34025
- const spawned = new Ctor(deps.workerFilename);
34026
- attach(spawned);
34027
- worker = spawned;
34028
- spawnedAt = Date.now();
34029
- return spawned;
34030
- } catch {
34067
+ const slot = {
34068
+ worker: new Ctor(deps.workerFilename, { resourceLimits: DEMUX_WORKER_RESOURCE_LIMITS }),
34069
+ pending: /* @__PURE__ */ new Map(),
34070
+ spawnedAt: now(),
34071
+ idleTimer: void 0,
34072
+ lastHeapBytes: 0
34073
+ };
34074
+ attach(slot);
34075
+ current = slot;
34076
+ deps.logger?.info("demux worker spawned", { meta: {
34077
+ resourceLimits: DEMUX_WORKER_RESOURCE_LIMITS,
34078
+ idleMs
34079
+ } });
34080
+ return slot;
34081
+ } catch (err) {
34082
+ deps.logger?.warn(`demux worker spawn failed: ${String(err)} — parsing in-process`);
34031
34083
  return null;
34032
34084
  }
34033
34085
  };
34034
- const parse = async (bytes) => {
34086
+ const parse = async (bytes, ctx) => {
34035
34087
  if (!deps.existsSync(deps.workerFilename)) return deps.parseInProcess(bytes);
34036
- const w = ensureWorker();
34037
- if (!w) return deps.parseInProcess(bytes);
34088
+ const slot = ensureWorker();
34089
+ if (!slot) return deps.parseInProcess(bytes);
34090
+ timers.clear(slot.idleTimer);
34091
+ slot.idleTimer = void 0;
34038
34092
  const id = nextId;
34039
34093
  nextId += 1;
34040
34094
  const copy = new Uint8Array(bytes);
@@ -34043,26 +34097,30 @@ function createOffThreadDemuxClient(deps) {
34043
34097
  bytes: copy
34044
34098
  };
34045
34099
  return new Promise((resolve) => {
34046
- pending.set(id, resolve);
34047
- w.postMessage(request, [copy.buffer]);
34100
+ slot.pending.set(id, {
34101
+ resolve,
34102
+ deviceId: ctx?.deviceId
34103
+ });
34104
+ slot.worker.postMessage(request, [copy.buffer]);
34048
34105
  });
34049
34106
  };
34050
- const readHeaps = async (now = Date.now()) => {
34051
- const live = worker;
34107
+ const readHeaps = async (at = Date.now()) => {
34108
+ const live = current;
34052
34109
  const empty = {
34053
34110
  workers: 0,
34054
34111
  heapBytes: 0,
34055
34112
  physicalBytes: 0,
34056
34113
  usedBytes: 0,
34057
34114
  maxAgeMs: 0,
34058
- idleTerminates: false
34115
+ idleTerminates: true
34059
34116
  };
34060
34117
  if (live === null) return empty;
34061
- const stats = await live.getHeapStatistics?.().catch(() => null);
34118
+ const stats = await live.worker.getHeapStatistics?.().catch(() => null);
34119
+ if (stats) live.lastHeapBytes = stats.total_heap_size;
34062
34120
  return {
34063
34121
  ...empty,
34064
34122
  workers: 1,
34065
- maxAgeMs: Math.max(0, now - spawnedAt),
34123
+ maxAgeMs: Math.max(0, at - live.spawnedAt),
34066
34124
  ...stats ? {
34067
34125
  heapBytes: stats.total_heap_size,
34068
34126
  physicalBytes: stats.total_physical_size,
@@ -34080,15 +34138,21 @@ function defaultOffThreadClient() {
34080
34138
  workerFilename: defaultWorkerFilename(),
34081
34139
  existsSync,
34082
34140
  parseInProcess: demuxSegmentFullJs,
34083
- WorkerCtor: Worker
34141
+ WorkerCtor: Worker,
34142
+ ...sharedLogger ? { logger: sharedLogger } : {}
34084
34143
  });
34085
34144
  }
34086
34145
  var shared;
34087
34146
  /** Process-wide JS parse: one Worker, shared by every WebRTC session. */
34088
- var offThreadJsParse = (bytes) => {
34147
+ var offThreadJsParse = (bytes, ctx) => {
34089
34148
  shared ??= defaultOffThreadClient();
34090
- return shared.parse(bytes);
34149
+ return shared.parse(bytes, ctx);
34091
34150
  };
34151
+ var sharedLogger;
34152
+ /** Give the process-wide client a logger; call before the first parse. */
34153
+ function setDemuxWorkerLogger(logger) {
34154
+ sharedLogger = logger;
34155
+ }
34092
34156
  /** Heap of the shared demux worker, for the `[broker-mem]` probe (tick only). */
34093
34157
  async function readDemuxWorkerHeaps(now) {
34094
34158
  return shared ? shared.readHeaps(now) : {
@@ -34097,7 +34161,7 @@ async function readDemuxWorkerHeaps(now) {
34097
34161
  physicalBytes: 0,
34098
34162
  usedBytes: 0,
34099
34163
  maxAgeMs: 0,
34100
- idleTerminates: false
34164
+ idleTerminates: true
34101
34165
  };
34102
34166
  }
34103
34167
  /**
@@ -35000,6 +35064,7 @@ var BrokerWebrtcServer = class {
35000
35064
  stopped = false;
35001
35065
  constructor(options) {
35002
35066
  this.logger = options.logger;
35067
+ setDemuxWorkerLogger(options.logger.child("demux-worker"));
35003
35068
  this.getIceServersFn = options.getIceServers;
35004
35069
  this.staticIceServers = options.iceServers;
35005
35070
  this.icePortRange = options.icePortRange;
@@ -35665,6 +35730,7 @@ var BrokerWebrtcServer = class {
35665
35730
  if (locateRecorded && readRecorded) {
35666
35731
  const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
35667
35732
  ffmpegPath: resolveFfmpeg(),
35733
+ deviceId,
35668
35734
  onDemuxPath: (outcome) => {
35669
35735
  const report = demuxTally.record(outcome, Date.now());
35670
35736
  if (!report) return;
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CRxJwkgE.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CSajlHKC.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.367",
3
+ "version": "1.2.368",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",