@displayxr/inline3d 1.28.0 → 1.30.0

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/call.d.ts CHANGED
@@ -4,71 +4,33 @@
4
4
  // P1 of docs/rfcs/0002-video-call.md: full-mesh WebRTC (<= 4 participants), pluggable signalling,
5
5
  // side-by-side stereo from a stereo camera, convergence from `hello` + `hint`, SDK chrome or
6
6
  // headless. P2a: mono peers lifted to 3D through `@displayxr/inline3d/lift` (`mono3D`).
7
+ // RFC 0003 C1: `mountCall(el, opts?)` (the one-line path, the wall from `sharedInline3D()`), the
8
+ // `warning` event (`lift-not-bundled`), `key`; `<dxr-call>` is `./call/element`, and
9
+ // `./call/full` is this entry with lift statically imported.
10
+ // RFC 0003 C2 (§2): THIS is the public surface — what a page needs to run, observe and dress a
11
+ // call. The test helpers that used to be exported here are internal (importable by file path
12
+ // from `js/call/*.js`); 1.30 keeps them as `@deprecated` wrappers (the block at the end), 1.31
13
+ // removes them. Options are grouped (`theme`, `invite`, `landing`, `liftOptions.max`), the camera
14
+ // is `@displayxr/inline3d/camera` (`camera: cam`), and `diagnostics()` holds the debug data.
15
+ //
16
+ // The public surface of this file is pinned by test/api-snapshot: a change here fails `npm test`
17
+ // until the snapshot is updated in the same change (`node tools/api-surface.mjs --update`).
7
18
 
8
- /**
9
- * The `lift()` of `@displayxr/inline3d/lift`, as the call uses it: `lift(video, { mode: 'live',
10
- * wall, ui: 'none', convergence, priority, quality })` → a handle. Only the members below are
11
- * used; `setPriority` / `setConvergence` / `on` / `native` / `canvas` / `state` are optional.
12
- */
13
- export interface CallLiftHandle {
14
- remove(): void;
15
- setPriority?(p: LiftStreamPriority): boolean;
16
- setConvergence?(x: 'auto' | number): void;
17
- on?(type: string, cb: (d: any) => void): () => void;
18
- readonly native?: boolean;
19
- readonly woven?: boolean;
20
- readonly state?: string;
21
- readonly canvas?: HTMLCanvasElement;
22
- readonly stats?: { provider?: string | null } & Record<string, unknown>;
23
- }
24
- export type CallLiftFunction = ((element: HTMLVideoElement, opts: Record<string, unknown> & {
25
- mode: 'live';
26
- wall: unknown;
27
- ui: 'none';
28
- convergence: 'auto' | number;
29
- priority: LiftStreamPriority;
30
- }) => Promise<CallLiftHandle>) & { liftCapabilities?: (o?: { webFallback?: boolean }) => Promise<any> };
31
-
32
- /** A lifted stream's scheduling (the native provider's per-stream priority). */
33
- export type LiftStreamPriority = 'high' | 'normal' | 'low' | 'paused';
34
-
35
- /** Why a mono peer is not lifted (or `'lifted'`). */
36
- export type Mono3DReason = 'unavailable' | 'off' | 'lifted' | 'budget' | 'failed';
19
+ import type { Inline3D, Inline3DUnsupported } from './index.js';
20
+ import type { StereoCamera } from './camera.js';
37
21
 
38
22
  /** What a participant sends: a side-by-side pair (left eye left) or one flat picture. */
39
23
  export type CallFormat = 'sbs' | 'mono';
40
24
 
41
- /** How this page draws a remote participant (RFC §3). */
42
- export type CallRoute = 'woven-sbs' | 'flat-left' | 'flat' | 'lifted';
43
-
44
- /** A remote participant's connection state as the tile shows it. */
45
25
  /**
46
- * `'unreachable'`: known to exist but no connection for ~10 s — usually a network that needs a
47
- * relay (TURN). Sticky until it connects; retries continue in the background.
26
+ * A remote participant's connection state as the tile shows it. `'unreachable'`: known to exist
27
+ * but no connection for ~10 s — usually a network that needs a relay (TURN). Sticky until it
28
+ * connects; retries continue in the background.
48
29
  */
49
30
  export type PeerState = 'new' | 'connecting' | 'connected' | 'reconnecting' | 'unreachable' | 'left';
50
31
 
51
- /** The out-of-band 3D flag, sent on the data channel when it opens. A peer that sends none is mono. */
52
- export interface CallHello {
53
- type: 'hello';
54
- v: number;
55
- format: CallFormat;
56
- width: number | null;
57
- height: number | null;
58
- /** Camera baseline, mm. Needed (with `hfovDeg`) for automatic convergence. */
59
- baselineMm: number | null;
60
- /** Horizontal field of view of ONE eye, degrees. */
61
- hfovDeg: number | null;
62
- /** False for a raw stereo camera (the P1 default); true only after a calibrated rectify step. */
63
- rectified: boolean;
64
- sdk: string | null;
65
- }
66
-
67
- export interface CallCalibration {
68
- baselineMm?: number;
69
- hfovDeg?: number;
70
- rectified?: boolean;
71
- }
32
+ /** How THIS side shows a participant: a woven stereo pair, a mono peer lifted to 3D, or flat. */
33
+ export type CallDisplay = '3D' | '2D→3D' | '2D';
72
34
 
73
35
  /**
74
36
  * The signalling seam. Any object with `join()` works — bring your own accounts / push service.
@@ -102,65 +64,96 @@ export interface SignalingSession {
102
64
  leave(): void;
103
65
  }
104
66
 
67
+ /** The hosted DisplayXR signalling server (`dxrSignaling()` with no URL). */
68
+ export const DXR_SIGNAL_DEFAULT: string;
105
69
  /**
106
70
  * The `dxr-signal/1` JSON-over-WebSocket client (protocol: signaling/README.md). `url` is your
107
71
  * server's base, e.g. `wss://signal.example.com` or `ws://localhost:8787` — the reference servers
108
- * are `signaling/worker.mjs` (Cloudflare) and `node signaling/dev-server.mjs`. There is no hosted
109
- * default yet, so the URL is required.
72
+ * are `signaling/worker.mjs` (Cloudflare) and `node signaling/dev-server.mjs`. No URL = the hosted
73
+ * server. `key`: a publishable key for the hosted service (public by design; sent on connect).
110
74
  */
