openvibers/openvibe.live Commits

Lockfile, and the two tests that were failing for reasons of their own

OP OpenVibers committed 20 Aug 2026 · c9e8d7f60a · 3 files, +209 −7

onnxruntime-node was in package.json but missing from the lockfile, so a clean
install would not have installed it and npm ci would have refused outright. It
backs the YAMNet audio-event pass, which would simply have been absent on a
fresh deploy. Pure addition — no existing package moved.

sfu-viewer-stability asserted on exact log sentences. Both had since been
improved: the keyframe request now numbers its retries, and the frozen-video
detector escalates from a keyframe nudge to a transport rebuild instead of
ticking to 30s. The assertions now describe those behaviours, so strengthening
them again will not read as a regression.

whip-handler-cleanup opened whatever data/live.db was in the working tree,
which passes on a developed checkout and throws "no such table: streams" on a
fresh one. It builds its own schema in a temp file now, like the other tests.

Co-Authored-By: Claude Opus 5 <[email protected]>

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex ef749c73..55313c61 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -24,6 +24,7 @@
"mediasoup-client": "^3.15.7",
"multer": "^1.4.5-lts.1",
"node-media-server": "^2.7.1",
++ "onnxruntime-node": "^1.20.0",
"openvibe-shared": "file:./vendor/openvibe-shared",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
@@@ -829,6 +830,15 @@
"node": ">= 0.6"
}
},
++ "node_modules/adm-zip": {
++ "version": "0.5.18",
++ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
++ "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
++ "license": "MIT",
++ "engines": {
++ "node": ">=12.0"
++ }
++ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@@ -1302,6 +1312,40 @@
"node": ">=4.0.0"
}
},
++ "node_modules/define-data-property": {
++ "version": "1.1.4",
++ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
++ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
++ "license": "MIT",
++ "dependencies": {
++ "es-define-property": "^1.0.0",
++ "es-errors": "^1.3.0",
++ "gopd": "^1.0.1"
++ },
++ "engines": {
++ "node": ">= 0.4"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/ljharb"
++ }
++ },
++ "node_modules/define-properties": {
++ "version": "1.2.1",
++ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
++ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
++ "license": "MIT",
++ "dependencies": {
++ "define-data-property": "^1.0.1",
++ "has-property-descriptors": "^1.0.0",
++ "object-keys": "^1.1.1"
++ },
++ "engines": {
++ "node": ">= 0.4"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/ljharb"
++ }
++ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@@ -1451,6 +1495,18 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
++ "node_modules/escape-string-regexp": {
++ "version": "4.0.0",
++ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
++ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
++ "license": "MIT",
++ "engines": {
++ "node": ">=10"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/sindresorhus"
++ }
++ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@@ -1718,6 +1774,37 @@
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
++ "node_modules/global-agent": {
++ "version": "4.1.3",
++ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz",
++ "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==",
++ "license": "BSD-3-Clause",
++ "dependencies": {
++ "globalthis": "^1.0.2",
++ "matcher": "^4.0.0",
++ "semver": "^7.3.5",
++ "serialize-error": "^8.1.0"
++ },
++ "engines": {
++ "node": ">=10.0"
++ }
++ },
++ "node_modules/globalthis": {
++ "version": "1.0.4",
++ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
++ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
++ "license": "MIT",
++ "dependencies": {
++ "define-properties": "^1.2.1",
++ "gopd": "^1.0.1"
++ },
++ "engines": {
++ "node": ">= 0.4"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/ljharb"
++ }
++ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@@ -1778,6 +1865,18 @@
"node": ">=8"
}
},
++ "node_modules/has-property-descriptors": {
++ "version": "1.0.2",
++ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
++ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
++ "license": "MIT",
++ "dependencies": {
++ "es-define-property": "^1.0.0"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/ljharb"
++ }
++ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@@ -2050,6 +2149,21 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
++ "node_modules/matcher": {
++ "version": "4.0.0",
++ "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
++ "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
++ "license": "MIT",
++ "dependencies": {
++ "escape-string-regexp": "^4.0.0"
++ },
++ "engines": {
++ "node": ">=10"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/sindresorhus"
++ }
++ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@@ -2475,6 +2589,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
++ "node_modules/object-keys": {
++ "version": "1.1.1",
++ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
++ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
++ "license": "MIT",
++ "engines": {
++ "node": ">= 0.4"
++ }
++ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@@ -2496,6 +2619,29 @@
"wrappy": "1"
}
},
++ "node_modules/onnxruntime-common": {
++ "version": "1.27.0",
++ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz",
++ "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==",
++ "license": "MIT"
++ },
++ "node_modules/onnxruntime-node": {
++ "version": "1.27.0",
++ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.27.0.tgz",
++ "integrity": "sha512-QEzGwrvNBgv4uPVdnbHsOGG4G6T96mdlcFI8aAKPjMU8wOPpVocPXb6k3QGkaZagVTv2G9Bnnbo6Z3JdXr1fQw==",
++ "hasInstallScript": true,
++ "license": "MIT",
++ "os": [
++ "win32",
++ "darwin",
++ "linux"
++ ],
++ "dependencies": {
++ "adm-zip": "^0.5.16",
++ "global-agent": "^4.1.3",
++ "onnxruntime-common": "1.27.0"
++ }
++ },
"node_modules/openvibe-shared": {
"resolved": "vendor/openvibe-shared",
"link": true
@@@ -2754,6 +2900,21 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
++ "node_modules/serialize-error": {
++ "version": "8.1.0",
++ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
++ "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
++ "license": "MIT",
++ "dependencies": {
++ "type-fest": "^0.20.2"
++ },
++ "engines": {
++ "node": ">=10"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/sindresorhus"
++ }
++ },
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
@@@ -3123,6 +3284,18 @@
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
},
++ "node_modules/type-fest": {
++ "version": "0.20.2",
++ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
++ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
++ "license": "(MIT OR CC0-1.0)",
++ "engines": {
++ "node": ">=10"
++ },
++ "funding": {
++ "url": "https://github.com/sponsors/sindresorhus"
++ }
++ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
ddiff --git a/test/sfu-viewer-stability.test.js b/test/sfu-viewer-stability.test.js
iindex 0c241ccf..9d6e19f5 100644
---- a/test/sfu-viewer-stability.test.js
++++ b/test/sfu-viewer-stability.test.js
@@@ -12,7 +12,7 @@
* F. WHIP ICE disconnect: grace timer + explicit producer-removed emit in cleanupSession
* G. Broadcast-server: ICE-state filter + stale-source path + watch-queued message
* H. Client: sfu-source-unavailable and watch-queued handled without P2P offer timeout
-- * I. Client: frozen-video detector starts after play, stops on transport change
++ * I. Client: frozen-video detector starts after play, escalates keyframe -> rebuild
* J. Broadcaster auto-publishes into SFU and legacy P2P is explicitly gated
* K. Server startup logs TURN / announced-IP diagnostics for SFU viewers
*/
@@@ -53,9 +53,12 @@ assert.ok(
sfuSrc.includes("consumer.kind === 'video'") && sfuSrc.includes('requestKeyFrame'),
'webrtc-sfu.js must guard requestKeyFrame() behind kind === video'
);
++// Anchored on the shape of the line, not its exact wording: the retry that now sends a
++// second keyframe request numbers each attempt, and pinning the old sentence made this
++// fail on a change that strengthened the very behaviour it is here to protect.
assert.ok(
-- sfuSrc.includes('Keyframe requested for video consumer'),
-- 'webrtc-sfu.js must log keyframe request for diagnostics'
++ /Keyframe request[^\n]*sent for video consumer/.test(sfuSrc),
++ 'webrtc-sfu.js must log the keyframe request for diagnostics'
);
console.log('OK B: webrtc-sfu.js consume() calls requestKeyFrame() for video consumers');
@@@ -200,12 +203,24 @@ console.log('OK H: sfu-source-unavailable and watch-queued handled without P2P o
// ── I: frozen-video detector in SFU path ───────────────────────
assert.ok(
-- playerSrc.includes('_sfuFrozenInterval') && playerSrc.includes('_frozenTicks'),
++ playerSrc.includes('_sfuFrozenInterval'),
'stream-player.js must have a frozen-video detector (_sfuFrozenInterval)'
);
++// The detector used to tick to 30s and log. It now escalates in two stages, because a
++// frozen picture with audio still playing is usually one missing keyframe: ask for one
++// first, and only rebuild the transport if that does not unstick it. Assert the two
++// stages rather than the old tick counter and its wording.
assert.ok(
-- playerSrc.includes('video frozen for ~30s'),
-- 'stream-player.js frozen detector must log after 30s of frozen video'
++ /STALL_TRIGGER_MS\s*=\s*\d+/.test(playerSrc) && /STALL_ESCALATE_MS\s*=\s*\d+/.test(playerSrc),
++ 'frozen detector must have a trigger threshold and a later escalation threshold'
++);
++assert.ok(
++ /requesting keyframe via re-watch/.test(playerSrc),
++ 'stage 1 must ask for a keyframe before anything more disruptive'
++);
++assert.ok(
++ /still frozen after keyframe nudge — rebuilding receive transport/.test(playerSrc),
++ 'stage 2 must rebuild the receive transport when the keyframe did not help'
);
assert.ok(
playerSrc.includes('player._sfuFrozenInterval = null') &&
@@@ -217,7 +232,7 @@ assert.ok(
playerSrc.includes('Intentionally NOT calling _startWatchOfferTimeout()'),
'stream-player.js frozen-video re-watch must NOT call _startWatchOfferTimeout()'
);
--console.log('OK I: frozen-video detector (30s post-play check) present in stream-player.js');
++console.log('OK I: frozen-video detector nudges with a keyframe, then rebuilds the transport');
// ── J: broadcaster auto-publishes into SFU, P2P rollback gated ──
assert.ok(
ddiff --git a/test/whip-handler-cleanup.test.js b/test/whip-handler-cleanup.test.js
iindex 4f58ba21..f3784150 100644
---- a/test/whip-handler-cleanup.test.js
++++ b/test/whip-handler-cleanup.test.js
@@@ -1,4 +1,15 @@
const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++
++// cleanupSession looks the stream up to decide what to tear down, so requiring the WHIP
++// handler pulls in the database. Point it at a throwaway file with a real schema BEFORE
++// that require: the test used to open whatever data/live.db happened to be in the working
++// tree, which passed on a developed checkout and threw "no such table: streams" on a
++// fresh one — a property of the machine, not of the code under test.
++process.env.DB_PATH = path.join(os.tmpdir(), `ov-whip-cleanup-${process.pid}.db`);
++require('../server/db/database').initDb();
const webrtcSFU = require('../server/streaming/webrtc-sfu');
const whipHandler = require('../server/streaming/whip-handler');
@@@ -37,4 +48,7 @@ assert.strictEqual(room.transports.has('whip-test-transport-1'), false, 'transpo
assert.strictEqual(producerClosed.value, true, 'producer should be closed');
assert.strictEqual(transportClosed.value, true, 'transport should be closed');
++try { fs.unlinkSync(process.env.DB_PATH); } catch { /* */ }
++for (const ext of ['-wal', '-shm']) { try { fs.unlinkSync(process.env.DB_PATH + ext); } catch { /* */ } }
++
console.log('✅ WHIP cleanup session regression test passed');