@smartspectra/node-sdk 3.4.0-rc.4 → 3.4.0-rc.6
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/README.md +25 -7
- package/js/camera-selection.js +57 -0
- package/js/ffi.js +71 -12
- package/js/index.d.ts +46 -4
- package/js/index.js +2 -0
- package/js/smartspectra.js +48 -12
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ the missing package.
|
|
|
106
106
|
- Electron desktop app: use `@smartspectra/node-sdk/main`,
|
|
107
107
|
`@smartspectra/node-sdk/preload`, and `@smartspectra/node-sdk/renderer`.
|
|
108
108
|
- Headless Node process with a local camera: use `@smartspectra/node-sdk`
|
|
109
|
-
directly with `useCamera()`.
|
|
109
|
+
directly with `useCamera(CameraSelection.default)`.
|
|
110
110
|
- Headless or server-side Node process with host-provided frames: use
|
|
111
111
|
`useCustomInput()` / `sendFrame()`.
|
|
112
112
|
- Runnable reference app: [electron-quickstart](https://github.com/Presage-Security/SmartSpectra/tree/main/nodejs/samples/electron-quickstart)
|
|
@@ -118,7 +118,7 @@ the missing package.
|
|
|
118
118
|
> wrap the `await` calls in an `async` function.
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
|
-
import { SmartSpectraSDK, breathingMetrics, cardioMetrics, decodeMetrics } from '@smartspectra/node-sdk';
|
|
121
|
+
import { SmartSpectraSDK, CameraSelection, breathingMetrics, cardioMetrics, decodeMetrics } from '@smartspectra/node-sdk';
|
|
122
122
|
|
|
123
123
|
const sdk = new SmartSpectraSDK({
|
|
124
124
|
apiKey: 'YOUR_API_KEY',
|
|
@@ -134,7 +134,7 @@ sdk.on('metrics', (buf, ts) => {
|
|
|
134
134
|
sdk.on('error', (code, message, retryable) =>
|
|
135
135
|
console.error('SmartSpectra error', code, message, 'retryable=', retryable));
|
|
136
136
|
|
|
137
|
-
sdk.useCamera();
|
|
137
|
+
sdk.useCamera(CameraSelection.default);
|
|
138
138
|
sdk.start();
|
|
139
139
|
|
|
140
140
|
console.log('Measuring from the default camera. Press Ctrl+C to stop.');
|
|
@@ -147,10 +147,28 @@ process.on('SIGINT', async () => {
|
|
|
147
147
|
});
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`useCamera()`
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
`useCamera(CameraSelection.default)` selects the default camera while stopped; capture begins at
|
|
151
|
+
`start()`. You can discover cameras without creating an SDK instance:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { CameraSelection, SmartSpectraSDK } from '@smartspectra/node-sdk';
|
|
155
|
+
|
|
156
|
+
const cameras = SmartSpectraSDK.availableCameras();
|
|
157
|
+
if (cameras.length > 0) {
|
|
158
|
+
sdk.useCamera(CameraSelection.byId(cameras[0].id), { width: 1280, height: 720 });
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`CameraSelection.default`, `.front`, `.back`, and `.byId(id)` share the camera
|
|
163
|
+
selection contract across SDKs. Explicit requests never fall back. Stop/start/reset
|
|
164
|
+
retain the chosen input. Only macOS currently supports discovery and explicit
|
|
165
|
+
selectors in native Node.js; Linux/Windows report `kConfigurationFailed` for those
|
|
166
|
+
operations and continue to support default capture. See the
|
|
167
|
+
[migration guide](docs/migration-guide.md) and [API reference](docs/api-reference.md#cameraoptions).
|
|
168
|
+
|
|
169
|
+
Electron renderer capture uses browser-owned streams and `useMediaStream()`;
|
|
170
|
+
browser device IDs cannot be used for native camera selection. Use the
|
|
171
|
+
custom-input path below when your app already owns frame capture.
|
|
154
172
|
|
|
155
173
|
## Custom Input Quickstart
|
|
156
174
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { SmartSpectraErrorCode } = require('./constants');
|
|
6
|
+
|
|
7
|
+
const CameraSelection = Object.freeze({
|
|
8
|
+
default: Object.freeze({ kind: 'default' }),
|
|
9
|
+
front: Object.freeze({ kind: 'front' }),
|
|
10
|
+
back: Object.freeze({ kind: 'back' }),
|
|
11
|
+
byId: (id) => Object.freeze({ kind: 'byId', id }),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function configurationError(message) {
|
|
15
|
+
return Object.assign(new Error(message), {
|
|
16
|
+
code: SmartSpectraErrorCode.kConfigurationFailed, retryable: false,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeCameraSelection(selection) {
|
|
21
|
+
if (!selection || typeof selection !== 'object') {
|
|
22
|
+
throw configurationError('useCamera: expected a CameraSelection');
|
|
23
|
+
}
|
|
24
|
+
switch (selection.kind) {
|
|
25
|
+
case 'default': case 'front': case 'back':
|
|
26
|
+
if (selection.id !== undefined) throw configurationError('Camera ID requires byId selection');
|
|
27
|
+
return Object.freeze({ kind: selection.kind });
|
|
28
|
+
case 'byId':
|
|
29
|
+
if (typeof selection.id !== 'string' || !selection.id || selection.id.includes('\0')) {
|
|
30
|
+
throw configurationError('Camera ID must be a non-empty string without NUL characters');
|
|
31
|
+
}
|
|
32
|
+
return CameraSelection.byId(selection.id);
|
|
33
|
+
default: throw configurationError('Unknown camera selection kind');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeCameraOptions(options) {
|
|
38
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
39
|
+
throw configurationError('useCamera: expected capture options');
|
|
40
|
+
}
|
|
41
|
+
if ('deviceIndex' in options) throw configurationError('Use CameraSelection.byId(id) instead of deviceIndex');
|
|
42
|
+
const copy = {};
|
|
43
|
+
for (const field of ['width', 'height', 'fps', 'frameTransform']) {
|
|
44
|
+
const value = options[field] ?? 0;
|
|
45
|
+
const max = field === 'frameTransform' ? 5 : 2147483647;
|
|
46
|
+
if (!Number.isInteger(value) || value < 0 || value > max) {
|
|
47
|
+
throw configurationError(`Invalid camera ${field}`);
|
|
48
|
+
}
|
|
49
|
+
copy[field] = value;
|
|
50
|
+
}
|
|
51
|
+
if (Boolean(copy.width) !== Boolean(copy.height)) {
|
|
52
|
+
throw configurationError('Camera width and height must be specified together');
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze(copy);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { CameraSelection, normalizeCameraSelection, normalizeCameraOptions };
|
package/js/ffi.js
CHANGED
|
@@ -27,6 +27,8 @@ const lib = koffi.load(resolveNativeLibrary());
|
|
|
27
27
|
|
|
28
28
|
// Opaque session token. C side: `struct smartspectra_session_s*`.
|
|
29
29
|
koffi.opaque('smartspectra_session_t');
|
|
30
|
+
koffi.opaque('smartspectra_camera_list_t');
|
|
31
|
+
koffi.struct('smartspectra_camera_info_t', { id: 'const char *', name: 'const char *', facing: 'int', lens_type: 'int' });
|
|
30
32
|
|
|
31
33
|
// smartspectra_error_t — keep field order identical to the C struct; koffi
|
|
32
34
|
// computes offsets by declaration order.
|
|
@@ -102,13 +104,6 @@ koffi.struct('smartspectra_file_options_t', {
|
|
|
102
104
|
max_duration_ms: 'int',
|
|
103
105
|
frame_transform: 'int',
|
|
104
106
|
});
|
|
105
|
-
koffi.struct('smartspectra_camera_options_t', {
|
|
106
|
-
device_index: 'int',
|
|
107
|
-
width: 'int',
|
|
108
|
-
height: 'int',
|
|
109
|
-
fps: 'int',
|
|
110
|
-
frame_transform: 'int',
|
|
111
|
-
});
|
|
112
107
|
|
|
113
108
|
// Non-blocking file source: configure + start, returns immediately. Caller
|
|
114
109
|
// blocks via sessionWait when it wants end-of-file.
|
|
@@ -117,10 +112,48 @@ const sessionStartFile = lib.func(
|
|
|
117
112
|
'const char *video_path, const char *timestamps_path, ' +
|
|
118
113
|
'const smartspectra_file_options_t *opts, _Out_ smartspectra_error_t *out_err)');
|
|
119
114
|
|
|
120
|
-
|
|
115
|
+
koffi.struct('smartspectra_camera_options_t', {
|
|
116
|
+
device_index: 'int', width: 'int', height: 'int', fps: 'int', frame_transform: 'int',
|
|
117
|
+
});
|
|
118
|
+
const sessionStartLegacyCamera = lib.func(
|
|
121
119
|
'int smartspectra_session_start_camera(smartspectra_session_t *sess, ' +
|
|
122
120
|
'const smartspectra_camera_options_t *opts, _Out_ smartspectra_error_t *out_err)');
|
|
123
121
|
|
|
122
|
+
koffi.struct('smartspectra_camera_capture_options_t', {
|
|
123
|
+
width: 'int', height: 'int', fps: 'int', frame_transform: 'int',
|
|
124
|
+
});
|
|
125
|
+
const sessionStartCamera = lib.func(
|
|
126
|
+
'int smartspectra_session_start_selected_camera(smartspectra_session_t *sess, ' +
|
|
127
|
+
'int selection_kind, const char *camera_id, const smartspectra_camera_capture_options_t *opts, ' +
|
|
128
|
+
'_Out_ smartspectra_error_t *out_err)');
|
|
129
|
+
const discoverCameras = lib.func('int smartspectra_available_cameras(' +
|
|
130
|
+
'_Out_ smartspectra_camera_list_t **out_list, _Out_ smartspectra_error_t *out_err)');
|
|
131
|
+
const cameraListSize = lib.func('size_t smartspectra_camera_list_size(const smartspectra_camera_list_t *list)');
|
|
132
|
+
const cameraListGet = lib.func('const smartspectra_camera_info_t *smartspectra_camera_list_get(' +
|
|
133
|
+
'const smartspectra_camera_list_t *list, size_t index)');
|
|
134
|
+
const cameraListFree = lib.func('void smartspectra_camera_list_free(smartspectra_camera_list_t *list)');
|
|
135
|
+
|
|
136
|
+
function availableCameras() {
|
|
137
|
+
const out = [null];
|
|
138
|
+
const error = {};
|
|
139
|
+
if (discoverCameras(out, error) !== 0) throw makeFfiError(error);
|
|
140
|
+
const list = out[0];
|
|
141
|
+
try {
|
|
142
|
+
const cameras = [];
|
|
143
|
+
const count = Number(cameraListSize(list));
|
|
144
|
+
for (let index = 0; index < count; index++) {
|
|
145
|
+
const camera = koffi.decode(cameraListGet(list, index), 'smartspectra_camera_info_t');
|
|
146
|
+
cameras.push({ id: camera.id, name: camera.name,
|
|
147
|
+
facing: camera.facing === 1 ? 'front' : camera.facing === 2 ? 'back' : 'unknown',
|
|
148
|
+
lensType: camera.lens_type === 1 ? 'wideAngle' : camera.lens_type === 2 ? 'ultraWide' :
|
|
149
|
+
camera.lens_type === 3 ? 'telephoto' : 'unknown' });
|
|
150
|
+
}
|
|
151
|
+
return cameras;
|
|
152
|
+
} finally {
|
|
153
|
+
cameraListFree(list);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
124
157
|
const sessionStartCustom = lib.func(
|
|
125
158
|
'int smartspectra_session_start_custom(smartspectra_session_t *sess, ' +
|
|
126
159
|
'int frame_transform, _Out_ smartspectra_error_t *out_err)');
|
|
@@ -343,8 +376,7 @@ class Session {
|
|
|
343
376
|
this._takeKeepAlive();
|
|
344
377
|
}
|
|
345
378
|
|
|
346
|
-
|
|
347
|
-
startCamera(options = null) {
|
|
379
|
+
startLegacyCamera(options = null) {
|
|
348
380
|
this._assertAlive();
|
|
349
381
|
const err = {};
|
|
350
382
|
const opts = options ? {
|
|
@@ -354,7 +386,23 @@ class Session {
|
|
|
354
386
|
fps: options.fps | 0,
|
|
355
387
|
frame_transform: options.frameTransform | 0,
|
|
356
388
|
} : null;
|
|
357
|
-
const rc =
|
|
389
|
+
const rc = sessionStartLegacyCamera(this._sess, opts, err);
|
|
390
|
+
if (rc !== 0) throw makeFfiError(err);
|
|
391
|
+
this._takeKeepAlive();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Selection and capture settings are separate, matching the C++ API.
|
|
395
|
+
startCamera(selection, options = null) {
|
|
396
|
+
this._assertAlive();
|
|
397
|
+
const err = {};
|
|
398
|
+
const opts = options ? {
|
|
399
|
+
width: options.width | 0,
|
|
400
|
+
height: options.height | 0,
|
|
401
|
+
fps: options.fps | 0,
|
|
402
|
+
frame_transform: options.frameTransform | 0,
|
|
403
|
+
} : null;
|
|
404
|
+
const kind = { default: 0, front: 1, back: 2, byId: 3 }[selection.kind];
|
|
405
|
+
const rc = sessionStartCamera(this._sess, kind, selection.id ?? null, opts, err);
|
|
358
406
|
if (rc !== 0) throw makeFfiError(err);
|
|
359
407
|
this._takeKeepAlive();
|
|
360
408
|
}
|
|
@@ -416,8 +464,18 @@ class Session {
|
|
|
416
464
|
return sessionGetStatus(this._sess);
|
|
417
465
|
}
|
|
418
466
|
|
|
419
|
-
|
|
467
|
+
assertSourceSelectionAllowed() {
|
|
468
|
+
const status = this.getStatus();
|
|
469
|
+
if (this._destroyed || this._stopAsyncPromise || (status !== 0 && status !== 1)) {
|
|
470
|
+
throw Object.assign(new Error('Select an input while stopped'), { code: 1, retryable: false });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
reset(requireStopSettled = false) {
|
|
420
475
|
this._assertAlive();
|
|
476
|
+
if (requireStopSettled && this._stopAsyncPromise) {
|
|
477
|
+
throw Object.assign(new Error('Await stopAsync before reset'), { code: 1, retryable: false });
|
|
478
|
+
}
|
|
421
479
|
const err = {};
|
|
422
480
|
const rc = sessionReset(this._sess, err);
|
|
423
481
|
if (rc !== 0) throw makeFfiError(err);
|
|
@@ -587,6 +645,7 @@ function ensurePreconfigured() {
|
|
|
587
645
|
|
|
588
646
|
module.exports = {
|
|
589
647
|
Session,
|
|
648
|
+
availableCameras,
|
|
590
649
|
preconfigure,
|
|
591
650
|
ensurePreconfigured,
|
|
592
651
|
};
|
package/js/index.d.ts
CHANGED
|
@@ -127,9 +127,36 @@ export interface VideoFileOptions {
|
|
|
127
127
|
frameTransform?: FrameTransformValue;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/** Camera capture
|
|
130
|
+
/** Camera request resolved at capture startup. Explicit requests never fall back. */
|
|
131
|
+
export type CameraSelection =
|
|
132
|
+
| Readonly<{ kind: 'default' }>
|
|
133
|
+
| Readonly<{ kind: 'front' }>
|
|
134
|
+
| Readonly<{ kind: 'back' }>
|
|
135
|
+
| Readonly<{ kind: 'byId'; id: string }>;
|
|
136
|
+
|
|
137
|
+
export declare const CameraSelection: {
|
|
138
|
+
readonly default: Readonly<{ kind: 'default' }>;
|
|
139
|
+
readonly front: Readonly<{ kind: 'front' }>;
|
|
140
|
+
readonly back: Readonly<{ kind: 'back' }>;
|
|
141
|
+
readonly byId: (id: string) => Readonly<{ kind: 'byId'; id: string }>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type CameraFacing = 'front' | 'back' | 'unknown';
|
|
145
|
+
|
|
146
|
+
/** Best-effort classification. Missing metadata and multi-lens cameras are unknown; independent of digital zoom. */
|
|
147
|
+
export type CameraLensType = 'unknown' | 'wideAngle' | 'ultraWide' | 'telephoto';
|
|
148
|
+
|
|
149
|
+
/** Discovery snapshot. IDs are opaque and platform-local; rediscover before reuse. */
|
|
150
|
+
export interface CameraInfo {
|
|
151
|
+
readonly id: string;
|
|
152
|
+
readonly name: string | null;
|
|
153
|
+
readonly facing: CameraFacing;
|
|
154
|
+
readonly lensType: CameraLensType;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Capture settings, separate from camera selection. */
|
|
131
158
|
export interface CameraOptions {
|
|
132
|
-
/**
|
|
159
|
+
/** @deprecated Use useCamera(CameraSelection.byId(id), options) instead. */
|
|
133
160
|
deviceIndex?: number;
|
|
134
161
|
/** Capture width in px; omit/0 = SDK default. */
|
|
135
162
|
width?: number;
|
|
@@ -163,15 +190,21 @@ export declare class SmartSpectraSDK {
|
|
|
163
190
|
/** SDK package version. */
|
|
164
191
|
static readonly version: string;
|
|
165
192
|
|
|
193
|
+
/** No SDK instance, authentication, or capture required. Throws when discovery is unsupported. */
|
|
194
|
+
static availableCameras(): CameraInfo[];
|
|
195
|
+
|
|
166
196
|
constructor(options?: SmartSpectraOptions);
|
|
167
197
|
|
|
168
|
-
/** Initialize and begin
|
|
198
|
+
/** Initialize and begin the input configured with useCamera, useCustomInput, or useFile. */
|
|
169
199
|
start(): void;
|
|
170
200
|
/** Request the session to stop. Idempotent. */
|
|
171
201
|
stop(): void;
|
|
172
202
|
/** Async variant of stop() — the native stop blocks until the pipeline drains; prefer this in event-loop-sensitive contexts. */
|
|
173
203
|
stopAsync(): Promise<void>;
|
|
174
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Rebuild after kError. Clears the source unless useCamera(selection, options) opted into retention.
|
|
206
|
+
* After typed selection, throws kInvalidState while stopAsync() is pending; await it before resetting.
|
|
207
|
+
*/
|
|
175
208
|
reset(): void;
|
|
176
209
|
/**
|
|
177
210
|
* Block until the session reaches a terminal state (idle or error), or the
|
|
@@ -211,6 +244,15 @@ export declare class SmartSpectraSDK {
|
|
|
211
244
|
* pumps frames internally on `start()` — no `sendFrame()` needed. Returns
|
|
212
245
|
* `this` for chaining. Captures in THIS process — for Electron, prefer
|
|
213
246
|
* the renderer SDK's `useMediaStream()`.
|
|
247
|
+
* Select while stopped; explicit requests never fall back. Selection is
|
|
248
|
+
* resolved at startup and retained across stop/start/reset. Invalid or busy
|
|
249
|
+
* requests preserve the previous source. Pass CameraSelection.default for SDK selection.
|
|
250
|
+
*/
|
|
251
|
+
useCamera(selection: CameraSelection, options?: Omit<CameraOptions, 'deviceIndex'>): this;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Select a camera by legacy index (default 0). Reset clears the source.
|
|
255
|
+
* @deprecated Pass an explicit CameraSelection and separate capture options instead.
|
|
214
256
|
*/
|
|
215
257
|
useCamera(options?: CameraOptions): this;
|
|
216
258
|
|
package/js/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
SmartSpectraSDK,
|
|
10
|
+
CameraSelection,
|
|
10
11
|
SmartSpectraLogLevel,
|
|
11
12
|
ProcessingStatus,
|
|
12
13
|
PixelFormat,
|
|
@@ -45,6 +46,7 @@ function decodeMetrics(buf) {
|
|
|
45
46
|
|
|
46
47
|
module.exports = {
|
|
47
48
|
SmartSpectraSDK,
|
|
49
|
+
CameraSelection,
|
|
48
50
|
SmartSpectraLogLevel,
|
|
49
51
|
ProcessingStatus,
|
|
50
52
|
PixelFormat,
|
package/js/smartspectra.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const { Session, ensurePreconfigured } = require('./ffi');
|
|
8
|
+
const { Session, ensurePreconfigured, availableCameras } = require('./ffi');
|
|
9
|
+
const { CameraSelection, normalizeCameraSelection, normalizeCameraOptions } = require('./camera-selection');
|
|
9
10
|
const {
|
|
10
11
|
ProcessingStatus,
|
|
11
12
|
SmartSpectraLogLevel,
|
|
@@ -41,6 +42,9 @@ class SmartSpectraSDK {
|
|
|
41
42
|
/** SDK package version (matches `package.json#version`). */
|
|
42
43
|
static get version() { return PACKAGE_VERSION; }
|
|
43
44
|
|
|
45
|
+
/** Discover cameras without a session, authentication, or capture. */
|
|
46
|
+
static availableCameras() { return availableCameras(); }
|
|
47
|
+
|
|
44
48
|
/**
|
|
45
49
|
* @param {Object} [options]
|
|
46
50
|
* @param {string} [options.apiKey]
|
|
@@ -64,7 +68,9 @@ class SmartSpectraSDK {
|
|
|
64
68
|
// consumed by start(). Shape: { kind: 'custom'|'camera'|'file', ... }.
|
|
65
69
|
// The last use* call wins.
|
|
66
70
|
this._source = null;
|
|
71
|
+
this._explicitCameraSelection = false;
|
|
67
72
|
this._session = null; // lazy: created on start()
|
|
73
|
+
this._destroyPromise = null;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// ---------- public API --------------------------------------------------
|
|
@@ -92,6 +98,7 @@ class SmartSpectraSDK {
|
|
|
92
98
|
* Spatial transform applied to every pushed frame.
|
|
93
99
|
*/
|
|
94
100
|
useCustomInput(frameTransform = DEFAULT_FRAME_TRANSFORM) {
|
|
101
|
+
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
95
102
|
this._source = { kind: 'custom', frameTransform: frameTransform | 0 };
|
|
96
103
|
return this;
|
|
97
104
|
}
|
|
@@ -101,14 +108,28 @@ class SmartSpectraSDK {
|
|
|
101
108
|
* pumps frames internally on start() — no sendFrame() needed. Returns
|
|
102
109
|
* `this` for chaining.
|
|
103
110
|
*
|
|
104
|
-
*
|
|
111
|
+
* Selection is stopped-only and resolved at startup. Explicit requests never
|
|
112
|
+
* fall back. Stop/start/reset retain the selection. Capture options are separate.
|
|
113
|
+
* The deprecated useCamera(options) overload retains index-based capture
|
|
114
|
+
* and the legacy source lifecycle; omit options for camera index zero.
|
|
105
115
|
*
|
|
106
116
|
* Captures from a camera in THIS process (headless / Node use). In an
|
|
107
117
|
* Electron app the camera is normally owned by the renderer via
|
|
108
118
|
* getUserMedia — use the renderer SDK's useMediaStream() there instead.
|
|
109
119
|
*/
|
|
110
|
-
useCamera(options =
|
|
111
|
-
|
|
120
|
+
useCamera(selection = null, options = {}) {
|
|
121
|
+
const isSelection = selection !== null && typeof selection === 'object' && 'kind' in selection;
|
|
122
|
+
if (!isSelection && arguments.length < 2) {
|
|
123
|
+
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
124
|
+
this._source = { kind: 'camera', options: selection };
|
|
125
|
+
this._explicitCameraSelection = false;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
this._requireSourceSelectionAllowed();
|
|
129
|
+
const request = normalizeCameraSelection(selection);
|
|
130
|
+
const capture = normalizeCameraOptions(options);
|
|
131
|
+
this._source = { kind: 'camera', selection: request, options: capture };
|
|
132
|
+
this._explicitCameraSelection = true;
|
|
112
133
|
return this;
|
|
113
134
|
}
|
|
114
135
|
|
|
@@ -123,6 +144,7 @@ class SmartSpectraSDK {
|
|
|
123
144
|
* { timestampsPath?, interframeDelayMs?, startOffsetMs?, maxDurationMs?, frameTransform? }
|
|
124
145
|
*/
|
|
125
146
|
useFile(videoPath, options = null) {
|
|
147
|
+
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
126
148
|
if (typeof videoPath !== 'string' || !videoPath) {
|
|
127
149
|
throw new TypeError('useFile: videoPath must be a non-empty string');
|
|
128
150
|
}
|
|
@@ -134,9 +156,10 @@ class SmartSpectraSDK {
|
|
|
134
156
|
* Initialize the session and start the configured input source. Returns
|
|
135
157
|
* once running — for custom input the graph is armed for sendFrame(); for
|
|
136
158
|
* camera/file the SDK pumps frames itself. Configure a source with
|
|
137
|
-
* useCustomInput()
|
|
159
|
+
* useCustomInput(), useCamera(), or useFile() first.
|
|
138
160
|
*/
|
|
139
161
|
start() {
|
|
162
|
+
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
140
163
|
const source = this._source;
|
|
141
164
|
if (!source) {
|
|
142
165
|
throw new Error(
|
|
@@ -149,7 +172,8 @@ class SmartSpectraSDK {
|
|
|
149
172
|
this._session.startCustom(source.frameTransform);
|
|
150
173
|
break;
|
|
151
174
|
case 'camera':
|
|
152
|
-
this._session.startCamera(source.options);
|
|
175
|
+
if (source.selection) this._session.startCamera(source.selection, source.options);
|
|
176
|
+
else this._session.startLegacyCamera(source.options);
|
|
153
177
|
break;
|
|
154
178
|
case 'file':
|
|
155
179
|
this._session.startFile(
|
|
@@ -177,14 +201,14 @@ class SmartSpectraSDK {
|
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
/**
|
|
180
|
-
* Recover from a kError state.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
204
|
+
* Recover from a kError state. Clears the input source for legacy callers.
|
|
205
|
+
* After useCamera(selection, options), retains the selected input.
|
|
206
|
+
* Typed selection requires any pending stopAsync() to settle before reset.
|
|
183
207
|
*/
|
|
184
208
|
reset() {
|
|
185
209
|
if (!this._session) return;
|
|
186
|
-
this._session.reset();
|
|
187
|
-
this._source = null;
|
|
210
|
+
this._session.reset(this._explicitCameraSelection);
|
|
211
|
+
if (!this._explicitCameraSelection) this._source = null;
|
|
188
212
|
}
|
|
189
213
|
|
|
190
214
|
/**
|
|
@@ -239,15 +263,26 @@ class SmartSpectraSDK {
|
|
|
239
263
|
* replacement session, since SDK state is process-global.
|
|
240
264
|
*/
|
|
241
265
|
destroy() {
|
|
266
|
+
if (this._destroyPromise) return this._destroyPromise;
|
|
242
267
|
if (!this._session) return Promise.resolve();
|
|
243
268
|
const teardown = this._session.destroy();
|
|
244
269
|
this._session = null;
|
|
245
270
|
this._source = null;
|
|
246
|
-
|
|
271
|
+
this._destroyPromise = Promise.resolve(teardown).finally(() => { this._destroyPromise = null; });
|
|
272
|
+
return this._destroyPromise;
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
// ---------- internal ----------------------------------------------------
|
|
250
276
|
|
|
277
|
+
_requireSourceSelectionAllowed() {
|
|
278
|
+
if (this._destroyPromise) {
|
|
279
|
+
throw Object.assign(new Error('SmartSpectra is shutting down'), {
|
|
280
|
+
code: SmartSpectraErrorCode.kInvalidState, retryable: false,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
this._session?.assertSourceSelectionAllowed();
|
|
284
|
+
}
|
|
285
|
+
|
|
251
286
|
_ensureSession() {
|
|
252
287
|
if (this._session) return;
|
|
253
288
|
// ARTERIAL_PRESSURE_TRACE, EDA_TRACE, and micromotion metrics need the
|
|
@@ -289,6 +324,7 @@ class SmartSpectraSDK {
|
|
|
289
324
|
|
|
290
325
|
module.exports = {
|
|
291
326
|
SmartSpectraSDK,
|
|
327
|
+
CameraSelection,
|
|
292
328
|
SmartSpectraLogLevel,
|
|
293
329
|
ProcessingStatus,
|
|
294
330
|
PixelFormat,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartspectra/node-sdk",
|
|
3
|
-
"version": "3.4.0-rc.
|
|
3
|
+
"version": "3.4.0-rc.6",
|
|
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",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"prepare": "node scripts/gen-proto.cjs",
|
|
42
42
|
"build:proto": "node scripts/gen-proto.cjs",
|
|
43
|
-
"test": "node --test test/log-level-pin.js test/renderer-timestamp.js test/smoke.js",
|
|
43
|
+
"test": "npm run test:types && node --test test/log-level-pin.js test/renderer-timestamp.js test/camera-selection.js test/reset-lifecycle.js test/smoke.js",
|
|
44
|
+
"test:types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext test/camera-selection-types.ts",
|
|
44
45
|
"test:models-allowlist": "node test/models-allowlist.js"
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
@@ -70,14 +71,15 @@
|
|
|
70
71
|
"dependencies": {
|
|
71
72
|
"koffi": "^2.10.0",
|
|
72
73
|
"protobufjs": "^7.6.5",
|
|
73
|
-
"@smartspectra/node-sdk-linux-x64": "3.4.0-rc.
|
|
74
|
-
"@smartspectra/node-sdk-linux-arm64": "3.4.0-rc.
|
|
75
|
-
"@smartspectra/node-sdk-darwin-arm64": "3.4.0-rc.
|
|
76
|
-
"@smartspectra/node-sdk-win32-x64": "3.4.0-rc.
|
|
74
|
+
"@smartspectra/node-sdk-linux-x64": "3.4.0-rc.6",
|
|
75
|
+
"@smartspectra/node-sdk-linux-arm64": "3.4.0-rc.6",
|
|
76
|
+
"@smartspectra/node-sdk-darwin-arm64": "3.4.0-rc.6",
|
|
77
|
+
"@smartspectra/node-sdk-win32-x64": "3.4.0-rc.6"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
80
|
"@types/node": "^24.0.0",
|
|
80
|
-
"protobufjs-cli": "^1.1.3"
|
|
81
|
+
"protobufjs-cli": "^1.1.3",
|
|
82
|
+
"typescript": "^6.0.2"
|
|
81
83
|
},
|
|
82
84
|
"_overrides_doc": "tmp is pulled in transitively (protobufjs-cli → tmp) as a build-only devDependency; it is never a runtime dependency and is not shipped to consumers (the published tarball ships js/ + prebuilds only, no node_modules). Pinned to an exact version (not a >= range) to clear GHSA-ph9p-34f9-6g65 (path traversal, affects tmp <0.2.6) and GHSA-7c78-jf6q-g5cm (type-confusion bypass of the 0.2.6 _assertPath guard, affects tmp >=0.2.6) AND to keep installs deterministic — an open range would let a future install pull a brand-new, not-yet-vetted release. Bump deliberately. Drop this once protobufjs-cli ships a release whose tmp range is already >=0.2.7.",
|
|
83
85
|
"overrides": {
|