111
- /** The hosted DisplayXR signalling server (`dxrSignaling()` with no URL). */
112
- export const DXR_SIGNAL_DEFAULT: string;
113
- export function dxrSignaling(url?: string, opts?: { WebSocket?: any; pingMs?: number }): SignalingAdapter;
75
+ export function dxrSignaling(url?: string, opts?: { WebSocket?: any; pingMs?: number; key?: string }): SignalingAdapter;
76
+
77
+ /** The eight named accents `theme.accent` takes (the same names `/player` uses). */
78
+ export type CallAccent = 'azure' | 'violet' | 'magenta' | 'sunset' | 'amber' | 'lime' | 'mint' | 'ice';
114
79
 
115
80
  /**
116
- * DEMO ONLY — the free public PeerJS broker. No uptime guarantee and not operated by DisplayXR.
117
- * `Peer` defaults to `globalThis.Peer`, else the ESM build is imported from jsDelivr.
81
+ * Every string the chrome shows, by key (docs/call.md lists them with their defaults). A partial
82
+ * map overrides any subset — which is also how the chrome is localised. `{var}` placeholders are
83
+ * filled by the module.
118
84
  */
119
- export function peerjsCloud(opts?: {
120
- Peer?: any;
121
- peerOptions?: object;
122
- url?: string;
123
- /** Upper bound on the join-time wait for slots to say hello (ms, default 5000). */
124
- helloMs?: number;
125
- /** A taken slot silent this long is reported unreachable (ms, default 10000). */
126
- unreachableMs?: number;
127
- /** Background re-dial interval for silent slots (ms, default 10000). */
128
- retryMs?: number;
129
- heartbeatMs?: number;
130
- settleMs?: number;
131
- }): SignalingAdapter;
85
+ export type CallStrings = Partial<Record<
86
+ | 'lobbyStartTitle' | 'lobbyJoinTitle' | 'lobbyFullTitle' | 'lobbyLeftTitle' | 'lobbyFullText' | 'lobbyText'
87
+ | 'cameraDefault' | 'kindBusy' | 'kindNone' | 'kindSbs' | 'kindMono'
88
+ | 'liftOff' | 'liftChecking' | 'liftMissing' | 'liftNoProvider' | 'liftNative' | 'liftWeb' | 'liftProven' | 'liftUnproven'
89
+ | 'start' | 'join' | 'rejoin' | 'retryCamera' | 'cameraSelect' | 'joining'
90
+ | 'waitingTitle' | 'waitingText' | 'inviteTitle' | 'inviteText' | 'inviteLink' | 'copyLink' | 'copied' | 'pressCopy' | 'qrLabel'
91
+ | 'banner2D' | 'bannerLink'
92
+ | 'mute' | 'unmute' | 'cameraOn' | 'cameraOff' | 'cameraRetry' | 'cameraBusyRetry' | 'depth' | 'invite' | 'leave'
93
+ | 'connecting' | 'reconnecting' | 'leftCall' | 'cameraOffPlate' | 'noCamera' | 'unreachable' | 'cameraBusy'
94
+ | 'you' | 'badge3D' | 'badge2D3D' | 'badge2D' | 'badgePending',
95
+ string
96
+ >>;
132
97
 
133
- export type CallAccent = 'azure' | 'violet' | 'magenta' | 'sunset' | 'amber' | 'lime' | 'mint' | 'ice';
98
+ /**
99
+ * Theming (RFC 0003 §2). The colour / radius / font entries are written as CSS custom properties
100
+ * on the host (`--dxr-accent`, `--dxr-ink`, `--dxr-shell`, `--dxr-danger`, `--dxr-radius`,
101
+ * `--dxr-font`), which a stylesheet can set instead; the chrome also exposes `part` names
102
+ * (`bar`, `invite`, `badge`, `plate`, `lobby`, `banner`, `self`, `tile`, `grid`) — reached as
103
+ * `dxr-call [part="bar"]`, since the chrome is light DOM. Every one of these styles chrome only:
104
+ * a woven tile and its ancestors stay visually bare (woven-canvas rule 7), and no theme can reach
105
+ * them.
106
+ */
107
+ export interface CallTheme {
108
+ /** A named accent or any CSS colour → `--dxr-accent`. Default `'azure'`. */
109
+ accent?: CallAccent | (string & {});
110
+ /** Text on chrome → `--dxr-ink`. */
111
+ ink?: string;
112
+ /** Chrome background → `--dxr-shell` (near-solid on purpose: no blur over the weave). */
113
+ shell?: string;
114
+ /** Leave / muted → `--dxr-danger`. */
115
+ danger?: string;
116
+ /** Chrome corner radius (px, or any CSS length) → `--dxr-radius`. Never applied to a tile. */
117
+ radius?: number | string;
118
+ /** Chrome font family → `--dxr-font`. */
119
+ font?: string;
120
+ /** Tile aspect (w/h) = the woven buffer's per-eye aspect. Default 16/9. */
121
+ tileAspect?: number;
122
+ /** Overrides for the chrome's text (also localisation). */
123
+ strings?: CallStrings;
124
+ }
134
125
 
