@camstack/addon-pipeline 0.1.14 → 0.1.15

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 (76) hide show
  1. package/dist/audio-analyzer/index.js +2 -4
  2. package/dist/audio-analyzer/index.js.map +1 -1
  3. package/dist/audio-analyzer/index.mjs +2 -4
  4. package/dist/audio-analyzer/index.mjs.map +1 -1
  5. package/dist/audio-codec-nodeav/index.js +1 -1
  6. package/dist/audio-codec-nodeav/index.mjs +1 -1
  7. package/dist/decoder-nodeav/index.js +552 -18
  8. package/dist/decoder-nodeav/index.js.map +1 -1
  9. package/dist/decoder-nodeav/index.mjs +553 -19
  10. package/dist/decoder-nodeav/index.mjs.map +1 -1
  11. package/dist/detection-pipeline/index.js +2 -4
  12. package/dist/detection-pipeline/index.js.map +1 -1
  13. package/dist/detection-pipeline/index.mjs +2 -4
  14. package/dist/detection-pipeline/index.mjs.map +1 -1
  15. package/dist/{index-DKh0uEve.mjs → index-CVzLrojg.mjs} +539 -97
  16. package/dist/index-CVzLrojg.mjs.map +1 -0
  17. package/dist/{index-CFPKrb2Y.js → index-p-6GfKOg.js} +539 -97
  18. package/dist/index-p-6GfKOg.js.map +1 -0
  19. package/dist/motion-wasm/index.js +2 -4
  20. package/dist/motion-wasm/index.js.map +1 -1
  21. package/dist/motion-wasm/index.mjs +2 -4
  22. package/dist/motion-wasm/index.mjs.map +1 -1
  23. package/dist/pipeline-runner/index.js +133 -54
  24. package/dist/pipeline-runner/index.js.map +1 -1
  25. package/dist/pipeline-runner/index.mjs +133 -54
  26. package/dist/pipeline-runner/index.mjs.map +1 -1
  27. package/dist/stream-broker/@mf-types.zip +0 -0
  28. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +19 -0
  29. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-B4l8Nb2y.mjs +20 -0
  30. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DePVYdid.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DAssX3h0.mjs} +4 -2
  31. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CBlCGyx5.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-DFoJJhpt.mjs} +1 -1
  32. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DZchZKbW.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-x7XMEeuJ.mjs} +1 -1
  33. package/dist/stream-broker/_stub.js +2 -2
  34. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CqeKw-Ig.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWHjxwIc.mjs} +6 -6
  35. package/dist/stream-broker/{client-BK73l2KT.mjs → client-CZXrddDR.mjs} +2990 -3217
  36. package/dist/stream-broker/{hostInit-DkjoXTMb.mjs → hostInit-B86vUcFC.mjs} +12 -12
  37. package/dist/stream-broker/{index-BP0-1QYT.mjs → index-BCEx31Mh.mjs} +3808 -3100
  38. package/dist/stream-broker/{index-lmXLeXy8.mjs → index-BvV3RVTZ.mjs} +1 -1
  39. package/dist/stream-broker/{index-IUYKHbxX.mjs → index-C0BzaWmB.mjs} +1 -1
  40. package/dist/stream-broker/index-CWkKuNLr.mjs +232 -0
  41. package/dist/stream-broker/{index-ns1fRD30.mjs → index-CZNxa0ad.mjs} +1 -1
  42. package/dist/stream-broker/index-Kb4xa8FX.mjs +36403 -0
  43. package/dist/stream-broker/{index-BxHaCH3N.mjs → index-KtR7Pp0O.mjs} +1 -1
  44. package/dist/stream-broker/{index-Ss9m7Jum.mjs → index-cYW01SNH.mjs} +1 -1
  45. package/dist/stream-broker/index.js +802 -541
  46. package/dist/stream-broker/index.js.map +1 -1
  47. package/dist/stream-broker/index.mjs +802 -519
  48. package/dist/stream-broker/index.mjs.map +1 -1
  49. package/dist/stream-broker/{jsx-runtime-ZdY5pIZz.mjs → jsx-runtime-B_evVsXl.mjs} +1 -1
  50. package/dist/stream-broker/remoteEntry.js +1 -1
  51. package/package.json +23 -31
  52. package/dist/index-CFPKrb2Y.js.map +0 -1
  53. package/dist/index-DKh0uEve.mjs.map +0 -1
  54. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CpCK52pE.mjs +0 -19
  55. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BN3K4dM8.mjs +0 -20
  56. package/dist/stream-broker/index-DKercbDS.mjs +0 -20855
  57. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  58. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  59. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  61. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  62. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  63. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  64. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  65. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  66. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  67. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  70. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  71. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  72. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  73. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  76. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
@@ -23,7 +23,229 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
25
25
  const crypto = require("node:crypto");
