@camstack/addon-pipeline 1.1.39 → 1.1.41

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 (38) hide show
  1. package/dist/audio-analyzer/index.js +168 -61
  2. package/dist/audio-analyzer/index.mjs +168 -61
  3. package/dist/detection-pipeline/index.js +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
  6. package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
  7. package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
  8. package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +893 -12
  12. package/dist/pipeline-runner/index.mjs +892 -13
  13. package/dist/recorder/index.js +2 -2
  14. package/dist/recorder/index.mjs +2 -2
  15. package/dist/session-decode/decode-worker-child.js +544 -0
  16. package/dist/session-decode/decode-worker-child.mjs +543 -0
  17. package/dist/stream-broker/_stub.js +44 -44
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2l0kmD4.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-DHaCRMjM.mjs} +3 -3
  22. package/dist/stream-broker/index.js +268 -38
  23. package/dist/stream-broker/index.mjs +268 -38
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/worker-protocol-BCfO8gUF.js +67 -0
  26. package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-B6fza7ic.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-Cy-4iTog.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-CYX33lJb.js} +1 -1
  30. package/embed-dist/assets/index-BPkayx9u.js +81 -0
  31. package/embed-dist/assets/index-CSFtK41z.css +2 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +6 -3
  34. package/python/inference_pool.py +3 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs +0 -26
  37. package/embed-dist/assets/index-C5UpuPr8.css +0 -2
  38. package/embed-dist/assets/index-CR367rYR.js +0 -81