135
126
  export interface CallOptions {
136
- /** Default `dxrSignaling()` (the hosted DisplayXR server). Or `dxrSignaling(url)` to self-host,
137
- * `peerjsCloud()` (demo only), or your own adapter. */
138
- signaling?: SignalingAdapter;
127
+ /**
128
+ * The inline-3D manager to put the tiles on. `mountCall` defaults it to the document's shared
129
+ * one (`sharedInline3D()`); `addCall` takes it as its first argument instead.
130
+ */
131
+ wall?: Inline3D | Inline3DUnsupported | null;
139
132
  /**
140
133
  * `'auto'` (default): the room in this page's `#room=` fragment, else a new one on join. Or a
141
134
  * room id / invite link. Room ids are >= 96 random bits (generated ones: 128), base64url.
142
135
  */
143
- room?: 'auto' | string;
136
+ room?: 'auto' | (string & {});
137
+ /** Default `dxrSignaling()` (the hosted DisplayXR server). Or `dxrSignaling(url)` to self-host, or your own adapter. */
138
+ signaling?: SignalingAdapter;
139
+ /**
140
+ * A publishable key for the hosted signalling service (`pk_…`, RFC 0003 §5a). Public by design
141
+ * — it sits in page source — and only used when `signaling` is left at the default.
142
+ */
143
+ key?: string;
144
144
  /** Overrides the server's STUN/TURN list. Default: public STUN + the server's TURN, if any. */
145
145
  iceServers?: RTCIceServer[];
146
146
  /**
147
- * `'auto'` (default): a device delivering > 2.5:1 frames is a stereo pair, sent as `'sbs'`;
148
- * otherwise the default webcam, `'mono'`. A camera held by another process (e.g. an eye
149
- * tracker: NotReadableError) is skipped, never fatal. `'stereo'` prefers the pair (falls back
150
- * to mono), `'mono'` never probes, a string is a deviceId, a MediaStream is used as given with
151
- * `format` declaring what it is.
147
+ * `'auto'` (default): the call opens the best camera through `@displayxr/inline3d/camera`
148
+ * (`openCamera({ prefer: 'auto' })`): a stereo device when one is present, else the default
149
+ * webcam; a camera held by another process (an eye tracker) is skipped, never fatal. `'stereo'`
150
+ * prefers the pair (falls back to mono), `'mono'` never probes, a string is a `deviceId`, a
151
+ * `MediaStream` is used as given with `format` declaring what it is, and a `StereoCamera` you
152
+ * opened yourself is used as is (and left open when the call ends).
152
153
  */
153
- camera?: 'auto' | 'stereo' | 'mono' | string | MediaStream;
154
- /** The format of a page-supplied `camera` stream. Default `'mono'` — 3D-ness is never guessed. */
154
+ camera?: 'auto' | 'stereo' | 'mono' | (string & {}) | MediaStream | StereoCamera;
155
+ /** The format of a page-supplied `MediaStream`. Default `'mono'` — 3D-ness is never guessed. */
155
156
  format?: CallFormat;
156
- /** Sent in `hello`; enables automatic convergence on the receivers. */
157
- calibration?: CallCalibration;
158
- /**
159
- * P2 seam: a calibrated rectification step for a raw stereo stream (needs the camera's
160
- * intrinsics/extrinsics, which come from a plug-in or the runtime). Return a rectified SBS
161
- * stream; the hello then says `rectified: true`. P1 ships none: the raw pair is sent.
162
- */
163
- rectify?: (stream: MediaStream, info: { width: number; height: number; deviceId: string | null; label: string }) => MediaStream | Promise<MediaStream>;
164
157
  /** Microphone (echo cancellation + noise suppression). Default true. */
165
158
  audio?: boolean;
166
159
  /**
@@ -168,207 +161,325 @@ export interface CallOptions {
168
161
  * `@displayxr/inline3d/lift` lazily from this copy of the SDK, and use it when it reports a
169
162
  * native provider or a WebGPU web fallback — otherwise mono peers stay flat (one log line, no
170
163
  * error). `'off'` / `false`: flat. A function: your own `lift` (a bundled app imports
171
- * `lift` from `@displayxr/inline3d/lift` and passes it here). Switch at runtime with
172
- * `setMono3D()`.
164
+ * `lift` from `@displayxr/inline3d/lift` and passes it here, or uses `./call/full`). Switch at
165
+ * runtime with `setMono3D()`.
173
166
  */
174
- mono3D?: 'auto' | 'off' | false | CallLiftFunction;
167
+ mono3D?: 'auto' | 'off' | false | LiftFunction;
175
168
  /**
176
169
  * Extra `lift()` options for lifted tiles — e.g. `models` (where the depth model is served from),
177
- * `ort`, `quality`, `providers`. The call's own keys (`mode`, `wall`, `ui`, `convergence`,
178
- * `priority`) always win.
170
+ * `ort`, `quality`, `providers` — plus `max`: concurrent lifted tiles (default 4 = the mesh
171
+ * maximum; further mono peers stay flat). The call's own keys (`mode`, `wall`, `ui`,
172
+ * `convergence`, `priority`) always win.
173
+ */
174
+ liftOptions?: Record<string, unknown> & { max?: number };
175
+ /**
176
+ * Auto-convergence (default true): measure the disparity of the point between each SBS peer's
177
+ * eyes on the received frames and shift the two eyes by half of it each, so the remote person sits
178
+ * at the display plane. No calibration needed. The depth slider stays an offset on top.
179
179
  */
180
- liftOptions?: Record<string, unknown>;
181
- /** Concurrent lifted tiles. Default 4 (= the mesh maximum); further mono peers stay flat. */
182
- maxLifted?: number;
180
+ autoConverge?: boolean;
183
181
  /** Participants INCLUDING you. Default 4, clamped to 2..4 (full mesh). */
184
182
  maxPeers?: number;
185
- /** `'grid'` (default) or `'speaker'` (the active speaker spans the row). */
186
- layout?: 'grid' | 'speaker';
187
- /** SDK chrome: lobby, invite (link + QR), bottom bar, badges, banner. Default true. */
188
- ui?: boolean;
189
- /** Join without the lobby. Default: `!ui`. */
183
+ /**
184
+ * `'grid'` (default), `'speaker'` (the active speaker spans the row), or `'none'`: the module
185
+ * creates the tiles but does not position them — each carries `data-dxr-peer="<id>"` and
186
+ * `tile(id)` returns it, for the page's own CSS grid on the host.
187
+ */
188
+ layout?: 'grid' | 'speaker' | 'none';
189
+ /**
190
+ * SDK chrome. `true` (default): lobby, invite (link + QR), bottom bar, badges, plates, banner.
191
+ * `'tiles'`: badges and state plates only — your bar, the module's correct per-tile chrome.
192
+ * `false`: nothing. Tiles are module-owned in every mode (they are woven canvases).
193
+ */
194
+ ui?: boolean | 'tiles';
195
+ /** Join without the lobby. Default: true unless `ui` is `true`. */
190
196
  autoJoin?: boolean;
191
- /** The small self view (mirrored; a stereo self view is mirrored AND eye-swapped). Default true. */
197
+ /** The small self view (mirrored; a stereo self view is mirrored AND eye-swapped — an `addCameraView`). Default true. */
192
198
  selfView?: boolean;
193
- /** Tile aspect (w/h) = the woven buffer's per-eye aspect. Default 16/9. */
194
- tileAspect?: number;
195
- /** A named accent or any CSS colour, written to `--dxr-accent`. */
196
- accent?: CallAccent | string;
197
- /** Base URL of invite links (default: this page's URL, query kept, fragment replaced). */
198
- inviteBase?: string;
199
- /** Write `#room=` into this page's URL on join (so a reload rejoins). Default: `ui`. */
200
- updateUrl?: boolean;
201
- /** Where the "View in 3D with DisplayXR Browser" banner links. */
202
- browserUrl?: string;
203
- /** Re-open the inline-3D session if it ends without the page closing it. Default true. */
204
- recoverSession?: boolean;
205
- /** Scroll the call block to the top of the viewport once, on join, so every tile (and the self
206
- * view) is on screen and woven. Default true (only with `ui`). */
207
- scrollIntoView?: boolean;
208
- /** `createInline3D()` options used for that re-open. */
209
- wallOptions?: object;
199
+ /** Colours, radius, font, tile aspect and strings — see {@link CallTheme}. */
200
+ theme?: CallTheme;
201
+ /** Invite links: `base` (default this page's URL, query kept, fragment replaced); `updateUrl` writes `#room=` into the page URL on join (default: `ui === true`). */
202
+ invite?: { base?: string; updateUrl?: boolean };
203
+ /** The "see it in 3D" offer for recipients on ordinary browsers: `browserUrl` (where it links), `allow2D` (false = 3D-only kiosk pages). */
204
+ landing?: { browserUrl?: string; allow2D?: boolean };
205
+ /** Verbose console logging. `diagnostics()` has the rest. */
210
206
  debug?: boolean;
211
- log?: (tag: string, detail: object) => void;
212
207
  }
