@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/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
- return list
44
- .filter((d) => d && d.kind === 'videoinput' && isAppShareDevice(d))
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
- const appName = sanitizeName(hint.appName) || sanitizeName(appNameFromLabel(track && track.label)) || sanitizeName(appNameFromLabel(fallbackLabel)) || null;
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'`). The track is
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
- if (!track || !(isAppShareDevice(track) || deviceId.startsWith(APP_SHARE_ID_PREFIX))) {
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
  }
@@ -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.source === 'app'`. Never throws. Pure.
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 && typeof hint === 'object' && hint.source === 'app') return true;
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;
@@ -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
- const appShare = isAppShareDevice({ label: t.label || '', deviceId: st.deviceId || '' }) || !!(st.displayxrStereo && st.displayxrStereo.source === 'app');
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.0",
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",