openvibers/openvibe.sdk Commits

0.95.0: openvibe-sdk/cam loads the OpenVibe camera element (#130)

OP OpenVibers committed 6 days ago · 1134a2e202 · 15 files, +340 −13

defineOvCamera() injects https://openvibe.cam/embed/ov-camera.js once
and resolves to the element class; OV_CAMERA_SRC is the default and a
src option loads a self-hosted copy. Browser-only by nature: in Node it
rejects without touching document.

types/cam.d.ts types the attributes, both events, the detail shapes and
start/stop/capture/flip, plus the HTMLElementTagNameMap entry. The ESM
entry, API surface, pack test, browser scan, README, CHANGELOG, STATUS
and SDK_VERSION follow the new module; the browser bundle carries the
new version header.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 20b6b4d..66a02a9 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,15 @@
# Changelog
++## 0.95.0 (2026-11-07)
++
++- Add `openvibe-sdk/cam`: `defineOvCamera({ src })` injects OpenVibe.Cam's framework-free `<ov-camera>` element
++ once (default `https://openvibe.cam/embed/ov-camera.js`) and resolves to the class. It is browser-only by nature:
++ calling it in Node rejects instead of touching `document`. `OV_CAMERA_SRC` is the default URL.
++- Type the element: `OvCameraElement` (`start()`, `stop()`, `capture()`, `flip()`), `OvCameraAttributes` (facing,
++ filters, timer, mode, upload), `OvCameraCaptureDetail` / `OvCameraErrorDetail`, the two event types and the
++ `HTMLElementTagNameMap` entry. `upload="off"` is still the default: a capture is a `Blob` in the page until the
++ site sends it somewhere.
++
## 0.94.0 (2026-11-07)
- `openvibe-sdk/streams` gains S7: `schedule()`, `listSchedules()`, `cancelSchedule()` and `remindSchedule()` for
ddiff --git a/README.md b/README.md
iindex 3c80c46..588c074 100644
---- a/README.md
++++ b/README.md
@@@ -2,16 +2,16 @@
> Supported browser and server clients for the OpenVibe platform.
--**Status:** alpha, v0.94.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
++**Status:** alpha, v0.95.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
--After v0.94.0 is tagged:
++After v0.95.0 is tagged:
```bash
--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0
++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.95.0
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
@@@ -488,6 +488,31 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr
`browser/openvibe-sdk.mjs` is generated by `node scripts/browser-bundle.js` from the CommonJS sources reachable from `browser.js` (no dependencies, no transpiling) and checked in. It exports core at the top level and `auth` (browser build), `registry`, `modules`, `realtime`, `media`, `community`, `bot`, `jobs`, `tools` and `projects` as namespaces. `test/bundle.test.js` fails when it is stale, and the browser secret scan covers it.
++### The OpenVibe camera element
++
++`openvibe-sdk/cam` loads the camera app's framework-free `<ov-camera>` element from OpenVibe.Cam and types its
++attributes, events and methods. It injects the embed script once and resolves to the element class:
++
++```js
++import { defineOvCamera } from 'openvibe-sdk/cam';
++
++await defineOvCamera(); // https://openvibe.cam/embed/ov-camera.js, once
++const camera = document.querySelector('ov-camera');
++camera.addEventListener('ov-camera:capture', (event) => {
++ // event.detail = { blob, width, height, filter }; nothing was uploaded
++ upload(event.detail.blob);
++});
++await camera.start(); // or let the person click the element's own button
++```
++
++```html
++<ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>
++```
++
++`upload="off"` (the default) keeps the picture in the page. `upload="openvibe"` posts it to `POST /api/v1/photos` as
++the signed-in person, and only on openvibe.cam. `defineOvCamera({ src })` loads a self-hosted copy of the same file.
++The full attribute, event and method reference is at [openvibe.cam/developers](https://openvibe.cam/developers).
++
## Versioning
- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.
ddiff --git a/STATUS.json b/STATUS.json
iindex 118cfeb..7c60bde 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,12 +3,13 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.94.0",
++ "version": "0.95.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"contracts": "openvibe-contracts v0.312.0",
"features": [
++ "cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex c5cf3cb..f3e9c54 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.94.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.95.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, locker, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -48,7 +48,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.94.0';
++const SDK_VERSION = '0.95.0';
module.exports = {
createClient,
ddiff --git a/docs/api-surface.json b/docs/api-surface.json
iindex 77b66c9..c545b15 100644
---- a/docs/api-surface.json
++++ b/docs/api-surface.json
@@@ -479,6 +479,10 @@
"openvibe-sdk/cache": {
"createCache": "function"
},
++ "openvibe-sdk/cam": {
++ "OV_CAMERA_SRC": "string",
++ "defineOvCamera": "function"
++ },
"openvibe-sdk/chat": {
"createChatClient": "function",
"createFeedPublisher": "function",
ddiff --git a/esm/cam.mjs b/esm/cam.mjs
nnew file mode 100644
iindex 0000000..7b89bfd
---- /dev/null
++++ b/esm/cam.mjs
@@@ -0,0 +1,7 @@
++// Generated by scripts/esm.js from src/cam/index.js. Do not edit.
++import cjs from '../src/cam/index.js';
++
++export const {
++ defineOvCamera, OV_CAMERA_SRC,
++} = cjs;
++export default cjs;
ddiff --git a/package-lock.json b/package-lock.json
iindex 997f7ff..d94a192 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.94.0",
++ "version": "0.95.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.94.0",
++ "version": "0.95.0",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex 22c02a8..70e7235 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.94.0",
++ "version": "0.95.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"types": "types/index.d.ts",
@@@ -347,6 +347,11 @@
"import": "./esm/app-signin.mjs",
"default": "./src/app-signin/index.js"
},
++ "./cam": {
++ "types": "./types/cam.d.ts",
++ "import": "./esm/cam.mjs",
++ "default": "./src/cam/index.js"
++ },
"./run": {
"types": "./types/run.d.ts",
"browser": null,
ddiff --git a/scripts/esm.js b/scripts/esm.js
iindex 3d386bb..0ce74d6 100644
---- a/scripts/esm.js
++++ b/scripts/esm.js
@@@ -28,6 +28,7 @@ const ENTRIES = {
'events.mjs': '../src/events.js',
'sso.mjs': '../src/sso/index.js',
'app-signin.mjs': '../src/app-signin/index.js',
++ 'cam.mjs': '../src/cam/index.js',
'realtime.mjs': '../src/realtime.js',
'media.mjs': '../src/media.js',
'community.mjs': '../src/community.js',
ddiff --git a/src/cam/index.js b/src/cam/index.js
nnew file mode 100644
iindex 0000000..d560640
---- /dev/null
++++ b/src/cam/index.js
@@@ -0,0 +1,61 @@
++'use strict';
++/**
++ * openvibe-sdk/cam: the OpenVibe camera element, from https://openvibe.cam/embed/ov-camera.js.
++ *
++ * In an app: import { defineOvCamera } from the package's cam entry.
++ * await defineOvCamera();
++ * const camera = document.querySelector('ov-camera');
++ * camera.addEventListener('ov-camera:capture', (event) => upload(event.detail.blob));
++ *
++ * The embed itself is OpenVibe.Cam's framework-free custom element. This loader only injects one
++ * <script> per origin (the page may already include it) and resolves to the class once the browser
++ * has defined it. Nothing here touches the camera: the element starts only from a click or an
++ * explicit start(), and its default upload="off" means a capture never leaves the page.
++ *
++ * <ov-camera facing="user" filters="none,mono,warm" timer="3" mode="photo" upload="off"></ov-camera>
++ *
++ * Browser-only by nature; in Node the call rejects and the module still loads (the types and the
++ * pack test import it). Pass `src` to self-host the same element file.
++ */
++const OV_CAMERA_SRC = 'https://openvibe.cam/embed/ov-camera.js';
++
++// One promise per src: a second call while the script is in flight gets the same load, never a second tag.
++const pending = new Map();
++
++/**
++ * Inject the <ov-camera> embed once and resolve to the element class.
++ * @param {{ src?: string }} [options]
++ * @returns {Promise<CustomElementConstructor>}
++ */
++function defineOvCamera(options = {}) {
++ const src = String((options && options.src) || OV_CAMERA_SRC);
++ if (typeof window === 'undefined' || typeof document === 'undefined') {
++ return Promise.reject(new Error('openvibe-sdk/cam: defineOvCamera() needs a browser with custom elements.'));
++ }
++ const defined = () => (window.customElements ? window.customElements.get('ov-camera') : undefined);
++ const already = defined();
++ if (already) return Promise.resolve(already);
++ if (pending.has(src)) return pending.get(src);
++
++ const load = new Promise((resolve, reject) => {
++ const script = document.createElement('script');
++ script.src = src;
++ script.async = true;
++ script.dataset.ovCamera = '';
++ script.addEventListener('load', () => {
++ const ctor = defined();
++ if (ctor) resolve(ctor);
++ else reject(new Error(`openvibe-sdk/cam: ${src} loaded but did not define <ov-camera>.`));
++ });
++ script.addEventListener('error', () => {
++ script.remove();
++ reject(new Error(`openvibe-sdk/cam: could not load ${src}`));
++ });
++ (document.head || document.documentElement).appendChild(script);
++ }).finally(() => pending.delete(src));
++
++ pending.set(src, load);
++ return load;
++}
++
++module.exports = { defineOvCamera, OV_CAMERA_SRC };
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 08abcec..035ba45 100644
---- a/src/core/index.js
++++ b/src/core/index.js
@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.94.0';
++const SDK_VERSION = '0.95.0';
module.exports = {
createClient,
ddiff --git a/test/browser-entries.test.js b/test/browser-entries.test.js
iindex 0f34107..6e16b08 100644
---- a/test/browser-entries.test.js
++++ b/test/browser-entries.test.js
@@@ -78,7 +78,7 @@ run([
assert.deepEqual(problems, []);
assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './consent', './cost', './coupons', './db', './deals', './delivery', './download', './events', './exchange', './food', './govern', './host', './identity', './inventory', './limits', './live', './news', './node', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);
const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows
-- for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
++ for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
assert.ok(rel.includes(must), `${must} is scanned`);
}
for (const never of ['src/auth/tokens.js', 'src/auth/jwt.js', 'src/auth/oauth.js', 'src/events.js', 'src/identity.js', 'src/sso/index.js', 'src/app-signin/index.js', 'src/service/index.js', 'src/service-outbox.js', 'src/share.js', 'src/share/store.js', 'src/testing/index.js']) {
ddiff --git a/test/cam.test.js b/test/cam.test.js
nnew file mode 100644
iindex 0000000..6faeea2
---- /dev/null
++++ b/test/cam.test.js
@@@ -0,0 +1,96 @@
++'use strict';
++/**
++ * openvibe-sdk/cam: the export resolves by name (CJS and ESM), the loader injects the embed once in a fake DOM
++ * and resolves to the element class, Node gets a rejection instead of a document crash, and the type file names
++ * the runtime exports and the whole element surface.
++ */
++const assert = require('node:assert/strict');
++const fs = require('node:fs');
++const path = require('node:path');
++const { run } = require('./helpers');
++
++const ROOT = path.join(__dirname, '..');
++const cam = require('../src/cam');
++const pkg = require('../package.json');
++
++run([
++ ['the entry resolves by name and names the loader and the default script', async () => {
++ assert.deepEqual(Object.keys(cam).sort(), ['OV_CAMERA_SRC', 'defineOvCamera']);
++ assert.equal(cam.OV_CAMERA_SRC, 'https://openvibe.cam/embed/ov-camera.js');
++ assert.equal(require('openvibe-sdk/cam'), cam);
++ assert.deepEqual(pkg.exports['./cam'], {
++ types: './types/cam.d.ts',
++ import: './esm/cam.mjs',
++ default: './src/cam/index.js',
++ });
++ const esm = await import('openvibe-sdk/cam');
++ assert.equal(esm.default, cam);
++ assert.equal(esm.OV_CAMERA_SRC, cam.OV_CAMERA_SRC);
++ assert.equal(typeof esm.defineOvCamera, 'function');
++ }],
++
++ ['in Node it rejects instead of touching a document', async () => {
++ assert.equal(typeof global.window, 'undefined');
++ await assert.rejects(cam.defineOvCamera(), /needs a browser/);
++ await assert.rejects(cam.defineOvCamera({ src: 'https://example.test/ov-camera.js' }), /needs a browser/);
++ }],
++
++ ['a browser gets one <script> per load and the class; a defined element resolves without another', async () => {
++ const appended = [];
++ const listeners = {};
++ let ctor = null;
++ const script = {
++ dataset: {},
++ addEventListener(type, fn) { listeners[type] = fn; },
++ remove() { this.removed = true; },
++ };
++ const hadWindow = Object.prototype.hasOwnProperty.call(global, 'window');
++ const hadDocument = Object.prototype.hasOwnProperty.call(global, 'document');
++ const oldWindow = global.window;
++ const oldDocument = global.document;
++ global.window = { customElements: { get: (name) => (name === 'ov-camera' ? ctor : undefined) } };
++ global.document = {
++ createElement: () => script,
++ head: {
++ appendChild: (s) => {
++ appended.push(s.src);
++ ctor = function OvCamera() {};
++ queueMicrotask(() => listeners.load());
++ },
++ },
++ documentElement: { appendChild: () => { throw new Error('head exists; documentElement must not be used'); } },
++ };
++ try {
++ const [a, b] = await Promise.all([cam.defineOvCamera(), cam.defineOvCamera()]);
++ assert.equal(appended.length, 1, 'two concurrent calls share one script');
++ assert.equal(a, ctor);
++ assert.equal(b, ctor);
++ assert.equal(script.dataset.ovCamera, '', 'the tag is marked as ours');
++ assert.equal(script.async, true);
++
++ const again = await cam.defineOvCamera();
++ assert.equal(again, ctor);
++ assert.equal(appended.length, 1, 'a defined element needs no second script');
++
++ // A custom src is honoured when the page has not defined the element yet.
++ ctor = null;
++ const custom = await cam.defineOvCamera({ src: 'https://cdn.example/ov-camera.js' });
++ assert.equal(typeof custom, 'function');
++ assert.deepEqual(appended, ['https://openvibe.cam/embed/ov-camera.js', 'https://cdn.example/ov-camera.js']);
++ } finally {
++ if (hadWindow) global.window = oldWindow; else delete global.window;
++ if (hadDocument) global.document = oldDocument; else delete global.document;
++ }
++ }],
++
++ ['the types file names the runtime exports and the element surface', async () => {
++ const dts = fs.readFileSync(path.join(ROOT, 'types/cam.d.ts'), 'utf8');
++ for (const name of [
++ 'OV_CAMERA_SRC', 'defineOvCamera', 'DefineOvCameraOptions',
++ 'OvCameraAttributes', 'OvCameraFacing', 'OvCameraFilter', 'OvCameraTimer', 'OvCameraMode', 'OvCameraUpload',
++ 'OvCameraElement', 'OvCameraCaptureDetail', 'OvCameraErrorCode', 'OvCameraErrorDetail',
++ 'OvCameraCaptureEvent', 'OvCameraErrorEvent', 'OvCameraEventMap', 'HTMLElementTagNameMap',
++ 'start(): Promise<void>', 'stop(): void', 'capture(): Promise<', 'flip(): Promise<OvCameraFacing>',
++ ]) assert.ok(dts.includes(name), name);
++ }],
++]);
ddiff --git a/test/pack.test.js b/test/pack.test.js
iindex 6f9862d..4b1a712 100644
---- a/test/pack.test.js
++++ b/test/pack.test.js
@@@ -24,7 +24,7 @@ run([
const out = execFileSync(npm, ['pack', '--json', '--pack-destination', tmp], { cwd: ROOT, env, encoding: 'utf8', shell });
const info = JSON.parse(out)[0];
const files = info.files.map((f) => f.path);
-- for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {
++ for (const must of ['package.json', 'index.js', 'browser.js', 'LICENSE', 'README.md', 'CHANGELOG.md', 'src/core/client.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'src/vip.js', 'esm/vip.mjs', 'types/vip.d.ts', 'src/vibes.js', 'esm/vibes.mjs', 'types/vibes.d.ts', 'src/exchange.js', 'esm/exchange.mjs', 'types/exchange.d.ts', 'docs/exchange.md', 'src/bot.js', 'esm/bot.mjs', 'types/bot.d.ts', 'src/frame.js', 'esm/frame.mjs', 'types/frame.d.ts', 'src/cam/index.js', 'esm/cam.mjs', 'types/cam.d.ts', 'src/service/index.js', 'esm/service.mjs', 'types/service.d.ts', 'docs/service.md', 'esm/core.mjs', 'esm/jobs.mjs', 'esm/tools.mjs', 'types/tools.d.ts', 'types/core.d.ts', 'types/contracts.d.ts', 'types/bundle.d.ts', 'src/resources.js', 'esm/resources.mjs', 'types/resources.d.ts', 'src/vision.js', 'esm/vision.mjs', 'types/vision.d.ts', 'src/redact.js', 'esm/redact.mjs', 'types/redact.d.ts', 'docs/redact.md', 'src/share.js', 'src/share/store.js', 'esm/share.mjs', 'types/share.d.ts', 'docs/share.md', 'src/locker.js', 'src/locker/seal.js', 'src/locker/server.js', 'esm/locker.mjs', 'esm/locker-server.mjs', 'types/locker.d.ts', 'types/locker-server.d.ts', 'types/locker-vectors.d.ts', 'esm/locker-vectors.mjs', 'docs/locker.md', 'src/streams.js', 'esm/streams.mjs', 'types/streams.d.ts', 'docs/streams.md', 'browser/openvibe-sdk.mjs']) {
assert.ok(files.includes(must), `tarball contains ${must}`);
}
assert.ok(!files.some((f) => f.startsWith('test/') || f.startsWith('scripts/') || f.startsWith('.github/')), 'no tests or tooling in the tarball');
@@@ -68,7 +68,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
-- assert.equal(subpaths.length, 86); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports
++ assert.equal(subpaths.length, 87); // 0.90.0 assertions, 0.91.0 streams, 0.92.0 chat-apps, 0.94.0 reports, 0.95.0 cam
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
ddiff --git a/types/cam.d.ts b/types/cam.d.ts
nnew file mode 100644
iindex 0000000..631eb32
---- /dev/null
++++ b/types/cam.d.ts
@@@ -0,0 +1,117 @@
++/**
++ * openvibe-sdk/cam: the <ov-camera> element from OpenVibe.Cam.
++ *
++ * The element is defined by https://openvibe.cam/embed/ov-camera.js (or a self-hosted copy); this module
++ * injects it and gives the attributes, events and methods a type.
++ */
++
++/** The embed script the loader injects when no `src` is given. */
++export declare const OV_CAMERA_SRC: string;
++
++/** The front camera (`user`) or the back camera (`environment`). */
++export type OvCameraFacing = 'user' | 'environment';
++
++/** The filters the element knows; `filters` is a comma list of these names. */
++export type OvCameraFilter = 'none' | 'mono' | 'warm' | 'cool' | 'fade' | 'vivid' | 'blur';
++
++/** A countdown before the capture: off, three seconds or ten. */
++export type OvCameraTimer = 0 | 3 | 10;
++
++/** One photo, or a burst of three 450 ms apart. */
++export type OvCameraMode = 'photo' | 'burst';
++
++/** `off` keeps every picture in the page; `openvibe` posts to Cam, only on openvibe.cam and signed in. */
++export type OvCameraUpload = 'off' | 'openvibe';
++
++/** The attributes on the tag. Every one is optional; the defaults are in the comment. */
++export interface OvCameraAttributes {
++ /** `user` (the front camera, the default) or `environment` (the back camera). */
++ facing?: OvCameraFacing;
++ /** A comma list of {@link OvCameraFilter} names; unknown names are dropped and an empty list is `none`. */
++ filters?: string;
++ /** A countdown in seconds; anything other than 0, 3 or 10 is 0. Default 0. */
++ timer?: OvCameraTimer | '0' | '3' | '10';
++ /** Default `photo`. */
++ mode?: OvCameraMode;
++ /** Default `off`: the blob is only delivered as an event. */
++ upload?: OvCameraUpload;
++}
++
++/** The `detail` of `ov-camera:capture`, and what `capture()` resolves to. */
++export interface OvCameraCaptureDetail {
++ /** The encoded picture (`image/jpeg`, quality 0.92). */
++ blob: Blob;
++ width: number;
++ height: number;
++ /** The selected filter name ({@link OvCameraFilter}). */
++ filter: string;
++}
++
++export type OvCameraErrorCode =
++ | 'camera_unavailable'
++ | 'permission_denied'
++ | 'not_started'
++ | 'capture_failed'
++ | 'file_unreadable'
++ | 'upload_unavailable'
++ | 'upload_denied'
++ | 'upload_failed';
++
++/** The `detail` of `ov-camera:error`. */
++export interface OvCameraErrorDetail {
++ code: OvCameraErrorCode;
++ message: string;
++}
++
++export interface OvCameraCaptureEvent extends CustomEvent<OvCameraCaptureDetail> {
++ type: 'ov-camera:capture';
++}
++
++export interface OvCameraErrorEvent extends CustomEvent<OvCameraErrorDetail> {
++ type: 'ov-camera:error';
++}
++
++/** The events the element fires, both bubbling out of its shadow root. */
++export interface OvCameraEventMap {
++ 'ov-camera:capture': OvCameraCaptureEvent;
++ 'ov-camera:error': OvCameraErrorEvent;
++}
++
++/** The element itself, as `customElements.get('ov-camera')` returns it. */
++export interface OvCameraElement extends HTMLElement {
++ /** Ask for the camera. It is never called on connect; a click is the safe place to call it. */
++ start(): Promise<void>;
++ /** Stop every track and show the off card again. */
++ stop(): void;
++ /** Take a photo through the shutter path, honouring `timer` and `mode`; null when there is no camera. */
++ capture(): Promise<OvCameraCaptureDetail | OvCameraCaptureDetail[] | null>;
++ /** Switch front/back and restart the camera if it is on; resolves to the new facing. */
++ flip(): Promise<OvCameraFacing>;
++ addEventListener<K extends keyof OvCameraEventMap>(
++ type: K,
++ listener: (this: OvCameraElement, ev: OvCameraEventMap[K]) => void,
++ options?: boolean | AddEventListenerOptions,
++ ): void;
++ addEventListener(
++ type: string,
++ listener: EventListenerOrEventListenerObject,
++ options?: boolean | AddEventListenerOptions,
++ ): void;
++}
++
++export interface DefineOvCameraOptions {
++ /** The embed script to inject; defaults to {@link OV_CAMERA_SRC}. */
++ src?: string;
++}
++
++/**
++ * Inject the element's script once and resolve to its class. A second call reuses the load; a page that
++ * already defines <ov-camera> resolves without a new tag.
++ */
++export declare function defineOvCamera(options?: DefineOvCameraOptions): Promise<CustomElementConstructor>;
++
++declare global {
++ interface HTMLElementTagNameMap {
++ 'ov-camera': OvCameraElement;
++ }
++}