@camstack/addon-pipeline 1.1.42 → 1.1.44

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 (36) hide show
  1. package/dist/audio-analyzer/index.js +5 -4
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/chunk-D6vf50IK.js +28 -0
  4. package/dist/detection-pipeline/index.js +535 -115
  5. package/dist/detection-pipeline/index.mjs +529 -110
  6. package/dist/{dist-GFd8M6KO.mjs → dist-CgBGV988.mjs} +214 -186
  7. package/dist/{dist-NvwN60Fq.js → dist-CySHUkXY.js} +219 -185
  8. package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
  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 +122 -287
  12. package/dist/pipeline-runner/index.mjs +118 -285
  13. package/dist/recorder/index.js +78 -31
  14. package/dist/recorder/index.mjs +75 -29
  15. package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-B4hRwchu.mjs} +219 -2
  16. package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-D9m0z1N9.js} +236 -1
  17. package/dist/session-decode/decode-worker-child.js +118 -12
  18. package/dist/session-decode/decode-worker-child.mjs +115 -12
  19. package/dist/stream-broker/_stub.js +550 -523
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-BSzP3BIj.mjs} +2 -2
  24. package/dist/stream-broker/index.js +840 -196
  25. package/dist/stream-broker/index.mjs +831 -188
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-TTWGGaqv.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js} +1 -1
  30. package/embed-dist/assets/index-C1oaL9s7.js +81 -0
  31. package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +2 -2
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs +0 -26
  36. package/embed-dist/assets/index-CX-rILhw.js +0 -81
@@ -1,5 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_chunk = require("../chunk-D6vf50IK.js");
2
3
  const require_worker_protocol = require("../worker-protocol-BCfO8gUF.js");
4
+ let node_url = require("node:url");
5
+ let node_fs = require("node:fs");
6
+ node_fs = require_chunk.__toESM(node_fs);
7
+ let node_path = require("node:path");
8
+ node_path = require_chunk.__toESM(node_path);
3
9
  //#region src/session-decode/frame-slot.ts
