@smartspectra/node-sdk 3.3.0-rc.7 → 3.4.0-rc.2

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.
package/NOTICE CHANGED
@@ -17,7 +17,6 @@ The following Apache-licensed components are included in this shipped artifact:
17
17
  - Kompute
18
18
  - MediaPipe
19
19
  - ml_dtypes
20
- - OpenCensus Proto
21
20
  - OpenCV
22
21
  - protoc-gen-validate
23
22
  - ruy
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
- title: Node.js SDK
3
- description: Build Node.js apps — including Electron desktop apps — with SmartSpectra via a packaged native runtime loaded through koffi.
2
+ title: Node.js and Electron SDK
3
+ sidebarTitle: Overview
4
+ description: Build Node.js and Electron apps that measure pulse and breathing from a camera on Linux, macOS, and Windows, using a prebuilt native runtime from npm.
4
5
  ---
5
6
 
6
7
  # @smartspectra/node-sdk
@@ -104,26 +105,20 @@ the missing package.
104
105
 
105
106
  - Electron desktop app: use `@smartspectra/node-sdk/main`,
106
107
  `@smartspectra/node-sdk/preload`, and `@smartspectra/node-sdk/renderer`.
107
- - Headless or server-side Node process: use `@smartspectra/node-sdk`
108
- directly and push frames with `useCustomInput()` / `sendFrame()`.
108
+ - Headless Node process with a local camera: use `@smartspectra/node-sdk`
109
+ directly with `useCamera()`.
110
+ - Headless or server-side Node process with host-provided frames: use
111
+ `useCustomInput()` / `sendFrame()`.
109
112
  - Runnable reference app: [electron-quickstart](https://github.com/Presage-Security/SmartSpectra/tree/main/nodejs/samples/electron-quickstart)
110
113
 
111
- ## Headless Node Quickstart
114
+ ## Camera Quickstart
112
115
 
113
116
  > The snippets below are ES modules (`import` + top-level `await`) — save them as `.mjs`, or
114
117
  > set `"type": "module"` in your `package.json`. In a CommonJS project, use `require()` and
115
118
  > wrap the `await` calls in an `async` function.
116
119
 
117
120
  ```ts
118
- import {
119
- SmartSpectraSDK, PixelFormat, FrameTransform, ProcessingStatus,
120
- breathingMetrics, cardioMetrics,
121
- decodeMetrics, setMetricsClass,
122
- } from '@smartspectra/node-sdk';
123
-
124
- // Optional: override the default Metrics decoder.
125
- // import { Metrics } from './generated/metrics_pb';
126
- // setMetricsClass(Metrics);
121
+ import { SmartSpectraSDK, breathingMetrics, cardioMetrics, decodeMetrics } from '@smartspectra/node-sdk';
127
122
 
128
123
  const sdk = new SmartSpectraSDK({
129
124
  apiKey: 'YOUR_API_KEY',
@@ -134,20 +129,39 @@ sdk.on('processingStatus', (status) => console.log('Processing status:', status)
134
129
  sdk.on('validationStatus', (code, ts, hint) =>
135
130
  console.log('Validation:', code, hint, 'at', ts, 'µs'));
136
131
  sdk.on('metrics', (buf, ts) => {
137
- const m = decodeMetrics(buf);
138
- console.log('Metrics at', ts, 'µs');
132
+ console.log('Metrics at', ts, 'µs:', decodeMetrics(buf));
139
133
  });
140
134
  sdk.on('error', (code, message, retryable) =>
141
135
  console.error('SmartSpectra error', code, message, 'retryable=', retryable));
142
136
 
143
- sdk.useCustomInput(FrameTransform.kNone);
137
+ sdk.useCamera();
144
138
  sdk.start();
145
139
 
146
- // In your capture loop:
147
- sdk.sendFrame(rgbBuf, width, height, width * 3, PixelFormat.kRGB, captureTsUs);
140
+ console.log('Measuring from the default camera. Press Ctrl+C to stop.');
148
141
 
149
142
  // On shutdown:
150
- await sdk.destroy();
143
+ process.on('SIGINT', async () => {
144
+ await sdk.stopAsync();
145
+ await sdk.destroy();
146
+ process.exit(0);
147
+ });
148
+ ```
149
+
150
+ `useCamera()` opens the default camera and sends frames after `start()`. Pass a
151
+ device index or capture dimensions to select another camera; see the [API
152
+ reference](docs/api-reference.md#cameraoptions). Use the custom-input path
153
+ below only when your app already owns frame capture.
154
+
155
+ ## Custom Input Quickstart
156
+
157
+ To supply frames from another source, replace the camera setup with:
158
+
159
+ ```ts
160
+ import { FrameTransform, PixelFormat } from '@smartspectra/node-sdk';
161
+
162
+ sdk.useCustomInput(FrameTransform.kNone);
163
+ sdk.start();
164
+ sdk.sendFrame(rgbBuf, width, height, width * 3, PixelFormat.kRGB, captureTsUs);
151
165
  ```
152
166
 
153
167
  ## API reference
@@ -21,7 +21,6 @@ Apache-2.0
21
21
  - Kompute
22
22
  - MediaPipe
23
23
  - ml_dtypes
24
- - OpenCensus Proto
25
24
  - OpenCV
26
25
  - protoc-gen-validate
27
26
  - ruy
@@ -295,7 +294,7 @@ Full license texts included in this file
295
294
  See the License for the specific language governing permissions and
296
295
  limitations under the License.
297
296
 
298
- Applies to: Abseil, CNCF xDS API, Envoy data-plane-api, FlatBuffers, gemmlowp, Google APIs, gRPC, HighwayHash, KleidiAI, Kompute, MediaPipe, ml_dtypes, OpenCensus Proto, OpenCV, protoc-gen-validate, ruy, TensorFlow (TensorFlow Lite)
297
+ Applies to: Abseil, CNCF xDS API, Envoy data-plane-api, FlatBuffers, gemmlowp, Google APIs, gRPC, HighwayHash, KleidiAI, Kompute, MediaPipe, ml_dtypes, OpenCV, protoc-gen-validate, ruy, TensorFlow (TensorFlow Lite)
299
298
 
300
299
  ===== BSD-2-Clause =====
301
300
  Copyright (c) <year> <owner>. All rights reserved.
package/js/constants.js CHANGED
@@ -9,6 +9,8 @@
9
9
  //
10
10
  // Integer values must match the SmartSpectra C++ headers and
11
11
  // metric_types.proto wire values; stable across SDK versions.
12
+ // ValidationCode / SmartSpectraErrorCode are governed by
13
+ // smartspectra/error_codes.yaml (checked by `task ci:error-codes:verify`).
12
14
 
13
15
  'use strict';
14
16
 
package/js/ffi.js CHANGED
@@ -262,6 +262,17 @@ class Session {
262
262
  }
263
263
 
264
264
  this._registeredThunks = [];
265
+ // Held from a successful start*() until native teardown completes
266
+ // (_teardownNative(), the destroy() path) — NOT released by
267
+ // stop()/stopAsync()/reset(). Those calls can return while a worker
268
+ // thread is still mid-relay of a trailing callback (e.g. the idle
269
+ // status transition after stop()); only session_destroy's joins
270
+ // guarantee no SDK thread can still call our trampolines. koffi's
271
+ // registered thunks do NOT ref the libuv loop, so without this a host
272
+ // whose only pending work is awaiting an SDK-callback-resolved
273
+ // promise drains its loop, Node disposes the isolate, and the next
274
+ // relayed callback segfaults the process (exit 139).
275
+ this._keepAlive = null;
265
276
  this._destroyed = false;
266
277
  // In-flight stopAsync() promise, if any. session_stop runs on a koffi
267
278
  // worker thread; destroy() must wait for it to settle before deleting
@@ -329,6 +340,7 @@ class Session {
329
340
  } : null;
330
341
  const rc = sessionStartFile(this._sess, videoPath, timestampsPath, opts, err);
331
342
  if (rc !== 0) throw makeFfiError(err);
343
+ this._takeKeepAlive();
332
344
  }
333
345
 
334
346
  // options (optional): { deviceIndex, width, height, fps, frameTransform }
@@ -344,6 +356,7 @@ class Session {
344
356
  } : null;
345
357
  const rc = sessionStartCamera(this._sess, opts, err);
346
358
  if (rc !== 0) throw makeFfiError(err);
359
+ this._takeKeepAlive();
347
360
  }
