@displayxr/inline3d 1.47.3 → 1.47.4
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 +29 -0
- package/dist/call.js +5 -5
- package/js/camera/capture.js +55 -19
- package/package.json +1 -1
package/js/camera/capture.js
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
// - On some laptops the stereo camera is held EXCLUSIVELY by the eye tracker, so opening it
|
|
10
10
|
// fails with NotReadableError ("Device in use"). That device is skipped silently — a call must
|
|
11
11
|
// never fail because an optional camera is busy.
|
|
12
|
-
// - Labels are empty until one getUserMedia has succeeded
|
|
13
|
-
//
|
|
12
|
+
// - Labels are empty until one getUserMedia has succeeded. Without them, permission is primed on
|
|
13
|
+
// an environment-facing camera where there is one (a phone's front lens is the head tracker's,
|
|
14
|
+
// and opening it while the tracker holds it can reset the whole camera stack), then the list
|
|
15
|
+
// is read again and a stereo device is opened by id BEFORE the default. The default is the
|
|
16
|
+
// fallback.
|
|
14
17
|
//
|
|
15
18
|
// RECTIFICATION is not done here (P2): it needs the camera's calibration (intrinsics/extrinsics
|
|
16
19
|
// for the ACTIVE device), which comes from a plug-in or the runtime. P1 sends the raw pair with
|
|
@@ -101,6 +104,9 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
101
104
|
log('camera-skip', { label, error: err.name });
|
|
102
105
|
};
|
|
103
106
|
const stop = (r) => r && r.stream.getTracks().forEach((t) => t.stop());
|
|
107
|
+
// A track that declares itself a side-by-side pair IS one, whatever its aspect (a phone's portrait
|
|
108
|
+
// eyes make a 1440x1280 pair). Shape is only the guess for an unknown device, e.g. a USB pair.
|
|
109
|
+
const isPair = (r) => !!r && (!!r.stereo || looksSbs(r.width, r.height));
|
|
104
110
|
const result = (r, format) => ({
|
|
105
111
|
stream: r.stream, format, width: r.width, height: r.height, deviceId: r.deviceId, label: r.label, owned: true, skipped,
|
|
106
112
|
calibration: {
|
|
@@ -126,7 +132,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
126
132
|
skip(want, err);
|
|
127
133
|
throw Object.assign(new Error(`camera unavailable: ${err.message}`), { code: noCameraCode(skipped), skipped });
|
|
128
134
|
}
|
|
129
|
-
if (
|
|
135
|
+
if (isPair(r)) return result(r, 'sbs');
|
|
130
136
|
stop(r);
|
|
131
137
|
return result(await open({ deviceId: { exact: want }, ...MONO_CONSTRAINTS }), 'mono');
|
|
132
138
|
}
|
|
@@ -136,14 +142,16 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
136
142
|
// camera start can take seconds (a busy capture stack took 7-20 s per start on a field box), and
|
|
137
143
|
// the default-first probe below costs two or three of them before it reaches the pair.
|
|
138
144
|
const tried = new Set();
|
|
139
|
-
|
|
140
|
-
let early = [];
|
|
145
|
+
const listCameras = async () => {
|
|
141
146
|
try {
|
|
142
|
-
|
|
147
|
+
return (await md.enumerateDevices()).filter((d) => d.kind === 'videoinput');
|
|
143
148
|
} catch {
|
|
144
|
-
|
|
149
|
+
return []; // no enumeration: the paths below still work
|
|
145
150
|
}
|
|
146
|
-
|
|
151
|
+
};
|
|
152
|
+
const tryNamedPairs = async (list) => {
|
|
153
|
+
for (const d of list) {
|
|
154
|
+
if (!d.deviceId || !stereoLabelHint(d.label) || tried.has(d.deviceId)) continue;
|
|
147
155
|
tried.add(d.deviceId);
|
|
148
156
|
let r = null;
|
|
149
157
|
try {
|
|
@@ -152,23 +160,51 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
152
160
|
skip(d.label, err);
|
|
153
161
|
continue;
|
|
154
162
|
}
|
|
155
|
-
if (
|
|
163
|
+
if (isPair(r)) {
|
|
156
164
|
log('camera-sbs', { label: r.label, width: r.width, height: r.height, direct: true });
|
|
157
|
-
return
|
|
165
|
+
return r;
|
|
158
166
|
}
|
|
159
167
|
stop(r);
|
|
160
168
|
}
|
|
169
|
+
return null;
|
|
170
|
+
};
|
|
171
|
+
let mono = null;
|
|
172
|
+
if (want !== 'mono') {
|
|
173
|
+
const listed = await listCameras();
|
|
174
|
+
const pair = await tryNamedPairs(listed);
|
|
175
|
+
if (pair) return result(pair, 'sbs');
|
|
176
|
+
// No labels yet (no permission). Prime it on an environment-facing camera, so a phone does not
|
|
177
|
+
// open its front lens (the head tracker's) just to learn the names; then open a pair by id.
|
|
178
|
+
if (listed.length && listed.every((d) => !d.label)) {
|
|
179
|
+
let primed = null;
|
|
180
|
+
try {
|
|
181
|
+
primed = await open({ ...MONO_CONSTRAINTS, facingMode: { ideal: 'environment' } });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
skip('default', err);
|
|
184
|
+
}
|
|
185
|
+
if (primed && isPair(primed)) return result(primed, 'sbs');
|
|
186
|
+
const named = await tryNamedPairs(await listCameras());
|
|
187
|
+
if (named) {
|
|
188
|
+
stop(primed);
|
|
189
|
+
return result(named, 'sbs');
|
|
190
|
+
}
|
|
191
|
+
// A camera that faces the user (or does not say: a desktop webcam) is the fallback as is; a
|
|
192
|
+
// rear one is not what a self view falls back to, so the default is opened below instead.
|
|
193
|
+
if (primed && primed.track?.getSettings?.().facingMode !== 'environment') mono = primed;
|
|
194
|
+
else stop(primed);
|
|
195
|
+
}
|
|
161
196
|
}
|
|
162
197
|
|
|
163
|
-
// The default camera
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
// The default camera: the fallback (and, with labels already visible, also the probe base).
|
|
199
|
+
if (!mono) {
|
|
200
|
+
try {
|
|
201
|
+
mono = await open(MONO_CONSTRAINTS);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
skip('default', err);
|
|
204
|
+
}
|
|
169
205
|
}
|
|
170
206
|
if (want === 'mono' && mono) return result(mono, 'mono');
|
|
171
|
-
if (mono &&
|
|
207
|
+
if (mono && isPair(mono)) return result(mono, 'sbs'); // the default IS the pair
|
|
172
208
|
|
|
173
209
|
let devices = [];
|
|
174
210
|
try {
|
|
@@ -189,7 +225,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
189
225
|
skip(d.label, err);
|
|
190
226
|
continue;
|
|
191
227
|
}
|
|
192
|
-
if (
|
|
228
|
+
if (isPair(r)) {
|
|
193
229
|
stop(mono);
|
|
194
230
|
log('camera-sbs', { label: r.label, width: r.width, height: r.height });
|
|
195
231
|
return result(r, 'sbs');
|
|
@@ -202,7 +238,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
202
238
|
}
|
|
203
239
|
const code = noCameraCode(skipped);
|
|
204
240
|
throw Object.assign(
|
|
205
|
-
new Error(code === 'camera-busy' ? '
|
|
241
|
+
new Error(code === 'camera-busy' ? 'camera unavailable: in use by another app (e.g. eye tracking) or unable to deliver a picture' : code === 'permission-denied' ? 'camera access was denied' : 'no camera could be opened'),
|
|
206
242
|
{ code, skipped }
|
|
207
243
|
);
|
|
208
244
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.47.
|
|
3
|
+
"version": "1.47.4",
|
|
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",
|