@smartspectra/node-sdk 3.3.0-rc.3 → 3.3.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.
@@ -15,6 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { FrameTimestampAnchor } = require('./timestamp');
19
+
18
20
  const SUPPORTED_EVENTS = new Set([
19
21
  'processingStatus',
20
22
  'validationStatus',
@@ -112,13 +114,14 @@ class SmartSpectraSDK {
112
114
  this._reader = null;
113
115
  this._frameLoopActive = false;
114
116
  this._frameLoopPromise = null;
115
- // Monotonic frame-timestamp ratchet. Lives on the instance (not the
116
- // frame loop) so it persists across stop()/start() cycles: the main
117
- // process reuses one native session whose per-session monotonic guard
118
- // is NOT reset by stop/start, so a fresh camera clock starting lower
119
- // than the previous session's last timestamp would otherwise trip
120
- // kNonMonotonicTimestamp.
121
- this._lastFrameTs = 0n;
117
+ // Epoch-anchor + monotonic-ratchet for VideoFrame.timestamp. Lives on
118
+ // the instance (not the frame loop) so it persists across
119
+ // stop()/start() cycles: the main process reuses one native session
120
+ // whose per-session monotonic guard is NOT reset by stop/start, so a
121
+ // fresh camera clock starting lower than the previous session's last
122
+ // timestamp would otherwise trip kNonMonotonicTimestamp. See
123
+ // ./timestamp.js for why the epoch anchor is needed at all.
124
+ this._frameTimestamps = new FrameTimestampAnchor();
122
125
  // Set true once the AE/AWB/focus lock has been attempted post-Running
123
126
  // so we don't try to relock every frame after kRunning.
124
127
  this._cameraLocked = false;
@@ -500,13 +503,11 @@ class SmartSpectraSDK {
500
503
  ctx.drawImage(vf, 0, 0);
501
504
  const imageData = ctx.getImageData(0, 0, vf.codedWidth, vf.codedHeight);
502
505
 
503
- // VideoFrame.timestamp is already in microseconds.
504
- // Ratchet to guarantee strict monotonicity across stream
505
- // pauses, re-attaches, and stop()/start() restarts (see
506
- // _lastFrameTs).
507
- let ts = BigInt(vf.timestamp || 0);
508
- if (ts <= this._lastFrameTs) ts = this._lastFrameTs + 1n;
509
- this._lastFrameTs = ts;
506
+ // VideoFrame.timestamp is already in microseconds, but in
507
+ // the frame's own media-clock domain — anchor it to the
508
+ // Unix epoch and ratchet it monotonic (see
509
+ // ./timestamp.js).
510
+ const ts = this._frameTimestamps.next(vf.timestamp);
510
511
 
511
512
  const ab = imageData.data.buffer;
512
513
  this._bridge.postMessage({
@@ -0,0 +1,56 @@
1
+ // js/renderer/timestamp.js
2
+ // Copyright (C) 2026 Presage Technologies, Inc.
3
+ //
4
+ // SPDX-License-Identifier: LicenseRef-Proprietary
5
+ //
6
+ // Anchors a WebCodecs VideoFrame.timestamp (relative to an unspecified
7
+ // internal media clock, NOT the Unix epoch) to wall-clock epoch
8
+ // microseconds, then ratchets it to guarantee strict monotonicity across
9
+ // stream pauses, re-attaches, and stop()/start() restarts. Extracted from
10
+ // the renderer's frame loop into its own module so the anchor + ratchet
11
+ // math is unit-testable in plain Node — renderer/index.js itself needs
12
+ // OffscreenCanvas/MediaStreamTrackProcessor, which don't exist there.
13
+ //
14
+ // Android/iOS/Linux each apply an equivalent one-time boot/mach-to-epoch
15
+ // offset before a camera frame's timestamp reaches the graph
16
+ // (SmartSpectraVitalsProcessor.kt, avfoundation_camera_source.mm,
17
+ // v4l2_camera_source.cc); this is the Electron/WebCodecs binding's
18
+ // counterpart, so Metrics.timestamp is wall-clock-comparable across every
19
+ // binding instead of only self-consistent within one Electron session.
20
+
21
+ 'use strict';
22
+
23
+ class FrameTimestampAnchor {
24
+ constructor() {
25
+ // Set from the first frame passed to next(); null means "not yet
26
+ // anchored". Persists for the anchor's lifetime (kept on the
27
+ // instance, not recomputed per stop()/start(), the same reasoning
28
+ // as _lastFrameTs below — VideoFrame.timestamp's clock domain is
29
+ // page-navigation-relative, not per-stream).
30
+ this._epochOffsetUs = null;
31
+ // Last timestamp handed out, for the monotonic ratchet.
32
+ this._lastFrameTs = 0n;
33
+ }
34
+
35
+ /**
36
+ * @param {number|bigint} rawTimestampUs VideoFrame.timestamp — already
37
+ * microseconds, but in its own media-clock domain.
38
+ * @param {number} [nowMs] Wall-clock epoch milliseconds at the moment of
39
+ * the first call. Defaults to Date.now(); overridable so callers (and
40
+ * tests) can pin a deterministic anchor.
41
+ * @returns {bigint} Epoch microseconds, strictly greater than every
42
+ * previous value returned by this instance.
43
+ */
44
+ next(rawTimestampUs, nowMs = Date.now()) {
45
+ const raw = BigInt(rawTimestampUs || 0);
46
+ if (this._epochOffsetUs === null) {
47
+ this._epochOffsetUs = BigInt(nowMs) * 1000n - raw;
48
+ }
49
+ let ts = raw + this._epochOffsetUs;
50
+ if (ts <= this._lastFrameTs) ts = this._lastFrameTs + 1n;
51
+ this._lastFrameTs = ts;
52
+ return ts;
53
+ }
54
+ }
55
+
56
+ module.exports = { FrameTimestampAnchor };
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { Session } = require('./ffi');
8
+ const { Session, ensurePreconfigured } = require('./ffi');
9
9
  const {
10
10
  ProcessingStatus,
11
11
  SmartSpectraLogLevel,
@@ -250,6 +250,10 @@ class SmartSpectraSDK {
250
250
 
251
251
  _ensureSession() {
252
252
  if (this._session) return;
253
+ // ARTERIAL_PRESSURE_TRACE, EDA_TRACE, and micromotion metrics need the
254
+ // EMD cache directory Preconfigure() sets up before the graph starts;
255
+ // best-effort and idempotent, so plain metrics work even if it fails.
256
+ ensurePreconfigured();
253
257
  // Listeners may be added/replaced after start(), so the lookup
254
258
  // happens at emit time rather than capture time.
255
259
  this._session = new Session(this._options, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartspectra/node-sdk",
3
- "version": "3.3.0-rc.3",
3
+ "version": "3.3.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",
@@ -33,12 +33,14 @@
33
33
  "COPYING.LGPLv2.1",
34
34
  "COPYING.LGPLv3",
35
35
  "THIRD_PARTY_FFMPEG.md",
36
+ "THIRD_PARTY_NOTICES.txt",
37
+ "NOTICE",
36
38
  "README.md"
37
39
  ],
38
40
  "scripts": {
39
41
  "prepare": "node scripts/gen-proto.cjs",
40
42
  "build:proto": "node scripts/gen-proto.cjs",
41
- "test": "node test/log-level-pin.js && node test/smoke.js",
43
+ "test": "node test/log-level-pin.js && node test/renderer-timestamp.js && node test/smoke.js",
42
44
  "test:models-allowlist": "node test/models-allowlist.js"
43
45
  },
44
46
  "keywords": [
@@ -62,19 +64,19 @@
62
64
  "registry": "https://registry.npmjs.org/"
63
65
  },
64
66
  "engines": {
65
- "node": ">=18.0.0"
67
+ "node": ">=20.0.0"
66
68
  },
67
69
  "_nativeRuntime_doc": "Per-platform native packages (@smartspectra/node-sdk-<plat>-<arch>) are injected into dependencies at publish time (absent here so source installs don't fetch them). Why hard deps, not optionalDependencies: see js/resolve-native.js.",
68
70
  "dependencies": {
69
71
  "koffi": "^2.10.0",
70
72
  "protobufjs": "^7.5.0",
71
- "@smartspectra/node-sdk-linux-x64": "3.3.0-rc.3",
72
- "@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.3",
73
- "@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.3",
74
- "@smartspectra/node-sdk-win32-x64": "3.3.0-rc.3"
73
+ "@smartspectra/node-sdk-linux-x64": "3.3.0-rc.6",
74
+ "@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.6",
75
+ "@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.6",
76
+ "@smartspectra/node-sdk-win32-x64": "3.3.0-rc.6"
75
77
  },
76
78
  "devDependencies": {
77
- "@types/node": "^20.0.0",
79
+ "@types/node": "^24.0.0",
78
80
  "protobufjs-cli": "^1.1.3"
79
81
  },
80
82
  "_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 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.6.",