@camstack/system 1.1.24 → 1.1.26

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.
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { BaseAddon, EventCategory, errMsg, platformProbeCapability, scoreRuntimes } from "@camstack/types";
2
+ import { BaseAddon, EventCategory, emitReadiness, errMsg, platformProbeCapability, scoreRuntimes } from "@camstack/types";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as os from "node:os";
@@ -11,7 +11,7 @@ import * as os from "node:os";
11
11
  * entire Node process (WS handshakes, tRPC subscriptions, metrics — all
12
12
  * stalled). Keeping it async turns the probe into a true background task.
13
13
  */
14
- var execFileAsync$2 = promisify(execFile);
14
+ var execFileAsync = promisify(execFile);
15
15
  /** Minimal no-op logger for default parameter */
16
16
  var noopLogger = {
17
17
  debug() {},
@@ -39,7 +39,7 @@ async function getAvailableRAM_MB() {
39
39
  const platform = os.platform();
40
40
  try {
41
41
  if (platform === "darwin") {
42
- const { stdout: output } = await execFileAsync$2("vm_stat", [], {
42
+ const { stdout: output } = await execFileAsync("vm_stat", [], {
43
43
  encoding: "utf8",
44
44
  timeout: 3e3
45
45
  });
@@ -164,7 +164,7 @@ var PlatformScorer = class {
164
164
  }
165
165
  if (platform === "linux") {
166
166
  try {
167
- const { stdout } = await execFileAsync$2("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
167
+ const { stdout } = await execFileAsync("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader"], {
168
168
  encoding: "utf8",
169
169
  timeout: 5e3
170
170
  });
@@ -230,7 +230,7 @@ var PlatformScorer = class {
230
230
  "python"
231
231
  ];
232
232
  for (const cmd of candidates) try {
233
- await execFileAsync$2(cmd, ["--version"], { timeout: 5e3 });
233
+ await execFileAsync(cmd, ["--version"], { timeout: 5e3 });
234
234
  return cmd;
235
235
  } catch (err) {
236
236
  this.logger.debug(`Python command "${cmd}" not found`, { meta: { error: errMsg(err) } });
@@ -328,296 +328,91 @@ var InferenceConfigResolver = class {
328
328
  }
329
329
  };
330
330
  //#endregion
331
- //#region src/builtins/platform-probe/hardware-encoder-probe.ts
332
- var execFileAsync$1 = promisify(execFile);
333
- function buildCandidates(hardware) {
334
- const out = [];
335
- if (hardware.platform === "darwin") {
336
- out.push({
337
- encoder: "h264_videotoolbox",
338
- codec: "H264",
339
- family: "videotoolbox"
340
- });
341
- out.push({
342
- encoder: "hevc_videotoolbox",
343
- codec: "H265",
344
- family: "videotoolbox"
345
- });
346
- }
347
- if (hardware.platform === "linux") {
348
- if (hardware.gpu?.type === "nvidia") {
349
- out.push({
350
- encoder: "h264_nvenc",
351
- codec: "H264",
352
- family: "nvenc"
353
- });
354
- out.push({
355
- encoder: "hevc_nvenc",
356
- codec: "H265",
357
- family: "nvenc"
358
- });
359
- }
360
- if (hardware.gpu?.type === "intel" || hardware.gpu?.type === "amd" || !hardware.gpu) {
361
- out.push({
362
- encoder: "h264_vaapi",
363
- codec: "H264",
364
- family: "vaapi"
365
- });
366
- out.push({
367
- encoder: "hevc_vaapi",
368
- codec: "H265",
369
- family: "vaapi"
370
- });
371
- }
372
- }
373
- return out;
374
- }
375
- async function runTestEncode(candidate, opts) {
376
- const ffmpeg = opts.ffmpegPath ?? "ffmpeg";
377
- const timeout = opts.timeoutMs ?? 8e3;
378
- const nullSink = os.platform() === "win32" ? "NUL" : "/dev/null";
379
- const baseArgs = [
380
- "-hide_banner",
381
- "-loglevel",
382
- "error"
383
- ];
384
- const inArgs = [
385
- "-f",
386
- "lavfi",
387
- "-i",
388
- "testsrc=duration=0.04:size=16x16:rate=25"
389
- ];
390
- let outArgs;
391
- if (candidate.family === "vaapi") outArgs = [
392
- "-vaapi_device",
393
- "/dev/dri/renderD128",
394
- "-vf",
395
- "format=nv12,hwupload",
396
- "-c:v",
397
- candidate.encoder,
398
- "-frames:v",
399
- "1",
400
- "-f",
401
- "null",
402
- nullSink
403
- ];
404
- else outArgs = [
405
- "-c:v",
406
- candidate.encoder,
407
- "-frames:v",
408
- "1",
409
- "-f",
410
- "null",
411
- nullSink
412
- ];
413
- try {
414
- await execFileAsync$1(ffmpeg, [
415
- ...baseArgs,
416
- ...inArgs,
417
- ...outArgs
418
- ], {
419
- timeout,
420
- encoding: "utf8"
421
- });
422
- return {
423
- encoder: candidate.encoder,
424
- codec: candidate.codec,
425
- family: candidate.family,
426
- available: true
427
- };
428
- } catch (err) {
429
- return {
430
- encoder: candidate.encoder,
431
- codec: candidate.codec,
432
- family: candidate.family,
433
- available: false,
434
- reason: errMsg(err)
435
- };
436
- }
437
- }
438
- var HardwareEncoderProber = class {
439
- cached = null;
440
- inflight = null;
441
- logger;
442
- ffmpegPath;
443
- constructor(logger, ffmpegPath = "ffmpeg") {
444
- this.logger = logger;
445
- this.ffmpegPath = ffmpegPath;
446
- }
447
- getCached() {
448
- return this.cached;
449
- }
450
- async probe(hardware, options = {}) {
451
- if (!options.force && this.cached) return this.cached;
452
- if (this.inflight) return this.inflight;
453
- this.inflight = this.runProbe(hardware).then((res) => {
454
- this.cached = res;
455
- this.inflight = null;
456
- return res;
457
- }).catch((err) => {
458
- this.inflight = null;
459
- throw err;
460
- });
461
- return this.inflight;
462
- }
463
- async runProbe(hardware) {
464
- const candidates = buildCandidates(hardware);
465
- const start = Date.now();
466
- this.logger.info("Probing hardware encoders", { meta: {
467
- platform: hardware.platform,
468
- arch: hardware.arch,
469
- candidates: candidates.length
470
- } });
471
- const probes = await Promise.all(candidates.map((c) => runTestEncode(c, { ffmpegPath: this.ffmpegPath })));
472
- probes.push({
473
- encoder: "libx264",
474
- codec: "H264",
475
- family: "software",
476
- available: true
477
- });
478
- probes.push({
479
- encoder: "libx265",
480
- codec: "H265",
481
- family: "software",
482
- available: true
483
- });
484
- const pickDefault = (codec) => {
485
- const hw = probes.find((p) => p.codec === codec && p.available && p.family !== "software");
486
- if (hw) return hw.encoder;
487
- return codec === "H264" ? "libx264" : "libx265";
488
- };
489
- const result = {
490
- encoders: probes,
491
- defaultH264: pickDefault("H264"),
492
- defaultH265: pickDefault("H265"),
493
- probedAt: Date.now()
494
- };
495
- const elapsed = Date.now() - start;
496
- this.logger.info("Hardware encoder probe complete", { meta: {
497
- elapsedMs: elapsed,
498
- defaultH264: result.defaultH264,
499
- defaultH265: result.defaultH265,
500
- availableHw: probes.filter((p) => p.available && p.family !== "software").map((p) => p.encoder)
501
- } });
502
- return result;
503
- }
504
- };
505
- //#endregion
506
- //#region src/builtins/platform-probe/hardware-decode-accel-probe.ts
507
- var execFileAsync = promisify(execFile);
508
- var defaultExecFn = async (ffmpegPath, args) => {
509
- const { stdout } = await execFileAsync(ffmpegPath, [...args], {
510
- timeout: 8e3,
511
- encoding: "utf8"
512
- });
513
- return { stdout };
514
- };
331
+ //#region src/builtins/platform-probe/index.ts
515
332
  /**
516
- * Parse the output of `ffmpeg -hwaccels`. The listing looks like:
517
- *
518
- * Hardware acceleration methods:
519
- * videotoolbox
520
- *
521
- * The header line is dropped; remaining non-empty trimmed lines are the
522
- * method names, lowercased and de-duplicated (order preserved).
333
+ * The decode-hwaccel backends `ctx.kernel.hwaccel.resolve` accepts. The cap
334
+ * input enum is WIDER (it also carries EP-only names — coreml/openvino/… —
335
+ * shared with other probe surfaces), so the provider param must stay
336
+ * `string`-typed to satisfy the `InferProvider` contract. We narrow it here
337
+ * with a type guard instead of a cast: any value that is not a known decode
338
+ * backend (or the `'none'` sentinel) resolves to `null` auto-probe.
523
339
  */
524
- function parseHwAccelsOutput(stdout) {
525
- const seen = /* @__PURE__ */ new Set();
526
- const methods = [];
527
- for (const raw of stdout.split("\n")) {
528
- const line = raw.trim().toLowerCase();
529
- if (line.length === 0) continue;
530
- if (line.endsWith("methods:")) continue;
531
- if (seen.has(line)) continue;
532
- seen.add(line);
533
- methods.push(line);
534
- }
535
- return methods;
340
+ var HWACCEL_DECODE_BACKENDS = [
341
+ "videotoolbox",
342
+ "cuda",
343
+ "nvdec",
344
+ "vaapi",
345
+ "qsv",
346
+ "d3d11va",
347
+ "dxva2",
348
+ "amf",
349
+ "vdpau",
350
+ "drm"
351
+ ];
352
+ function isHwAccelBackend(value) {
353
+ return HWACCEL_DECODE_BACKENDS.includes(value);
354
+ }
355
+ function narrowHwAccelPrefer(value) {
356
+ if (value === "none") return "none";
357
+ if (typeof value === "string" && isHwAccelBackend(value)) return value;
358
+ return null;
536
359
  }
537
- /**
538
- * Probes which `-hwaccel` decode backends the configured ffmpeg binary
539
- * supports. Mirrors {@link HardwareEncoderProber} (configured binary, cached,
540
- * single-flight, forceable). Never throws: a probe failure yields an empty
541
- * method list so callers fall back to software decode.
542
- */
543
- var HardwareDecodeAccelProber = class {
544
- cached = null;
545
- inflight = null;
546
- logger;
547
- ffmpegPath;
548
- execFn;
549
- constructor(logger, ffmpegPath = "ffmpeg", execFn = defaultExecFn) {
550
- this.logger = logger;
551
- this.ffmpegPath = ffmpegPath;
552
- this.execFn = execFn;
553
- }
554
- getCached() {
555
- return this.cached;
556
- }
557
- async probe(options = {}) {
558
- if (!options.force && this.cached) return this.cached;
559
- if (this.inflight) return this.inflight;
560
- this.inflight = this.runProbe().then((res) => {
561
- this.cached = res;
562
- this.inflight = null;
563
- return res;
564
- }).catch((err) => {
565
- this.inflight = null;
566
- throw err;
567
- });
568
- return this.inflight;
569
- }
570
- async runProbe() {
571
- try {
572
- const { stdout } = await this.execFn(this.ffmpegPath, ["-hide_banner", "-hwaccels"]);
573
- const methods = parseHwAccelsOutput(stdout);
574
- this.logger.info("Hardware decode-accel probe complete", { meta: { methods } });
575
- return {
576
- methods,
577
- probedAt: Date.now()
578
- };
579
- } catch (err) {
580
- this.logger.warn("Hardware decode-accel probe failed — assuming none available", { meta: { error: errMsg(err) } });
581
- return {
582
- methods: [],
583
- probedAt: Date.now()
584
- };
585
- }
586
- }
587
- };
588
- //#endregion
589
- //#region src/builtins/platform-probe/index.ts
590
360
  var PlatformProbeNativeAddon = class extends BaseAddon {
591
361
  scorer = null;
592
- encoderProber = null;
593
- decodeAccelProber = null;
594
362
  cachedCaps = null;
363
+ /**
364
+ * Per-boot generation stamp for the manual readiness emissions below.
365
+ * Constant for the lifetime of this addon instance (== one process boot);
366
+ * consumer-side registries derive a monotonic epoch from generation
367
+ * transitions. Mirrors `BaseAddon._readinessGeneration` (private there).
368
+ */
369
+ readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
595
370
  constructor() {
596
371
  super({});
597
372
  }
598
373
  /**
599
- * Resolve the ffmpeg binary the encoder probe should test, from the cluster
600
- * `ffmpeg` config section (`binaryPath`). The probe MUST exercise the same
601
- * binary the broker/recorder spawn, or it may report encoders for a different
602
- * ffmpeg. Defaults to PATH `ffmpeg` on any miss.
374
+ * Manual readiness protocol. The provider registers synchronously from
375
+ * `onInitialize`, but the REAL hardware + EP scoring is the async
376
+ * `probePromise` (embedded-Python install included). The BaseAddon
377
+ * auto-emit would flip `ready` at registration time — BEFORE any
378
+ * accelerator is visible — so probe-gated consumers (detection-pipeline
379
+ * engine auto-pick) would read accelerator-blind results and stick on
380
+ * onnx-CPU. Instead: `starting` at init, the single authoritative
381
+ * `ready` once the probe resolves, `down` on shutdown.
603
382
  */
604
- async resolveFfmpegBinaryPath() {
383
+ get autoEmitReadiness() {
384
+ return false;
385
+ }
386
+ /** Bare cluster node id — readiness is scoped `{type:'node', nodeId}`. */
387
+ bareLocalNodeId() {
388
+ const raw = this.ctx.kernel?.localNodeId ?? "hub";
389
+ return raw.includes("/") ? raw.split("/")[0] : raw;
390
+ }
391
+ /** Emit a `system.ready-state` transition for the platform-probe cap. */
392
+ emitProbeReadiness(state) {
393
+ const ctx = this.ctxIfReady;
394
+ if (!ctx) return;
395
+ const nodeId = this.bareLocalNodeId();
605
396
  try {
606
- const bp = (await this.ctx.settings?.getSection("ffmpeg") ?? {})["binaryPath"];
607
- return typeof bp === "string" && bp.trim().length > 0 ? bp : "ffmpeg";
608
- } catch {
609
- return "ffmpeg";
610
- }
397
+ emitReadiness(ctx.eventBus, {
398
+ capName: platformProbeCapability.name,
399
+ scope: {
400
+ type: "node",
401
+ nodeId
402
+ },
403
+ state,
404
+ generation: this.readinessGeneration,
405
+ sourceNodeId: nodeId
406
+ });
407
+ } catch {}
611
408
  }
612
409
  async onInitialize() {
410
+ this.emitProbeReadiness("starting");
613
411
  const embeddedPython = await this.ctx.deps.ensurePython().catch((err) => {
614
412
  this.ctx.logger.debug("ensurePython unavailable for platform probe", { meta: { error: errMsg(err) } });
615
413
  return null;
616
414
  });
617
415
  this.scorer = new PlatformScorer(this.ctx.logger, embeddedPython);
618
- const ffmpegPath = await this.resolveFfmpegBinaryPath();
619
- this.encoderProber = new HardwareEncoderProber(this.ctx.logger, ffmpegPath);
620
- this.decodeAccelProber = new HardwareDecodeAccelProber(this.ctx.logger, ffmpegPath);
621
416
  const emitPhase = (phase, payload) => {
622
417
  this.ctx.eventBus?.emit({
623
418
  id: `platform-probe-${phase}-${Date.now()}`,
@@ -644,6 +439,7 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
644
439
  bestReason: caps.bestScore.reason,
645
440
  bestScore: caps.bestScore.score
646
441
  } });
442
+ this.emitProbeReadiness("ready");
647
443
  return caps;
648
444
  }).catch((err) => {
649
445
  const msg = errMsg(err);
@@ -669,42 +465,24 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
669
465
  },
670
466
  resolveHwAccel: async (input) => {
671
467
  const hwaccel = this.ctx.kernel.hwaccel;
672
- if (!hwaccel) return { preferred: [] };
673
- return { preferred: (await hwaccel.resolve(input.prefer ?? null)).preferred };
674
- },
675
- getHardwareEncoders: async () => {
676
- const prober = this.encoderProber;
677
- if (!prober) throw new Error("Hardware encoder prober not initialized");
678
- const cached = prober.getCached();
679
- if (cached) return cached;
680
- const caps = await getCaps();
681
- return prober.probe(caps.hardware);
682
- },
683
- refreshHardwareEncoders: async () => {
684
- const prober = this.encoderProber;
685
- if (!prober) throw new Error("Hardware encoder prober not initialized");
686
- const caps = await getCaps();
687
- return prober.probe(caps.hardware, { force: true });
688
- },
689
- getHardwareDecodeAccels: async () => {
690
- const prober = this.decodeAccelProber;
691
- if (!prober) throw new Error("Hardware decode-accel prober not initialized");
692
- return prober.probe();
693
- },
694
- refreshHardwareDecodeAccels: async () => {
695
- const prober = this.decodeAccelProber;
696
- if (!prober) throw new Error("Hardware decode-accel prober not initialized");
697
- return prober.probe({ force: true });
468
+ if (!hwaccel) return {
469
+ preferred: [],
470
+ rationale: "kernel hwaccel unavailable"
471
+ };
472
+ const res = await hwaccel.resolve(narrowHwAccelPrefer(input.prefer));
473
+ return {
474
+ preferred: res.preferred,
475
+ rationale: res.rationale
476
+ };
698
477
  }
699
478
  }
700
479
  }];
701
480
  }
702
481
  async onShutdown() {
482
+ this.emitProbeReadiness("down");
703
483
  this.scorer = null;
704
- this.encoderProber = null;
705
- this.decodeAccelProber = null;
706
484
  this.cachedCaps = null;
707
485
  }
708
486
  };
709
487
  //#endregion
710
- export { HardwareDecodeAccelProber, HardwareEncoderProber, InferenceConfigResolver, PlatformProbeNativeAddon, PlatformProbeNativeAddon as default, PlatformScorer };
488
+ export { InferenceConfigResolver, PlatformProbeNativeAddon, PlatformProbeNativeAddon as default, PlatformScorer };
@@ -44,8 +44,7 @@ let _camstack_types = require("@camstack/types");
44
44
  /** Section key → user-facing section title. */
45
45
  var SECTION_TITLES = {
46
46
  server: "Server",
47
- auth: "Authentication",
48
- ffmpeg: "FFmpeg"
47
+ auth: "Authentication"
49
48
  };
50
49
  /** Keys that map 1:1 to a given yml section — used by `updateGlobalSettings`
51
50
  * to figure out which section owns each key in the incoming patch. */
@@ -53,10 +52,7 @@ var KEY_TO_SECTION = {
53
52
  port: "server",
54
53
  host: "server",
55
54
  dataPath: "server",
56
- tokenExpiry: "auth",
57
- binaryPath: "ffmpeg",
58
- hwAccel: "ffmpeg",
59
- threadCount: "ffmpeg"
55
+ tokenExpiry: "auth"
60
56
  };
61
57
  var SystemConfigAddon = class extends _camstack_types.BaseAddon {
62
58
  id = "system-config";
@@ -67,119 +63,56 @@ var SystemConfigAddon = class extends _camstack_types.BaseAddon {
67
63
  this.ctx.logger.info("Initialized — exposes yml-backed sections via getGlobalSettings");
68
64
  }
69
65
  buildGlobalSchema() {
70
- return { sections: [
71
- {
72
- id: "system-config-server",
73
- title: SECTION_TITLES["server"],
74
- description: "Core server connection settings — read-only, loaded from config.yaml at bootstrap.",
75
- columns: 2,
76
- fields: [
77
- {
78
- type: "info",
79
- key: "server-restart-note",
80
- label: "Restart required",
81
- content: "Server settings are read-only. Change them in config.yaml and restart.",
82
- variant: "warning"
83
- },
84
- {
85
- type: "text",
86
- key: "port",
87
- label: "Port",
88
- description: "Listening port",
89
- disabled: true
90
- },
91
- {
92
- type: "text",
93
- key: "host",
94
- label: "Host",
95
- description: "Bind address",
96
- disabled: true
97
- },
98
- {
99
- type: "text",
100
- key: "dataPath",
101
- label: "Data Path",
102
- description: "Root data directory",
103
- disabled: true,
104
- span: 2
105
- }
106
- ]
107
- },
108
- {
109
- id: "system-config-auth",
110
- title: SECTION_TITLES["auth"],
111
- description: "Token and session settings.",
112
- columns: 1,
113
- fields: [{
66
+ return { sections: [{
67
+ id: "system-config-server",
68
+ title: SECTION_TITLES["server"],
69
+ description: "Core server connection settings — read-only, loaded from config.yaml at bootstrap.",
70
+ columns: 2,
71
+ fields: [
72
+ {
73
+ type: "info",
74
+ key: "server-restart-note",
75
+ label: "Restart required",
76
+ content: "Server settings are read-only. Change them in config.yaml and restart.",
77
+ variant: "warning"
78
+ },
79
+ {
114
80
  type: "text",
115
- key: "tokenExpiry",
116
- label: "Token Expiry",
117
- description: "JWT token lifetime (e.g. 24h, 7d, 1h)",
118
- placeholder: "24h",
119
- default: "24h"
120
- }]
121
- },
122
- {
123
- id: "system-config-ffmpeg",
124
- title: SECTION_TITLES["ffmpeg"],
125
- description: "FFmpeg binary and hardware acceleration settings.",
126
- columns: 2,
127
- fields: [
128
- {
129
- type: "text",
130
- key: "binaryPath",
131
- label: "Binary Path",
132
- description: "Path to ffmpeg executable",
133
- placeholder: "ffmpeg",
134
- default: "ffmpeg",
135
- span: 2
136
- },
137
- {
138
- type: "select",
139
- key: "hwAccel",
140
- label: "Hardware Acceleration",
141
- description: "GPU decoding/encoding backend",
142
- default: "auto",
143
- options: [
144
- {
145
- value: "auto",
146
- label: "Auto-detect"
147
- },
148
- {
149
- value: "none",
150
- label: "None (CPU only)"
151
- },
152
- {
153
- value: "videotoolbox",
154
- label: "VideoToolbox (macOS)"
155
- },
156
- {
157
- value: "vaapi",
158
- label: "VA-API (Linux Intel/AMD)"
159
- },
160
- {
161
- value: "qsv",
162
- label: "QSV (Intel Quick Sync)"
163
- },
164
- {
165
- value: "cuda",
166
- label: "CUDA (NVIDIA)"
167
- }
168
- ]
169
- },
170
- {
171
- type: "number",
172
- key: "threadCount",
173
- label: "Thread Count",
174
- description: "0 = auto (let FFmpeg decide)",
175
- min: 0,
176
- max: 16,
177
- step: 1,
178
- default: 0
179
- }
180
- ]
181
- }
182
- ] };
81
+ key: "port",
82
+ label: "Port",
83
+ description: "Listening port",
84
+ disabled: true
85
+ },
86
+ {
87
+ type: "text",
88
+ key: "host",
89
+ label: "Host",
90
+ description: "Bind address",
91
+ disabled: true
92
+ },
93
+ {
94
+ type: "text",
95
+ key: "dataPath",
96
+ label: "Data Path",
97
+ description: "Root data directory",
98
+ disabled: true,
99
+ span: 2
100
+ }
101
+ ]
102
+ }, {
103
+ id: "system-config-auth",
104
+ title: SECTION_TITLES["auth"],
105
+ description: "Token and session settings.",
106
+ columns: 1,
107
+ fields: [{
108
+ type: "text",
109
+ key: "tokenExpiry",
110
+ label: "Token Expiry",
111
+ description: "JWT token lifetime (e.g. 24h, 7d, 1h)",
112
+ placeholder: "24h",
113
+ default: "24h"
114
+ }]
115
+ }] };
183
116
  }
184
117
  async getGlobalSettings() {
185
118
  const schema = this.buildGlobalSchema();