@camstack/addon-pipeline 0.1.8

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 (107) hide show
  1. package/dist/audio-analyzer/index.js +723 -0
  2. package/dist/audio-analyzer/index.js.map +1 -0
  3. package/dist/audio-analyzer/index.mjs +683 -0
  4. package/dist/audio-analyzer/index.mjs.map +1 -0
  5. package/dist/audio-codec-nodeav/index.js +467 -0
  6. package/dist/audio-codec-nodeav/index.js.map +1 -0
  7. package/dist/audio-codec-nodeav/index.mjs +467 -0
  8. package/dist/audio-codec-nodeav/index.mjs.map +1 -0
  9. package/dist/decoder-nodeav/index.js +929 -0
  10. package/dist/decoder-nodeav/index.js.map +1 -0
  11. package/dist/decoder-nodeav/index.mjs +907 -0
  12. package/dist/decoder-nodeav/index.mjs.map +1 -0
  13. package/dist/detection-pipeline/index.js +5766 -0
  14. package/dist/detection-pipeline/index.js.map +1 -0
  15. package/dist/detection-pipeline/index.mjs +5725 -0
  16. package/dist/detection-pipeline/index.mjs.map +1 -0
  17. package/dist/index-D_cl0Qqb.js +5791 -0
  18. package/dist/index-D_cl0Qqb.js.map +1 -0
  19. package/dist/index-UbcdLS7a.mjs +5790 -0
  20. package/dist/index-UbcdLS7a.mjs.map +1 -0
  21. package/dist/motion-wasm/index.js +476 -0
  22. package/dist/motion-wasm/index.js.map +1 -0
  23. package/dist/motion-wasm/index.mjs +454 -0
  24. package/dist/motion-wasm/index.mjs.map +1 -0
  25. package/dist/pipeline-runner/index.js +1669 -0
  26. package/dist/pipeline-runner/index.js.map +1 -0
  27. package/dist/pipeline-runner/index.mjs +1647 -0
  28. package/dist/pipeline-runner/index.mjs.map +1 -0
  29. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/StreamBrokerPanel.d.ts +21 -0
  30. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/index.d.ts +13 -0
  31. package/dist/stream-broker/@mf-types/widgets.d.ts +2 -0
  32. package/dist/stream-broker/@mf-types.d.ts +3 -0
  33. package/dist/stream-broker/@mf-types.zip +0 -0
  34. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +12 -0
  35. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-C-URP6DW.mjs +17 -0
  36. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-69eEmXwl.mjs +20 -0
  37. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-U1EUeEPs.mjs +104 -0
  38. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-DeouEaSs.mjs +85 -0
  39. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-DHUwjbb9.mjs +62 -0
  40. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DePVYdid.mjs +85 -0
  41. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CBlCGyx5.mjs +29 -0
  42. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-gBEZsQrp.mjs +36 -0
  43. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-DYEKzzY-.mjs +45 -0
  44. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DZchZKbW.mjs +6 -0
  45. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DICOtMTl.mjs +34 -0
  46. package/dist/stream-broker/_stub.js +752 -0
  47. package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D6o1e2ed.mjs +156 -0
  48. package/dist/stream-broker/client-BK73l2KT.mjs +10063 -0
  49. package/dist/stream-broker/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +211 -0
  50. package/dist/stream-broker/hostInit-RCeroTVY.mjs +168 -0
  51. package/dist/stream-broker/index-BYclbfM0.mjs +15806 -0
  52. package/dist/stream-broker/index-BhXZh4lQ.mjs +1617 -0
  53. package/dist/stream-broker/index-BxHaCH3N.mjs +725 -0
  54. package/dist/stream-broker/index-D2-K2YJ7.mjs +19268 -0
  55. package/dist/stream-broker/index-IUYKHbxX.mjs +185 -0
  56. package/dist/stream-broker/index-Ss9m7Jum.mjs +2603 -0
  57. package/dist/stream-broker/index-ns1fRD30.mjs +435 -0
  58. package/dist/stream-broker/index-xncRG7-x.mjs +2713 -0
  59. package/dist/stream-broker/index.js +11171 -0
  60. package/dist/stream-broker/index.js.map +1 -0
  61. package/dist/stream-broker/index.mjs +11130 -0
  62. package/dist/stream-broker/index.mjs.map +1 -0
  63. package/dist/stream-broker/jsx-runtime-ZdY5pIZz.mjs +55 -0
  64. package/dist/stream-broker/remoteEntry.js +2973 -0
  65. package/dist/stream-broker/virtualExposes-pCd777Rp.mjs +42 -0
  66. package/package.json +258 -0
  67. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  68. package/python/inference_pool.py +1088 -0
  69. package/python/postprocessors/__init__.py +24 -0
  70. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  71. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  72. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  73. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  76. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  77. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  78. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  79. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  80. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  81. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  82. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  83. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  84. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  85. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  86. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  87. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  88. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  89. package/python/postprocessors/arcface.py +31 -0
  90. package/python/postprocessors/ctc.py +68 -0
  91. package/python/postprocessors/saliency.py +44 -0
  92. package/python/postprocessors/scrfd.py +212 -0
  93. package/python/postprocessors/softmax.py +43 -0
  94. package/python/postprocessors/yamnet.py +41 -0
  95. package/python/postprocessors/yolo.py +278 -0
  96. package/python/postprocessors/yolo_seg.py +247 -0
  97. package/python/requirements-coreml.txt +4 -0
  98. package/python/requirements-onnxruntime.txt +3 -0
  99. package/python/requirements-openvino.txt +3 -0
  100. package/python/requirements.txt +9 -0
  101. package/swift/audio-analyzer/apple-sound-classifier +0 -0
  102. package/swift/audio-analyzer/apple-sound-classifier.swift +213 -0
  103. package/swift/detection-pipeline/apple-sound-classifier +0 -0
  104. package/swift/detection-pipeline/apple-sound-classifier.swift +196 -0
  105. package/wasm/assembly/index.ts +290 -0
  106. package/wasm/assembly/tsconfig.json +4 -0
  107. package/wasm/motion.wasm +0 -0