213
208
 
209
+ /**
210
+ * The `lift()` of `@displayxr/inline3d/lift`, as the call uses it: `lift(video, { mode: 'live',
211
+ * wall, ui: 'none', convergence, priority, quality })` → a handle it can `remove()`. Only the
212
+ * members below are used; everything else is optional.
213
+ */
214
+ export type LiftFunction = ((element: HTMLVideoElement, opts: Record<string, unknown> & {
215
+ mode: 'live';
216
+ wall: unknown;
217
+ ui: 'none';
218
+ convergence: 'auto' | number;
219
+ priority: 'high' | 'normal' | 'low' | 'paused';
220
+ }) => Promise<{
221
+ remove(): void;
222
+ setPriority?(p: 'high' | 'normal' | 'low' | 'paused'): boolean;
223
+ setConvergence?(x: 'auto' | number): void;
224
+ on?(type: string, cb: (d: any) => void): () => void;
225
+ readonly native?: boolean;
226
+ readonly state?: string;
227
+ readonly canvas?: HTMLCanvasElement;
228
+ }>) & { liftCapabilities?: (o?: { webFallback?: boolean }) => Promise<any> };
229
+
230
+ /** The documented shape of the `quality` event's `in` / `out` (values informative, shape stable). */
214
231
  export interface CallQuality {
215
232
  in: { width: number; height: number; fps: number; codec: string | null; kbps: number | null; dropped: number; decoder: string | null } | null;
216
233
  out: { width: number; height: number; fps: number; codec: string | null; kbps: number | null; limitation: string | null; encoder: string | null } | null;
217
234
  }
218
235
 
236
+ /** A remote participant, as a page needs it. Debug data (hello, route, convergence, lift) is in `diagnostics()`. */
219
237
  export interface CallPeer {
220
238
  readonly id: string;
221
239
  readonly format: CallFormat;
222
- readonly route: CallRoute | null;
240
+ readonly display: CallDisplay;
223
241
  readonly state: PeerState;
224
242
  readonly muted: boolean;
225
243
  readonly cameraOff: boolean;
226
244
  readonly speaking: boolean;
227
- readonly rectified: boolean;
228
- readonly hello: CallHello | null;
229
- readonly quality: CallQuality | null;
230
- /** The per-eye convergence shift currently painted, source px. */
231
- readonly convergencePx: number;
232
- /** A lifted tile: whether lift is showing yet, native or web, and its current priority. */
233
- readonly lift: { readonly live: boolean; readonly native: boolean; readonly priority: LiftStreamPriority | null; readonly state: string | null } | null;
234
245
  }
235
246
 
