@displayxr/inline3d 1.49.0 → 1.49.1
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/CHANGELOG.md +21 -0
- package/call.d.ts +10 -3
- package/dist/call.js +6 -6
- package/js/call/share.js +40 -8
- package/js/camera/app-share.js +62 -2
- package/js/camera/capture.js +11 -3
- package/package.json +1 -1
package/js/call/share.js
CHANGED
|
@@ -14,11 +14,21 @@
|
|
|
14
14
|
// runtime asks the user on the SENDER's machine, every start ("Share <app> with <browser>?"), when
|
|
15
15
|
// `openAppShare()` opens the device; a Deny rejects it (`code: 'share-denied'`). The track ends
|
|
16
16
|
// (`ended`) when the app exits or the share is stopped from the runtime side.
|
|
17
|
+
//
|
|
18
|
+
// Browsers WITHOUT the app-share device patch (DisplayXR Browser 1.9.x) list an app through their
|
|
19
|
+
// stereo-camera factory as `3D Camera (DisplayXR) - <app>` — and, whenever the runtime lists more
|
|
20
|
+
// than one source, give the real camera the same shape. So when no ADR-form device exists,
|
|
21
|
+
// `listShareableApps()` returns those as CANDIDATES (`legacy: true`), and `openAppShare()` verifies
|
|
22
|
+
// the opened track (`isAppShareHint`: baseline 0 and FOV 0 is an app) — a real camera is closed
|
|
23
|
+
// again with `code: 'not-app-share'`.
|
|
17
24
|
|
|
18
|
-
import { isAppShareDevice, appNameFromLabel, APP_SHARE_ID_PREFIX, APP_SHARE_LABEL_RE } from '../camera/app-share.js';
|
|
25
|
+
import { isAppShareDevice, isAppShareHint, legacyAppCandidate, appNameFromLabel, APP_SHARE_ID_PREFIX, APP_SHARE_LABEL_RE, APP_SHARE_LEGACY_LABEL_RE } from '../camera/app-share.js';
|
|
19
26
|
import { sanitizeName, CONV_MAX_FRAC } from './wire.js';
|
|
20
27
|
|
|
21
|
-
export { isAppShareDevice, appNameFromLabel, APP_SHARE_ID_PREFIX, APP_SHARE_LABEL_RE };
|
|
28
|
+
export { isAppShareDevice, isAppShareHint, legacyAppCandidate, appNameFromLabel, APP_SHARE_ID_PREFIX, APP_SHARE_LABEL_RE, APP_SHARE_LEGACY_LABEL_RE };
|
|
29
|
+
|
|
30
|
+
/** The app name a label carries: the ADR form, else a legacy candidate's `<name>`. */
|
|
31
|
+
const nameOfLabel = (label) => sanitizeName(appNameFromLabel(label)) || sanitizeName((legacyAppCandidate(label) || {}).appName) || null;
|
|
22
32
|
|
|
23
33
|
/** What `openAppShare` asks for by default: the runtime's default stream (ADR-061 decision 5). */
|
|
24
34
|
const SHARE_CONSTRAINTS = Object.freeze({ width: 2560, height: 720, frameRate: 30 });
|
|
@@ -29,6 +39,11 @@ const mdOf = (o) => (o && o.mediaDevices) || (globalThis.navigator && navigator.
|
|
|
29
39
|
* The running apps this browser offers to share: `[{ deviceId, label, appName }]`, in the
|
|
30
40
|
* browser's order. Empty without camera permission (the browser lists none before it), on any
|
|
31
41
|
* other browser, or when nothing is shareable. Never rejects.
|
|
42
|
+
*
|
|
43
|
+
* On a browser without the app-share device patch (no ADR-form device listed), the entries are the
|
|
44
|
+
* `3D Camera (DisplayXR) - <name>` CANDIDATES instead, marked `legacy: true`. A real camera can be
|
|
45
|
+
* among them (that browser labels every source that way once there are two); `openAppShare()`
|
|
46
|
+
* tells them apart and rejects a camera with `not-app-share`.
|
|
32
47
|
* @param {{ mediaDevices?: MediaDevices }} [o]
|
|
33
48
|
*/
|
|
34
49
|
export async function listShareableApps(o = {}) {
|
|
@@ -40,9 +55,14 @@ export async function listShareableApps(o = {}) {
|
|
|
40
55
|
} catch {
|
|
41
56
|
return [];
|
|
42
57
|
}
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
const videos = list.filter((d) => d && d.kind === 'videoinput');
|
|
59
|
+
const apps = videos
|
|
60
|
+
.filter((d) => isAppShareDevice(d))
|
|
45
61
|
.map((d) => Object.freeze({ deviceId: d.deviceId, label: d.label || '', appName: sanitizeName(appNameFromLabel(d.label)) || sanitizeName(d.label) || null }));
|
|
62
|
+
if (apps.length) return apps;
|
|
63
|
+
return videos
|
|
64
|
+
.filter((d) => d.deviceId && legacyAppCandidate(d))
|
|
65
|
+
.map((d) => Object.freeze({ deviceId: d.deviceId, label: d.label || '', appName: nameOfLabel(d.label), legacy: true }));
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
/**
|
|
@@ -61,7 +81,7 @@ export function watchShareableApps(cb, o = {}) {
|
|
|
61
81
|
const mine = ++seq;
|
|
62
82
|
const apps = await listShareableApps({ mediaDevices: md });
|
|
63
83
|
if (!live || mine !== seq) return; // a newer read is in flight (devicechange bursts)
|
|
64
|
-
const key = JSON.stringify(apps.map((a) => [a.deviceId, a.label]));
|
|
84
|
+
const key = JSON.stringify(apps.map((a) => [a.deviceId, a.label, !!a.legacy]));
|
|
65
85
|
if (key === last) return;
|
|
66
86
|
last = key;
|
|
67
87
|
try {
|
|
@@ -97,7 +117,8 @@ export function describeAppShare(track, fallbackLabel = '') {
|
|
|
97
117
|
const hint = st.displayxrStereo && typeof st.displayxrStereo === 'object' ? st.displayxrStereo : {};
|
|
98
118
|
const z = hint.zeroDisparityShiftFrac;
|
|
99
119
|
const frac = typeof z === 'number' && Number.isFinite(z) ? Math.max(-CONV_MAX_FRAC, Math.min(CONV_MAX_FRAC, z)) : 0;
|
|
100
|
-
|
|
120
|
+
// The hint's name, else the label's: the ADR form, or a legacy candidate's ` - <name>`.
|
|
121
|
+
const appName = sanitizeName(hint.appName) || nameOfLabel(track && track.label) || nameOfLabel(fallbackLabel) || null;
|
|
101
122
|
return {
|
|
102
123
|
kind: 'content',
|
|
103
124
|
format: hint.layout && hint.layout !== 'side-by-side' ? 'mono' : 'sbs',
|
|
@@ -111,7 +132,9 @@ export function describeAppShare(track, fallbackLabel = '') {
|
|
|
111
132
|
/**
|
|
112
133
|
* Open one shareable app (a `deviceId` from `listShareableApps`) as a side-by-side track. This is
|
|
113
134
|
* when the runtime asks the user on this machine; Deny rejects with `code: 'share-denied'`. A device
|
|
114
|
-
* that turns out not to be an app share is closed again (`code: 'not-app-share'`)
|
|
135
|
+
* that turns out not to be an app share is closed again (`code: 'not-app-share'`) — on a browser
|
|
136
|
+
* without the app-share device patch that is how a `legacy` candidate that is really a camera
|
|
137
|
+
* (its track's baseline is not 0) is told apart from an app. The track is
|
|
115
138
|
* yours: stop it when you stop sharing (`track.stop()`), or the runtime keeps the share open.
|
|
116
139
|
*
|
|
117
140
|
* @param {string} deviceId
|
|
@@ -134,7 +157,16 @@ export async function openAppShare(deviceId, o = {}) {
|
|
|
134
157
|
throw Object.assign(new Error(`could not share the app: ${(err && err.message) || err}`), { code: denied ? 'share-denied' : 'share-failed', cause: err });
|
|
135
158
|
}
|
|
136
159
|
const track = stream.getVideoTracks()[0] || null;
|
|
137
|
-
|
|
160
|
+
let hint = null;
|
|
161
|
+
try {
|
|
162
|
+
hint = track && typeof track.getSettings === 'function' ? (track.getSettings() || {}).displayxrStereo : null;
|
|
163
|
+
} catch {
|
|
164
|
+
hint = null;
|
|
165
|
+
}
|
|
166
|
+
// A track whose hint describes a camera (a `source` other than 'app', or a real baseline) is not
|
|
167
|
+
// an app, whatever its label or id; otherwise the label, the id or the hint must say "app".
|
|
168
|
+
const cameraHint = !!hint && typeof hint === 'object' && !isAppShareHint(hint) && (typeof hint.source === 'string' || hint.baselineMm > 0);
|
|
169
|
+
if (!track || cameraHint || !(isAppShareDevice(track) || deviceId.startsWith(APP_SHARE_ID_PREFIX))) {
|
|
138
170
|
stream.getTracks().forEach((t) => t.stop());
|
|
139
171
|
throw Object.assign(new Error('that device is not a shared DisplayXR app'), { code: 'not-app-share' });
|
|
140
172
|
}
|
package/js/camera/app-share.js
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
// `dxr-app:`, and on the opened track `getSettings().displayxrStereo.source === 'app'` (plus
|
|
13
13
|
// `appName`, `zeroDisparityShiftFrac`). Browsers that predate the hint fields still use the label
|
|
14
14
|
// and the id, so any ONE of the three is enough.
|
|
15
|
+
//
|
|
16
|
+
// Browsers WITHOUT the app-share device patch (DisplayXR Browser 1.9.x) list a development-gated
|
|
17
|
+
// app source through their stereo-CAMERA device factory instead: label `3D Camera (DisplayXR) -
|
|
18
|
+
// <app>`, id `dxr-stereo:…`, no `source` in the hint. That label shape proves nothing — such a
|
|
19
|
+
// browser appends ` - <displayName>` to EVERY entry whenever the runtime lists more than one, so
|
|
20
|
+
// the real camera next to the app reads `3D Camera (DisplayXR) - <camera>` too. What does tell
|
|
21
|
+
// them apart is the opened track: an app source advertises `baselineMm: 0` and
|
|
22
|
+
// `horizontalFovDeg: 0` (runtime ADR-061 decision 2); a real camera never has baseline 0. So
|
|
23
|
+
// `isAppShareHint` checks the hint, and `legacyAppCandidate` only ever names a CANDIDATE, which
|
|
24
|
+
// must be opened and verified before anything treats it as an app.
|
|
15
25
|
|
|
16
26
|
/** `deviceId` prefix of an app-share device (cameras use `dxr-stereo:`). */
|
|
17
27
|
export const APP_SHARE_ID_PREFIX = 'dxr-app:';
|
|
@@ -19,6 +29,54 @@ export const APP_SHARE_ID_PREFIX = 'dxr-app:';
|
|
|
19
29
|
/** The label of an app-share device; group 1 is the app's name. */
|
|
20
30
|
export const APP_SHARE_LABEL_RE = /^DisplayXR: (.+) \(3D\)$/;
|
|
21
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The label a browser without the app-share device patch gives EVERY runtime stereo source when
|
|
34
|
+
* there is more than one (cameras and apps alike); group 1 is the source's display name. A shape,
|
|
35
|
+
* not an identity: see legacyAppCandidate.
|
|
36
|
+
*/
|
|
37
|
+
export const APP_SHARE_LEGACY_LABEL_RE = /^3D Camera \(DisplayXR\)\s+-\s+(.+)$/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Does a track's `displayxrStereo` hint describe a running app rather than a camera? True when
|
|
41
|
+
* `source === 'app'`; with no `source` (browsers without the app-share device patch), when the
|
|
42
|
+
* layout is side-by-side (or unstated) and BOTH `baselineMm` and `horizontalFovDeg` are exactly 0
|
|
43
|
+
* — what an app source advertises (ADR-061 decision 2) and no real camera does. A `source` that is
|
|
44
|
+
* anything else says "not an app". Never throws. Pure.
|
|
45
|
+
* @param {unknown} hint
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isAppShareHint(hint) {
|
|
49
|
+
if (!hint || typeof hint !== 'object') return false;
|
|
50
|
+
try {
|
|
51
|
+
const h = /** @type {any} */ (hint);
|
|
52
|
+
if (typeof h.source === 'string') return h.source === 'app';
|
|
53
|
+
if (h.layout != null && h.layout !== 'side-by-side') return false;
|
|
54
|
+
return h.baselineMm === 0 && h.horizontalFovDeg === 0;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* On a browser WITHOUT the app-share device patch: is this listed device a CANDIDATE app source
|
|
62
|
+
* (`3D Camera (DisplayXR) - <name>`)? Returns `{ appName }` or null. A candidate is verified only
|
|
63
|
+
* after opening (`isAppShareHint` on the track's settings) — the same label shape is given to real
|
|
64
|
+
* cameras whenever the runtime lists more than one source. Never throws. Pure.
|
|
65
|
+
* @param {unknown} device a `MediaDeviceInfo`, `{ label }`, or a label string
|
|
66
|
+
* @returns {{ appName: string } | null}
|
|
67
|
+
*/
|
|
68
|
+
export function legacyAppCandidate(device) {
|
|
69
|
+
let label = '';
|
|
70
|
+
try {
|
|
71
|
+
label = typeof device === 'string' ? device : device && typeof device === 'object' ? /** @type {any} */ (device).label : '';
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const m = typeof label === 'string' ? APP_SHARE_LEGACY_LABEL_RE.exec(label.trim()) : null;
|
|
76
|
+
const appName = m ? m[1].trim() : '';
|
|
77
|
+
return appName ? { appName } : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
22
80
|
/** The app name an app-share label carries, or null. Pure. */
|
|
23
81
|
export function appNameFromLabel(label) {
|
|
24
82
|
const m = typeof label === 'string' ? APP_SHARE_LABEL_RE.exec(label.trim()) : null;
|
|
@@ -29,7 +87,9 @@ export function appNameFromLabel(label) {
|
|
|
29
87
|
* Is this an app-share device rather than a camera? Takes a `MediaDeviceInfo` (or any
|
|
30
88
|
* `{ label, deviceId }`), a label string, a track's settings (`{ displayxrStereo }`), or a
|
|
31
89
|
* `MediaStreamTrack` (its label and settings). True on ANY of: the label shape, the `dxr-app:` id
|
|
32
|
-
* prefix, or `displayxrStereo
|
|
90
|
+
* prefix, or a `displayxrStereo` hint that `isAppShareHint` accepts (`source: 'app'`, or baseline
|
|
91
|
+
* and FOV both 0). The legacy `3D Camera (DisplayXR) - <name>` label alone is NOT enough (cameras
|
|
92
|
+
* get it too). Never throws. Pure.
|
|
33
93
|
* @param {unknown} x
|
|
34
94
|
* @returns {boolean}
|
|
35
95
|
*/
|
|
@@ -42,7 +102,7 @@ export function isAppShareDevice(x) {
|
|
|
42
102
|
if (typeof o.label === 'string' && APP_SHARE_LABEL_RE.test(o.label.trim())) return true;
|
|
43
103
|
if (typeof o.deviceId === 'string' && o.deviceId.startsWith(APP_SHARE_ID_PREFIX)) return true;
|
|
44
104
|
const hint = o.displayxrStereo || (typeof o.getSettings === 'function' ? (o.getSettings() || {}).displayxrStereo : null);
|
|
45
|
-
if (hint
|
|
105
|
+
if (isAppShareHint(hint)) return true;
|
|
46
106
|
if (typeof o.getSettings === 'function') {
|
|
47
107
|
const id = (o.getSettings() || {}).deviceId;
|
|
48
108
|
if (typeof id === 'string' && id.startsWith(APP_SHARE_ID_PREFIX)) return true;
|
package/js/camera/capture.js
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
// It is a pair, it names itself "3D", and it is NOT a camera: no probe path opens one, whatever
|
|
30
30
|
// `exclude` says (`exclude: false` included). Each is reported in `skipped` as `excluded` with
|
|
31
31
|
// `reason: 'app-share'`. `/call`'s `listShareableApps()` / `openAppShare()` are how a page uses them.
|
|
32
|
+
// A browser WITHOUT the app-share device patch lists such an app as a stereo camera instead
|
|
33
|
+
// ("3D Camera (DisplayXR) - <app>", no `source` in the hint) and gives the REAL camera the same
|
|
34
|
+
// label shape whenever there is more than one source — so the label cannot decide. Those are
|
|
35
|
+
// opened like any named pair, and the opened track's hint decides: baseline 0 and FOV 0 is an
|
|
36
|
+
// app (ADR-061 decision 2) → closed again, `reason: 'app-share'`, probing goes on.
|
|
32
37
|
//
|
|
33
38
|
// RECTIFICATION is not done here (P2): it needs the camera's calibration (intrinsics/extrinsics
|
|
34
39
|
// for the ACTIVE device), which comes from a plug-in or the runtime. P1 sends the raw pair with
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
// `(stream, info) => MediaStream | Promise<MediaStream>`, returning a rectified SBS stream.
|
|
37
42
|
|
|
38
43
|
import { looksSbs } from './geometry.js';
|
|
39
|
-
import { isAppShareDevice } from './app-share.js';
|
|
44
|
+
import { isAppShareDevice, isAppShareHint } from './app-share.js';
|
|
40
45
|
|
|
41
46
|
const MONO_CONSTRAINTS = { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } };
|
|
42
47
|
// Ask a candidate stereo device for its widest mode; a 2560x720 or 3840x1080 pair comes back as
|
|
@@ -160,8 +165,11 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
160
165
|
// eye-tracking camera shared without stealing it) describes itself on the opened track:
|
|
161
166
|
// `displayxrStereo: {layout, rectified, baselineMm, horizontalFovDeg}`.
|
|
162
167
|
const stereo = st.displayxrStereo && st.displayxrStereo.layout === 'side-by-side' ? st.displayxrStereo : null;
|
|
163
|
-
// A running app's views (3D screen share, ADR-061) describe themselves too: `source: 'app'
|
|
164
|
-
|
|
168
|
+
// A running app's views (3D screen share, ADR-061) describe themselves too: `source: 'app'`,
|
|
169
|
+
// or — on a browser without the app-share device patch, which lists the app as a "3D Camera
|
|
170
|
+
// (DisplayXR) - <app>" — baseline 0 and FOV 0, which no real camera advertises. Every probe
|
|
171
|
+
// path checks this on the OPENED track (rejectOpened), since the label cannot tell them apart.
|
|
172
|
+
const appShare = isAppShareDevice({ label: t.label || '', deviceId: st.deviceId || '' }) || isAppShareHint(st.displayxrStereo);
|
|
165
173
|
return { stream: s, track: t, width: st.width, height: st.height, deviceId: st.deviceId || null, label: t.label || '', stereo, appShare };
|
|
166
174
|
};
|
|
167
175
|
const skip = (label, err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.49.
|
|
3
|
+
"version": "1.49.1",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|