26
- const index = require("../index-CFPKrb2Y.js");
26
+ const index = require("../index-p-6GfKOg.js");
27
+ const shmRing = require("@camstack/shm-ring");
28
+ const RING_BUDGET_MB = (() => {
29
+ const raw = Number(process.env["CAMSTACK_SHM_RING_BUDGET_MB"]);
30
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 128;
31
+ })();
32
+ const RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
33
+ function makeSegmentName(seed, generation) {
34
+ let hash = 5381;
35
+ for (let i = 0; i < seed.length; i += 1) {
36
+ hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
37
+ }
38
+ const tag = (hash >>> 0).toString(36);
39
+ return `csf.${tag}.${generation}`;
40
+ }
41
+ class DecoderFrameRingSink {
42
+ seed;
43
+ logger;
44
+ nodeId;
45
+ segment = null;
46
+ writer = null;
47
+ segmentName = null;
48
+ slotByteLength = 0;
49
+ generation = 0;
50
+ destroyed = false;
51
+ /** Frames committed into the ring across this sink's lifetime (all generations). */
52
+ framesWritten = 0;
53
+ constructor(options) {
54
+ const salt = Math.random().toString(36).slice(2, 8);
55
+ this.seed = `${options.seed}.${salt}`;
56
+ this.logger = options.logger;
57
+ this.nodeId = options.nodeId;
58
+ }
59
+ /** Whether a segment has been created (i.e. at least one frame written). */
60
+ get isArmed() {
61
+ return this.writer !== null;
62
+ }
63
+ /** The current segment name, or `null` before the first frame. */
64
+ get currentSegmentName() {
65
+ return this.segmentName;
66
+ }
67
+ /**
68
+ * Write one decoded frame into the ring and return its `FrameHandle`.
69
+ *
70
+ * On the first call (or after a geometry change that overflows the current
71
+ * slot) the segment is created / re-created sized for this frame. Returns
72
+ * `null` only when the sink has been destroyed.
73
+ *
74
+ * This is the copy-in convenience form (it copies `pixels` into the slot).
75
+ * The decoder's hot path uses the zero-copy {@link beginFrame} /
76
+ * {@link commitFrame} scatter-write pair instead — the scaler produces its
77
+ * packed output directly into the slot, eliminating the write-side memcpy.
78
+ */
79
+ writeFrame(pixels, meta) {
80
+ if (this.destroyed) return null;
81
+ if (this.writer === null || shmRing.computeSlotByteLength(meta.width, meta.height, meta.format) > this.slotByteLength) {
82
+ this.recreateSegment(
83
+ shmRing.computeSlotByteLength(meta.width, meta.height, meta.format)
84
+ );
85
+ }
86
+ const writer = this.writer;
87
+ if (writer === null) {
88
+ return null;
89
+ }
90
+ const handle = writer.writeFrame(pixels, meta);
91
+ this.framesWritten += 1;
92
+ return handle;
93
+ }
94
+ /**
95
+ * Reserve a ring slot for a frame of the given geometry — the **zero-copy**
96
+ * scatter-write entry point (Phase 5 / D9 Task 7c).
97
+ *
98
+ * The segment is created / re-created here if this is the first frame or the
99
+ * geometry overflows the current slot capacity, so the slot is correctly
100
+ * sized before the caller fills it. The returned `buffer` is a writable view
101
+ * **directly over the mapped segment** — the node-av scaler scatters its
102
+ * packed output straight into it, with no intermediate copy. The caller MUST
103
+ * call {@link commitFrame} with the returned `slot` once the slot is filled.
104
+ *
105
+ * Returns `null` when the sink is destroyed or the segment cannot be created.
106
+ */
107
+ beginFrame(width, height, format) {
108
+ if (this.destroyed) return null;
109
+ const requiredSlotBytes = shmRing.computeSlotByteLength(width, height, format);
110
+ if (this.writer === null || requiredSlotBytes > this.slotByteLength) {
111
+ this.recreateSegment(requiredSlotBytes);
112
+ }
113
+ const writer = this.writer;
114
+ if (writer === null) {
115
+ return null;
116
+ }
117
+ const { slot, buffer } = writer.beginFrame();
118
+ return { slot, buffer };
119
+ }
120
+ /**
121
+ * Publish the frame whose slot was reserved by {@link beginFrame} and filled
122
+ * in place by the caller. `slot` MUST be the value from the matching
123
+ * `beginFrame`. Returns the published `FrameHandle`, or `null` if the sink
124
+ * was destroyed (or the segment lost) between begin and commit.
125
+ */
126
+ commitFrame(slot, meta) {
127
+ if (this.destroyed) return null;
128
+ const writer = this.writer;
129
+ if (writer === null) return null;
130
+ const handle = writer.commitFrame(slot, meta);
131
+ this.framesWritten += 1;
132
+ return handle;
133
+ }
134
+ /**
135
+ * Current shm ring usage — `null` until the first frame arms the segment.
136
+ * Surfaced through `decoder.getShmStats` so a downstream consumer can
137
+ * observe ring pressure (slot depth, byte budget, frames written).
138
+ */
139
+ getShmStats() {
140
+ if (this.writer === null) return null;
141
+ return {
142
+ slotCount: this.writer.slotCount,
143
+ slotByteLength: this.slotByteLength,
144
+ segmentBytes: shmRing.computeSegmentSize(this.writer.slotCount, this.slotByteLength),
145
+ framesWritten: this.framesWritten
146
+ };
147
+ }
148
+ /**
149
+ * Abandon a slot reserved by {@link beginFrame} **without publishing it** —
150
+ * the degenerate-path counterpart of {@link commitFrame}.
151
+ *
152
+ * A caller that reserved a slot but then could not produce valid pixels (no
153
+ * decoded source planes, or the scaler threw) MUST call this instead of
154
+ * `commitFrame`: it closes the open seqlock without advancing `writeIndex`,
155
+ * so no reader ever sees the slot's uninitialised bytes as a real frame, and
156
+ * no `FrameHandle` is handed downstream. `slot` MUST be the value from the
157
+ * matching `beginFrame`. A no-op if the sink was destroyed (or the segment
158
+ * lost) between begin and abort.
159
+ */
160
+ abortFrame(slot) {
161
+ if (this.destroyed) return;
162
+ const writer = this.writer;
163
+ if (writer === null) return;
164
+ writer.abortFrame(slot);
165
+ }
166
+ /** Close + unlink the segment. Idempotent. */
167
+ destroy() {
168
+ if (this.destroyed) return;
169
+ this.destroyed = true;
170
+ this.releaseSegment();
171
+ }
172
+ /**
173
+ * Create a fresh segment sized for at least `slotByteLength` bytes per slot,
174
+ * replacing any prior one. A re-create bumps the generation so the new
175
+ * segment has a distinct name — a consumer holding the old mapping is never
176
+ * silently handed a resized segment.
177
+ */
178
+ recreateSegment(slotByteLength) {
179
+ this.releaseSegment();
180
+ this.generation += 1;
181
+ const name = makeSegmentName(this.seed, this.generation);
182
+ const slotCount = shmRing.deriveSlotCount(RING_BUDGET_BYTES, slotByteLength);
183
+ if (slotCount === shmRing.MIN_RING_SLOTS && shmRing.MIN_RING_SLOTS * slotByteLength > RING_BUDGET_BYTES) {
184
+ this.logger.warn(
185
+ "decoder shm ring: budget too small for resolution — using MIN slots",
186
+ { meta: { slotByteLength, budgetMb: RING_BUDGET_MB } }
187
+ );
188
+ }
189
+ const totalBytes = shmRing.computeSegmentSize(slotCount, slotByteLength);
190
+ try {
191
+ const segment = shmRing.createSegment(name, totalBytes);
192
+ this.segment = segment;
193
+ this.segmentName = name;
194
+ this.slotByteLength = slotByteLength;
195
+ this.writer = new shmRing.FrameRingWriter(
196
+ segment.buffer,
197
+ name,
198
+ slotCount,
199
+ slotByteLength,
200
+ this.nodeId
201
+ );
202
+ this.logger.info("decoder shm ring: segment created", {
203
+ meta: {
204
+ segment: name,
205
+ slotCount,
206
+ slotByteLength,
207
+ totalBytes,
208
+ generation: this.generation
209
+ }
210
+ });
211
+ } catch (err) {
212
+ this.segment = null;
213
+ this.writer = null;
214
+ this.segmentName = null;
215
+ this.slotByteLength = 0;
216
+ this.logger.error("decoder shm ring: segment create failed", {
217
+ meta: {
218
+ segment: name,
219
+ slotByteLength,
220
+ error: err instanceof Error ? err.message : String(err)
221
+ }
222
+ });
223
+ }
224
+ }
225
+ /** Unmap + unlink the current segment, if any. */
226
+ releaseSegment() {
227
+ const segment = this.segment;
228
+ if (segment === null) return;
229
+ this.segment = null;
230
+ this.writer = null;
231
+ const name = this.segmentName;
232
+ this.segmentName = null;
233
+ try {
234
+ segment.close();
235
+ segment.unlink();
236
+ this.logger.info("decoder shm ring: segment released", {
237
+ meta: { segment: name }
238
+ });
239
+ } catch (err) {
240
+ this.logger.warn("decoder shm ring: segment release failed", {
241
+ meta: {
242
+ segment: name,
243
+ error: err instanceof Error ? err.message : String(err)
244
+ }
245
+ });
246
+ }
247
+ }
248
+ }
27
249
  function backendToHwDeviceConst(backend, consts) {
28
250
  switch (backend) {
29
251
  case "videotoolbox":
@@ -115,7 +337,19 @@ class NodeAvDecoderSession {
115
337
  config;
116
338
  logger;
117
339
  frameCallbacks = /* @__PURE__ */ new Set();
340
+ handleCallbacks = /* @__PURE__ */ new Set();
118
341
  destroyed = false;
342
+ /**
343
+ * Frame delivery mode (see {@link DecoderFrameSink}). `'shm'` lazily
344
+ * constructs `frameRingSink` and routes every decoded frame through it.
345
+ */
346
+ frameSink;
347
+ /**
348
+ * The shared-memory ring writer for this stream. Created lazily on the
349
+ * first decoded frame in `'shm'` mode (the segment cannot be sized until
350
+ * the output geometry is known) and torn down in `destroy`.
351
+ */
352
+ frameRingSink = null;
119
353
  // Low-level node-av objects (initialized on first keyframe)
120
354
  parser = null;
121
355
  codecCtx = null;
@@ -207,6 +441,8 @@ class NodeAvDecoderSession {
207
441
  startTime = Date.now();
208
442
  hwaccelPref;
209
443
  hwaccelResolver;
444
+ /** Cluster node id stamped into every `FrameHandle` the `'shm'` sink emits. */
445
+ nodeId;
210
446
  /** The backend that actually initialised successfully — `'none'` = software fallback. */
211
447
  activeHwAccel = "none";
212
448
  hwDevice = null;
@@ -221,6 +457,39 @@ class NodeAvDecoderSession {
221
457
  this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
222
458
  this.hwaccelPref = options?.hwaccel ?? "auto";
223
459
  this.hwaccelResolver = options?.hwaccelResolver ?? null;
460
+ this.frameSink = options?.frameSink ?? "callback";
461
+ this.nodeId = options?.nodeId ?? "local";
462
+ }
463
+ /**
464
+ * The shared-memory ring sink for this stream, or `null` before the first
465
+ * `'shm'`-mode frame lazily creates it. Exposed so the owning addon can
466
+ * surface ring stats via `decoder.getShmStats`.
467
+ */
468
+ get frameRingSinkOrNull() {
469
+ return this.frameRingSink;
470
+ }
471
+ /**
472
+ * Lazily build the shared-memory ring sink for this stream. The segment
473
+ * itself is still created lazily by the sink on its first `writeFrame`,
474
+ * once the decoded geometry is known.
475
+ */
476
+ ensureFrameRingSink() {
477
+ if (this.frameRingSink === null) {
478
+ const seedParts = [];
479
+ if (typeof this.config.deviceId === "number") {
480
+ seedParts.push(String(this.config.deviceId));
481
+ }
482
+ if (typeof this.config.tag === "string" && this.config.tag.length > 0) {
483
+ seedParts.push(this.config.tag);
484
+ }
485
+ const seed = seedParts.length > 0 ? seedParts.join(":") : "anon";
486
+ this.frameRingSink = new DecoderFrameRingSink({
487
+ seed,
488
+ logger: this.logger,
489
+ nodeId: this.nodeId
490
+ });
491
+ }
492
+ return this.frameRingSink;
224
493
  }
225
494
  /**
226
495
  * Resolve the backend preference list and try each one against
@@ -521,6 +790,10 @@ class NodeAvDecoderSession {
521
790
  }
522
791
  if (!this.dstFrame || !this.scaler) return;
523
792
  const decodeStart = performance.now();
793
+ if (this.frameSink === "shm" && this.outputMode !== "jpeg") {
794
+ this.scaleIntoRingSlot(frame, decodeStart);
795
+ return;
796
+ }
524
797
  try {
525
798
  this.dstFrame.makeWritable();
526
799
  this.scaler.scaleFrameSync(this.dstFrame, frame);
@@ -566,6 +839,104 @@ class NodeAvDecoderSession {
566
839
  this.emitRawFrame(rawBuf, "gray", decodeStart);
567
840
  }
568
841
  }
842
+ /**
843
+ * Scale a decoded frame **directly into a shared-memory ring slot** — the
844
+ * zero write-side copy path (Phase 5 / D9 Task 7c).
845
+ *
846
+ * `beginFrame` reserves the slot (its seqlock open / odd — readers skip it);
847
+ * `SoftwareScaleContext.scaleSync` (the low-level `sws_scale` mapping) writes
848
+ * the packed pixels straight into the slot buffer with `linesize = width ×
849
+ * bpp` — no linesize padding, so there is nothing to strip and nothing to
850
+ * memcpy; `commitFrame` writes the metadata, closes the seqlock and publishes
851
+ * the slot. The decoded source planes feed `scaleSync` as `srcSlice` +
852
+ * `srcStride` (`frame.data` / `frame.linesize`).
853
+ *
854
+ * Used only for raw output formats (rgb/bgr/gray). The jpeg path keeps the
855
+ * `dstFrame` + sharp encode route because sharp must consume an RGB buffer
856
+ * before the final (variable-length) jpeg bytes exist.
857
+ */
858
+ scaleIntoRingSlot(frame, decodeStart) {
859
+ if (!this.scaler) return;
860
+ const format = this.outputMode === "gray" ? "gray" : "rgb";
861
+ const channels = this.outputMode === "gray" ? 1 : 3;
862
+ const sink = this.ensureFrameRingSink();
863
+ const reserved = sink.beginFrame(this.outWidth, this.outHeight, format);
864
+ if (reserved === null) {
865
+ this.droppedFrames++;
866
+ return;
867
+ }
868
+ const srcPlanes = frame.data;
869
+ if (!srcPlanes || srcPlanes.length === 0) {
870
+ sink.abortFrame(reserved.slot);
871
+ this.droppedFrames++;
872
+ return;
873
+ }
874
+ const dstStride = this.outWidth * channels;
875
+ try {
876
+ const srcStrides = Array.from(frame.linesize).slice(0, srcPlanes.length);
877
+ this.scaler.scaleSync(
878
+ Array.from(srcPlanes),
879
+ srcStrides,
880
+ 0,
881
+ frame.height,
882
+ [reserved.buffer],
883
+ [dstStride]
884
+ );
885
+ } catch (err) {
886
+ this.logger.warn("node-av scale-into-slot error", {
887
+ meta: { error: index.errMsg(err) }
888
+ });
889
+ sink.abortFrame(reserved.slot);
890
+ this.droppedFrames++;
891
+ return;
892
+ }
893
+ const decodeMs = performance.now() - decodeStart;
894
+ this.totalDecodeTimeMs += decodeMs;
895
+ this.outputFrames++;
896
+ this.lastEmitTime = performance.now();
897
+ if (!this.firstFrameLogged) {
898
+ this.firstFrameLogged = true;
899
+ this.logger.info("node-av: scaled directly into shm ring slot", {
900
+ meta: {
901
+ phase: "frame-debug",
902
+ width: this.outWidth,
903
+ height: this.outHeight,
904
+ dstStride,
905
+ format
906
+ }
907
+ });
908
+ }
909
+ if (this.outputFrames === 1 || this.outputFrames % 500 === 0) {
910
+ this.logger.info("node-av frame emitted", {
911
+ meta: {
912
+ frameNumber: this.outputFrames,
913
+ width: this.outWidth,
914
+ height: this.outHeight,
915
+ format,
916
+ decodeMs,
917
+ sink: "shm",
918
+ subs: this.handleCallbacks.size
919
+ }
920
+ });
921
+ }
922
+ const handle = sink.commitFrame(reserved.slot, {
923
+ width: this.outWidth,
924
+ height: this.outHeight,
925
+ format,
926
+ // pts uses performance.now() (monotonic, process-relative) for
927
+ // ring-ordering; DecodedFrameHandle.timestamp carries the wall clock.
928
+ pts: performance.now(),
929
+ byteLength: dstStride * this.outHeight
930
+ });
931
+ if (handle === null) {
932
+ this.droppedFrames++;
933
+ return;
934
+ }
935
+ const delivered = { handle, timestamp: Date.now() };
936
+ for (const cb of this.handleCallbacks) {
937
+ cb(delivered);
938
+ }
939
+ }
569
940
  /**
570
941
  * Extract packed pixel buffer from a decoded frame.
571
942
  * FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
@@ -628,10 +999,15 @@ class NodeAvDecoderSession {
628
999
  height: this.outHeight,
629
1000
  format,
630
1001
  decodeMs,
631
- subs: this.frameCallbacks.size
1002
+ sink: this.frameSink,
1003
+ subs: this.frameSink === "shm" ? this.handleCallbacks.size : this.frameCallbacks.size
632
1004
  }
633
1005
  });
634
1006
  }
1007
+ if (this.frameSink === "shm") {
1008
+ this.emitShmFrame(data, format);
1009
+ return;
1010
+ }
635
1011
  const decodedFrame = {
636
1012
  data,
637
1013
  width: this.outWidth,
@@ -643,12 +1019,53 @@ class NodeAvDecoderSession {
643
1019
  cb(decodedFrame);
644
1020
  }
645
1021
  }
1022
+ /**
1023
+ * Write a decoded frame into this stream's shared-memory ring and deliver
1024
+ * the resulting `FrameHandle` to `onFrameHandle` subscribers. No pixel
1025
+ * `Buffer` is handed to callbacks — consumers open the segment and read the
1026
+ * pixels zero-copy via a `FrameRingReader`.
1027
+ */
1028
+ emitShmFrame(data, format) {
1029
+ const sink = this.ensureFrameRingSink();
1030
+ const handle = sink.writeFrame(data, {
1031
+ width: this.outWidth,
1032
+ height: this.outHeight,
1033
+ format,
1034
+ // The decoder does not carry a source PTS down to this point; use a
1035
+ // monotone-ish wall clock so a consumer can still order frames.
1036
+ // NOTE: pts uses performance.now() (monotonic, process-relative) while
1037
+ // DecodedFrameHandle.timestamp uses Date.now() (wall-clock epoch).
1038
+ // The two clocks are intentionally distinct — pts is for ring-ordering,
1039
+ // timestamp is for wall-clock correlation — and must not be compared.
1040
+ pts: performance.now(),
1041
+ byteLength: data.byteLength
1042
+ });
1043
+ if (handle === null) {
1044
+ this.droppedFrames++;
1045
+ return;
1046
+ }
1047
+ const frame = { handle, timestamp: Date.now() };
1048
+ for (const cb of this.handleCallbacks) {
1049
+ cb(frame);
1050
+ }
1051
+ }
646
1052
  onFrame(callback) {
647
1053
  this.frameCallbacks.add(callback);
648
1054
  return () => {
649
1055
  this.frameCallbacks.delete(callback);
650
1056
  };
651
1057
  }
1058
+ /**
1059
+ * Subscribe to shared-memory frame handles. Fires only when the session was
1060
+ * created with `frameSink: 'shm'`; in the legacy `'callback'` mode no
1061
+ * handles are produced and this subscription never fires.
1062
+ */
1063
+ onFrameHandle(callback) {
1064
+ this.handleCallbacks.add(callback);
1065
+ return () => {
1066
+ this.handleCallbacks.delete(callback);
1067
+ };
1068
+ }
652
1069
  updateConfig(update) {
653
1070
  const prevFormat = this.config.outputFormat;
654
1071
  this.config = { ...this.config, ...update };
@@ -683,6 +1100,9 @@ class NodeAvDecoderSession {
683
1100
  if (this.destroyed) return;
684
1101
  this.destroyed = true;
685
1102
  this.frameCallbacks.clear();
1103
+ this.handleCallbacks.clear();
1104
+ this.frameRingSink?.destroy();
1105
+ this.frameRingSink = null;
686
1106
  this.dstFrame?.[Symbol.dispose]?.();
687
1107
  this.avFrame?.[Symbol.dispose]?.();
688
1108
  this.avPacket?.[Symbol.dispose]?.();
@@ -715,10 +1135,28 @@ class NodeAvDecoderSession {
715
1135
  }
716
1136
  const FRAME_BUFFER_CAPACITY = 32;
717
1137
  class DecoderNodeAvAddon extends index.BaseAddon {
1138
+ /**
1139
+ * Sessions are stored as the concrete `NodeAvDecoderSession` (the only type
1140
+ * this addon ever creates) so `getShmStats` can reach the per-session shm
1141
+ * ring sink — `IDecoderSession` does not expose it.
1142
+ */
718
1143
  sessions = /* @__PURE__ */ new Map();
1144
+ /** Pixel-frame buffers — populated only for `frameSink: 'callback'` sessions. */
719
1145
  frameBuffers = /* @__PURE__ */ new Map();
1146
+ /** `FrameHandle` buffers — populated only for `frameSink: 'shm'` sessions. */
1147
+ handleBuffers = /* @__PURE__ */ new Map();
720
1148
  unsubscribers = /* @__PURE__ */ new Map();
721
1149
  sessionMeta = /* @__PURE__ */ new Map();
1150
+ /**
1151
+ * Per-shm-segment `FrameRingReader` cache backing `getFrame`. Opens each
1152
+ * named segment once and reuses the reader for every later handle on it;
1153
+ * built in `onInitialize` (it needs the scoped logger) and closed in
1154
+ * `onShutdown`.
1155
+ */
1156
+ frameReaders = null;
1157
+ /** Running `getFrame` hit/miss counters surfaced via `getShmStats`. */
1158
+ getFrameHits = 0;
1159
+ getFrameMisses = 0;
722
1160
  constructor() {
723
1161
  super(index.DEFAULT_DECODER_HWACCEL_CONFIG);
724
1162
  }
@@ -755,6 +1193,7 @@ class DecoderNodeAvAddon extends index.BaseAddon {
755
1193
  }
756
1194
  async onInitialize() {
757
1195
  this.ctx.logger.info("node-av decoder addon initialized");
1196
+ this.frameReaders = new shmRing.FrameRingReaderCache(this.ctx.logger);
758
1197
  if (!this.config.probedBestHwaccel) {
759
1198
  this.reprobeHwaccel().catch((err) => {
760
1199
  this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", {
@@ -813,15 +1252,48 @@ class DecoderNodeAvAddon extends index.BaseAddon {
813
1252
  priority: 10
814
1253
  };
815
1254
  }
1255
+ /**
1256
+ * The cluster node id of this decoder — the Moleculer nodeID (`hub`,
1257
+ * `dev-agent-0`, …), not the addon id. Stamped into session-owned
1258
+ * `FrameHandle`s so a downstream consumer routes `getFrame` to the node
1259
+ * that holds the shm ring. A hierarchical broker nodeID (`hub/...`) is
1260
+ * collapsed to the cluster-visible parent; in-process boot is left as-is.
1261
+ * Mirrors the resolution `PipelineRunnerAddon.onInitialize` performs.
1262
+ */
1263
+ resolveLocalNodeId() {
1264
+ const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
1265
+ return raw.includes("/") ? raw.split("/")[0] : raw;
1266
+ }
816
1267
  async createSession(config) {
817
1268
  const sessionId = crypto.randomUUID();
818
1269
  const hwaccel = this.resolveHwAccelPref();
1270
+ const { frameSink } = config;
1271
+ const nodeId = this.resolveLocalNodeId();
819
1272
  const session = new NodeAvDecoderSession(config, this.ctx.logger, {
820
1273
  hwaccel,
821
- hwaccelResolver: this.ctx.kernel.hwaccel
1274
+ hwaccelResolver: this.ctx.kernel.hwaccel,
1275
+ frameSink,
1276
+ nodeId
822
1277
  });
1278
+ const unsub = frameSink === "shm" ? this.wireShmSink(sessionId, session) : this.wireCallbackSink(sessionId, session);
1279
+ this.sessions.set(sessionId, session);
1280
+ this.unsubscribers.set(sessionId, unsub);
1281
+ this.sessionMeta.set(sessionId, {
1282
+ codec: config.codec,
1283
+ outputFormat: config.outputFormat,
1284
+ createdAtMs: Date.now()
1285
+ });
1286
+ this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel, frameSink, nodeId } });
1287
+ return { sessionId, nodeId };
1288
+ }
1289
+ /**
1290
+ * Subscribe a `'callback'` session's pixel frames into a per-session
1291
+ * ring buffer drained by `pullFrames`.
1292
+ */
1293
+ wireCallbackSink(sessionId, session) {
823
1294
  const ringBuffer = new index.RingBuffer(FRAME_BUFFER_CAPACITY);
824
- const unsub = session.onFrame((frame) => {
1295
+ this.frameBuffers.set(sessionId, ringBuffer);
1296
+ return session.onFrame((frame) => {
825
1297
  const { format } = frame;
826
1298
  if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
827
1299
  const arrayBuf = new ArrayBuffer(frame.data.byteLength);
@@ -836,16 +1308,18 @@ class DecoderNodeAvAddon extends index.BaseAddon {
836
1308
  };
837
1309
  ringBuffer.push(capFrame);
838
1310
  });
839
- this.sessions.set(sessionId, session);
840
- this.frameBuffers.set(sessionId, ringBuffer);
841
- this.unsubscribers.set(sessionId, unsub);
842
- this.sessionMeta.set(sessionId, {
843
- codec: config.codec,
844
- outputFormat: config.outputFormat,
845
- createdAtMs: Date.now()
1311
+ }
1312
+ /**
1313
+ * Subscribe a `'shm'` session's `FrameHandle`s into a per-session ring
1314
+ * buffer drained by `pullHandles`. No pixel bytes cross the cap boundary
1315
+ * — the broker opens the named segment and reads the pixels zero-copy.
1316
+ */
1317
+ wireShmSink(sessionId, session) {
1318
+ const handleRing = new index.RingBuffer(FRAME_BUFFER_CAPACITY);
1319
+ this.handleBuffers.set(sessionId, handleRing);
1320
+ return session.onFrameHandle((frame) => {
1321
+ handleRing.push(frame.handle);
846
1322
  });
847
- this.ctx.logger.info("node-av: created session", { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } });
848
- return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? "local" };
849
1323
  }
850
1324
  async destroySession(input) {
851
1325
  const { sessionId } = input;
@@ -858,6 +1332,7 @@ class DecoderNodeAvAddon extends index.BaseAddon {
858
1332
  await session.destroy();
859
1333
  this.sessions.delete(sessionId);
860
1334
  this.frameBuffers.delete(sessionId);
1335
+ this.handleBuffers.delete(sessionId);
861
1336
  this.unsubscribers.delete(sessionId);
862
1337
  this.sessionMeta.delete(sessionId);
863
1338
  this.ctx.logger.info("node-av: destroyed session", { meta: { sessionId } });
@@ -883,17 +1358,24 @@ class DecoderNodeAvAddon extends index.BaseAddon {
883
1358
  if (!session) {
884
1359
  throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
885
1360
  }
886
- if (session.openStream) {
887
- await session.openStream(input.url);
888
- }
1361
+ void input.url;
889
1362
  }
890
1363
  async pullFrames(input) {
891
- const ringBuffer = this.frameBuffers.get(input.sessionId);
892
- if (!ringBuffer) {
1364
+ if (!this.sessions.has(input.sessionId)) {
893
1365
  throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
894
1366
  }
1367
+ const ringBuffer = this.frameBuffers.get(input.sessionId);
1368
+ if (!ringBuffer) return [];
895
1369
  return ringBuffer.drain(input.maxCount);
896
1370
  }
1371
+ async pullHandles(input) {
1372
+ if (!this.sessions.has(input.sessionId)) {
1373
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1374
+ }
1375
+ const handleRing = this.handleBuffers.get(input.sessionId);
1376
+ if (!handleRing) return [];
1377
+ return handleRing.drain(input.maxCount);
1378
+ }
897
1379
  async updateConfig(input) {
898
1380
  const session = this.sessions.get(input.sessionId);
899
1381
  if (!session) {
@@ -908,6 +1390,53 @@ class DecoderNodeAvAddon extends index.BaseAddon {
908
1390
  }
909
1391
  return session.getStats();
910
1392
  }
1393
+ /**
1394
+ * Read back the pixels a `FrameHandle` refers to from this node's shm ring
1395
+ * (Phase 5 / D9 downstream access). Returns `null` when the slot was
1396
+ * already recycled (latest-wins drop) or the segment could not be opened.
1397
+ * The reader cache opens each named segment once and reuses the reader.
1398
+ *
1399
+ * The reader hands back a `Buffer` view over its **reusable scratch
1400
+ * buffer**, only valid until the next `read` on the same reader; the
1401
+ * cap caller may keep the frame across reads, so the pixels are copied
1402
+ * into a fresh detached `ArrayBuffer` here. This matches the existing
1403
+ * callback-path copy in `wireCallbackSink`.
1404
+ */
1405
+ async getFrame(input) {
1406
+ const frame = this.frameReaders?.read(input.handle) ?? null;
1407
+ if (!frame) {
1408
+ this.getFrameMisses += 1;
1409
+ return null;
1410
+ }
1411
+ this.getFrameHits += 1;
1412
+ const arrayBuf = new ArrayBuffer(frame.data.byteLength);
1413
+ new Uint8Array(arrayBuf).set(frame.data);
1414
+ return {
1415
+ data: new Uint8Array(arrayBuf),
1416
+ width: frame.width,
1417
+ height: frame.height,
1418
+ format: frame.format,
1419
+ timestamp: frame.timestamp
1420
+ };
1421
+ }
1422
+ /**
1423
+ * shm ring usage stats for a `frameSink: 'shm'` session — slot geometry,
1424
+ * frames written, byte budget, plus this addon's running `getFrame`
1425
+ * hit/miss counters. Returns `null` for an unknown session or one whose
1426
+ * shm ring has not yet been armed by a first decoded frame.
1427
+ */
1428
+ async getShmStats(input) {
1429
+ const session = this.sessions.get(input.sessionId);
1430
+ const stats = session?.frameRingSinkOrNull?.getShmStats() ?? null;
1431
+ if (!stats) return null;
1432
+ return {
1433
+ sessionId: input.sessionId,
1434
+ ...stats,
1435
+ budgetMb: RING_BUDGET_MB,
1436
+ getFrameHits: this.getFrameHits,
1437
+ getFrameMisses: this.getFrameMisses
1438
+ };
1439
+ }
911
1440
  async onShutdown() {
912
1441
  this.ctx.logger.info("node-av decoder addon shutdown — destroying all sessions");
913
1442
  const destroyPromises = [];
@@ -919,11 +1448,16 @@ class DecoderNodeAvAddon extends index.BaseAddon {
919
1448
  await Promise.all(destroyPromises);
920
1449
  this.sessions.clear();
921
1450
  this.frameBuffers.clear();
1451
+ this.handleBuffers.clear();
922
1452
  this.unsubscribers.clear();
923
1453
  this.sessionMeta.clear();
1454
+ this.frameReaders?.close();
1455
+ this.frameReaders = null;
924
1456
  }
925
1457
  }
1458
+ exports.DecoderFrameRingSink = DecoderFrameRingSink;
926
1459
  exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
927
1460
  exports.NodeAvDecoderSession = NodeAvDecoderSession;
928
1461
  exports.default = DecoderNodeAvAddon;
1462
+ exports.makeSegmentName = makeSegmentName;
929
1463
  //# sourceMappingURL=index.js.map