236
- export interface CallMono3DInfo {
237
- readonly on: boolean;
238
- readonly state: 'off' | 'idle' | 'loading' | 'ready' | 'unavailable';
239
- /** Why it is unavailable: `'import-failed'`, `'no-lift-export'`, `'no-provider'`, `'no-webgpu'`. */
240
- readonly reason: string | null;
241
- /** From `liftCapabilities()` when the lift module has it; null when unknown. */
242
- readonly native: boolean | null;
243
- readonly provider: string | null;
244
- readonly lifted: number;
245
- readonly max: number;
246
- /** A lift has gone live in this call. */
247
- readonly proven: boolean;
248
- }
247
+ /**
248
+ * `error` codes — fatal for a feature, never for the widget. `'camera-busy'` (held by another
249
+ * app: audio-only until `retryCamera()`), `'no-camera'`, `'permission-denied'`, `'unreachable'`
250
+ * (`error.peer` = the tile), `'room-full'`, `'session-ended'`, `'signaling-closed'`,
251
+ * `'join-failed'`, `'camera-failed'`, `'rectify-failed'`, `'mount-failed'` (the element).
252
+ */
253
+ export type CallErrorCode =
254
+ | 'camera-busy'
255
+ | 'no-camera'
256
+ | 'permission-denied'
257
+ | 'unreachable'
258
+ | 'room-full'
259
+ | 'session-ended'
260
+ | 'signaling-closed'
261
+ | 'join-failed'
262
+ | 'camera-failed'
263
+ | 'rectify-failed'
264
+ | 'mount-failed'
265
+ // Hosted-service codes (RFC 0003 §5, phase C3) are added here by the service work.
266
+ | (string & {});
267
+
268
+ /** `warning` codes — degraded, not broken. */
269
+ export type CallWarningCode = 'lift-not-bundled' | (string & {});
249
270
 
250
271
  export interface CallEvents {
272
+ joined: { room: string; id: string };
273
+ /** `reason`: `'left'` (you called `leave()` / the element was removed) or `'pagehide'`. */
274
+ left: { room: string | null; reason: string };
251
275
  peer: { id: string };
252
276
  peerleft: { id: string; reason: string };
253
- format: { id: string; format: CallFormat; route: CallRoute; mono3d: Mono3DReason | null; hello: CallHello | null };
277
+ state: { id: string; state: PeerState };
278
+ /** How this side shows a participant changed (a hello arrived, lift went live, the wall came back). */
279
+ display: { id: string; display: CallDisplay };
280
+ speaker: { id: string | null };
254
281
  /**
255
282
  * Per peer (`id`) every 2 s; or, with `id: null` and `lift`, when lifted tiles on the web
256
283
  * provider degrade the page's frame rate (~< 20 fps for 3 s) and when it recovers.
257
284
  */
258
285
  quality: ({ id: string } & CallQuality) | ({ id: null; in: null; out: null; lift: { degraded: boolean; frameMs: number; tiles: number } });
259
- speaker: { id: string | null };
260
- /** Codes include `'camera-busy'`, `'no-camera'`, `'unreachable'` (error.peer = the tile id), `'room-full'`, `'session-ended'`. */
261
- error: { code: string; message: string; error: Error | null };
262
- state: { id: string; state: PeerState };
263
- joined: { room: string; id: string };
264
- left: { room: string };
265
- session: { wall: unknown };
286
+ error: { code: CallErrorCode; message: string; error: Error | null };
287
+ /**
288
+ * `'lift-not-bundled'`: a 2D participant is on a 3D display, `mono3D` is `'auto'`, and the lift
289
+ * module could not be imported from this build — they stay flat. Fix: `mono3D: lift` or
290
+ * `./call/full`. Emitted once per call with one `console.warn`. A display that honestly cannot
291
+ * lift is NOT this: that is `handle.mono3D.reason` (`'no-provider'` / `'no-webgpu'`).
292
+ */
293
+ warning: { code: CallWarningCode; message: string };
266
294
  }
267
295
 
