@camstack/system 1.1.25 → 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.
@@ -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,264 +328,6 @@ 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
- };
515
- /**
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).
523
- */
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;
536
- }
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
331
  //#region src/builtins/platform-probe/index.ts
590
332
  /**
591
333
  * The decode-hwaccel backends `ctx.kernel.hwaccel.resolve` accepts. The cap
@@ -617,8 +359,6 @@ function narrowHwAccelPrefer(value) {
617
359
  }
618
360
  var PlatformProbeNativeAddon = class extends BaseAddon {
619
361
  scorer = null;
620
- encoderProber = null;
621
- decodeAccelProber = null;
622
362
  cachedCaps = null;
623
363
  /**
624
364
  * Per-boot generation stamp for the manual readiness emissions below.
@@ -666,20 +406,6 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
666
406
  });
667
407
  } catch {}
668
408
  }
669
- /**
670
- * Resolve the ffmpeg binary the encoder probe should test, from the cluster
671
- * `ffmpeg` config section (`binaryPath`). The probe MUST exercise the same
672
- * binary the broker/recorder spawn, or it may report encoders for a different
673
- * ffmpeg. Defaults to PATH `ffmpeg` on any miss.
674
- */
675
- async resolveFfmpegBinaryPath() {
676
- try {
677
- const bp = (await this.ctx.settings?.getSection("ffmpeg") ?? {})["binaryPath"];
678
- return typeof bp === "string" && bp.trim().length > 0 ? bp : "ffmpeg";
679
- } catch {
680
- return "ffmpeg";
681
- }
682
- }
683
409
  async onInitialize() {
684
410
  this.emitProbeReadiness("starting");
685
411
  const embeddedPython = await this.ctx.deps.ensurePython().catch((err) => {
@@ -687,9 +413,6 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
687
413
  return null;
688
414
  });
689
415
  this.scorer = new PlatformScorer(this.ctx.logger, embeddedPython);
690
- const ffmpegPath = await this.resolveFfmpegBinaryPath();
691
- this.encoderProber = new HardwareEncoderProber(this.ctx.logger, ffmpegPath);
692
- this.decodeAccelProber = new HardwareDecodeAccelProber(this.ctx.logger, ffmpegPath);
693
416
  const emitPhase = (phase, payload) => {
694
417
  this.ctx.eventBus?.emit({
695
418
  id: `platform-probe-${phase}-${Date.now()}`,
@@ -751,30 +474,6 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
751
474
  preferred: res.preferred,
752
475
  rationale: res.rationale
753
476
  };
754
- },
755
- getHardwareEncoders: async () => {
756
- const prober = this.encoderProber;
757
- if (!prober) throw new Error("Hardware encoder prober not initialized");
758
- const cached = prober.getCached();
759
- if (cached) return cached;
760
- const caps = await getCaps();
761
- return prober.probe(caps.hardware);
762
- },
763
- refreshHardwareEncoders: async () => {
764
- const prober = this.encoderProber;
765
- if (!prober) throw new Error("Hardware encoder prober not initialized");
766
- const caps = await getCaps();
767
- return prober.probe(caps.hardware, { force: true });
768
- },
769
- getHardwareDecodeAccels: async () => {
770
- const prober = this.decodeAccelProber;
771
- if (!prober) throw new Error("Hardware decode-accel prober not initialized");
772
- return prober.probe();
773
- },
774
- refreshHardwareDecodeAccels: async () => {
775
- const prober = this.decodeAccelProber;
776
- if (!prober) throw new Error("Hardware decode-accel prober not initialized");
777
- return prober.probe({ force: true });
778
477
  }
779
478
  }
780
479
  }];
@@ -782,10 +481,8 @@ var PlatformProbeNativeAddon = class extends BaseAddon {
782
481
  async onShutdown() {
783
482
  this.emitProbeReadiness("down");
784
483
  this.scorer = null;
785
- this.encoderProber = null;
786
- this.decodeAccelProber = null;
787
484
  this.cachedCaps = null;
788
485
  }
789
486
  };
790
487
  //#endregion
791
- 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();