4
10
  function toReply(held) {
5
11
  return {
@@ -104,6 +110,30 @@ var FrameSlot = class {
104
110
  };
105
111
  //#endregion
106
112
  //#region src/session-decode/decode-worker-child.ts
113
+ /**
114
+ * Decode-worker CHILD entry point — Epic C P1, Task 2.
115
+ *
116
+ * This file is the whole program run by a per-session `child_process.fork`
117
+ * that the pipeline-runner's session-decode coordinator (Task 3) spawns and
118
+ * tears down on session end, so the OS reclaims the VAAPI/GPU decode pool by
119
+ * process exit (Task 0 live-validated that reclaim-by-exit works — node-av
120
+ * does not release the hw_frames_ctx pool on `dispose()` alone). It decodes
121
+ * with node-av in-process and hands decoded frames to its parent as a
122
+ * request/response PULL over its own dedicated fork IPC channel. Frames
123
+ * NEVER touch `/dev/shm` — no shm sink is ever constructed here.
124
+ *
125
+ * ‼ SANCTIONED RAW IPC — READ BEFORE "FIXING" THIS FILE ‼
126
+ * CLAUDE.md rule #8 forbids `process.send`/`process.on('message')` for
127
+ * BUSINESS LOGIC — that rule governs cross-ADDON traffic, which must travel
128
+ * over the UDS cap/event channel (`LocalChildRegistry`/`LocalChildClient`)
129
+ * instead of raw IPC. This file is NOT a second addon: it is a private
130
+ * worker directly forked by the pipeline-runner addon, exactly like the
131
+ * Python inference pool's own subprocess. The Epic C P1 plan's Global
132
+ * Constraints explicitly carve this out — "a directly-forked worker child
133
+ * talks to its parent over its own IPC channel (this is NOT a cap call and
134
+ * NOT `$process`)". Raw `process.on('message')` / `process.send` are the
135
+ * correct, sanctioned mechanism for this specific file only.
136
+ */
107
137
  /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
108
138
  var REDIAL_MS = 3e3;
109
139
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
@@ -129,12 +159,66 @@ function pixelFilterName(format) {
129
159
  }
130
160
  var _nav = null;
131
161
  var _consts = null;
162
+ /**
163
+ * Candidate file URLs for a node-av entry (`.` or `./constants`) in the
164
+ * module locations an AGENT lays node-av out in.
165
+ *
166
+ * WHY: this worker runs as a plain `child_process.fork` (see `fork-decode-
167
+ * worker.ts`) which does NOT inherit the addon-runner's module resolution
168
+ * (NODE_PATH / the framework ESM hook). On the HUB the fork inherits that hook,
169
+ * so the bare `import('node-av')` resolves. On an AGENT it does not, so the
170
+ * bare import throws `Cannot find package 'node-av'` and we resolve node-av
171
+ * explicitly from the agent's known layout:
172
+ * • DOCKER agent — the framework overlay `CAMSTACK_FRAMEWORK_DIR`
173
+ * (`/data/framework/node_modules/node-av`).
174
+ * • ELECTRON agent — the app bundle beside `CAMSTACK_BUNDLED_ADDONS_DIR`
175
+ * (`<resources>/addons`): `<resources>/node_modules/node-av` (+ the
176
+ * `app.asar.unpacked` mirror).
177
+ * Both env vars point at real dirs only on the respective platform; a missing
178
+ * dir is skipped, and `[]` (dev / hub with the bare import already working)
179
+ * keeps the bare specifier the only path.
180
+ */
181
+ function fallbackNodeAvUrls(subpath) {
182
+ const rel = subpath === "." ? "dist/index.js" : "dist/constants/index.js";
183
+ const dirs = [];
184
+ const frameworkDir = process.env["CAMSTACK_FRAMEWORK_DIR"];
185
+ if (frameworkDir !== void 0 && frameworkDir.length > 0) dirs.push(node_path.join(frameworkDir, "node_modules", "node-av"));
186
+ const bundledAddons = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
187
+ if (bundledAddons !== void 0 && bundledAddons.length > 0) {
188
+ const resourcesRoot = node_path.dirname(bundledAddons);
189
+ dirs.push(node_path.join(resourcesRoot, "node_modules", "node-av"));
190
+ dirs.push(node_path.join(resourcesRoot, "app.asar.unpacked", "node_modules", "node-av"));
191
+ }
192
+ const urls = [];
193
+ for (const dir of dirs) {
194
+ const entry = node_path.join(dir, rel);
195
+ if (node_fs.existsSync(entry)) urls.push((0, node_url.pathToFileURL)(entry).href);
196
+ }
197
+ return urls;
198
+ }
199
+ /**
200
+ * Import a node-av entry, falling back to the agent's explicit node-av path
201
+ * when the bare specifier can't be resolved (see {@link fallbackNodeAvUrls}).
202
+ * Each candidate is tried in turn so a copy whose native binary fails to load
203
+ * doesn't mask a working one. A dynamic `import(url)` is typed `any`, so the
204
+ * caller's declared module type applies without a cast.
205
+ */
206
+ async function importNodeAv(bareSpecifier, subpath) {
207
+ try {
208
+ return await import(bareSpecifier);
209
+ } catch (bareErr) {
210
+ for (const url of fallbackNodeAvUrls(subpath)) try {
211
+ return await import(url);
212
+ } catch {}
213
+ throw bareErr;
214
+ }
215
+ }
132
216
  async function getNodeAv() {
133
- if (!_nav) _nav = await import("node-av");
217
+ if (!_nav) _nav = await importNodeAv("node-av", ".");
134
218
  return _nav;
135
219
  }
136
220
  async function getConstants() {
137
- if (!_consts) _consts = await import("node-av/constants");
221
+ if (!_consts) _consts = await importNodeAv("node-av/constants", "./constants");
138
222
  return _consts;
139
223
  }
140
224
  function errMessage(err) {
@@ -318,6 +402,16 @@ var DecodeWorkerChild = class {
318
402
  abortController = null;
319
403
  started = false;
320
404
  stopped = false;
405
+ /**
406
+ * The numeric device id this worker decodes for — learned from the `start`
407
+ * message's {@link VideoFrameSource}. The child's stderr is INHERITED by the
408
+ * pipeline-runner (silent:false fork), so these lines land in the runner's
409
+ * raw stderr WITHOUT passing through a scoped logger; prefixing every line
410
+ * with `[dev:<id>]` (see {@link emitStderr}) is what lets the per-device log
411
+ * UI attribute the forked worker's output to its camera. `null` only for the
412
+ * brief pre-`start` window (e.g. a malformed first message).
413
+ */
414
+ deviceId = null;
321
415
  sessionFormat = "rgb";
322
416
  minIntervalMs = 0;
323
417
  /**
@@ -390,16 +484,17 @@ var DecodeWorkerChild = class {
390
484
  const deliveredDelta = this.framesDelivered - this.lastDeliveredSnapshot;
391
485
  const deliveredFps = Math.round(deliveredDelta / windowMs * 1e3 * 10) / 10;
392
486
  const skipped = this.framesSkipped + this.frames.droppedCount;
393
- process.stderr.write(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
487
+ this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
394
488
  this.lastMetricsAt = now;
395
489
  this.lastDeliveredSnapshot = this.framesDelivered;
396
490
  }
397
491
  handleStart(source, opts) {
398
492
  if (this.started) {
399
- process.stderr.write("decode-worker-child: duplicate start ignored\n");
493
+ this.emitStderr("decode-worker-child: duplicate start ignored\n");
400
494
  return;
401
495
  }
402
496
  this.started = true;
497
+ this.deviceId = source.deviceId;
403
498
  this.sessionFormat = opts.format ?? "rgb";
404
499
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
405
500
  this.lastMetricsAt = Date.now();
@@ -456,7 +551,7 @@ var DecodeWorkerChild = class {
456
551
  await this.dialAndDecode(nav, C, url);
457
552
  } catch (err) {
458
553
  if (this.stopped) break;
459
- process.stderr.write(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
554
+ this.emitStderr(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
460
555
  } finally {
461
556
  this.closeInput();
462
557
  }
@@ -549,7 +644,7 @@ var DecodeWorkerChild = class {
549
644
  return;
550
645
  }
551
646
  this.hwFilterDecision = "hardware";
552
- process.stderr.write(`decode-worker-child: GPU crop+scale path active (${this.gpuScaleFilter})\n`);
647
+ this.emitStderr(`decode-worker-child: GPU crop+scale path active (${this.gpuScaleFilter})\n`);
553
648
  }
554
649
  this.publishFrame(frame);
555
650
  }
@@ -587,7 +682,7 @@ var DecodeWorkerChild = class {
587
682
  for (const out of outputs) out.free();
588
683
  return ok;
589
684
  } catch (err) {
590
- process.stderr.write(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
685
+ this.emitStderr(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
591
686
  return false;
592
687
  } finally {
593
688
  filter?.close();
@@ -764,7 +859,7 @@ var DecodeWorkerChild = class {
764
859
  if (deviceType === null) continue;
765
860
  const hw = nav.HardwareContext.create(deviceType);
766
861
  if (!hw) {
767
- process.stderr.write(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
862
+ this.emitStderr(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
768
863
  continue;
769
864
  }
770
865
  this.hwContext = hw;
@@ -772,7 +867,7 @@ var DecodeWorkerChild = class {
772
867
  this.hwFilterDecision = this.gpuScaleFilter !== null ? "undecided" : "software";
773
868
  return;
774
869
  }
775
- process.stderr.write("decode-worker-child: no hwaccel backend available — using software decode\n");
870
+ this.emitStderr("decode-worker-child: no hwaccel backend available — using software decode\n");
776
871
  }
777
872
  /** Interruptible re-dial backoff — `teardown` clears the pending timer. */
778
873
  sleep(ms) {
@@ -803,8 +898,19 @@ var DecodeWorkerChild = class {
803
898
  send(reply) {
804
899
  process.send?.(reply);
805
900
  }
901
+ /**
902
+ * Write one diagnostic line to the (inherited) stderr, prefixed with the
903
+ * device tag so the per-device log UI can attribute the forked worker's
904
+ * output. Public so the module-level process handlers — which only hold the
905
+ * `worker` instance — emit device-tagged lines too. Callers pass the line
906
+ * INCLUDING its trailing newline (unchanged from the prior direct writes).
907
+ */
908
+ emitStderr(line) {
909
+ const tag = this.deviceId === null ? "[dev:?]" : `[dev:${this.deviceId}]`;
910
+ process.stderr.write(`${tag} ${line}`);
911
+ }
806
912
  sendError(message, frameId) {
807
- process.stderr.write(`${message}\n`);
913
+ this.emitStderr(`${message}\n`);
808
914
  this.send(frameId === void 0 ? {
809
915
  kind: "error",
810
916
  message
@@ -824,12 +930,12 @@ process.on("disconnect", () => {
824
930
  process.exit(0);
825
931
  });
826
932
  process.on("uncaughtException", (err) => {
827
- process.stderr.write(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
933
+ worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
828
934
  worker.teardown();
829
935
  process.exit(1);
830
936
  });
831
937
  process.on("unhandledRejection", (reason) => {
832
- process.stderr.write(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
938
+ worker.emitStderr(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
833
939
  });
834
940
  //#endregion
835
941
  exports.DecodeWorkerChild = DecodeWorkerChild;
@@ -1,4 +1,7 @@
1
1
  import { n as isWorkerRequest } from "../worker-protocol-pk7qdYXt.mjs";
2
+ import { pathToFileURL } from "node:url";
3
+ import * as fs from "node:fs";
4
+ import * as path$1 from "node:path";
2
5
  //#region src/session-decode/frame-slot.ts
3
6
  function toReply(held) {
4
7
  return {
@@ -103,6 +106,30 @@ var FrameSlot = class {
103
106
  };
104
107
  //#endregion
105
108
  //#region src/session-decode/decode-worker-child.ts
109
+ /**
110
+ * Decode-worker CHILD entry point — Epic C P1, Task 2.
111
+ *
112
+ * This file is the whole program run by a per-session `child_process.fork`
113
+ * that the pipeline-runner's session-decode coordinator (Task 3) spawns and
114
+ * tears down on session end, so the OS reclaims the VAAPI/GPU decode pool by
115
+ * process exit (Task 0 live-validated that reclaim-by-exit works — node-av
116
+ * does not release the hw_frames_ctx pool on `dispose()` alone). It decodes
117
+ * with node-av in-process and hands decoded frames to its parent as a
118
+ * request/response PULL over its own dedicated fork IPC channel. Frames
119
+ * NEVER touch `/dev/shm` — no shm sink is ever constructed here.
120
+ *
121
+ * ‼ SANCTIONED RAW IPC — READ BEFORE "FIXING" THIS FILE ‼
122
+ * CLAUDE.md rule #8 forbids `process.send`/`process.on('message')` for
123
+ * BUSINESS LOGIC — that rule governs cross-ADDON traffic, which must travel
124
+ * over the UDS cap/event channel (`LocalChildRegistry`/`LocalChildClient`)
125
+ * instead of raw IPC. This file is NOT a second addon: it is a private
126
+ * worker directly forked by the pipeline-runner addon, exactly like the
127
+ * Python inference pool's own subprocess. The Epic C P1 plan's Global
128
+ * Constraints explicitly carve this out — "a directly-forked worker child
129
+ * talks to its parent over its own IPC channel (this is NOT a cap call and
130
+ * NOT `$process`)". Raw `process.on('message')` / `process.send` are the
131
+ * correct, sanctioned mechanism for this specific file only.
132
+ */
106
133
  /** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
107
134
  var REDIAL_MS = 3e3;
108
135
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
@@ -128,12 +155,66 @@ function pixelFilterName(format) {
128
155
  }
129
156
  var _nav = null;
130
157
  var _consts = null;
158
+ /**
159
+ * Candidate file URLs for a node-av entry (`.` or `./constants`) in the
160
+ * module locations an AGENT lays node-av out in.
161
+ *
162
+ * WHY: this worker runs as a plain `child_process.fork` (see `fork-decode-
163
+ * worker.ts`) which does NOT inherit the addon-runner's module resolution
164
+ * (NODE_PATH / the framework ESM hook). On the HUB the fork inherits that hook,
165
+ * so the bare `import('node-av')` resolves. On an AGENT it does not, so the
166
+ * bare import throws `Cannot find package 'node-av'` and we resolve node-av
167
+ * explicitly from the agent's known layout:
168
+ * • DOCKER agent — the framework overlay `CAMSTACK_FRAMEWORK_DIR`
169
+ * (`/data/framework/node_modules/node-av`).
170
+ * • ELECTRON agent — the app bundle beside `CAMSTACK_BUNDLED_ADDONS_DIR`
171
+ * (`<resources>/addons`): `<resources>/node_modules/node-av` (+ the
172
+ * `app.asar.unpacked` mirror).
173
+ * Both env vars point at real dirs only on the respective platform; a missing
174
+ * dir is skipped, and `[]` (dev / hub with the bare import already working)
175
+ * keeps the bare specifier the only path.
176
+ */
177
+ function fallbackNodeAvUrls(subpath) {
178
+ const rel = subpath === "." ? "dist/index.js" : "dist/constants/index.js";
179
+ const dirs = [];
180
+ const frameworkDir = process.env["CAMSTACK_FRAMEWORK_DIR"];
181
+ if (frameworkDir !== void 0 && frameworkDir.length > 0) dirs.push(path$1.join(frameworkDir, "node_modules", "node-av"));
182
+ const bundledAddons = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
183
+ if (bundledAddons !== void 0 && bundledAddons.length > 0) {
184
+ const resourcesRoot = path$1.dirname(bundledAddons);
185
+ dirs.push(path$1.join(resourcesRoot, "node_modules", "node-av"));
186
+ dirs.push(path$1.join(resourcesRoot, "app.asar.unpacked", "node_modules", "node-av"));
187
+ }
188
+ const urls = [];
189
+ for (const dir of dirs) {
190
+ const entry = path$1.join(dir, rel);
191
+ if (fs.existsSync(entry)) urls.push(pathToFileURL(entry).href);
192
+ }
193
+ return urls;
194
+ }
195
+ /**
196
+ * Import a node-av entry, falling back to the agent's explicit node-av path
197
+ * when the bare specifier can't be resolved (see {@link fallbackNodeAvUrls}).
198
+ * Each candidate is tried in turn so a copy whose native binary fails to load
199
+ * doesn't mask a working one. A dynamic `import(url)` is typed `any`, so the
200
+ * caller's declared module type applies without a cast.
201
+ */
202
+ async function importNodeAv(bareSpecifier, subpath) {
203
+ try {
204
+ return await import(bareSpecifier);
205
+ } catch (bareErr) {
206
+ for (const url of fallbackNodeAvUrls(subpath)) try {
207
+ return await import(url);
208
+ } catch {}
209
+ throw bareErr;
210
+ }
211
+ }
131
212
  async function getNodeAv() {
132
- if (!_nav) _nav = await import("node-av");
213
+ if (!_nav) _nav = await importNodeAv("node-av", ".");
133
214
  return _nav;
134
215
  }
135
216
  async function getConstants() {
136
- if (!_consts) _consts = await import("node-av/constants");
217
+ if (!_consts) _consts = await importNodeAv("node-av/constants", "./constants");
137
218
  return _consts;
138
219
  }
139
220
  function errMessage(err) {
@@ -317,6 +398,16 @@ var DecodeWorkerChild = class {
317
398
  abortController = null;
318
399
  started = false;
319
400
  stopped = false;
401
+ /**
402
+ * The numeric device id this worker decodes for — learned from the `start`
403
+ * message's {@link VideoFrameSource}. The child's stderr is INHERITED by the
404
+ * pipeline-runner (silent:false fork), so these lines land in the runner's
405
+ * raw stderr WITHOUT passing through a scoped logger; prefixing every line
406
+ * with `[dev:<id>]` (see {@link emitStderr}) is what lets the per-device log
407
+ * UI attribute the forked worker's output to its camera. `null` only for the
408
+ * brief pre-`start` window (e.g. a malformed first message).
409
+ */
410
+ deviceId = null;
320
411
  sessionFormat = "rgb";
321
412
  minIntervalMs = 0;
322
413
  /**
@@ -389,16 +480,17 @@ var DecodeWorkerChild = class {
389
480
  const deliveredDelta = this.framesDelivered - this.lastDeliveredSnapshot;
390
481
  const deliveredFps = Math.round(deliveredDelta / windowMs * 1e3 * 10) / 10;
391
482
  const skipped = this.framesSkipped + this.frames.droppedCount;
392
- process.stderr.write(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
483
+ this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
393
484
  this.lastMetricsAt = now;
394
485
  this.lastDeliveredSnapshot = this.framesDelivered;
395
486
  }
396
487
  handleStart(source, opts) {
397
488
  if (this.started) {
398
- process.stderr.write("decode-worker-child: duplicate start ignored\n");
489
+ this.emitStderr("decode-worker-child: duplicate start ignored\n");
399
490
  return;
400
491
  }
401
492
  this.started = true;
493
+ this.deviceId = source.deviceId;
402
494
  this.sessionFormat = opts.format ?? "rgb";
403
495
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
404
496
  this.lastMetricsAt = Date.now();
@@ -455,7 +547,7 @@ var DecodeWorkerChild = class {
455
547
  await this.dialAndDecode(nav, C, url);
456
548
  } catch (err) {
457
549
  if (this.stopped) break;
458
- process.stderr.write(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
550
+ this.emitStderr(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
459
551
  } finally {
460
552
  this.closeInput();
461
553
  }
@@ -548,7 +640,7 @@ var DecodeWorkerChild = class {
548
640
  return;
549
641
  }
550
642
  this.hwFilterDecision = "hardware";
551
- process.stderr.write(`decode-worker-child: GPU crop+scale path active (${this.gpuScaleFilter})\n`);
643
+ this.emitStderr(`decode-worker-child: GPU crop+scale path active (${this.gpuScaleFilter})\n`);
552
644
  }
553
645
  this.publishFrame(frame);
554
646
  }
@@ -586,7 +678,7 @@ var DecodeWorkerChild = class {
586
678
  for (const out of outputs) out.free();
587
679
  return ok;
588
680
  } catch (err) {
589
- process.stderr.write(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
681
+ this.emitStderr(`decode-worker-child: GPU filter probe failed — using software decode: ${errMessage(err)}\n`);
590
682
  return false;
591
683
  } finally {
592
684
  filter?.close();
@@ -763,7 +855,7 @@ var DecodeWorkerChild = class {
763
855
  if (deviceType === null) continue;
764
856
  const hw = nav.HardwareContext.create(deviceType);
765
857
  if (!hw) {
766
- process.stderr.write(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
858
+ this.emitStderr(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
767
859
  continue;
768
860
  }
769
861
  this.hwContext = hw;
@@ -771,7 +863,7 @@ var DecodeWorkerChild = class {
771
863
  this.hwFilterDecision = this.gpuScaleFilter !== null ? "undecided" : "software";
772
864
  return;
773
865
  }
774
- process.stderr.write("decode-worker-child: no hwaccel backend available — using software decode\n");
866
+ this.emitStderr("decode-worker-child: no hwaccel backend available — using software decode\n");
775
867
  }
776
868
  /** Interruptible re-dial backoff — `teardown` clears the pending timer. */
777
869
  sleep(ms) {
@@ -802,8 +894,19 @@ var DecodeWorkerChild = class {
802
894
  send(reply) {
803
895
  process.send?.(reply);
804
896
  }
897
+ /**
898
+ * Write one diagnostic line to the (inherited) stderr, prefixed with the
899
+ * device tag so the per-device log UI can attribute the forked worker's
900
+ * output. Public so the module-level process handlers — which only hold the
901
+ * `worker` instance — emit device-tagged lines too. Callers pass the line
902
+ * INCLUDING its trailing newline (unchanged from the prior direct writes).
903
+ */
904
+ emitStderr(line) {
905
+ const tag = this.deviceId === null ? "[dev:?]" : `[dev:${this.deviceId}]`;
906
+ process.stderr.write(`${tag} ${line}`);
907
+ }
805
908
  sendError(message, frameId) {
806
- process.stderr.write(`${message}\n`);
909
+ this.emitStderr(`${message}\n`);
807
910
  this.send(frameId === void 0 ? {
808
911
  kind: "error",
809
912
  message
@@ -823,12 +926,12 @@ process.on("disconnect", () => {
823
926
  process.exit(0);
824
927
  });
825
928
  process.on("uncaughtException", (err) => {
826
- process.stderr.write(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
929
+ worker.emitStderr(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
827
930
  worker.teardown();
828
931
  process.exit(1);
829
932
  });
830
933
  process.on("unhandledRejection", (reason) => {
831
- process.stderr.write(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
934
+ worker.emitStderr(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
832
935
  });
833
936
  //#endregion
834
937
  export { DecodeWorkerChild };