@@ -0,0 +1,929 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
25
+ const crypto = require("node:crypto");
26
+ const types = require("@camstack/types");
27
+ function backendToHwDeviceConst(backend, consts) {
28
+ switch (backend) {
29
+ case "videotoolbox":
30
+ return consts.AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
31
+ case "cuda":
32
+ return consts.AV_HWDEVICE_TYPE_CUDA;
33
+ case "nvdec":
34
+ return consts.AV_HWDEVICE_TYPE_CUDA;
35
+ // node-av exposes only CUDA; nvdec aliases to it
36
+ case "vaapi":
37
+ return consts.AV_HWDEVICE_TYPE_VAAPI;
38
+ case "qsv":
39
+ return consts.AV_HWDEVICE_TYPE_QSV;
40
+ case "d3d11va":
41
+ return consts.AV_HWDEVICE_TYPE_D3D11VA;
42
+ case "dxva2":
43
+ return consts.AV_HWDEVICE_TYPE_DXVA2;
44
+ case "amf":
45
+ return consts.AV_HWDEVICE_TYPE_AMF;
46
+ case "vdpau":
47
+ return consts.AV_HWDEVICE_TYPE_VDPAU;
48
+ case "drm":
49
+ return consts.AV_HWDEVICE_TYPE_DRM;
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+ function backendToHwPixFmt(backend, consts) {
55
+ switch (backend) {
56
+ case "videotoolbox":
57
+ return consts.AV_PIX_FMT_VIDEOTOOLBOX;
58
+ case "cuda":
59
+ return consts.AV_PIX_FMT_CUDA;
60
+ case "nvdec":
61
+ return consts.AV_PIX_FMT_CUDA;
62
+ case "vaapi":
63
+ return consts.AV_PIX_FMT_VAAPI;
64
+ case "qsv":
65
+ return consts.AV_PIX_FMT_QSV;
66
+ case "d3d11va":
67
+ return consts.AV_PIX_FMT_D3D11;
68
+ case "dxva2":
69
+ return consts.AV_PIX_FMT_DXVA2_VLD;
70
+ case "amf":
71
+ return consts.AV_PIX_FMT_D3D11;
72
+ case "vdpau":
73
+ return consts.AV_PIX_FMT_VDPAU;
74
+ case "drm":
75
+ return consts.AV_PIX_FMT_DRM_PRIME;
76
+ default:
77
+ return null;
78
+ }
79
+ }
80
+ let _nav = null;
81
+ let _consts = null;
82
+ let _sharp = null;
83
+ async function getNodeAv() {
84
+ if (!_nav) _nav = await import("node-av");
85
+ return _nav;
86
+ }
87
+ async function getConstants() {
88
+ if (!_consts) _consts = await Promise.resolve().then(() => require("../index-D_cl0Qqb.js"));
89
+ return _consts;
90
+ }
91
+ async function getSharp() {
92
+ if (!_sharp) {
93
+ const mod = await import("sharp");
94
+ _sharp = mod.default;
95
+ }
96
+ return _sharp;
97
+ }
98
+ const noopLogger = {
99
+ debug() {
100
+ },
101
+ info() {
102
+ },
103
+ warn() {
104
+ },
105
+ error() {
106
+ },
107
+ child() {
108
+ return noopLogger;
109
+ },
110
+ withTags() {
111
+ return noopLogger;
112
+ }
113
+ };
114
+ class NodeAvDecoderSession {
115
+ config;
116
+ logger;
117
+ frameCallbacks = /* @__PURE__ */ new Set();
118
+ destroyed = false;
119
+ // Low-level node-av objects (initialized on first keyframe)
120
+ parser = null;
121
+ codecCtx = null;
122
+ scaler = null;
123
+ avPacket = null;
124
+ avFrame = null;
125
+ dstFrame = null;
126
+ // Cached constants (loaded during init, used in hot path)
127
+ // Hard-coded numeric defaults are replaced in initDecoder() with the real
128
+ // branded constants from `node-av/constants`. The `as` cast at the literal
129
+ // is a one-time branding bridge — once initDecoder runs we hold the exact
130
+ // AVPixelFormat/SwsFlags/AVError values.
131
+ EAGAIN = -11;
132
+ PIX_FMT_GRAY8 = 8;
133
+ PIX_FMT_RGB24 = 2;
134
+ SWS_FAST_BILINEAR = 1;
135
+ /**
136
+ * Decoder output mode. Drives both the scaler's destination pixel
137
+ * format and whether sharp runs the JPEG encode at the end:
138
+ *
139
+ * - `'jpeg'` — scaler→RGB24 → sharp encode → emit JPEG bytes
140
+ * - `'rgb'` — scaler→RGB24 → emit raw RGB24 (no sharp)
141
+ * - `'gray'` — scaler→GRAY8 → emit raw GRAY8 (no sharp)
142
+ *
143
+ * The broker holds the policy decision on which mode to request based
144
+ * on its active subscribers; on-the-fly conversion (e.g. RGB→JPEG for
145
+ * a WebRTC consumer that joined while detection holds the decoder in
146
+ * RGB mode) happens broker-side via the per-frame conversion cache.
147
+ */
148
+ outputMode;
149
+ sharpFn = null;
150
+ /**
151
+ * Backpressure for the sharp JPEG encode pipeline. The broker
152
+ * currently creates sessions with `maxFps: 0` (unlimited) and relies
153
+ * on per-subscriber throttling, so without a bound the
154
+ * fire-and-forget `sharp(...).toBuffer()` chain would accumulate
155
+ * unboundedly whenever sharp falls behind the decoder. Cap at
156
+ * `MAX_JPEG_INFLIGHT` pending encodes per session — any frame that
157
+ * arrives while the cap is saturated is dropped and counted.
158
+ */
159
+ static MAX_JPEG_INFLIGHT = 2;
160
+ jpegEncodeInFlight = 0;
161
+ /**
162
+ * Map a `DecoderSessionConfig.outputFormat` value to one of the three
163
+ * native scaler/encoder modes the session understands. The cap-level
164
+ * format vocabulary is broader (it accepts `bgr`, `yuv420`) than what
165
+ * libav's scaler is wired for here — anything else degrades to RGB
166
+ * (the canonical raw mode) and the broker is expected to convert
167
+ * downstream if a subscriber needs a different shape.
168
+ */
169
+ static resolveOutputMode(format) {
170
+ if (format === "jpeg" || format === void 0) return "jpeg";
171
+ if (format === "gray") return "gray";
172
+ return "rgb";
173
+ }
174
+ initialized = false;
175
+ initializing = false;
176
+ scalerInitializing = false;
177
+ /**
178
+ * Monotonic counter incremented by `updateConfig` whenever the
179
+ * scaler + dstFrame get invalidated (e.g. output format toggle).
180
+ * `initScaler` captures the current value at entry and aborts — or
181
+ * disposes the locally-built scaler — if the epoch moved while
182
+ * its async init was in flight. Without this, a toggle racing an
183
+ * in-flight init could leave two scalers allocated natively while
184
+ * `this.scaler` only holds a reference to one → libav leak.
185
+ */
186
+ scalerEpoch = 0;
187
+ /**
188
+ * One-shot guard for the "first frame" diagnostic log + raw frame
189
+ * dump. Setting this synchronously inside `emitDecodedFrame`
190
+ * prevents re-entry — without it we were using `outputFrames === 0`
191
+ * which stays true until the async sharp encode callback runs, so
192
+ * several decoded frames could trigger the dump before the first
193
+ * JPEG landed.
194
+ */
195
+ firstFrameLogged = false;
196
+ // Output dimensions
197
+ outWidth = 0;
198
+ outHeight = 0;
199
+ // FPS limiter
200
+ lastEmitTime = 0;
201
+ minIntervalMs;
202
+ // Stats
203
+ inputPackets = 0;
204
+ outputFrames = 0;
205
+ droppedFrames = 0;
206
+ totalDecodeTimeMs = 0;
207
+ startTime = Date.now();
208
+ hwaccelPref;
209
+ hwaccelResolver;
210
+ /** The backend that actually initialised successfully — `'none'` = software fallback. */
211
+ activeHwAccel = "none";
212
+ hwDevice = null;
213
+ swTransferFrame = null;
214
+ constructor(config, logger = noopLogger, options) {
215
+ this.config = { ...config };
216
+ const sessionTags = {};
217
+ if (typeof config.deviceId === "number") sessionTags["deviceId"] = config.deviceId;
218
+ if (typeof config.tag === "string" && config.tag.length > 0) sessionTags["tag"] = config.tag;
219
+ this.logger = Object.keys(sessionTags).length > 0 ? logger.withTags(sessionTags) : logger;
220
+ this.minIntervalMs = config.maxFps > 0 ? 1e3 / config.maxFps : 0;
221
+ this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
222
+ this.hwaccelPref = options?.hwaccel ?? "auto";
223
+ this.hwaccelResolver = options?.hwaccelResolver ?? null;
224
+ }
225
+ /**
226
+ * Resolve the backend preference list and try each one against
227
+ * node-av's HW context APIs. The first backend whose
228
+ * `HardwareDeviceContext.create()` succeeds gets attached to
229
+ * `codecCtx.hwDeviceCtx` + its hw pixel format registered via
230
+ * `setHardwarePixelFormat`. On any failure, falls through to the
231
+ * next backend; if all fail, returns with `activeHwAccel='none'`
232
+ * and the decoder runs in software on the same context.
233
+ */
234
+ async tryAttachHwAccel(nav, C) {
235
+ if (!this.codecCtx) return;
236
+ if (this.hwaccelPref === "none") {
237
+ this.activeHwAccel = "none";
238
+ return;
239
+ }
240
+ const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
241
+ const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? { preferred: [explicit], rationale: "explicit (no resolver)" } : { preferred: [], rationale: "auto + no resolver → software" };
242
+ if (resolution.preferred.length === 0) {
243
+ this.activeHwAccel = "none";
244
+ return;
245
+ }
246
+ for (const backend of resolution.preferred) {
247
+ const deviceType = backendToHwDeviceConst(backend, C);
248
+ const hwPixFmt = backendToHwPixFmt(backend, C);
249
+ if (!deviceType || !hwPixFmt) continue;
250
+ const device = new nav.HardwareDeviceContext();
251
+ const rc = device.create(deviceType);
252
+ if (rc < 0) {
253
+ this.logger.warn("node-av: hwaccel device create failed — trying next", {
254
+ meta: { backend, rc }
255
+ });
256
+ device.free();
257
+ continue;
258
+ }
259
+ try {
260
+ this.codecCtx.hwDeviceCtx = device;
261
+ this.codecCtx.setHardwarePixelFormat(hwPixFmt);
262
+ } catch (err) {
263
+ this.logger.warn("node-av: hwaccel context attach failed — trying next", {
264
+ meta: { backend, error: types.errMsg(err) }
265
+ });
266
+ device.free();
267
+ continue;
268
+ }
269
+ this.hwDevice = device;
270
+ this.activeHwAccel = backend;
271
+ return;
272
+ }
273
+ this.logger.warn("node-av: no hwaccel backend initialised — using software", {
274
+ meta: { attempted: resolution.preferred.join(","), rationale: resolution.rationale }
275
+ });
276
+ this.activeHwAccel = "none";
277
+ }
278
+ /**
279
+ * Download a HW frame (format == hw pix fmt) into a SW frame so the
280
+ * rest of the pipeline (scaler, JPEG encoder, grayscale passthrough)
281
+ * handles it identically to the pure-software path. Uses the sync
282
+ * variant so the synchronous receive loop below doesn't need to be
283
+ * async-ified. Returns `null` on transfer failure, meaning the
284
+ * caller should drop the frame.
285
+ */
286
+ transferHwFrame(hwFrame) {
287
+ if (this.activeHwAccel === "none" || !this.swTransferFrame) return hwFrame;
288
+ const rc = hwFrame.hwframeTransferDataSync(this.swTransferFrame, 0);
289
+ if (rc < 0) {
290
+ this.logger.warn("node-av: hwframeTransferData failed", { meta: { rc } });
291
+ return null;
292
+ }
293
+ return this.swTransferFrame;
294
+ }
295
+ /**
296
+ * Initialize the decoder pipeline on the first keyframe.
297
+ * After this returns, all hot-path methods are fully synchronous (except JPEG encode).
298
+ */
299
+ async initDecoder() {
300
+ if (this.initialized || this.initializing || this.destroyed) return;
301
+ this.initializing = true;
302
+ try {
303
+ const nav = await getNodeAv();
304
+ const C = await getConstants();
305
+ this.EAGAIN = C.AVERROR_EAGAIN;
306
+ this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
307
+ this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
308
+ this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
309
+ if (this.outputMode === "jpeg") {
310
+ this.sharpFn = await getSharp();
311
+ }
312
+ nav.Log.setLevel(C.AV_LOG_FATAL);
313
+ const isHevc = this.config.codec === "h265" || this.config.codec === "hevc";
314
+ const codecId = isHevc ? C.AV_CODEC_ID_HEVC : C.AV_CODEC_ID_H264;
315
+ this.parser = new nav.CodecParser();
316
+ this.parser.init(codecId);
317
+ const codec = nav.Codec.findDecoder(codecId);
318
+ if (!codec) {
319
+ this.logger.error("node-av: no decoder found", { meta: { codec: this.config.codec } });
320
+ return;
321
+ }
322
+ this.codecCtx = new nav.CodecContext();
323
+ this.codecCtx.allocContext3(codec);
324
+ if (this.config.width && this.config.height) {
325
+ this.codecCtx.width = this.config.width;
326
+ this.codecCtx.height = this.config.height;
327
+ }
328
+ this.codecCtx.threadCount = 1;
329
+ await this.tryAttachHwAccel(nav, C);
330
+ const ret = await this.codecCtx.open2(codec);
331
+ if (ret < 0) {
332
+ if (this.activeHwAccel !== "none") {
333
+ this.logger.warn("node-av: open2 failed with hwaccel — retrying in software", {
334
+ meta: { ret, hwAccel: this.activeHwAccel }
335
+ });
336
+ this.hwDevice?.free();
337
+ this.hwDevice = null;
338
+ this.activeHwAccel = "none";
339
+ this.codecCtx = new nav.CodecContext();
340
+ this.codecCtx.allocContext3(codec);
341
+ if (this.config.width && this.config.height) {
342
+ this.codecCtx.width = this.config.width;
343
+ this.codecCtx.height = this.config.height;
344
+ }
345
+ this.codecCtx.threadCount = 1;
346
+ const retry = await this.codecCtx.open2(codec);
347
+ if (retry < 0) {
348
+ this.logger.error("node-av: failed to open decoder (sw fallback)", { meta: { ret: retry } });
349
+ return;
350
+ }
351
+ } else {
352
+ this.logger.error("node-av: failed to open decoder", { meta: { ret } });
353
+ return;
354
+ }
355
+ }
356
+ if (this.activeHwAccel !== "none") {
357
+ this.swTransferFrame = new nav.Frame();
358
+ this.swTransferFrame.alloc();
359
+ }
360
+ this.avPacket = new nav.Packet();
361
+ this.avPacket.alloc();
362
+ this.avFrame = new nav.Frame();
363
+ this.avFrame.alloc();
364
+ this.initialized = true;
365
+ this.logger.info("node-av push decoder initialized", {
366
+ meta: {
367
+ codec: this.config.codec,
368
+ output: this.outputMode,
369
+ // Reports the backend that actually succeeded at
370
+ // `open2(codec)` with `hwDeviceCtx` attached, or `'none'` if
371
+ // we fell back to software (explicit `hwaccel: 'none'`
372
+ // override, empty resolver output, or every attempted
373
+ // backend failed init).
374
+ hwAccel: this.activeHwAccel
375
+ }
376
+ });
377
+ } catch (err) {
378
+ this.logger.error("node-av init error", { meta: { error: types.errMsg(err) } });
379
+ } finally {
380
+ this.initializing = false;
381
+ }
382
+ }
383
+ /**
384
+ * Initialize the scaler after the first frame tells us the actual
385
+ * dimensions. Output pixel format: RGB24 for JPEG encoding, GRAY8
386
+ * for raw motion.
387
+ *
388
+ * Builds `scaler` + `dstFrame` on local variables and publishes
389
+ * them onto `this` in a single atomic step at the end. Captures
390
+ * `scalerEpoch` at entry; if `updateConfig` invalidated the scaler
391
+ * while this init was in flight (epoch mismatch), the locally
392
+ * built pair is disposed and discarded so the later init wins.
393
+ * Without the local-first approach, partial state (scaler set,
394
+ * dstFrame still null) could be observed by a concurrent
395
+ * `emitDecodedFrame` call.
396
+ */
397
+ async initScaler(srcW, srcH, srcFmt) {
398
+ if (this.scalerInitializing) return;
399
+ this.scalerInitializing = true;
400
+ const myEpoch = this.scalerEpoch;
401
+ let localScaler = null;
402
+ let localDstFrame = null;
403
+ try {
404
+ const nav = await getNodeAv();
405
+ if (this.destroyed || myEpoch !== this.scalerEpoch) return;
406
+ const scale = this.config.scale > 1 ? this.config.scale : 1;
407
+ const maxW = Math.floor(640 / scale);
408
+ const outWidth = Math.min(srcW, maxW);
409
+ const outHeight = Math.round(outWidth * srcH / srcW);
410
+ const dstFmt = this.outputMode === "gray" ? this.PIX_FMT_GRAY8 : this.PIX_FMT_RGB24;
411
+ const fmtName = this.outputMode === "gray" ? "gray8" : "rgb24";
412
+ localScaler = new nav.SoftwareScaleContext();
413
+ localScaler.getContext(
414
+ srcW,
415
+ srcH,
416
+ srcFmt,
417
+ outWidth,
418
+ outHeight,
419
+ dstFmt,
420
+ this.SWS_FAST_BILINEAR
421
+ );
422
+ const ret = localScaler.initContext();
423
+ if (ret < 0) {
424
+ this.logger.error("node-av: sws_init_context failed", { meta: { ret } });
425
+ return;
426
+ }
427
+ localDstFrame = new nav.Frame();
428
+ localDstFrame.alloc();
429
+ localDstFrame.width = outWidth;
430
+ localDstFrame.height = outHeight;
431
+ localDstFrame.format = dstFmt;
432
+ const allocRet = localDstFrame.allocBuffer();
433
+ if (allocRet < 0) {
434
+ this.logger.error("node-av: dst frame allocBuffer failed", { meta: { ret: allocRet } });
435
+ return;
436
+ }
437
+ if (this.destroyed || myEpoch !== this.scalerEpoch) return;
438
+ this.scaler?.[Symbol.dispose]?.();
439
+ this.dstFrame?.[Symbol.dispose]?.();
440
+ this.scaler = localScaler;
441
+ this.dstFrame = localDstFrame;
442
+ this.outWidth = outWidth;
443
+ this.outHeight = outHeight;
444
+ localScaler = null;
445
+ localDstFrame = null;
446
+ this.logger.info("node-av scaler initialized", {
447
+ meta: { srcWidth: srcW, srcHeight: srcH, outWidth, outHeight, format: fmtName }
448
+ });
449
+ } catch (err) {
450
+ this.logger.error("Scaler init failed", { meta: { error: types.errMsg(err) } });
451
+ } finally {
452
+ localScaler?.[Symbol.dispose]?.();
453
+ localDstFrame?.[Symbol.dispose]?.();
454
+ this.scalerInitializing = false;
455
+ }
456
+ }
457
+ pushPacket(packet) {
458
+ if (this.destroyed) return;
459
+ this.inputPackets++;
460
+ if (!this.initialized && !this.initializing && packet.keyframe) {
461
+ this.initDecoder().then(() => {
462
+ if (this.initialized) this.decodeRawData(packet.data, packet.pts ?? Date.now());
463
+ }).catch((err) => {
464
+ this.logger.error("node-av init failed", { meta: { error: types.errMsg(err) } });
465
+ });
466
+ return;
467
+ }
468
+ if (!this.initialized) return;
469
+ this.decodeRawData(packet.data, packet.pts ?? Date.now());
470
+ }
471
+ decodeRawData(data, pts) {
472
+ if (!this.parser || !this.codecCtx || !this.avPacket || !this.avFrame) return;
473
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
474
+ const bigPts = BigInt(pts);
475
+ let offset = 0;
476
+ while (offset < buf.length) {
477
+ const remaining = buf.subarray(offset);
478
+ const consumed = this.parser.parse2(
479
+ this.codecCtx,
480
+ this.avPacket,
481
+ remaining,
482
+ bigPts,
483
+ bigPts,
484
+ offset
485
+ );
486
+ if (consumed < 0) {
487
+ this.logger.warn("node-av parser error", { meta: { ret: consumed } });
488
+ break;
489
+ }
490
+ offset += consumed;
491
+ if (this.avPacket.size > 0) {
492
+ this.decodePacket();
493
+ this.avPacket.unref();
494
+ }
495
+ }
496
+ }
497
+ decodePacket() {
498
+ if (!this.codecCtx || !this.avFrame) return;
499
+ const sendRet = this.codecCtx.sendPacketSync(this.avPacket);
500
+ if (sendRet < 0 && sendRet !== this.EAGAIN) {
501
+ this.logger.warn("node-av sendPacket error", { meta: { ret: sendRet } });
502
+ return;
503
+ }
504
+ while (true) {
505
+ const recvRet = this.codecCtx.receiveFrameSync(this.avFrame);
506
+ if (recvRet < 0) break;
507
+ const frame = this.transferHwFrame(this.avFrame);
508
+ if (!frame) continue;
509
+ this.emitDecodedFrame(frame);
510
+ }
511
+ }
512
+ emitDecodedFrame(frame) {
513
+ const now = performance.now();
514
+ if (this.minIntervalMs > 0 && now - this.lastEmitTime < this.minIntervalMs) {
515
+ this.droppedFrames++;
516
+ return;
517
+ }
518
+ if (!this.scaler && !this.scalerInitializing) {
519
+ this.initScaler(frame.width, frame.height, frame.format);
520
+ return;
521
+ }
522
+ if (!this.dstFrame || !this.scaler) return;
523
+ const decodeStart = performance.now();
524
+ try {
525
+ this.dstFrame.makeWritable();
526
+ this.scaler.scaleFrameSync(this.dstFrame, frame);
527
+ } catch (err) {
528
+ this.logger.warn("node-av scale error", { meta: { error: types.errMsg(err) } });
529
+ return;
530
+ }
531
+ if (!this.firstFrameLogged) {
532
+ this.firstFrameLogged = true;
533
+ const channels = this.outputMode === "gray" ? 1 : 3;
534
+ const ls = this.dstFrame.linesize;
535
+ const buf = this.dstFrame.toBuffer();
536
+ this.logger.info("dstFrame after scale", {
537
+ meta: {
538
+ phase: "frame-debug",
539
+ width: this.dstFrame.width,
540
+ height: this.dstFrame.height,
541
+ linesize: [ls[0], ls[1], ls[2]],
542
+ expectedStride: this.dstFrame.width * channels,
543
+ bufLen: buf.length,
544
+ expectedPacked: this.dstFrame.width * channels * this.dstFrame.height,
545
+ srcFormat: frame.format ?? "?"
546
+ }
547
+ });
548
+ if (this.outputMode !== "gray") {
549
+ import("node:fs").then((fsModule) => {
550
+ import("node:path").then((pathModule) => {
551
+ const dumpPath = pathModule.join(process.cwd(), "camstack-data", "debug-frame-rgb24.raw");
552
+ fsModule.writeFileSync(dumpPath, buf);
553
+ this.logger.info("Dumped first RGB24 frame", { meta: { phase: "frame-debug", path: dumpPath, bytes: buf.length } });
554
+ }).catch(() => {
555
+ });
556
+ }).catch(() => {
557
+ });
558
+ }
559
+ }
560
+ const rawBuf = this.extractPackedBuffer(this.dstFrame);
561
+ if (this.outputMode === "jpeg") {
562
+ this.encodeAndEmitJpeg(rawBuf, decodeStart);
563
+ } else if (this.outputMode === "rgb") {
564
+ this.emitRawFrame(rawBuf, "rgb", decodeStart);
565
+ } else {
566
+ this.emitRawFrame(rawBuf, "gray", decodeStart);
567
+ }
568
+ }
569
+ /**
570
+ * Extract packed pixel buffer from a decoded frame.
571
+ * FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
572
+ * Sharp and WASM consumers expect tightly-packed rows (stride = width * channels).
573
+ * If linesize matches expected stride, return the buffer directly (zero-copy).
574
+ */
575
+ extractPackedBuffer(frame) {
576
+ const channels = this.outputMode === "gray" ? 1 : 3;
577
+ const expectedStride = frame.width * channels;
578
+ const actualStride = frame.linesize[0] ?? expectedStride;
579
+ const src = frame.data?.[0];
580
+ if (!src) {
581
+ return frame.toBuffer();
582
+ }
583
+ if (actualStride === expectedStride) {
584
+ return Buffer.from(src.buffer, src.byteOffset, expectedStride * frame.height);
585
+ }
586
+ const dst = Buffer.allocUnsafe(expectedStride * frame.height);
587
+ for (let y = 0; y < frame.height; y++) {
588
+ src.copy(dst, y * expectedStride, y * actualStride, y * actualStride + expectedStride);
589
+ }
590
+ return dst;
591
+ }
592
+ /**
593
+ * Encode RGB24 raw buffer as JPEG and emit.
594
+ *
595
+ * Drops the frame (and counts it) when `MAX_JPEG_INFLIGHT` encodes
596
+ * are already pending — prevents unbounded growth of the
597
+ * fire-and-forget promise chain when sharp cannot keep up with the
598
+ * decode rate.
599
+ */
600
+ encodeAndEmitJpeg(rgb, decodeStart) {
601
+ if (!this.sharpFn) return;
602
+ if (this.jpegEncodeInFlight >= NodeAvDecoderSession.MAX_JPEG_INFLIGHT) {
603
+ this.droppedFrames++;
604
+ return;
605
+ }
606
+ this.jpegEncodeInFlight++;
607
+ this.sharpFn(rgb, {
608
+ raw: { width: this.outWidth, height: this.outHeight, channels: 3 }
609
+ }).jpeg({ quality: 80, mozjpeg: false }).toBuffer().then((jpegBuf) => {
610
+ if (this.destroyed) return;
611
+ this.emitRawFrame(jpegBuf, "jpeg", decodeStart);
612
+ }).catch((err) => {
613
+ this.logger.warn("sharp jpeg encode error", { meta: { error: types.errMsg(err) } });
614
+ }).finally(() => {
615
+ this.jpegEncodeInFlight--;
616
+ });
617
+ }
618
+ emitRawFrame(data, format, decodeStart) {
619
+ const decodeMs = performance.now() - decodeStart;
620
+ this.totalDecodeTimeMs += decodeMs;
621
+ this.outputFrames++;
622
+ this.lastEmitTime = performance.now();
623
+ if (this.outputFrames === 1 || this.outputFrames % 500 === 0) {
624
+ this.logger.info("node-av frame emitted", {
625
+ meta: {
626
+ frameNumber: this.outputFrames,
627
+ width: this.outWidth,
628
+ height: this.outHeight,
629
+ format,
630
+ decodeMs,
631
+ subs: this.frameCallbacks.size
632
+ }
633
+ });
634
+ }
635
+ const decodedFrame = {
636
+ data,
637
+ width: this.outWidth,
638
+ height: this.outHeight,
639
+ format,
640
+ timestamp: Date.now()
641
+ };
642
+ for (const cb of this.frameCallbacks) {
643
+ cb(decodedFrame);
644
+ }
645
+ }
646
+ onFrame(callback) {
647
+ this.frameCallbacks.add(callback);
648
+ return () => {
649
+ this.frameCallbacks.delete(callback);
650
+ };
651
+ }
652
+ updateConfig(update) {
653
+ const prevFormat = this.config.outputFormat;
654
+ this.config = { ...this.config, ...update };
655
+ if (update.maxFps !== void 0) {
656
+ this.minIntervalMs = update.maxFps > 0 ? 1e3 / update.maxFps : 0;
657
+ }
658
+ if (update.outputFormat !== void 0 && update.outputFormat !== prevFormat) {
659
+ const prevMode = this.outputMode;
660
+ this.outputMode = NodeAvDecoderSession.resolveOutputMode(update.outputFormat);
661
+ if (this.outputMode === prevMode) return;
662
+ this.scalerEpoch++;
663
+ if (this.scaler) {
664
+ this.scaler[Symbol.dispose]?.();
665
+ this.scaler = null;
666
+ }
667
+ if (this.dstFrame) {
668
+ this.dstFrame[Symbol.dispose]?.();
669
+ this.dstFrame = null;
670
+ }
671
+ if (this.outputMode === "jpeg" && !this.sharpFn) {
672
+ getSharp().then((fn) => {
673
+ this.sharpFn = fn;
674
+ }).catch(() => {
675
+ });
676
+ }
677
+ this.logger.info("node-av: output format changed — scaler will reinit", {
678
+ meta: { from: prevFormat, to: update.outputFormat, mode: this.outputMode }
679
+ });
680
+ }
681
+ }
682
+ async destroy() {
683
+ if (this.destroyed) return;
684
+ this.destroyed = true;
685
+ this.frameCallbacks.clear();
686
+ this.dstFrame?.[Symbol.dispose]?.();
687
+ this.avFrame?.[Symbol.dispose]?.();
688
+ this.avPacket?.[Symbol.dispose]?.();
689
+ this.scaler?.[Symbol.dispose]?.();
690
+ this.parser?.[Symbol.dispose]?.();
691
+ this.swTransferFrame?.[Symbol.dispose]?.();
692
+ this.codecCtx?.[Symbol.dispose]?.();
693
+ this.hwDevice?.free();
694
+ this.dstFrame = null;
695
+ this.avFrame = null;
696
+ this.avPacket = null;
697
+ this.scaler = null;
698
+ this.parser = null;
699
+ this.codecCtx = null;
700
+ this.swTransferFrame = null;
701
+ this.hwDevice = null;
702
+ }
703
+ getStats() {
704
+ const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
705
+ return {
706
+ inputFps: this.inputPackets / uptimeSec,
707
+ outputFps: this.outputFrames / uptimeSec,
708
+ avgDecodeTimeMs: this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0,
709
+ droppedFrames: this.droppedFrames
710
+ };
711
+ }
712
+ get isPullMode() {
713
+ return false;
714
+ }
715
+ }
716
+ const FRAME_BUFFER_CAPACITY = 32;
717
+ class DecoderNodeAvAddon extends types.BaseAddon {
718
+ sessions = /* @__PURE__ */ new Map();
719
+ frameBuffers = /* @__PURE__ */ new Map();
720
+ unsubscribers = /* @__PURE__ */ new Map();
721
+ sessionMeta = /* @__PURE__ */ new Map();
722
+ constructor() {
723
+ super(types.DEFAULT_DECODER_HWACCEL_CONFIG);
724
+ }
725
+ globalSettingsSchema() {
726
+ return this.schema({
727
+ sections: [{
728
+ id: "hwaccel",
729
+ title: "Hardware acceleration",
730
+ tab: "decoder",
731
+ description: 'Backend used by node-av decoder sessions. "Auto" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',
732
+ fields: [
733
+ this.field({
734
+ type: "select",
735
+ key: "hwaccel",
736
+ label: "Preferred backend",
737
+ options: [...types.HWACCEL_OPTIONS],
738
+ default: "auto",
739
+ immediate: true
740
+ }),
741
+ this.field({
742
+ type: "text",
743
+ key: "probedBestHwaccel",
744
+ label: "Probed best",
745
+ description: "Auto-detected best decoder backend on this host. Click the refresh icon to re-run the probe.",
746
+ readonlyField: true,
747
+ default: "",
748
+ actions: [
749
+ { action: "reprobe-hwaccel", icon: "refresh-cw", tooltip: "Re-probe hwaccel" }
750
+ ]
751
+ })
752
+ ]
753
+ }]
754
+ });
755
+ }
756
+ async onInitialize() {
757
+ this.ctx.logger.info("node-av decoder addon initialized");
758
+ if (!this.config.probedBestHwaccel) {
759
+ this.reprobeHwaccel().catch((err) => {
760
+ this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", {
761
+ meta: { error: err instanceof Error ? err.message : String(err) }
762
+ });
763
+ });
764
+ }
765
+ return [{ capability: types.decoderCapability, provider: this }];
766
+ }
767
+ /**
768
+ * Resolve the effective hwaccel backend for a new session. Reads
769
+ * this addon's own `hwaccel` setting first. `'auto'` defers to the
770
+ * session's local resolver (`ctx.kernel.hwaccel`) which probes the
771
+ * host and picks. No more orchestrator round-trip — decoder addon
772
+ * is self-sufficient for this setting as of phase 2d.
773
+ */
774
+ resolveHwAccelPref() {
775
+ return this.config.hwaccel;
776
+ }
777
+ /**
778
+ * Re-run the platform probe on this host and persist the detected
779
+ * backend as `probedBestHwaccel`. The operator's `hwaccel` setting
780
+ * is intentionally left alone — the probe only updates the hint.
781
+ */
782
+ async reprobeHwaccel() {
783
+ const resolver = this.ctx.kernel.hwaccel;
784
+ if (!resolver) {
785
+ this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
786
+ await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
787
+ return { backend: "none" };
788
+ }
789
+ try {
790
+ const res = await resolver.resolve();
791
+ const backend = res.preferred[0] ?? "none";
792
+ await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend });
793
+ this.ctx.logger.info("reprobeHwaccel: wrote probedBestHwaccel", {
794
+ meta: { backend, rationale: res.rationale, preferred: res.preferred }
795
+ });
796
+ return { backend };
797
+ } catch (err) {
798
+ this.ctx.logger.warn("reprobeHwaccel failed", {
799
+ meta: { error: err instanceof Error ? err.message : String(err) }
800
+ });
801
+ await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
802
+ return { backend: "none" };
803
+ }
804
+ }
805
+ async supportsCodec(input) {
806
+ return ["h264", "h265", "hevc"].includes(input.codec.toLowerCase());
807
+ }
808
+ async getInfo() {
809
+ return {
810
+ id: "decoder-nodeav",
811
+ name: "Decoder (node-av)",
812
+ isPullMode: false,
813
+ priority: 10
814
+ };
815
+ }
816
+ async createSession(config) {
817
+ const sessionId = crypto.randomUUID();
818
+ const hwaccel = this.resolveHwAccelPref();
819
+ const session = new NodeAvDecoderSession(config, this.ctx.logger, {
820
+ hwaccel,
821
+ hwaccelResolver: this.ctx.kernel.hwaccel
822
+ });
823
+ const ringBuffer = new types.RingBuffer(FRAME_BUFFER_CAPACITY);
824
+ const unsub = session.onFrame((frame) => {
825
+ const { format } = frame;
826
+ if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
827
+ const arrayBuf = new ArrayBuffer(frame.data.byteLength);
828
+ new Uint8Array(arrayBuf).set(frame.data);
829
+ const capData = new Uint8Array(arrayBuf);
830
+ const capFrame = {
831
+ data: capData,
832
+ width: frame.width,
833
+ height: frame.height,
834
+ format,
835
+ timestamp: frame.timestamp
836
+ };
837
+ ringBuffer.push(capFrame);
838
+ });
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()
846
+ });
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
+ }
850
+ async destroySession(input) {
851
+ const { sessionId } = input;
852
+ const session = this.sessions.get(sessionId);
853
+ if (!session) {
854
+ throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
855
+ }
856
+ const unsub = this.unsubscribers.get(sessionId);
857
+ if (unsub) unsub();
858
+ await session.destroy();
859
+ this.sessions.delete(sessionId);
860
+ this.frameBuffers.delete(sessionId);
861
+ this.unsubscribers.delete(sessionId);
862
+ this.sessionMeta.delete(sessionId);
863
+ this.ctx.logger.info("node-av: destroyed session", { meta: { sessionId } });
864
+ }
865
+ async listActiveSessions() {
866
+ const out = [];
867
+ for (const [sessionId, meta] of this.sessionMeta) {
868
+ out.push({ sessionId, codec: meta.codec, outputFormat: meta.outputFormat, createdAtMs: meta.createdAtMs });
869
+ }
870
+ return out;
871
+ }
872
+ async pushPacket(input) {
873
+ const session = this.sessions.get(input.sessionId);
874
+ if (!session) {
875
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
876
+ }
877
+ const rawData = input.packet.data;
878
+ const data = Buffer.isBuffer(rawData) ? rawData : rawData instanceof Uint8Array ? Buffer.from(rawData.buffer, rawData.byteOffset, rawData.byteLength) : Buffer.from(rawData);
879
+ session.pushPacket({ ...input.packet, data });
880
+ }
881
+ async openStream(input) {
882
+ const session = this.sessions.get(input.sessionId);
883
+ if (!session) {
884
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
885
+ }
886
+ if (session.openStream) {
887
+ await session.openStream(input.url);
888
+ }
889
+ }
890
+ async pullFrames(input) {
891
+ const ringBuffer = this.frameBuffers.get(input.sessionId);
892
+ if (!ringBuffer) {
893
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
894
+ }
895
+ return ringBuffer.drain(input.maxCount);
896
+ }
897
+ async updateConfig(input) {
898
+ const session = this.sessions.get(input.sessionId);
899
+ if (!session) {
900
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
901
+ }
902
+ session.updateConfig(input.config);
903
+ }
904
+ async getStats(input) {
905
+ const session = this.sessions.get(input.sessionId);
906
+ if (!session) {
907
+ throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
908
+ }
909
+ return session.getStats();
910
+ }
911
+ async onShutdown() {
912
+ this.ctx.logger.info("node-av decoder addon shutdown — destroying all sessions");
913
+ const destroyPromises = [];
914
+ for (const [sessionId, session] of this.sessions) {
915
+ const unsub = this.unsubscribers.get(sessionId);
916
+ if (unsub) unsub();
917
+ destroyPromises.push(session.destroy());
918
+ }
919
+ await Promise.all(destroyPromises);
920
+ this.sessions.clear();
921
+ this.frameBuffers.clear();
922
+ this.unsubscribers.clear();
923
+ this.sessionMeta.clear();
924
+ }
925
+ }
926
+ exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
927
+ exports.NodeAvDecoderSession = NodeAvDecoderSession;
928
+ exports.default = DecoderNodeAvAddon;
929
+ //# sourceMappingURL=index.js.map