348
361
 
349
362
  startCustom(frameTransform = 0) {
@@ -351,6 +364,7 @@ class Session {
351
364
  const err = {};
352
365
  const rc = sessionStartCustom(this._sess, frameTransform | 0, err);
353
366
  if (rc !== 0) throw makeFfiError(err);
367
+ this._takeKeepAlive();
354
368
  }
355
369
 
356
370
  sendFrame(buffer, width, height, strideBytes, pixelFormat, timestampUs) {
@@ -455,27 +469,21 @@ class Session {
455
469
  // Returns a promise that resolves once native teardown (including any
456
470
  // worker-thread joins inside session_destroy) has completed.
457
471
  _teardownNative() {
458
- if (this._sess === null) return Promise.resolve();
459
- // stop → wait → destroy → unregister. session_destroy joins every
460
- // worker thread SpawnWorker ever started for this session — including
461
- // one that's mid-flight relaying an onError/onStatus callback to JS
462
- // (e.g. an async graph failure). That relay can only complete once
463
- // the JS event loop is free to service it, so running session_destroy
464
- // synchronously here would deadlock: this call IS the JS thread, and
465
- // it would be blocked joining a thread that's blocked waiting on this
466
- // same thread's event loop. Run it on a koffi worker thread instead —
467
- // the same reason stopAsync() runs session_stop off-thread above —
468
- // so the event loop stays free no matter what a worker is mid-relay
469
- // of. _releaseThunks() only runs after the callback fires, matching
470
- // the original synchronous ordering (unregister only after the joins
471
- // that guarantee no SDK thread can still call our trampolines).
472
- sessionStop(this._sess);
473
- sessionWait(this._sess, 0);
472
+ if (this._sess === null) {
473
+ this._releaseKeepAlive();
474
+ return Promise.resolve();
475
+ }
476
+ // session_destroy performs the required best-effort stop and worker
477
+ // joins. Run that entire drain off the JS thread: an SDK worker may be
478
+ // blocked relaying a callback onto this event loop, so synchronously
479
+ // stopping or waiting here would deadlock both threads. Unregister the
480
+ // thunks only after destroy has joined every possible caller.
474
481
  const sess = this._sess;
475
482
  this._sess = null;
476
483
  return new Promise((resolve) => {
477
484
  sessionDestroy.async(sess, () => {
478
485
  this._releaseThunks();
486
+ this._releaseKeepAlive();
479
487
  resolve();
480
488
  });
481
489
  });
@@ -498,6 +506,19 @@ class Session {
498
506
  for (const t of this._registeredThunks) koffi.unregister(t);
499
507
  this._registeredThunks = [];
500
508
  }
509
+
510
+ _takeKeepAlive() {
511
+ if (this._keepAlive !== null) return;
512
+ // Max-int32 ms (~24.8 days): the timer exists only to hold a loop
513
+ // reference; it never meaningfully fires.
514
+ this._keepAlive = setInterval(() => {}, 2 ** 31 - 1);
515
+ }
516
+
517
+ _releaseKeepAlive() {
518
+ if (this._keepAlive === null) return;
519
+ clearInterval(this._keepAlive);
520
+ this._keepAlive = null;
521
+ }
501
522
  }
502
523
 
503
524
  // Wrap a populated smartspectra_error_t into a JS Error with numeric `code`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartspectra/node-sdk",
3
- "version": "3.3.0-rc.7",
3
+ "version": "3.4.0-rc.2",
4
4
  "description": "Node.js (Electron) FFI binding for SmartSpectra vitals measurement",
5
5
  "author": "Presage Technologies, Inc.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -70,10 +70,10 @@
70
70
  "dependencies": {
71
71
  "koffi": "^2.10.0",
72
72
  "protobufjs": "^7.6.5",
73
- "@smartspectra/node-sdk-linux-x64": "3.3.0-rc.7",
74
- "@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.7",
75
- "@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.7",
76
- "@smartspectra/node-sdk-win32-x64": "3.3.0-rc.7"
73
+ "@smartspectra/node-sdk-linux-x64": "3.4.0-rc.2",
74
+ "@smartspectra/node-sdk-linux-arm64": "3.4.0-rc.2",
75
+ "@smartspectra/node-sdk-darwin-arm64": "3.4.0-rc.2",
76
+ "@smartspectra/node-sdk-win32-x64": "3.4.0-rc.2"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/node": "^24.0.0",