@displayxr/inline3d 1.47.3 → 1.47.5

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.
@@ -9,8 +9,12 @@
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, so the default camera is opened first
13
- // (which is also the fallback), then the others are probed one at a time.
12
+ // - Labels are empty until one getUserMedia has succeeded. Without them, permission is primed on
13
+ // the DEFAULT camera with no constraints: in the DisplayXR Browser the runtime's 3D camera is
14
+ // listed first, so the prime opens the pair itself and no phone lens is opened at all (a plain
15
+ // open of one lens of the pair, or its teardown, crashes some phones' camera HAL; any facing or
16
+ // size constraint lets the engine pick a lens instead). Then the list is read again and a
17
+ // stereo device is opened by id BEFORE falling back. The primed default is the fallback.
14
18
  //
15
19
  // RECTIFICATION is not done here (P2): it needs the camera's calibration (intrinsics/extrinsics
16
20
  // for the ACTIVE device), which comes from a plug-in or the runtime. P1 sends the raw pair with
@@ -101,6 +105,20 @@ export async function openCamera(want = 'auto', o = {}) {
101
105
  log('camera-skip', { label, error: err.name });
102
106
  };
103
107
  const stop = (r) => r && r.stream.getTracks().forEach((t) => t.stop());
108
+ // Ask an open camera for another mode without a second start; keep it as it is if it refuses.
109
+ const withMode = async (r, constraints) => {
110
+ try {
111
+ await r.track.applyConstraints(constraints);
112
+ const st = r.track.getSettings();
113
+ if (st.width > 0 && st.height > 0) return { ...r, width: st.width, height: st.height };
114
+ } catch {
115
+ /* keep the mode it opened in */
116
+ }
117
+ return r;
118
+ };
119
+ // A track that declares itself a side-by-side pair IS one, whatever its aspect (a phone's portrait
120
+ // eyes make a 1440x1280 pair). Shape is only the guess for an unknown device, e.g. a USB pair.
121
+ const isPair = (r) => !!r && (!!r.stereo || looksSbs(r.width, r.height));
104
122
  const result = (r, format) => ({
105
123
  stream: r.stream, format, width: r.width, height: r.height, deviceId: r.deviceId, label: r.label, owned: true, skipped,
106
124
  calibration: {
@@ -126,7 +144,7 @@ export async function openCamera(want = 'auto', o = {}) {
126
144
  skip(want, err);
127
145
  throw Object.assign(new Error(`camera unavailable: ${err.message}`), { code: noCameraCode(skipped), skipped });
128
146
  }
129
- if (looksSbs(r.width, r.height)) return result(r, 'sbs');
147
+ if (isPair(r)) return result(r, 'sbs');
130
148
  stop(r);
131
149
  return result(await open({ deviceId: { exact: want }, ...MONO_CONSTRAINTS }), 'mono');
132
150
  }
@@ -136,14 +154,16 @@ export async function openCamera(want = 'auto', o = {}) {
136
154
  // camera start can take seconds (a busy capture stack took 7-20 s per start on a field box), and
137
155
  // the default-first probe below costs two or three of them before it reaches the pair.
138
156
  const tried = new Set();
139
- if (want !== 'mono') {
140
- let early = [];
157
+ const listCameras = async () => {
141
158
  try {
142
- early = (await md.enumerateDevices()).filter((d) => d.kind === 'videoinput' && d.deviceId && stereoLabelHint(d.label));
159
+ return (await md.enumerateDevices()).filter((d) => d.kind === 'videoinput');
143
160
  } catch {
144
- /* no enumeration yet: the normal path below */
161
+ return []; // no enumeration: the paths below still work
145
162
  }
146
- for (const d of early) {
163
+ };
164
+ const tryNamedPairs = async (list) => {
165
+ for (const d of list) {
166
+ if (!d.deviceId || !stereoLabelHint(d.label) || tried.has(d.deviceId)) continue;
147
167
  tried.add(d.deviceId);
148
168
  let r = null;
149
169
  try {
@@ -152,23 +172,57 @@ export async function openCamera(want = 'auto', o = {}) {
152
172
  skip(d.label, err);
153
173
  continue;
154
174
  }
155
- if (looksSbs(r.width, r.height)) {
175
+ if (isPair(r)) {
156
176
  log('camera-sbs', { label: r.label, width: r.width, height: r.height, direct: true });
157
- return result(r, 'sbs');
177
+ return r;
158
178
  }
159
179
  stop(r);
160
180
  }
181
+ return null;
182
+ };
183
+ let mono = null;
184
+ if (want !== 'mono') {
185
+ let listed = await listCameras();
186
+ let pair = await tryNamedPairs(listed);
187
+ if (pair) return result(pair, 'sbs');
188
+ // Labels visible but no stereo-named device: a runtime 3D camera can join the list late (after
189
+ // a runtime restart), so read it once more before falling back.
190
+ if (listed.some((d) => d.label) && !listed.some((d) => stereoLabelHint(d.label))) {
191
+ listed = await listCameras();
192
+ pair = await tryNamedPairs(listed);
193
+ if (pair) return result(pair, 'sbs');
194
+ }
195
+ // No labels yet (no permission). Prime it on the default camera with NO constraints: in the
196
+ // DisplayXR Browser that is the 3D camera (listed first), so no phone lens is opened; a facing
197
+ // or size constraint would let the engine pick a lens instead. Then open a pair by id.
198
+ if (listed.length && listed.every((d) => !d.label)) {
199
+ let primed = null;
200
+ try {
201
+ primed = await open(true);
202
+ } catch (err) {
203
+ skip('default', err);
204
+ }
205
+ if (primed && isPair(primed)) return result(primed, 'sbs');
206
+ const named = await tryNamedPairs(await listCameras());
207
+ if (named) {
208
+ stop(primed);
209
+ return result(named, 'sbs');
210
+ }
211
+ // The primed default is the fallback as is (one camera start); ask it for the usual mode.
212
+ if (primed) mono = await withMode(primed, MONO_CONSTRAINTS);
213
+ }
161
214
  }
162
215
 
163
- // The default camera first: permission, labels, and the fallback.
164
- let mono = null;
165
- try {
166
- mono = await open(MONO_CONSTRAINTS);
167
- } catch (err) {
168
- skip('default', err);
216
+ // The default camera: the fallback (and, with labels already visible, also the probe base).
217
+ if (!mono) {
218
+ try {
219
+ mono = await open(MONO_CONSTRAINTS);
220
+ } catch (err) {
221
+ skip('default', err);
222
+ }
169
223
  }
170
224
  if (want === 'mono' && mono) return result(mono, 'mono');
171
- if (mono && looksSbs(mono.width, mono.height)) return result(mono, 'sbs'); // the default IS the pair
225
+ if (mono && isPair(mono)) return result(mono, 'sbs'); // the default IS the pair
172
226
 
173
227
  let devices = [];
174
228
  try {
@@ -189,7 +243,7 @@ export async function openCamera(want = 'auto', o = {}) {
189
243
  skip(d.label, err);
190
244
  continue;
191
245
  }
192
- if (looksSbs(r.width, r.height)) {
246
+ if (isPair(r)) {
193
247
  stop(mono);
194
248
  log('camera-sbs', { label: r.label, width: r.width, height: r.height });
195
249
  return result(r, 'sbs');
@@ -202,7 +256,7 @@ export async function openCamera(want = 'auto', o = {}) {
202
256
  }
203
257
  const code = noCameraCode(skipped);
204
258
  throw Object.assign(
205
- new Error(code === 'camera-busy' ? 'every camera is in use by another app (e.g. eye tracking)' : code === 'permission-denied' ? 'camera access was denied' : 'no camera could be opened'),
259
+ 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
260
  { code, skipped }
207
261
  );
208
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.47.3",
3
+ "version": "1.47.5",
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",