268
296
  export interface CallHandle {
269
297
  readonly room: string | null;
270
298
  readonly id: string;
271
- readonly state: 'idle' | 'lobby' | 'joining' | 'in-call' | 'full' | 'left';
272
- readonly wall: unknown;
273
- readonly format: CallFormat | null;
299
+ readonly state: 'lobby' | 'joining' | 'in-call' | 'full' | 'left';
300
+ /** `'busy'` = held by another app (e.g. eye tracking): the call runs audio-only until `retryCamera()`. */
301
+ readonly camera: 'ok' | 'busy' | 'none' | 'pending';
302
+ /** What YOU send; null before a camera opened. */
303
+ readonly localFormat: CallFormat | null;
274
304
  readonly muted: boolean;
305
+ readonly cameraOff: boolean;
275
306
  readonly depth: number;
276
307
  readonly speaker: string | null;
277
- /** `'busy'` = held by another app (e.g. eye tracking): the call runs audio-only until `retryCamera()`. */
278
- readonly camera: 'ok' | 'busy' | 'none' | 'pending';
279
- /** mono→3D status (see {@link CallMono3DInfo}). */
280
- readonly mono3D: CallMono3DInfo;
281
308
  /** Read-only snapshot of the remote participants. */
282
309
  readonly peers: ReadonlyArray<CallPeer>;
283
- join(): Promise<CallHandle>;
310
+ /** mono→3D status. `reason` when unavailable: `'import-failed'`, `'no-lift-export'`, `'no-provider'`, `'no-webgpu'`. */
311
+ readonly mono3D: { readonly on: boolean; readonly state: 'off' | 'idle' | 'loading' | 'ready' | 'unavailable'; readonly reason: string | null; readonly provider: string | null; readonly lifted: number; readonly max: number };
312
+ join(): Promise<void>;
313
+ leave(): void;
284
314
  /** `…#room=<id>` — the room rides in the fragment, which never reaches a server. Null before a room exists. */
285
315
  inviteLink(): string | null;
286
316
  /** Mute (true), unmute (false) or toggle (no argument). Returns the new state. */
287
317
  mute(on?: boolean): boolean;
288
- cameraOff(on?: boolean): boolean;
289
- setCamera(idOrStream: string | MediaStream, opts?: { format?: CallFormat }): Promise<{ format: CallFormat; width: number; height: number; label: string }>;
318
+ /** Camera off (true), on (false) or toggle. Returns the new state (`cameraOff` reads it). */
319
+ setCameraOff(on?: boolean): boolean;
320
+ /** Switch cameras: a `deviceId`, a `MediaStream` (+ `format`), or a `StereoCamera`. Keeps the current one on failure. */
321
+ setCamera(src: string | MediaStream | StereoCamera, opts?: { format?: CallFormat }): Promise<{ format: CallFormat; width: number; height: number; label: string }>;
322
+ /** Try the configured camera again (e.g. after the eye tracker released it). */
323
+ retryCamera(): Promise<{ format: CallFormat; width: number; height: number; label: string }>;
290
324
  /**
291
325
  * The ONE depth control, [-1, 1] (+ = push back). Stereo tiles: ± 5% of the eye width added to
292
326
  * the automatic convergence. Lifted tiles: lift's convergence (0 → `'auto'`, else 0.5 + 0.5·v).
293
327
  */
294
328
  setDepth(v: number | null): number;
295
- /** Try the configured camera again (e.g. after the eye tracker released it). Keeps the current one on failure. */
296
- retryCamera(): Promise<{ format: CallFormat; width: number; height: number; label: string }>;
297
- /** Send `hint {subjectZmm}` (rate-limited to 5 Hz) if the page can estimate the face distance. */
298
- sendHint(subjectZmm: number): boolean;
299
329
  /** Turn mono→3D on/off at runtime (no argument toggles). Off releases every lift; tiles go flat. */
300
330
  setMono3D(on?: boolean): boolean;
301
- leave(): void;
331
+ /** A participant's tile element (for `layout: 'none'`), or null. Style it in place; never move it. */
332
+ tile(peerId: string): HTMLElement | null;
302
333
  on<K extends keyof CallEvents>(type: K, cb: (e: CallEvents[K]) => void): () => void;
303
334
  off<K extends keyof CallEvents>(type: K, cb: (e: CallEvents[K]) => void): void;
335
+ /** Explicitly UNSTABLE: the wall, the transport, per-peer hello / route / convergence / lift / quality, the lift pool. For devtools. */
336
+ diagnostics(): Record<string, unknown>;
337
+ }
338
+
339
+ export function addCall(wall: Inline3D | Inline3DUnsupported | null, container: HTMLElement, opts?: CallOptions): Promise<CallHandle>;
340
+
341
+ /**
342
+ * The one-line path (RFC 0003 §1): `addCall` with every argument optional. The wall is
343
+ * `opts.wall` if given, else the document's shared manager (`sharedInline3D()` — the page's
344
+ * existing session, never a second one). Hosted signalling, `camera: 'auto'` and the SDK chrome
345
+ * by default; resolves once the lobby shows (or, with `autoJoin` / `ui: false`, once joined).
346
+ * `<dxr-call>` is this as markup.
347
+ */
348
+ export function mountCall(el: HTMLElement, opts?: CallOptions): Promise<CallHandle>;
349
+
350
+ /** The room in an invite link (or a `location`), or null — for an SPA router to read the room without mounting a call. */
351
+ export function parseInviteLink(link: string | { hash?: string; href?: string } | null | undefined): string | null;
352
+
353
+ // ── <dxr-call> — the one-line path as markup ───────────────────────────────────────────────
354
+ //
355
+ // Importing this entry (or `./call/full`, or the CDN bundle `dist/call.js`) registers the element.
356
+ // It mounts on connect (`mountCall(this, { ...attrsToOpts(this), ...this.options })`), leaves on
357
+ // disconnect (a DOM move IS a teardown — woven-canvas rule 2), re-dispatches every call event as
358
+ // a bubbling, composed `CustomEvent` named `dxr-call:<event>` with the payload in `detail`, and
359
+ // exposes the handle as `el.call`. Attributes (read once, at connect): `room`, `signaling` (a
360
+ // URL), `key`, `camera`, `layout`, `accent`, `max-peers`, `no-ui`, `ui="tiles"`, `auto-join`,
361
+ // `mono3d="off"`, `no-audio`, `no-self-view`, `no-auto-converge`, `tile-aspect`, `invite-base`,
362
+ // `browser-url`, `debug`. Everything else is `el.options`, set before connect; options win.
363
+
364
+ export class DxrCallElement extends HTMLElement {
365
+ /** The mount seam: null = `mountCall` of `./call`; `./call/full` sets its own. */
366
+ static mount: ((el: HTMLElement, opts?: CallOptions) => Promise<CallHandle>) | null;
367
+ /** Non-string options (a `MediaStream`, an adapter, `lift`, a `wall`, a `theme`) — set BEFORE connecting. */
368
+ options: CallOptions | null;
369
+ /** The handle once mounted; null before, and again after disconnect. */
370
+ readonly call: CallHandle | null;
371
+ /** Resolves with the handle when the mount lands (null if the element left the DOM first); rejects if it failed. */
372
+ readonly ready: Promise<CallHandle | null> | null;
373
+ connectedCallback(): void;
374
+ disconnectedCallback(): void;
375
+ }
376
+
377
+ /** The attribute → option mapping the element applies at connect. Pure; unset attributes contribute nothing. */
378
+ export function attrsToOpts(source: { getAttribute(name: string): string | null } | ((name: string) => string | null)): Partial<CallOptions>;
379
+
380
+ /**
381
+ * Register the element (default name `dxr-call`) once. True when this call registered it; false
382
+ * when the name was taken or there is no `customElements` registry. The entries call it for you.
383
+ */
384
+ export function defineCallElement(name?: string, registry?: CustomElementRegistry): boolean;
385
+
386
+ /** `'dxr-call:'` — every DOM event the element dispatches starts with it. */
387
+ export const CALL_EVENT_PREFIX: 'dxr-call:';
388
+
389
+ /** The DOM events: one per {@link CallEvents} key, plus `dxr-call:ready` once the handle exists. */
390
+ export type DxrCallEventMap = { [K in keyof CallEvents as `dxr-call:${K}`]: CustomEvent<CallEvents[K]> } & {
391
+ 'dxr-call:ready': CustomEvent<{ call: CallHandle }>;
392
+ };
393
+
394
+ declare global {
395
+ interface HTMLElementTagNameMap {
396
+ 'dxr-call': DxrCallElement;
397
+ }
398
+ interface HTMLElementEventMap extends DxrCallEventMap {}
304
399
  }