@@ -1,10 +1,10 @@
1
1
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
2
- const require_dist = require("../dist-DbGdZ8Nr.js");
2
+ const require_dist = require("../dist-BJTPkJFw.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
+ let node_child_process = require("node:child_process");
4
5
  let node_fs = require("node:fs");
5
6
  let node_path = require("node:path");
6
7
  node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
7
- let node_child_process = require("node:child_process");
8
8
  //#region src/recorder/segment-path.ts
9
9
  function segmentRelPath(deviceId, profile, startMs, durMs, bytes) {
10
10
  if (profile.includes("/")) throw new Error(`segmentRelPath: profile must not contain '/': ${profile}`);
@@ -1,9 +1,9 @@
1
- import { A as recordingCapability, B as DeviceType, I as errMsg, J as selectAssignedProfileSlots, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, at as string, it as record, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-DvvYzO58.mjs";
1
+ import { A as recordingCapability, B as DeviceType, I as errMsg, J as selectAssignedProfileSlots, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, at as string, it as record, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-v6cKLmU3.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
+ import { spawn } from "node:child_process";
4
5
  import { promises } from "node:fs";
5
6
  import path from "node:path";
6
- import { spawn } from "node:child_process";
7
7
  //#region src/recorder/segment-path.ts
8
8
  function segmentRelPath(deviceId, profile, startMs, durMs, bytes) {
9
9
  if (profile.includes("/")) throw new Error(`segmentRelPath: profile must not contain '/': ${profile}`);
@@ -0,0 +1,544 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_worker_protocol = require("../worker-protocol-BCfO8gUF.js");
3
+ //#region src/session-decode/frame-slot.ts
4
+ function toReply(held) {
5
+ return {
6
+ frameId: held.frameId,
7
+ timestamp: held.timestamp,
8
+ width: held.width,
9
+ height: held.height
10
+ };
11
+ }
12
+ /**
13
+ * Latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
14
+ * pending-pull flag. One instance per decode-worker child session.
15
+ */
16
+ var FrameSlot = class {
17
+ nextFrameId = 1;
18
+ slot = null;
19
+ /**
20
+ * The frame handed to the MOST RECENT `pull`, retained so a following
21
+ * `toBuffer(frameId)` still resolves even though `publish` keeps advancing
22
+ * the latest-wins `slot`. Freed when the next `pull` supersedes it (or on
23
+ * `free()`).
24
+ */
25
+ reserved = null;
26
+ pendingPull = false;
27
+ /**
28
+ * Publish a newly decoded frame into the latest-wins slot, freeing
29
+ * whatever the slot held before (the drop-older half of "latest-wins").
30
+ * Returns the reply to send if a `pull` was pending (resolving it
31
+ * immediately and reserving the frame); otherwise returns `null` and the
32
+ * frame simply waits in the slot for the next `pull`.
33
+ */
34
+ publish(frame, timestamp, minIntervalMs) {
35
+ if (minIntervalMs > 0 && this.slot && timestamp - this.slot.timestamp < minIntervalMs) {
36
+ frame.free();
37
+ return null;
38
+ }
39
+ const previous = this.slot;
40
+ const frameId = this.nextFrameId++;
41
+ this.slot = {
42
+ frameId,
43
+ frame,
44
+ timestamp,
45
+ width: frame.width,
46
+ height: frame.height
47
+ };
48
+ previous?.frame.free();
49
+ if (this.pendingPull) {
50
+ this.pendingPull = false;
51
+ return this.reserveSlot();
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * Hand out the newest slot frame and RESERVE it (freeing the previously
57
+ * reserved frame), clearing the slot. Returns `null` if no frame has been
58
+ * decoded yet — the caller should then call `markPendingPull()`.
59
+ */
60
+ pull() {
61
+ if (!this.slot) return null;
62
+ return this.reserveSlot();
63
+ }
64
+ /** Record that a pull is waiting for the next `publish` to resolve it. */
65
+ markPendingPull() {
66
+ this.pendingPull = true;
67
+ }
68
+ /** The reserved frame for `frameId`, or `null` if unknown/stale. */
69
+ toBuffer(frameId) {
70
+ if (!this.reserved || this.reserved.frameId !== frameId) return null;
71
+ return this.reserved.frame;
72
+ }
73
+ /** Free both the slot and the reserved frame (teardown). */
74
+ free() {
75
+ this.slot?.frame.free();
76
+ this.slot = null;
77
+ this.reserved?.frame.free();
78
+ this.reserved = null;
79
+ }
80
+ reserveSlot() {
81
+ const held = this.slot;
82
+ if (!held) throw new Error("frame-slot: reserveSlot called with no slot held");
83
+ const previousReserved = this.reserved;
84
+ this.reserved = held;
85
+ this.slot = null;
86
+ previousReserved?.frame.free();
87
+ return toReply(held);
88
+ }
89
+ };
90
+ //#endregion
91
+ //#region src/session-decode/decode-worker-child.ts
92
+ /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
93
+ var REDIAL_MS = 3e3;
94
+ var _nav = null;
95
+ var _consts = null;
96
+ async function getNodeAv() {
97
+ if (!_nav) _nav = await import("node-av");
98
+ return _nav;
99
+ }
100
+ async function getConstants() {
101
+ if (!_consts) _consts = await import("node-av/constants");
102
+ return _consts;
103
+ }
104
+ function errMessage(err) {
105
+ if (err instanceof Error) return err.message;
106
+ return String(err);
107
+ }
108
+ /**
109
+ * RTSP low-latency demuxer options — mirrors
110
+ * `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
111
+ * than cross-imported: this file is a standalone forked entry point, not an
112
+ * addon consumer of that package). `analyzeduration`/`probesize` stay small
113
+ * but non-zero — zeroing them starves the Reolink rfc4571 restream demuxer.
114
+ */
115
+ function buildDemuxerOptions() {
116
+ return {
117
+ rtsp_transport: "tcp",
118
+ fflags: "nobuffer",
119
+ analyzeduration: "1000000",
120
+ probesize: "1000000",
121
+ max_delay: "0",
122
+ reorder_queue_size: "0",
123
+ user_agent: "decode-worker"
124
+ };
125
+ }
126
+ /** Ordered hwaccel candidates per platform — first successful create() wins, else software. */
127
+ function candidateBackends() {
128
+ switch (process.platform) {
129
+ case "darwin": return ["videotoolbox"];
130
+ case "linux": return [
131
+ "vaapi",
132
+ "qsv",
133
+ "cuda"
134
+ ];
135
+ case "win32": return [
136
+ "d3d11va",
137
+ "dxva2",
138
+ "cuda"
139
+ ];
140
+ default: return [];
141
+ }
142
+ }
143
+ /** Map a canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
144
+ function backendToHwDeviceConst(backend, consts) {
145
+ switch (backend) {
146
+ case "videotoolbox": return consts.AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
147
+ case "cuda":
148
+ case "nvdec": return consts.AV_HWDEVICE_TYPE_CUDA;
149
+ case "vaapi": return consts.AV_HWDEVICE_TYPE_VAAPI;
150
+ case "qsv": return consts.AV_HWDEVICE_TYPE_QSV;
151
+ case "d3d11va": return consts.AV_HWDEVICE_TYPE_D3D11VA;
152
+ case "dxva2": return consts.AV_HWDEVICE_TYPE_DXVA2;
153
+ case "amf": return consts.AV_HWDEVICE_TYPE_AMF;
154
+ case "vdpau": return consts.AV_HWDEVICE_TYPE_VDPAU;
155
+ case "drm": return consts.AV_HWDEVICE_TYPE_DRM;
156
+ default: return null;
157
+ }
158
+ }
159
+ /** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
160
+ function clampEven(value, min, max) {
161
+ const bounded = Math.min(Math.max(value, min), max);
162
+ return bounded - bounded % 2;
163
+ }
164
+ /** Resolve `FrameImageOptions.crop` against the decoded frame's dimensions, defaulting to the full frame. */
165
+ function resolveCropRegion(frameWidth, frameHeight, crop) {
166
+ const left = clampEven(crop?.left ?? 0, 0, Math.max(0, frameWidth - 2));
167
+ const top = clampEven(crop?.top ?? 0, 0, Math.max(0, frameHeight - 2));
168
+ return {
169
+ left,
170
+ top,
171
+ width: clampEven(crop?.width ?? frameWidth - left, 2, frameWidth - left),
172
+ height: clampEven(crop?.height ?? frameHeight - top, 2, frameHeight - top)
173
+ };
174
+ }
175
+ /** Resolve `FrameImageOptions.resize`, defaulting to the (already cropped) source dimensions. */
176
+ function resolveResizeTarget(cropWidth, cropHeight, resize) {
177
+ if (!resize) return {
178
+ width: cropWidth,
179
+ height: cropHeight
180
+ };
181
+ return {
182
+ width: Math.max(1, Math.round(resize.width)),
183
+ height: Math.max(1, Math.round(resize.height))
184
+ };
185
+ }
186
+ /**
187
+ * The forked child's whole runtime: decode-loop lifecycle, the latest-wins
188
+ * frame slot, and the ROI scaler for `toBuffer`. One instance per process.
189
+ */
190
+ var DecodeWorkerChild = class {
191
+ nav = null;
192
+ consts = null;
193
+ hwContext = null;
194
+ /**
195
+ * Reused software scaler + the geometry/format key it was built for.
196
+ * Building a fresh `SoftwareScaleContext` on EVERY `toBuffer` (24fps)
197
+ * churned native libav memory unboundedly (a persistent worker grew to
198
+ * ~10GB/h). Detection uses a stable ROI + resize target, so the scaler is
199
+ * built once and reused, rebuilt only when the key changes — mirroring the
200
+ * shared decoder session (`nodeav-decoder-session.ts`, which reuses one
201
+ * `this.scaler` for the same reason). Disposed in `teardown`.
202
+ */
203
+ scaler = null;
204
+ scalerKey = "";
205
+ demuxer = null;
206
+ decoder = null;
207
+ abortController = null;
208
+ started = false;
209
+ stopped = false;
210
+ sessionFormat = "rgb";
211
+ minIntervalMs = 0;
212
+ /**
213
+ * The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
214
+ * pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
215
+ * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
216
+ * node-av. See that file's doc comment for the full semantics.
217
+ */
218
+ frames = new FrameSlot();
219
+ /**
220
+ * Mirrors whether `this.frames` currently has a pull waiting, so
221
+ * `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
222
+ * intentionally exposes no pending-pull query, only `markPendingPull()`,
223
+ * so the child tracks this in lockstep (set in `handlePull`, cleared
224
+ * whenever `consumeDecodedFrame`'s `publish` resolves it).
225
+ */
226
+ pendingPull = false;
227
+ ended = false;
228
+ redialTimer = null;
229
+ handleMessage(message) {
230
+ if (!require_worker_protocol.isWorkerRequest(message)) {
231
+ this.sendError("decode-worker-child: malformed request");
232
+ return;
233
+ }
234
+ switch (message.kind) {
235
+ case "start":
236
+ this.handleStart(message.source, message.opts);
237
+ return;
238
+ case "pull":
239
+ this.handlePull();
240
+ return;
241
+ case "toBuffer":
242
+ this.handleToBuffer(message.frameId, message.opts);
243
+ return;
244
+ case "stop":
245
+ this.handleStop();
246
+ return;
247
+ }
248
+ }
249
+ /** Channel teardown (explicit `stop` or the parent disconnecting) — dispose, then exit. */
250
+ teardown() {
251
+ this.stopped = true;
252
+ this.ended = true;
253
+ if (this.redialTimer) {
254
+ clearTimeout(this.redialTimer);
255
+ this.redialTimer = null;
256
+ }
257
+ this.abortController?.abort();
258
+ this.closeInput();
259
+ this.frames.free();
260
+ this.scaler?.[Symbol.dispose]?.();
261
+ this.scaler = null;
262
+ this.scalerKey = "";
263
+ this.hwContext?.[Symbol.dispose]?.();
264
+ this.hwContext = null;
265
+ if (this.pendingPull) {
266
+ this.pendingPull = false;
267
+ this.send({ kind: "ended" });
268
+ }
269
+ }
270
+ handleStart(source, opts) {
271
+ if (this.started) {
272
+ process.stderr.write("decode-worker-child: duplicate start ignored\n");
273
+ return;
274
+ }
275
+ this.started = true;
276
+ this.sessionFormat = opts.format ?? "rgb";
277
+ this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
278
+ this.runDecodeLoop(source.restreamUrl).catch((err) => {
279
+ this.sendError(`decode-worker-child: decode loop crashed — ${errMessage(err)}`);
280
+ });
281
+ }
282
+ handlePull() {
283
+ if (this.ended) {
284
+ this.send({ kind: "ended" });
285
+ return;
286
+ }
287
+ const reply = this.frames.pull();
288
+ if (reply) {
289
+ this.send(this.toFrameReply(reply));
290
+ return;
291
+ }
292
+ this.pendingPull = true;
293
+ this.frames.markPendingPull();
294
+ }
295
+ handleToBuffer(frameId, opts) {
296
+ const frame = this.frames.toBuffer(frameId);
297
+ if (!frame) {
298
+ this.sendError(`decode-worker-child: toBuffer for unknown or stale frameId ${frameId}`, frameId);
299
+ return;
300
+ }
301
+ try {
302
+ const bytes = this.scaleRoiToBuffer(frame, opts);
303
+ this.send({
304
+ kind: "buffer",
305
+ frameId,
306
+ bytes
307
+ });
308
+ } catch (err) {
309
+ this.sendError(`decode-worker-child: toBuffer failed — ${errMessage(err)}`, frameId);
310
+ }
311
+ }
312
+ handleStop() {
313
+ this.teardown();
314
+ process.exit(0);
315
+ }
316
+ async runDecodeLoop(url) {
317
+ const nav = await getNodeAv();
318
+ const C = await getConstants();
319
+ if (this.stopped) return;
320
+ this.nav = nav;
321
+ this.consts = C;
322
+ nav.Log.setLevel(C.AV_LOG_FATAL);
323
+ await this.ensureHwContext(nav, C);
324
+ while (!this.stopped) {
325
+ try {
326
+ await this.dialAndDecode(nav, C, url);
327
+ } catch (err) {
328
+ if (this.stopped) break;
329
+ process.stderr.write(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
330
+ } finally {
331
+ this.closeInput();
332
+ }
333
+ if (this.stopped) break;
334
+ await this.sleep(REDIAL_MS);
335
+ }
336
+ }
337
+ /**
338
+ * One dial: open the demuxer, pick the video stream, build a HW/SW decoder
339
+ * that downloads + normalises frames to YUV420P system memory (so the ROI
340
+ * scaler always reads plain software planes regardless of hwaccel), and
341
+ * pump `demuxer.packets → decoder.frames → consumeDecodedFrame` until the
342
+ * stream ends, errors, or teardown aborts it.
343
+ *
344
+ * Copied from `nodeav-decoder-session.ts` `pullDialAndDecode` (lines
345
+ * ~916-996): same demuxer options, same `rescale.pixelFormat: YUV420P`
346
+ * download-always contract (keeping decoded frames on the GPU leaked the
347
+ * VAAPI surface pool on every re-dial — see that file's comment), same
348
+ * `exitOnError: false` bad-frame tolerance.
349
+ */
350
+ async dialAndDecode(nav, C, url) {
351
+ this.abortController = new AbortController();
352
+ const demuxer = await nav.Demuxer.open(url, {
353
+ options: buildDemuxerOptions(),
354
+ signal: this.abortController.signal
355
+ });
356
+ if (this.stopped) {
357
+ demuxer[Symbol.dispose]?.();
358
+ return;
359
+ }
360
+ this.demuxer = demuxer;
361
+ const videoStream = demuxer.video();
362
+ if (!videoStream) throw new Error("decode-worker-child: pull input has no video stream");
363
+ const decoder = await nav.Decoder.create(videoStream, {
364
+ ...this.hwContext ? { hardware: this.hwContext } : {},
365
+ rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
366
+ exitOnError: false
367
+ });
368
+ if (this.stopped) {
369
+ decoder[Symbol.dispose]?.();
370
+ return;
371
+ }
372
+ this.decoder = decoder;
373
+ for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
374
+ if (this.stopped) break;
375
+ if (!frame) continue;
376
+ this.consumeDecodedFrame(frame);
377
+ }
378
+ }
379
+ /**
380
+ * Publish a decoded frame into the latest-wins slot (mirrors Scrypted's
381
+ * `libav.py:32,50-53` single-slot queue) via `FrameSlot`, which handles
382
+ * drop-older, the `minIntervalMs` throttle, and resolving a pending `pull`.
383
+ */
384
+ consumeDecodedFrame(frame) {
385
+ if (frame.isHwFrame()) {
386
+ frame.free();
387
+ return;
388
+ }
389
+ const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
390
+ if (reply) {
391
+ this.pendingPull = false;
392
+ this.send(this.toFrameReply(reply));
393
+ }
394
+ }
395
+ toFrameReply(reply) {
396
+ return {
397
+ kind: "frame",
398
+ frameId: reply.frameId,
399
+ timestamp: reply.timestamp,
400
+ width: reply.width,
401
+ height: reply.height,
402
+ format: this.sessionFormat
403
+ };
404
+ }
405
+ /**
406
+ * ROI `toBuffer`: crop (via plane-pointer offset, not a re-decode) + resize
407
+ * + format-convert a retained decoded frame with `SoftwareScaleContext`.
408
+ * Writes straight into a tightly-packed destination buffer via the
409
+ * low-level `scaleSync` (no `dstFrame` + row-strip step needed — the same
410
+ * zero-copy destination technique as `scaleIntoRingSlot` in
411
+ * `nodeav-decoder-session.ts`, adapted to also OFFSET the source planes so
412
+ * a single scaler call handles the crop.
413
+ */
414
+ scaleRoiToBuffer(frame, opts) {
415
+ const nav = this.nav;
416
+ const C = this.consts;
417
+ if (!nav || !C) throw new Error("node-av not initialized");
418
+ const region = resolveCropRegion(frame.width, frame.height, opts?.crop);
419
+ const target = resolveResizeTarget(region.width, region.height, opts?.resize);
420
+ const format = opts?.format ?? this.sessionFormat;
421
+ const dstFmt = format === "gray" ? C.AV_PIX_FMT_GRAY8 : C.AV_PIX_FMT_RGB24;
422
+ const channels = format === "gray" ? 1 : 3;
423
+ const planes = frame.data;
424
+ const strides = frame.linesize;
425
+ const yPlane = planes?.[0];
426
+ const uPlane = planes?.[1];
427
+ const vPlane = planes?.[2];
428
+ if (!yPlane || !uPlane || !vPlane) throw new Error("decode-worker-child: decoded frame missing planar YUV420P data");
429
+ const yStride = strides[0] ?? 0;
430
+ const cStride = strides[1] ?? 0;
431
+ const ySlice = yPlane.subarray(region.top * yStride + region.left);
432
+ const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
433
+ const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
434
+ const scaler = this.ensureScaler(nav, C, region.width, region.height, target.width, target.height, dstFmt);
435
+ const dstStride = target.width * channels;
436
+ const dstBuffer = Buffer.allocUnsafe(dstStride * target.height);
437
+ const scaledHeight = scaler.scaleSync([
438
+ ySlice,
439
+ uSlice,
440
+ vSlice
441
+ ], [
442
+ yStride,
443
+ cStride,
444
+ cStride
445
+ ], 0, region.height, [dstBuffer], [dstStride]);
446
+ if (scaledHeight < 0) throw new Error(`sws_scale failed: ${scaledHeight}`);
447
+ if (scaledHeight !== target.height) throw new Error(`scaler produced ${scaledHeight} of ${target.height} rows`);
448
+ return dstBuffer;
449
+ }
450
+ /**
451
+ * Build-once / reuse the {@link NavScaler} for a given src+dst geometry and
452
+ * dst format. Rebuilt (old disposed) only when the key changes — detection's
453
+ * ROI + resize target are stable, so this is a per-worker singleton in the
454
+ * steady state. Replaces the previous per-frame `new SoftwareScaleContext()`
455
+ * that leaked native libav memory at frame rate. Synchronous + single-thread
456
+ * (Node), so no epoch/race guard is needed (unlike the async shared session).
457
+ */
458
+ ensureScaler(nav, C, srcW, srcH, dstW, dstH, dstFmt) {
459
+ const key = `${srcW}x${srcH}->${dstW}x${dstH}:${dstFmt}`;
460
+ const cached = this.scaler;
461
+ if (cached && key === this.scalerKey) return cached;
462
+ this.scaler?.[Symbol.dispose]?.();
463
+ this.scaler = null;
464
+ this.scalerKey = "";
465
+ const scaler = new nav.SoftwareScaleContext();
466
+ scaler.getContext(srcW, srcH, C.AV_PIX_FMT_YUV420P, dstW, dstH, dstFmt, C.SWS_FAST_BILINEAR);
467
+ const initRet = scaler.initContext();
468
+ if (initRet < 0) {
469
+ scaler[Symbol.dispose]?.();
470
+ throw new Error(`sws_init_context failed: ${initRet}`);
471
+ }
472
+ this.scaler = scaler;
473
+ this.scalerKey = key;
474
+ return scaler;
475
+ }
476
+ /**
477
+ * Resolve the HW context ONCE (reused across every re-dial, freed in
478
+ * `teardown`). Tries the platform's ordered candidate list; the first
479
+ * `HardwareContext.create` that succeeds wins; every failure falls through
480
+ * to software decode (`hardware` omitted from `Decoder.create`).
481
+ */
482
+ async ensureHwContext(nav, C) {
483
+ for (const backend of candidateBackends()) {
484
+ const deviceType = backendToHwDeviceConst(backend, C);
485
+ if (deviceType === null) continue;
486
+ const hw = nav.HardwareContext.create(deviceType);
487
+ if (!hw) {
488
+ process.stderr.write(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
489
+ continue;
490
+ }
491
+ this.hwContext = hw;
492
+ return;
493
+ }
494
+ process.stderr.write("decode-worker-child: no hwaccel backend available — using software decode\n");
495
+ }
496
+ /** Interruptible re-dial backoff — `teardown` clears the pending timer. */
497
+ sleep(ms) {
498
+ return new Promise((resolve) => {
499
+ this.redialTimer = setTimeout(() => {
500
+ this.redialTimer = null;
501
+ resolve();
502
+ }, ms);
503
+ });
504
+ }
505
+ /** Dispose the CURRENT dial's demuxer + decoder (the HW context is reused across dials). */
506
+ closeInput() {
507
+ this.decoder?.[Symbol.dispose]?.();
508
+ this.decoder = null;
509
+ this.demuxer?.[Symbol.dispose]?.();
510
+ this.demuxer = null;
511
+ }
512
+ send(reply) {
513
+ process.send?.(reply);
514
+ }
515
+ sendError(message, frameId) {
516
+ process.stderr.write(`${message}\n`);
517
+ this.send(frameId === void 0 ? {
518
+ kind: "error",
519
+ message
520
+ } : {
521
+ kind: "error",
522
+ message,
523
+ frameId
524
+ });
525
+ }
526
+ };
527
+ var worker = new DecodeWorkerChild();
528
+ process.on("message", (message) => {
529
+ worker.handleMessage(message);
530
+ });
531
+ process.on("disconnect", () => {
532
+ worker.teardown();
533
+ process.exit(0);
534
+ });
535
+ process.on("uncaughtException", (err) => {
536
+ process.stderr.write(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
537
+ worker.teardown();
538
+ process.exit(1);
539
+ });
540
+ process.on("unhandledRejection", (reason) => {
541
+ process.stderr.write(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
542
+ });
543
+ //#endregion
544
+ exports.DecodeWorkerChild = DecodeWorkerChild;