@displayxr/inline3d 1.49.0 → 1.49.2
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 +46 -0
- package/call.d.ts +10 -3
- package/camera.d.ts +7 -0
- 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 +87 -8
- package/js/inline3d-camera.js +6 -2
- 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
|
|
@@ -44,6 +49,47 @@ const MONO_CONSTRAINTS = { width: { ideal: 1280 }, height: { ideal: 720 }, frame
|
|
|
44
49
|
const PROBE_CONSTRAINTS = { width: { ideal: 3840 }, height: { ideal: 1080 }, frameRate: { ideal: 30 } };
|
|
45
50
|
export const AUDIO_CONSTRAINTS = Object.freeze({ echoCancellation: true, noiseSuppression: true, autoGainControl: true });
|
|
46
51
|
|
|
52
|
+
// How long `'auto'` / `'stereo'` waits, in a DisplayXR Browser with labels visible, for the
|
|
53
|
+
// runtime's 3D camera to be listed before it opens any plain lens (see openCamera), and how often
|
|
54
|
+
// it re-reads the list meanwhile (a `devicechange` re-reads at once).
|
|
55
|
+
export const STEREO_WAIT_MS = 2500;
|
|
56
|
+
const STEREO_WAIT_STEP_MS = 500;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The default `stereoWaitMs`: STEREO_WAIT_MS in a DisplayXR Browser (the same test as
|
|
60
|
+
* inline3DAvailable(): a runtime 3D camera can be listed there), else 0. Pure apart from globals.
|
|
61
|
+
*/
|
|
62
|
+
export function defaultStereoWaitMs() {
|
|
63
|
+
return inDisplayXRBrowser() ? STEREO_WAIT_MS : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The same test as inline3DAvailable() (navigator.xr + XRDisplayLayer), without importing the core. */
|
|
67
|
+
function inDisplayXRBrowser() {
|
|
68
|
+
return typeof navigator !== 'undefined' && !!navigator.xr && typeof window !== 'undefined' && typeof window.XRDisplayLayer === 'function';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Resolves on the next `devicechange` of `md`, or after `ms`, whichever is first. */
|
|
72
|
+
function nextDeviceChange(md, ms) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
let timer = 0;
|
|
75
|
+
const done = () => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
try {
|
|
78
|
+
md.removeEventListener?.('devicechange', done);
|
|
79
|
+
} catch {
|
|
80
|
+
/* no events */
|
|
81
|
+
}
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
timer = setTimeout(done, ms);
|
|
85
|
+
try {
|
|
86
|
+
md.addEventListener?.('devicechange', done);
|
|
87
|
+
} catch {
|
|
88
|
+
/* no events: the timer alone */
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
47
93
|
/** Errors that mean "the device exists but another process holds it" (an eye tracker, say). */
|
|
48
94
|
export function isBusyError(err) {
|
|
49
95
|
return !!err && (err.name === 'NotReadableError' || err.name === 'TrackStartError' || /in use|busy/i.test(err.message || ''));
|
|
@@ -124,9 +170,12 @@ export function stereoLabelHint(label) {
|
|
|
124
170
|
* @param {'auto'|'stereo'|'mono'|string|MediaStream} want 'auto' | 'stereo' | 'mono' | a
|
|
125
171
|
* deviceId | a page-supplied MediaStream
|
|
126
172
|
* @param {{ format?: 'sbs'|'mono', calibration?: {baselineMm?: number, hfovDeg?: number, rectified?: boolean},
|
|
127
|
-
* mediaDevices?: MediaDevices, log?: Function, exclude?: false|Array<string|RegExp
|
|
173
|
+
* mediaDevices?: MediaDevices, log?: Function, exclude?: false|Array<string|RegExp>,
|
|
174
|
+
* stereoWaitMs?: number }} [o]
|
|
128
175
|
* `exclude`: devices no probe may open (see cameraExcluder); an explicit deviceId is the
|
|
129
176
|
* page's own choice and is opened regardless.
|
|
177
|
+
* `stereoWaitMs`: with labels visible and no stereo-named device listed, how long to wait
|
|
178
|
+
* for one before opening a plain lens (default defaultStereoWaitMs(); 0 = one re-read).
|
|
130
179
|
* @returns {Promise<{ stream: MediaStream, format: 'sbs'|'mono', width: number, height: number,
|
|
131
180
|
* deviceId: string|null, label: string, owned: boolean, skipped: Array<{label: string, error: string}>,
|
|
132
181
|
* calibration: object }>}
|
|
@@ -160,8 +209,11 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
160
209
|
// eye-tracking camera shared without stealing it) describes itself on the opened track:
|
|
161
210
|
// `displayxrStereo: {layout, rectified, baselineMm, horizontalFovDeg}`.
|
|
162
211
|
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
|
-
|
|
212
|
+
// A running app's views (3D screen share, ADR-061) describe themselves too: `source: 'app'`,
|
|
213
|
+
// or — on a browser without the app-share device patch, which lists the app as a "3D Camera
|
|
214
|
+
// (DisplayXR) - <app>" — baseline 0 and FOV 0, which no real camera advertises. Every probe
|
|
215
|
+
// path checks this on the OPENED track (rejectOpened), since the label cannot tell them apart.
|
|
216
|
+
const appShare = isAppShareDevice({ label: t.label || '', deviceId: st.deviceId || '' }) || isAppShareHint(st.displayxrStereo);
|
|
165
217
|
return { stream: s, track: t, width: st.width, height: st.height, deviceId: st.deviceId || null, label: t.label || '', stereo, appShare };
|
|
166
218
|
};
|
|
167
219
|
const skip = (label, err) => {
|
|
@@ -312,16 +364,38 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
312
364
|
}
|
|
313
365
|
return null;
|
|
314
366
|
};
|
|
367
|
+
// Re-read the list until a stereo-named device opens or `ms` is up: on each `devicechange` and
|
|
368
|
+
// every STEREO_WAIT_STEP_MS. `ms` <= 0: one re-read. Stops early once a named pair was listed and
|
|
369
|
+
// failed to open (busy): waiting longer would not free it.
|
|
370
|
+
const waitMs = Number.isFinite(o.stereoWaitMs) ? Math.max(0, o.stereoWaitMs) : defaultStereoWaitMs();
|
|
371
|
+
const waitForNamedPair = async (ms) => {
|
|
372
|
+
if (!(ms > 0)) return tryNamedPairs(await listCameras());
|
|
373
|
+
const deadline = Date.now() + ms;
|
|
374
|
+
log('camera-wait-stereo', { ms });
|
|
375
|
+
for (;;) {
|
|
376
|
+
const left = deadline - Date.now();
|
|
377
|
+
if (left <= 0) break;
|
|
378
|
+
await nextDeviceChange(md, Math.min(STEREO_WAIT_STEP_MS, left));
|
|
379
|
+
const list = await listCameras();
|
|
380
|
+
const r = await tryNamedPairs(list);
|
|
381
|
+
if (r) return r;
|
|
382
|
+
if (list.some((d) => stereoLabelHint(d.label) && !isApp(d))) break; // listed, tried, not usable
|
|
383
|
+
}
|
|
384
|
+
log('camera-no-stereo-listed', { ms });
|
|
385
|
+
return null;
|
|
386
|
+
};
|
|
315
387
|
let mono = null;
|
|
316
388
|
if (want !== 'mono') {
|
|
317
389
|
let listed = await listCameras();
|
|
318
390
|
let pair = await tryNamedPairs(listed);
|
|
319
391
|
if (pair) return result(pair, 'sbs');
|
|
320
392
|
// Labels visible but no stereo-named device: a runtime 3D camera can join the list late (after
|
|
321
|
-
// a runtime restart
|
|
393
|
+
// a runtime restart; on Android the runtime lists its cameras lazily). In a DisplayXR Browser
|
|
394
|
+
// wait for it BEFORE any plain lens is opened (stereoWaitMs): on some phones opening a physical
|
|
395
|
+
// lens while head tracking streams crashes the camera HAL, and the runtime's pair goes down with
|
|
396
|
+
// it. Elsewhere read the list once more.
|
|
322
397
|
if (listed.some((d) => d.label) && !listed.some((d) => stereoLabelHint(d.label))) {
|
|
323
|
-
|
|
324
|
-
pair = await tryNamedPairs(listed);
|
|
398
|
+
pair = await waitForNamedPair(waitMs);
|
|
325
399
|
if (pair) return result(pair, 'sbs');
|
|
326
400
|
}
|
|
327
401
|
// No labels yet (no permission). Prime it on the default camera with NO constraints (a facing
|
|
@@ -384,9 +458,14 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
384
458
|
}
|
|
385
459
|
// A label that names a pair first; never by list position (see the header).
|
|
386
460
|
devices.sort((a, b) => Number(stereoLabelHint(b.label)) - Number(stereoLabelHint(a.label)));
|
|
461
|
+
// In a DisplayXR Browser a stereo camera (a USB SBS webcam included) is the runtime's named
|
|
462
|
+
// "3D Camera", so once a plain lens is in hand no other plain lens is opened to test its shape:
|
|
463
|
+
// on some phones that extra open crashes the camera HAL.
|
|
464
|
+
const shapeProbe = !inDisplayXRBrowser();
|
|
387
465
|
for (const d of devices) {
|
|
388
466
|
if (!d.deviceId) continue; // no permission yet: an anonymous entry cannot be opened by id
|
|
389
467
|
if (mono && d.deviceId === mono.deviceId) continue;
|
|
468
|
+
if (mono && !shapeProbe && !stereoLabelHint(d.label)) continue;
|
|
390
469
|
if (tried.has(d.deviceId) || excludedIds.has(d.deviceId)) continue; // already opened (not a pair, or busy) or left out
|
|
391
470
|
if (listedExcluded(d)) continue; // the last resort never opens a sensor (a hand-tracking camera)
|
|
392
471
|
let r = null;
|
|
@@ -406,7 +485,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
406
485
|
// No default could be used (excluded, busy): the first plain camera found is the fallback.
|
|
407
486
|
if (!mono && !isPair(r)) {
|
|
408
487
|
mono = await withMode(r, MONO_CONSTRAINTS);
|
|
409
|
-
if (want === 'mono') return result(mono, 'mono');
|
|
488
|
+
if (want === 'mono' || !shapeProbe) return result(mono, 'mono');
|
|
410
489
|
continue;
|
|
411
490
|
}
|
|
412
491
|
stop(r);
|
package/js/inline3d-camera.js
CHANGED
|
@@ -441,7 +441,11 @@ class StereoCamera {
|
|
|
441
441
|
* are in `skipped` with `excluded: true`. An explicit `deviceId` or `label:` is the page's choice
|
|
442
442
|
* and is opened regardless.
|
|
443
443
|
*
|
|
444
|
-
*
|
|
444
|
+
* In a DisplayXR Browser with labels visible and no 3D camera listed yet, `'auto'` / `'stereo'`
|
|
445
|
+
* waits up to `stereoWaitMs` (default 2500) for the runtime's 3D camera to be listed before it
|
|
446
|
+
* opens any plain lens (docs/camera.md, "Waiting for the 3D camera").
|
|
447
|
+
*
|
|
448
|
+
* @param {object|string|MediaStream} [opts] `{ prefer, format, calibration, rectify, exclude, stereoWaitMs, debug }` (or just `prefer`)
|
|
445
449
|
*/
|
|
446
450
|
export async function openCamera(opts = {}) {
|
|
447
451
|
const o = typeof opts === 'string' || (opts && typeof opts.getVideoTracks === 'function') ? { prefer: opts } : opts || {};
|
|
@@ -451,7 +455,7 @@ export async function openCamera(opts = {}) {
|
|
|
451
455
|
// `horizontalFovDeg` is the public spelling (RFC 0003 §4); the hello wire field is `hfovDeg`.
|
|
452
456
|
if (cal.horizontalFovDeg > 0 && !(cal.hfovDeg > 0)) cal.hfovDeg = cal.horizontalFovDeg;
|
|
453
457
|
delete cal.horizontalFovDeg;
|
|
454
|
-
const r = await openDevice(prefer, { format: o.format, calibration: cal, mediaDevices: o.mediaDevices, log: log || undefined, exclude: o.exclude });
|
|
458
|
+
const r = await openDevice(prefer, { format: o.format, calibration: cal, mediaDevices: o.mediaDevices, log: log || undefined, exclude: o.exclude, stereoWaitMs: o.stereoWaitMs });
|
|
455
459
|
if (r.format === 'sbs' && typeof o.rectify === 'function') {
|
|
456
460
|
// A calibrated rectification step (plug-in or runtime supplied): its output is a rectified
|
|
457
461
|
// SBS stream, and the record then says so. A throwing hook keeps the raw pair.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.49.
|
|
3
|
+
"version": "1.49.2",
|
|
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",
|