305
400
 
306
- export function addCall(wall: unknown, container: HTMLElement, opts: CallOptions): Promise<CallHandle>;
401
+ // ── DEPRECATED (1.30 only) — the 1.29 exports that left the public surface in C2 ─────────
402
+ //
403
+ // Each is INTERNAL (RFC 0003 §2): still importable by file path from `js/call/*.js` /
404
+ // `js/camera/*.js`, kept on this entry for one release as a wrapper that `console.warn`s once,
405
+ // removed in 1.31. Do not write new code against them.
307
406
 
308
- // ── pure helpers (exported for tests and advanced pages) ──────────────────────────────────
407
+ /** @deprecated internal — js/call/options.js; removed from this entry in 1.31 */
309
408
  export function normalizeCallOptions(opts?: Partial<CallOptions>): Record<string, unknown>;
409
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
310
410
  export function newRoomId(getRandomValues?: (a: Uint8Array) => Uint8Array): string;
411
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
311
412
  export function isValidRoomId(room: unknown): boolean;
312
- export function parseInviteLink(link: string | { hash?: string; href?: string } | null | undefined): string | null;
413
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 (`handle.inviteLink()` is the API) */
313
414
  export function buildInviteLink(base: string, room: string): string;
314
- export function normalizeHello(msg: unknown): CallHello | null;
415
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 (the wire is a protocol: docs/call-wire.md) */
416
+ export function normalizeHello(msg: unknown): { type: 'hello'; v: number; format: CallFormat; width: number | null; height: number | null; baselineMm: number | null; hfovDeg: number | null; rectified: boolean; sdk: string | null } | null;
417
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
315
418
  export function makeHello(o?: { format?: CallFormat; width?: number; height?: number; baselineMm?: number; hfovDeg?: number; rectified?: boolean }): object;
316
- export function routeFor(p: { format?: string | null; woven: boolean; mono3D?: 'auto' | 'off' | false | CallLiftFunction; lift?: boolean; overBudget?: boolean; failed?: boolean }): { route: CallRoute; mono3d?: Mono3DReason };
317
- export function badgeFor(route: CallRoute | null): '3D' | '2D→3D' | '2D';
318
- export function normalizeMono3D(v: unknown): 'auto' | 'off' | CallLiftFunction;
319
- export function resolveLift(
320
- mono3D: 'auto' | 'off' | false | CallLiftFunction,
321
- o?: { importer?: () => Promise<any>; nav?: any; log?: (tag: string, o: object) => void; capsOpts?: object },
322
- ): Promise<{ lift: CallLiftFunction | null; capabilities: ((o?: object) => Promise<any>) | null; caps: any; source: 'injected' | 'module' | 'none'; reason: string | null }>;
323
- export function liftConvergenceFor(depth: number | null | undefined): 'auto' | number;
324
- export function liftPriorityFor(p: { id: string | null; speakerId: string | null; visible?: boolean }): LiftStreamPriority;
325
- /** The hook for the native provider's per-stream priority: forwards to `handle.setPriority` (no-op without one). */
326
- export function setLiftPriority(handle: CallLiftHandle | null | undefined, level: LiftStreamPriority): boolean;
327
- export function defaultLiftSpecifier(base?: string): string;
328
- export const LIFT_PRIORITY: Readonly<{ speaker: 'high'; other: 'normal'; hidden: 'paused' }>;
329
- export function createFrameWatch(o?: { degradedMs?: number; recoveredMs?: number; holdMs?: number; alpha?: number }): {
330
- feed(dtMs: number): 'degraded' | 'recovered' | null;
331
- reset(): void;
332
- readonly frameMs: number | null;
333
- readonly degraded: boolean;
334
- };
335
- export interface CallLiftPool {
336
- readonly size: number;
337
- max: number;
338
- has(id: string): boolean;
339
- handle(id: string): CallLiftHandle | null;
340
- priority(id: string): LiftStreamPriority | null;
341
- canAcquire(id: string): boolean;
342
- acquire(id: string, element: HTMLVideoElement, o?: { wall?: unknown; visible?: boolean; onError?: (e: unknown) => void }): Promise<CallLiftHandle | null>;
343
- release(id: string): void;
344
- releaseAll(): void;
345
- setSpeaker(id: string | null): void;
346
- setVisible(id: string, visible: boolean): void;
347
- setDepth(v: number): 'auto' | number;
348
- readonly anyWeb: boolean;
349
- list(): Array<{ id: string; priority: LiftStreamPriority | null; pending: boolean; native: boolean }>;
350
- }
351
- export function createLiftPool(o: { lift: CallLiftFunction; max?: number; log?: (tag: string, o: object) => void; options?: Record<string, unknown> }): CallLiftPool;
419
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 (`CallPeer.display` is the API) */
420
+ export function routeFor(p: { format?: string | null; woven: boolean; mono3D?: 'auto' | 'off' | false | LiftFunction; lift?: boolean; overBudget?: boolean; failed?: boolean }): { route: 'woven-sbs' | 'flat-left' | 'flat' | 'lifted'; mono3d?: string };
421
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
422
+ export function badgeFor(route: 'woven-sbs' | 'flat-left' | 'flat' | 'lifted' | null): CallDisplay;
423
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
424
+ export function createLiveGate(o?: { need?: number; needNoPose?: number }): { feed(viewCount: number | null): boolean; readonly live: boolean };
425
+ /** @deprecated internal — js/camera/converge.js; removed in 1.31 */
352
426
  export function convergenceShiftPx(p: { eyeWidthPx: number; hfovDeg?: number | null; baselineMm?: number | null; subjectZmm?: number | null }): number;
427
+ /** @deprecated internal — js/camera/converge.js; removed in 1.31 */
353
428
  export function lowPass(prev: number, target: number, alpha?: number): number;
429
+ /** @deprecated internal — js/camera/converge.js; removed in 1.31 */
354
430
  export function clampShift(px: number, eyeWidthPx: number, maxFraction?: number): number;
