@displayxr/inline3d 1.48.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 +76 -0
- package/call.d.ts +114 -7
- package/camera.d.ts +16 -8
- package/dist/call.js +9 -6
- package/js/call/element.js +3 -1
- package/js/call/options.js +2 -0
- package/js/call/share.js +179 -0
- package/js/call/transport.js +150 -9
- package/js/call/ui.js +3 -0
- package/js/call/wire.js +65 -1
- package/js/camera/app-share.js +114 -0
- package/js/camera/capture.js +108 -23
- package/js/inline3d-call.js +361 -36
- package/js/inline3d-camera.js +9 -5
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// camera/app-share.js — recognising a "3D screen share" device: a running DisplayXR app's
|
|
2
|
+
// pre-weave side-by-side views, which the DisplayXR Browser lists as an ordinary `videoinput`
|
|
3
|
+
// (runtime ADR-061, `XR_DXR_app_share`). Pure, no DOM.
|
|
4
|
+
//
|
|
5
|
+
// It lives under /camera, not /call, because the camera probe must know it too: such a device is
|
|
6
|
+
// NOT a camera. Its label ("DisplayXR: <app> (3D)") matches stereoLabelHint's `\b3d\b`, so without
|
|
7
|
+
// this rule `camera: 'auto'` would open somebody's running app and send it as their face. Every
|
|
8
|
+
// probe path in capture.js leaves it out, whatever `exclude` says. `/call`'s share.js builds the
|
|
9
|
+
// discovery API on top of it.
|
|
10
|
+
//
|
|
11
|
+
// The device contract (ADR-061 decision 13): label `DisplayXR: <app> (3D)`, device id prefix
|
|
12
|
+
// `dxr-app:`, and on the opened track `getSettings().displayxrStereo.source === 'app'` (plus
|
|
13
|
+
// `appName`, `zeroDisparityShiftFrac`). Browsers that predate the hint fields still use the label
|
|
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.
|
|
25
|
+
|
|
26
|
+
/** `deviceId` prefix of an app-share device (cameras use `dxr-stereo:`). */
|
|
27
|
+
export const APP_SHARE_ID_PREFIX = 'dxr-app:';
|
|
28
|
+
|
|
29
|
+
/** The label of an app-share device; group 1 is the app's name. */
|
|
30
|
+
export const APP_SHARE_LABEL_RE = /^DisplayXR: (.+) \(3D\)$/;
|
|
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
|
+
|
|
80
|
+
/** The app name an app-share label carries, or null. Pure. */
|
|
81
|
+
export function appNameFromLabel(label) {
|
|
82
|
+
const m = typeof label === 'string' ? APP_SHARE_LABEL_RE.exec(label.trim()) : null;
|
|
83
|
+
return m ? m[1].trim() || null : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Is this an app-share device rather than a camera? Takes a `MediaDeviceInfo` (or any
|
|
88
|
+
* `{ label, deviceId }`), a label string, a track's settings (`{ displayxrStereo }`), or a
|
|
89
|
+
* `MediaStreamTrack` (its label and settings). True on ANY of: the label shape, the `dxr-app:` id
|
|
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.
|
|
93
|
+
* @param {unknown} x
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isAppShareDevice(x) {
|
|
97
|
+
if (!x) return false;
|
|
98
|
+
if (typeof x === 'string') return APP_SHARE_LABEL_RE.test(x.trim()) || x.startsWith(APP_SHARE_ID_PREFIX);
|
|
99
|
+
if (typeof x !== 'object') return false;
|
|
100
|
+
const o = /** @type {any} */ (x);
|
|
101
|
+
try {
|
|
102
|
+
if (typeof o.label === 'string' && APP_SHARE_LABEL_RE.test(o.label.trim())) return true;
|
|
103
|
+
if (typeof o.deviceId === 'string' && o.deviceId.startsWith(APP_SHARE_ID_PREFIX)) return true;
|
|
104
|
+
const hint = o.displayxrStereo || (typeof o.getSettings === 'function' ? (o.getSettings() || {}).displayxrStereo : null);
|
|
105
|
+
if (isAppShareHint(hint)) return true;
|
|
106
|
+
if (typeof o.getSettings === 'function') {
|
|
107
|
+
const id = (o.getSettings() || {}).deviceId;
|
|
108
|
+
if (typeof id === 'string' && id.startsWith(APP_SHARE_ID_PREFIX)) return true;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
/* a hostile getter: not an app */
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
package/js/camera/capture.js
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
// description or by its label (stereoLabelHint), and after the prime the list is read again and
|
|
25
25
|
// a named pair is opened by id BEFORE a default that is a pair only by its shape is kept. The
|
|
26
26
|
// primed default is the fallback.
|
|
27
|
+
// - A DisplayXR Browser also lists every shareable running DisplayXR APP as a side-by-side
|
|
28
|
+
// `videoinput` ("DisplayXR: <app> (3D)", id `dxr-app:…`, runtime ADR-061) — 3D screen share.
|
|
29
|
+
// It is a pair, it names itself "3D", and it is NOT a camera: no probe path opens one, whatever
|
|
30
|
+
// `exclude` says (`exclude: false` included). Each is reported in `skipped` as `excluded` with
|
|
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.
|
|
27
37
|
//
|
|
28
38
|
// RECTIFICATION is not done here (P2): it needs the camera's calibration (intrinsics/extrinsics
|
|
29
39
|
// for the ACTIVE device), which comes from a plug-in or the runtime. P1 sends the raw pair with
|
|
@@ -31,6 +41,7 @@
|
|
|
31
41
|
// `(stream, info) => MediaStream | Promise<MediaStream>`, returning a rectified SBS stream.
|
|
32
42
|
|
|
33
43
|
import { looksSbs } from './geometry.js';
|
|
44
|
+
import { isAppShareDevice, isAppShareHint } from './app-share.js';
|
|
34
45
|
|
|
35
46
|
const MONO_CONSTRAINTS = { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } };
|
|
36
47
|
// Ask a candidate stereo device for its widest mode; a 2560x720 or 3840x1080 pair comes back as
|
|
@@ -69,21 +80,28 @@ export function noCameraCode(skipped) {
|
|
|
69
80
|
* tracker's or a "tracking camera"'s IR stream, a depth camera. Generic words only. Applied on
|
|
70
81
|
* every probe path unless `exclude: false`.
|
|
71
82
|
*/
|
|
72
|
-
|
|
83
|
+
// Generic words, plus the one hand tracker that enumerates under its bare brand name: on Windows
|
|
84
|
+
// an Ultraleap (Leap 2 / Leap Motion Controller) is a UVC "Ultraleap", nothing else in the label —
|
|
85
|
+
// the generic words alone let it through (win box, 2026-11-04: it was the browser's default camera).
|
|
86
|
+
export const CAMERA_EXCLUDE_BUILTIN = /\b(hand[- ]?track|tracking camera|eye[- ]?track|depth camera|ultraleap|leap motion|infrared|ir camera)/i;
|
|
73
87
|
|
|
74
88
|
/**
|
|
75
89
|
* The exclusion list in force: the built-in pattern plus `exclude` (strings match a label
|
|
76
|
-
* case-insensitively as a substring, or a deviceId exactly; RegExps test the label), or
|
|
77
|
-
*
|
|
90
|
+
* case-insensitively as a substring, or a deviceId exactly; RegExps test the label), or only the
|
|
91
|
+
* app-share rule for `exclude: false`. Returns a predicate `(label, deviceId) => boolean`. An
|
|
92
|
+
* app-share device (camera/app-share.js: a running app's views, not a camera) is ALWAYS excluded —
|
|
93
|
+
* `exclude: false` turns the sensor list off, never that. Pure.
|
|
78
94
|
* @param {false|null|undefined|ReadonlyArray<string|RegExp>|string|RegExp} exclude
|
|
79
95
|
*/
|
|
80
96
|
export function cameraExcluder(exclude) {
|
|
81
|
-
|
|
97
|
+
const app = (label, deviceId) => isAppShareDevice({ label: String(label || ''), deviceId: typeof deviceId === 'string' ? deviceId : '' });
|
|
98
|
+
if (exclude === false) return app;
|
|
82
99
|
const list = [CAMERA_EXCLUDE_BUILTIN];
|
|
83
100
|
for (const x of exclude == null ? [] : Array.isArray(exclude) ? exclude : [exclude]) {
|
|
84
101
|
if (x instanceof RegExp || (typeof x === 'string' && x)) list.push(x);
|
|
85
102
|
}
|
|
86
103
|
return (label, deviceId) => {
|
|
104
|
+
if (app(label, deviceId)) return true;
|
|
87
105
|
const l = String(label || '');
|
|
88
106
|
for (const x of list) {
|
|
89
107
|
if (x instanceof RegExp) {
|
|
@@ -95,8 +113,12 @@ export function cameraExcluder(exclude) {
|
|
|
95
113
|
};
|
|
96
114
|
}
|
|
97
115
|
|
|
98
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* A label that names a stereo camera, tried first when probing. Never an app-share device's
|
|
118
|
+
* ("DisplayXR: <app> (3D)" says "3D" but is a running app, not a camera). Pure.
|
|
119
|
+
*/
|
|
99
120
|
export function stereoLabelHint(label) {
|
|
121
|
+
if (isAppShareDevice(String(label || ''))) return false;
|
|
100
122
|
return /stereo|\b3d\b|\bsbs\b|dual/i.test(label || '');
|
|
101
123
|
}
|
|
102
124
|
|
|
@@ -143,34 +165,64 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
143
165
|
// eye-tracking camera shared without stealing it) describes itself on the opened track:
|
|
144
166
|
// `displayxrStereo: {layout, rectified, baselineMm, horizontalFovDeg}`.
|
|
145
167
|
const stereo = st.displayxrStereo && st.displayxrStereo.layout === 'side-by-side' ? st.displayxrStereo : null;
|
|
146
|
-
|
|
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);
|
|
173
|
+
return { stream: s, track: t, width: st.width, height: st.height, deviceId: st.deviceId || null, label: t.label || '', stereo, appShare };
|
|
147
174
|
};
|
|
148
175
|
const skip = (label, err) => {
|
|
149
176
|
skipped.push({ label, error: `${err.name}: ${err.message}`, busy: isBusyError(err), denied: isDeniedError(err) });
|
|
150
177
|
log('camera-skip', { label, error: err.name });
|
|
151
178
|
};
|
|
152
179
|
const stop = (r) => r && r.stream.getTracks().forEach((t) => t.stop());
|
|
180
|
+
const listCameras = async () => {
|
|
181
|
+
try {
|
|
182
|
+
return (await md.enumerateDevices()).filter((d) => d.kind === 'videoinput');
|
|
183
|
+
} catch {
|
|
184
|
+
return []; // no enumeration: the paths below still work
|
|
185
|
+
}
|
|
186
|
+
};
|
|
153
187
|
// Excluded devices: never opened by id; a default that turns out to be one is closed again.
|
|
154
188
|
const excludedIds = new Set();
|
|
155
|
-
|
|
189
|
+
// `reason`: 'app-share' for a running app's views (never a camera), else 'excluded'.
|
|
190
|
+
const exclude = (label, deviceId, reason = 'excluded') => {
|
|
156
191
|
if (deviceId && excludedIds.has(deviceId)) return;
|
|
157
192
|
if (deviceId) excludedIds.add(deviceId);
|
|
158
|
-
skipped.push({ label, error: 'excluded', excluded: true });
|
|
159
|
-
log('camera-skip', { label, error: 'excluded' });
|
|
193
|
+
skipped.push({ label, error: 'excluded', excluded: true, reason });
|
|
194
|
+
log('camera-skip', { label, error: 'excluded', reason });
|
|
160
195
|
};
|
|
196
|
+
const isApp = (d) => isAppShareDevice({ label: d.label || '', deviceId: d.deviceId || '' });
|
|
161
197
|
const listedExcluded = (d) => {
|
|
198
|
+
if (isApp(d)) {
|
|
199
|
+
exclude(d.label || '', d.deviceId, 'app-share');
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
162
202
|
if (!d.label || !isExcluded(d.label, d.deviceId)) return false;
|
|
163
203
|
exclude(d.label, d.deviceId);
|
|
164
204
|
return true;
|
|
165
205
|
};
|
|
206
|
+
/** An opened device that must not be used: an app share (by its own hint too) or an excluded label. Closes it. */
|
|
207
|
+
const rejectOpened = (r) => {
|
|
208
|
+
if (r.appShare) {
|
|
209
|
+
stop(r);
|
|
210
|
+
exclude(r.label, r.deviceId, 'app-share');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (isExcluded(r.label, r.deviceId)) {
|
|
214
|
+
stop(r);
|
|
215
|
+
exclude(r.label, r.deviceId);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
};
|
|
166
220
|
// An open of the DEFAULT (no id): only after it opened is its label known. An excluded default
|
|
167
221
|
// is closed and never opened again in this probe.
|
|
168
222
|
let defaultExcluded = false;
|
|
169
223
|
const openDefault = async (constraints) => {
|
|
170
224
|
const r = await open(constraints);
|
|
171
|
-
if (
|
|
172
|
-
stop(r);
|
|
173
|
-
exclude(r.label, r.deviceId);
|
|
225
|
+
if (rejectOpened(r)) {
|
|
174
226
|
defaultExcluded = true;
|
|
175
227
|
return null;
|
|
176
228
|
}
|
|
@@ -206,6 +258,28 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
206
258
|
},
|
|
207
259
|
});
|
|
208
260
|
|
|
261
|
+
// `label:<text>`: the first camera whose label contains the text (case-insensitive) — a way to
|
|
262
|
+
// force a device from a URL or a config, where a per-origin deviceId is useless. Labels need
|
|
263
|
+
// permission: with none visible yet, the default camera is primed once and let go. Explicit like
|
|
264
|
+
// a deviceId, so it opens an excluded device too.
|
|
265
|
+
if (typeof want === 'string' && want.startsWith('label:')) {
|
|
266
|
+
const needle = want.slice('label:'.length).trim().toLowerCase();
|
|
267
|
+
let list = await listCameras();
|
|
268
|
+
if (needle && list.length && list.every((d) => !d.label)) {
|
|
269
|
+
try {
|
|
270
|
+
stop(await open(true));
|
|
271
|
+
} catch (err) {
|
|
272
|
+
skip('default', err);
|
|
273
|
+
}
|
|
274
|
+
list = await listCameras();
|
|
275
|
+
}
|
|
276
|
+
// Never a running app's views: `label:3d` means a camera (open an app with openAppShare()).
|
|
277
|
+
const d = needle ? list.find((x) => x.deviceId && !isApp(x) && (x.label || '').toLowerCase().includes(needle)) : null;
|
|
278
|
+
if (!d) throw Object.assign(new Error(`camera unavailable: no camera labelled "${needle}"`), { code: noCameraCode(skipped), skipped });
|
|
279
|
+
log('camera-label', { label: d.label, needle });
|
|
280
|
+
want = d.deviceId;
|
|
281
|
+
}
|
|
282
|
+
|
|
209
283
|
// An explicit device id.
|
|
210
284
|
if (typeof want === 'string' && !['auto', 'stereo', 'mono'].includes(want)) {
|
|
211
285
|
let r;
|
|
@@ -225,13 +299,6 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
225
299
|
// camera start can take seconds (a busy capture stack took 7-20 s per start on a field box), and
|
|
226
300
|
// the default-first probe below costs two or three of them before it reaches the pair.
|
|
227
301
|
const tried = new Set();
|
|
228
|
-
const listCameras = async () => {
|
|
229
|
-
try {
|
|
230
|
-
return (await md.enumerateDevices()).filter((d) => d.kind === 'videoinput');
|
|
231
|
-
} catch {
|
|
232
|
-
return []; // no enumeration: the paths below still work
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
302
|
const tryNamedPairs = async (list) => {
|
|
236
303
|
for (const d of list) {
|
|
237
304
|
if (!d.deviceId || !stereoLabelHint(d.label) || tried.has(d.deviceId)) continue;
|
|
@@ -244,6 +311,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
244
311
|
skip(d.label, err);
|
|
245
312
|
continue;
|
|
246
313
|
}
|
|
314
|
+
if (rejectOpened(r)) continue; // an app that described itself only on its track
|
|
247
315
|
if (isPair(r)) {
|
|
248
316
|
log('camera-sbs', { label: r.label, width: r.width, height: r.height, direct: true });
|
|
249
317
|
return r;
|
|
@@ -290,11 +358,27 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
290
358
|
}
|
|
291
359
|
|
|
292
360
|
// The default camera: the fallback (and, with labels already visible, also the probe base).
|
|
361
|
+
// With labels visible and an excluded device listed, the default is not opened blind — it may BE
|
|
362
|
+
// the sensor (an idle Ultraleap is the default camera on some Windows boxes, and an eye tracker's
|
|
363
|
+
// camera should not be started at all) — the first camera that is not excluded is opened by id.
|
|
293
364
|
if (!mono && !defaultExcluded) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
365
|
+
const listed = await listCameras();
|
|
366
|
+
const anyExcluded = listed.filter(listedExcluded).length > 0; // every excluded device is noted, not just the first
|
|
367
|
+
const first = anyExcluded ? listed.find((d) => d.deviceId && !excludedIds.has(d.deviceId)) || null : null;
|
|
368
|
+
if (first) {
|
|
369
|
+
if (first.deviceId) tried.add(first.deviceId);
|
|
370
|
+
try {
|
|
371
|
+
mono = await open({ deviceId: { exact: first.deviceId }, ...MONO_CONSTRAINTS });
|
|
372
|
+
if (rejectOpened(mono)) mono = null;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
skip(first.label, err);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
try {
|
|
378
|
+
mono = await openDefault(MONO_CONSTRAINTS);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
skip('default', err);
|
|
381
|
+
}
|
|
298
382
|
}
|
|
299
383
|
}
|
|
300
384
|
if (want === 'mono' && mono) return result(mono, 'mono');
|
|
@@ -321,6 +405,7 @@ export async function openCamera(want = 'auto', o = {}) {
|
|
|
321
405
|
skip(d.label, err);
|
|
322
406
|
continue;
|
|
323
407
|
}
|
|
408
|
+
if (rejectOpened(r)) continue;
|
|
324
409
|
if (isPair(r) && want !== 'mono') {
|
|
325
410
|
stop(mono);
|
|
326
411
|
log('camera-sbs', { label: r.label, width: r.width, height: r.height });
|