@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 +0 -1
- package/README.md +34 -20
- package/THIRD_PARTY_NOTICES.txt +1 -2
- package/js/constants.js +2 -0
- package/js/ffi.js +37 -16
- package/package.json +5 -5
package/NOTICE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Node.js SDK
|
|
3
|
-
|
|
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
|
|
108
|
-
directly
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
137
|
+
sdk.useCamera();
|
|
144
138
|
sdk.start();
|
|
145
139
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
package/THIRD_PARTY_NOTICES.txt
CHANGED
|
@@ -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,
|
|
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)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
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
|
+
"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.
|
|
74
|
-
"@smartspectra/node-sdk-linux-arm64": "3.
|
|
75
|
-
"@smartspectra/node-sdk-darwin-arm64": "3.
|
|
76
|
-
"@smartspectra/node-sdk-win32-x64": "3.
|
|
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",
|