431
+ /** @deprecated internal — js/camera/geometry.js; removed in 1.31 */
355
432
  export function eyeCropRect(eyeW: number, eyeH: number, aspect: number, shift: number, eye: 0 | 1): { sx: number; sy: number; sw: number; sh: number };
433
+ /** @deprecated internal — js/camera/geometry.js; removed in 1.31 (`addCameraView` of `/camera` mirrors a self view) */
356
434
  export function mirrorSwapOps(W: number, H: number): Array<{ src: 'L' | 'R'; sx: number; sw: number; sy: number; sh: number; dx: number; dw: number; mirror: true }>;
435
+ /** @deprecated internal — js/camera/geometry.js; removed in 1.31 */
357
436
  export function mirrorSwapPixels<T extends ArrayLike<number>>(px: T, W: number, H: number): T;
437
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
358
438
  export function maxBitrateKbps(format: CallFormat, remotePeers: number): number;
439
+ /** @deprecated internal — js/camera/disparity.js; removed in 1.31 (`autoConverge` is the behaviour) */
440
+ export function measureFocusDisparity(img: ArrayLike<number>, W: number, H: number, o?: { focus?: { x: number; y: number }; block?: number; dMin?: number; dMax?: number; dyMax?: number; minNcc?: number }): { d: number; x: number; y: number; c: number; method: 'focus' | 'mode'; blocks: number } | null;
441
+ /** @deprecated internal — js/call/sdp.js; removed in 1.31 */
359
442
  export function preferVideoCodecs(sdp: string, order?: readonly string[]): string;
443
+ /** @deprecated internal — js/call/sdp.js; removed in 1.31 */
360
444
  export function sortCodecCapabilities<T extends { mimeType?: string }>(codecs: T[], order?: readonly string[]): T[];
445
+ /** @deprecated internal — js/call/sdp.js; removed in 1.31 */
361
446
  export const VIDEO_CODEC_ORDER: readonly string[];
447
+ /** @deprecated internal — js/call/transport.js; removed in 1.31 (an SFU adapter will get a designed Transport seam) */
448
+ export class MeshTransport {
449
+ constructor(o: { signaling: SignalingAdapter; id: string; maxPeers?: number; iceServers?: RTCIceServer[]; RTCPeerConnection?: any; log?: (tag: string, obj: object) => void });
450
+ readonly size: number;
451
+ }
452
+ /** @deprecated internal — js/call/transport.js; removed in 1.31 */
362
453
  export function clampMaxPeers(n?: number): number;
454
+ /** @deprecated internal — js/call/qr.js; removed in 1.31 */
363
455
  export function qrEncode(text: string, opts?: { ecl?: 'L' | 'M'; mask?: number }): { version: number; size: number; mask: number; modules: boolean[][] };
456
+ /** @deprecated internal — js/call/signaling.js; removed in 1.31 (documented in signaling/README.md) */
364
457
  export function roomKey(room: string): Promise<string>;
458
+ /** @deprecated internal — js/call/signaling.js; removed in 1.31 */
365
459
  export const SIGNAL_PROTOCOL: string;
460
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
366
461
  export const WIRE_VERSION: number;
462
+ /** @deprecated internal — js/call/wire.js; removed in 1.31 */
367
463
  export const CALL_SDK: string;
464
+ /** @deprecated replaced by `theme.strings`; removed in 1.31 */
368
465
  export const PLATE_TEXT: Readonly<{ unreachable: string; cameraBusy: string }>;
369
- export function createLiveGate(o?: { need?: number; needNoPose?: number }): { feed(viewCount: number | null): boolean; readonly live: boolean };
466
+ /** @deprecated folded into `theme.accent` (which takes the names); removed in 1.31 */
370
467
  export const CALL_ACCENTS: Readonly<Record<CallAccent, string>>;
371
- export class MeshTransport {
372
- constructor(o: { signaling: SignalingAdapter; id: string; maxPeers?: number; iceServers?: RTCIceServer[]; RTCPeerConnection?: any; log?: (tag: string, obj: object) => void });
373
- readonly size: number;
374
- }
468
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
469
+ export function normalizeMono3D(v: unknown): 'auto' | 'off' | LiftFunction;
470
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
471
+ export function resolveLift(mono3D: 'auto' | 'off' | false | LiftFunction, o?: { importer?: () => Promise<any>; nav?: any; log?: (tag: string, o: object) => void; capsOpts?: object }): Promise<{ lift: LiftFunction | null; capabilities: ((o?: object) => Promise<any>) | null; caps: any; source: 'injected' | 'module' | 'none'; reason: string | null }>;
472
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
473
+ export function createLiftPool(o: { lift: LiftFunction; max?: number; log?: (tag: string, o: object) => void; options?: Record<string, unknown> }): { readonly size: number; max: number; has(id: string): boolean; release(id: string): void; releaseAll(): void; [k: string]: unknown };
474
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
475
+ export function createFrameWatch(o?: { degradedMs?: number; recoveredMs?: number; holdMs?: number; alpha?: number }): { feed(dtMs: number): 'degraded' | 'recovered' | null; reset(): void; readonly frameMs: number | null; readonly degraded: boolean };
476
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
477
+ export function liftConvergenceFor(depth: number | null | undefined): 'auto' | number;
478
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
479
+ export function liftPriorityFor(p: { id: string | null; speakerId: string | null; visible?: boolean }): 'high' | 'normal' | 'low' | 'paused';
480
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
481
+ export function setLiftPriority(handle: { setPriority?(p: 'high' | 'normal' | 'low' | 'paused'): boolean } | null | undefined, level: 'high' | 'normal' | 'low' | 'paused'): boolean;
482
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
483
+ export function defaultLiftSpecifier(base?: string): string;
484
+ /** @deprecated internal — js/call/lift.js; removed in 1.31 */
485
+ export const LIFT_PRIORITY: Readonly<{ speaker: 'high'; other: 'normal'; hidden: 'paused' }>;