@displayxr/inline3d 1.29.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.
@@ -1,7 +1,8 @@
1
1
  // inline3d-call.js — a 3D video call in any page, in one call.
2
2
  //
3
3
  // PREVIEW tier. Not covered by the SDK's 1.x semver promise — see docs/sdk-stability.md.
4
- // Implements P1 of docs/rfcs/0002-video-call.md and the one-line path of RFC 0003 §1 (C1).
4
+ // Implements P1 of docs/rfcs/0002-video-call.md, the one-line path of RFC 0003 §1 (C1) and the
5
+ // public surface of RFC 0003 §2 (C2).
5
6
  //
6
7
  // import { mountCall } from '@displayxr/inline3d/call';
7
8
  // const call = await mountCall(document.getElementById('call')); // hosted signalling, auto camera, SDK UI
@@ -16,6 +17,17 @@
16
17
  // on the plain `/call` entry a mono peer on a 3D display raises `warning { code:
17
18
  // 'lift-not-bundled' }` instead of silently staying flat.
18
19
  //
20
+ // THE PUBLIC SURFACE (RFC 0003 §2, C2) is what a page needs to RUN, OBSERVE and DRESS a call:
21
+ // `mountCall` / `addCall`, `dxrSignaling` + `DXR_SIGNAL_DEFAULT`, `parseInviteLink` (Decision 7),
22
+ // the `<dxr-call>` element, and the types. Everything that exists so the module can be unit-tested
23
+ // stays importable by file path (`js/call/*.js`) and is no longer part of this entry — for ONE
24
+ // release (1.30) the old names remain here as deprecated wrappers that `console.warn` once (the
25
+ // block at the end of this file); 1.31 removes them. The surface is pinned by test/api-snapshot.
26
+ //
27
+ // THE CAMERA is `@displayxr/inline3d/camera` (RFC 0003 §4): `camera: 'auto'` makes this module
28
+ // call `openCamera()` itself, `camera: cam` takes a StereoCamera the page opened, and the self
29
+ // view IS an `addCameraView` — one implementation of capture, mirroring and convergence.
30
+ //
19
31
  // WHAT IT OWNS. Unlike the other modules this one takes a CONTAINER, not a canvas: a call has a
20
32
  // variable number of participants, so it creates one persistent canvas per remote participant
21
33
  // (plus the self view) inside `container`, lays them out, and draws its own chrome around them.
@@ -72,71 +84,27 @@
72
84
  // restores it.
73
85
 
74
86
  import { sharedInline3D } from './inline3d.js';
75
- import {
76
- WIRE_VERSION,
77
- CALL_SDK,
78
- DEFAULT_MAX_PEERS,
79
- HINT_MAX_HZ,
80
- newRoomId,
81
- newPeerId,
82
- isValidRoomId,
83
- parseInviteLink,
84
- buildInviteLink,
85
- normalizeHello,
86
- makeHello,
87
- normalizeHint,
88
- normalizeState,
89
- rateGate,
90
- routeFor,
91
- badgeFor,
92
- createConvergence,
93
- eyeCropRect,
94
- eyeOutputSize,
95
- mirrorSwapOps,
96
- backoffMs,
97
- createLiveGate,
98
- } from './call/wire.js';
99
- import { MeshTransport, clampMaxPeers } from './call/transport.js';
100
- import { openCamera, openMic } from './call/capture.js';
87
+ import * as wire from './call/wire.js';
88
+ import { DEFAULT_MAX_PEERS, HINT_MAX_HZ, newPeerId, parseInviteLink, normalizeHint, normalizeState, rateGate, createConvergence, eyeOutputSize, backoffMs } from './call/wire.js';
89
+ import { MeshTransport as MeshTransportImpl, clampMaxPeers as clampMaxPeersImpl } from './call/transport.js';
90
+ import { openMic } from './camera/capture.js';
91
+ import { openCamera, addCameraView, isStereoCamera } from './inline3d-camera.js';
101
92
  import { drawQr } from './call/qr.js';
102
- import { injectCallStyle, ICONS, el, show, resolveCallAccent, CALL_ACCENTS } from './call/ui.js';
103
- import { normalizeMono3D, resolveLift, createLiftPool, createFrameWatch } from './call/lift.js';
93
+ import * as qrMod from './call/qr.js';
94
+ import * as sdp from './call/sdp.js';
95
+ import * as liftMod from './call/lift.js';
96
+ import * as disparity from './call/disparity.js';
97
+ import * as signalingMod from './call/signaling.js';
98
+ import { injectCallStyle, ICONS, el, show, CALL_ACCENTS as CALL_ACCENTS_IMPL, createStrings, CALL_STRINGS } from './call/ui.js';
99
+ import { normalizeCallOptions as normalizeOptions } from './call/options.js';
104
100
  import { defineCallElement } from './call/element.js';
105
- export { DxrCallElement, defineCallElement, attrsToOpts, CALL_EVENT_PREFIX } from './call/element.js';
106
-
107
- import { dxrSignaling } from './call/signaling.js';
108
101
  import { lumaFromRgba, createDisparityTrack, createFocusTracker, downsampleLuma } from './call/disparity.js';
109
- export { dxrSignaling, SIGNAL_PROTOCOL, roomKey, DXR_SIGNAL_DEFAULT } from './call/signaling.js';
110
- export {
111
- WIRE_VERSION,
112
- CALL_SDK,
113
- newRoomId,
114
- isValidRoomId,
115
- parseInviteLink,
116
- buildInviteLink,
117
- normalizeHello,
118
- makeHello,
119
- routeFor,
120
- badgeFor,
121
- CALL_ACCENTS,
122
- };
123
- export { createLiveGate } from './call/wire.js';
124
- export { measureFocusDisparity } from './call/disparity.js';
125
- export { convergenceShiftPx, lowPass, clampShift, eyeCropRect, mirrorSwapOps, mirrorSwapPixels, maxBitrateKbps } from './call/wire.js';
126
- export { preferVideoCodecs, sortCodecCapabilities, VIDEO_CODEC_ORDER } from './call/sdp.js';
127
- export { MeshTransport, clampMaxPeers } from './call/transport.js';
128
- export { qrEncode } from './call/qr.js';
129
- export {
130
- normalizeMono3D,
131
- resolveLift,
132
- createLiftPool,
133
- createFrameWatch,
134
- liftConvergenceFor,
135
- liftPriorityFor,
136
- setLiftPriority,
137
- defaultLiftSpecifier,
138
- LIFT_PRIORITY,
139
- } from './call/lift.js';
102
+ import { dxrSignaling } from './call/signaling.js';
103
+
104
+ // ── the public surface (RFC 0003 §2) ────────────────────────────────────────────────────────
105
+ export { DxrCallElement, defineCallElement, attrsToOpts, CALL_EVENT_PREFIX } from './call/element.js';
106
+ export { dxrSignaling, DXR_SIGNAL_DEFAULT } from './call/signaling.js';
107
+ export { parseInviteLink };
140
108
 
141
109
  const TAG = '[inline3d/call]';
142
110
  const HELLO_WAIT_MS = 2000;
@@ -151,65 +119,6 @@ const AUTO_CONV_INTERVAL_MS = 200;
151
119
  const AUTO_CONV_EYE_WIDTH = 240;
152
120
  // WebCodecs pixel formats whose plane 0 is luma (Y): read directly, no colour conversion.
153
121
  const Y_PLANE_FORMATS = new Set(['I420', 'I420A', 'I422', 'I444', 'NV12']);
154
- const DEFAULT_BROWSER_URL = 'https://github.com/DisplayXR/displayxr-browser';
155
- export const PLATE_TEXT = Object.freeze({
156
- unreachable: "Can't reach this participant — the network needs a relay (TURN)",
157
- cameraBusy: 'Camera busy — in use by another app (e.g. eye tracking)',
158
- });
159
-
160
- /**
161
- * Apply defaults and validate. Pure (no DOM, no network) — see test/call.test.mjs.
162
- * @param {object} [opts]
163
- */
164
- export function normalizeCallOptions(opts = {}) {
165
- const ui = opts.ui === undefined ? true : !!opts.ui;
166
- const aspect = typeof opts.tileAspect === 'number' && opts.tileAspect > 0.3 && opts.tileAspect < 4 ? opts.tileAspect : 16 / 9;
167
- let room = opts.room === undefined || opts.room === null ? 'auto' : opts.room;
168
- if (room !== 'auto' && !isValidRoomId(room)) {
169
- const fromLink = parseInviteLink(room);
170
- if (!fromLink) throw new Error(`@displayxr/inline3d/call: room "${room}" is not a valid room id (16-64 base64url chars) or invite link`);
171
- room = fromLink;
172
- }
173
- return {
174
- room,
175
- // Default: the hosted DisplayXR signalling server (which also mints TURN credentials). A
176
- // publishable key (RFC 0003 §5a) rides along on connect; the server may ignore it today.
177
- signaling: opts.signaling || dxrSignaling(undefined, typeof opts.key === 'string' && opts.key ? { key: opts.key } : {}),
178
- iceServers: Array.isArray(opts.iceServers) ? opts.iceServers : undefined,
179
- camera: opts.camera === undefined ? 'auto' : opts.camera,
180
- format: opts.format === 'sbs' ? 'sbs' : opts.format === 'mono' ? 'mono' : undefined,
181
- calibration: opts.calibration && typeof opts.calibration === 'object' ? { ...opts.calibration } : {},
182
- rectify: typeof opts.rectify === 'function' ? opts.rectify : null,
183
- audio: opts.audio === undefined ? true : !!opts.audio,
184
- // Auto-convergence (call/disparity.js): measure the disparity of the point between each SBS
185
- // peer's eyes and shift the eyes so it sits at the display plane. The depth slider stays an
186
- // offset on top. Off = the pair as sent (plus any `hint`).
187
- autoConverge: opts.autoConverge === undefined ? true : !!opts.autoConverge,
188
- mono3D: normalizeMono3D(opts.mono3D),
189
- maxPeers: clampMaxPeers(opts.maxPeers === undefined ? DEFAULT_MAX_PEERS : opts.maxPeers),
190
- // Extra lift() options for lifted tiles (models, ort, quality, providers). The call's own
191
- // keys (mode, wall, ui, convergence, priority) always win.
192
- liftOptions: opts.liftOptions && typeof opts.liftOptions === 'object' ? { ...opts.liftOptions } : null,
193
- // Concurrent lifted tiles. Default = maxPeers (4): every mono peer can be lifted.
194
- maxLifted: Number.isFinite(+opts.maxLifted) && opts.maxLifted !== null ? Math.max(0, Math.min(DEFAULT_MAX_PEERS, Math.floor(+opts.maxLifted))) : DEFAULT_MAX_PEERS,
195
- layout: opts.layout === 'speaker' ? 'speaker' : 'grid',
196
- ui,
197
- autoJoin: opts.autoJoin === undefined ? !ui : !!opts.autoJoin,
198
- selfView: opts.selfView === undefined ? true : !!opts.selfView,
199
- tileAspect: aspect,
200
- accent: typeof opts.accent === 'string' && opts.accent ? resolveCallAccent(opts.accent) : null,
201
- inviteBase: typeof opts.inviteBase === 'string' ? opts.inviteBase : null,
202
- updateUrl: opts.updateUrl === undefined ? ui : !!opts.updateUrl,
203
- browserUrl: typeof opts.browserUrl === 'string' ? opts.browserUrl : DEFAULT_BROWSER_URL,
204
- recoverSession: opts.recoverSession === undefined ? true : !!opts.recoverSession,
205
- scrollIntoView: opts.scrollIntoView === undefined ? true : !!opts.scrollIntoView,
206
- wallOptions: opts.wallOptions && typeof opts.wallOptions === 'object' ? opts.wallOptions : {},
207
- log: typeof opts.log === 'function' ? opts.log : opts.debug ? (tag, obj) => console.log(`${TAG} ${tag} ${JSON.stringify(obj)}`) : null,
208
- // Internal (tests): replaces the lift module importer behind `mono3D: 'auto'`.
209
- _liftImporter: typeof opts._liftImporter === 'function' ? opts._liftImporter : undefined,
210
- };
211
- }
212
-
213
122
  /**
214
123
  * Put a 3D video call in `container`. Resolves once the camera is open and (with `ui:true`) the
215
124
  * lobby is showing; with `ui:false` (or `autoJoin:true`) once the call is joined.
@@ -220,7 +129,7 @@ export function normalizeCallOptions(opts = {}) {
220
129
  */
221
130
  export async function addCall(wall, container, opts = {}) {
222
131
  if (!container || typeof container.appendChild !== 'function') throw new TypeError('@displayxr/inline3d/call: addCall(wall, container, opts) needs a container element');
223
- const o = normalizeCallOptions(opts);
132
+ const o = normalizeOptions(opts);
224
133
  if (!o.signaling || typeof o.signaling.join !== 'function') {
225
134
  throw new TypeError('@displayxr/inline3d/call: opts.signaling must be a SignalingAdapter — dxrSignaling([url]) or your own');
226
135
  }
@@ -249,6 +158,7 @@ export async function mountCall(el, opts = {}) {
249
158
  class Call {
250
159
  constructor(wall, container, o) {
251
160
  this.o = o;
161
+ this.t = createStrings(o.strings); // every chrome string, overridable via theme.strings
252
162
  this.container = container;
253
163
  this.wall = wall || null;
254
164
  this.wallLive = !!(wall && wall.supported);
@@ -278,7 +188,7 @@ class Call {
278
188
  this.liftProven = false;
279
189
  this.liftMissing = false; // the lift MODULE failed to import (vs. a display that can't lift)
280
190
  this._liftWarned = false;
281
- this._frameWatch = createFrameWatch();
191
+ this._frameWatch = liftMod.createFrameWatch();
282
192
  this.handle = this._makeHandle();
283
193
  if (this.wallLive) this._hookWall(this.wall);
284
194
  }
@@ -332,35 +242,31 @@ class Call {
332
242
  else this._setState('lobby');
333
243
  }
334
244
 
245
+ /**
246
+ * Open the camera through `@displayxr/inline3d/camera` (`want` = 'auto' | 'stereo' | 'mono' |
247
+ * deviceId | MediaStream | a StereoCamera the page opened) and the microphone, and rebuild the
248
+ * local stream. A camera this module opened is closed when replaced; a page's StereoCamera or
249
+ * MediaStream is left alone.
250
+ */
335
251
  async _openMedia(want, format, { keepOnFail = false } = {}) {
336
252
  const log = (t, x) => this.log(t, x);
337
253
  let cam = null;
338
254
  let camError = null;
255
+ const given = isStereoCamera(want) ? want : null;
339
256
  try {
340
- cam = await openCamera(want, { format: format || this.o.format, calibration: this.o.calibration, log });
257
+ cam = given || (await openCamera({ prefer: want, format: format || this.o.format, calibration: this.o.calibration, rectify: this.o.rectify || undefined, log }));
258
+ if (given && given.state !== 'live') throw Object.assign(new Error('the camera given is not live'), { code: 'no-camera', skipped: [] });
341
259
  } catch (err) {
342
260
  camError = err;
343
261
  // No camera is never fatal: the call goes on audio-only, the self view and every receiver
344
262
  // say why, and the user can retry (the tracker may let go) or pick another camera.
345
263
  const busy = err.code === 'camera-busy';
346
- this.error(busy ? 'camera-busy' : err.code || 'no-camera', busy ? `${PLATE_TEXT.cameraBusy}. Joining audio-only.` : `no camera: ${err.message}. Joining audio-only.`, err);
264
+ this.error(busy ? 'camera-busy' : err.code || 'no-camera', busy ? `${this.t('cameraBusy')}. Joining audio-only.` : `no camera: ${err.message}. Joining audio-only.`, err);
347
265
  if (keepOnFail && this.local && this.local.videoTrack) return this.local; // keep the working one
348
266
  this.camStatus = busy ? 'busy' : 'none';
349
267
  }
350
268
  if (cam) this.camStatus = 'ok';
351
- if (cam && cam.format === 'sbs' && this.o.rectify) {
352
- // P2 seam: a calibrated rectification step (plug-in or runtime supplied). Its output is a
353
- // rectified SBS stream; the hello then says so.
354
- try {
355
- const out = await this.o.rectify(cam.stream, { width: cam.width, height: cam.height, deviceId: cam.deviceId, label: cam.label });
356
- if (out && typeof out.getVideoTracks === 'function') {
357
- cam.stream = out;
358
- cam.calibration = { ...cam.calibration, rectified: true };
359
- }
360
- } catch (err) {
361
- this.error('rectify-failed', `rectify() threw — sending the raw pair: ${err.message}`, err);
362
- }
363
- }
269
+ if (cam && cam.rectifyError) this.error('rectify-failed', `rectify() threw — sending the raw pair: ${cam.rectifyError.message}`, cam.rectifyError);
364
270
  let audioTrack = this.local ? this.local.audioTrack : null;
365
271
  if (this.o.audio && !audioTrack) audioTrack = await openMic({ log });
366
272
  if (audioTrack) audioTrack.enabled = !this.muted;
@@ -375,28 +281,39 @@ class Call {
375
281
  videoTrack.enabled = !this.camOff;
376
282
  }
377
283
  const stream = new MediaStream([videoTrack, audioTrack].filter(Boolean));
378
- const video = this.local?.video || Object.assign(document.createElement('video'), { muted: true, playsInline: true, autoplay: true });
379
- video.srcObject = cam ? cam.stream : null;
380
- video.play().catch(() => {});
381
284
  const prev = this.local;
382
285
  this.local = {
383
286
  stream,
287
+ cam, // the StereoCamera (null = audio-only)
384
288
  camStream: cam ? cam.stream : null,
385
- video,
289
+ video: cam ? cam.video : null,
386
290
  videoTrack,
387
291
  audioTrack,
388
292
  format: cam ? cam.format : 'mono',
389
293
  width: cam ? cam.width : 0,
390
294
  height: cam ? cam.height : 0,
391
295
  calibration: cam ? cam.calibration : {},
392
- owned: cam ? cam.owned : false,
296
+ owned: cam ? cam.owned && cam !== given : false,
393
297
  label: cam ? cam.label : '',
394
298
  // Kept on the error path too: which cameras were held, and why, is the one clue a
395
299
  // 'camera-busy' / 'no-camera' report carries.
396
300
  skipped: cam ? cam.skipped : camError?.skipped || [],
397
301
  };
302
+ if (cam && cam !== prev?.cam) {
303
+ // The camera ending under us (revoked, unplugged, taken): audio-only + "Retry camera".
304
+ this._camEndedOff?.();
305
+ this._camEndedOff = cam.on('ended', () => {
306
+ if (this.local?.cam !== cam) return;
307
+ this.local.videoTrack = null;
308
+ this.camStatus = 'none';
309
+ this.error('no-camera', 'the camera ended (revoked, unplugged, or taken by another app). Audio-only until "Retry camera".');
310
+ this.transport?.broadcast(this.stateMsg());
311
+ this._refreshSelf();
312
+ this._refreshBar();
313
+ });
314
+ }
398
315
  this.log('camera', { format: this.local.format, width: this.local.width, height: this.local.height, label: this.local.label, skipped: this.local.skipped.length });
399
- if (prev && prev.owned && prev.camStream && prev.camStream !== this.local.camStream) prev.camStream.getVideoTracks().forEach((t) => t.stop());
316
+ if (prev && prev.owned && prev.cam && prev.cam !== cam) prev.cam.close();
400
317
  this._refreshSelf();
401
318
  this._refreshLobby();
402
319
  return this.local;
@@ -404,7 +321,7 @@ class Call {
404
321
 
405
322
  hello() {
406
323
  const l = this.local || {};
407
- return makeHello({
324
+ return wire.makeHello({
408
325
  format: l.format,
409
326
  width: l.width,
410
327
  height: l.height,
@@ -426,18 +343,18 @@ class Call {
426
343
  if (this.state === 'joining' || this.state === 'in-call') return this.handle;
427
344
  if (this.state === 'left' && !this.local?.videoTrack) await this._openMedia(this.o.camera);
428
345
  if (!this.room) {
429
- this.room = newRoomId();
346
+ this.room = wire.newRoomId();
430
347
  this.log('room-created', {});
431
348
  }
432
- if (this.o.updateUrl && globalThis.history && globalThis.location) {
349
+ if (this.o.invite.updateUrl && globalThis.history && globalThis.location) {
433
350
  try {
434
- history.replaceState(history.state, '', buildInviteLink(location.href, this.room));
351
+ history.replaceState(history.state, '', wire.buildInviteLink(location.href, this.room));
435
352
  } catch {
436
353
  /* sandboxed iframe */
437
354
  }
438
355
  }
439
356
  this._setState('joining');
440
- const t = new MeshTransport({
357
+ const t = new MeshTransportImpl({
441
358
  signaling: this.o.signaling,
442
359
  id: this.id,
443
360
  maxPeers: this.o.maxPeers,
@@ -480,7 +397,7 @@ class Call {
480
397
  // The grid is sized so the whole call block fits ONE viewport, but only if it starts at the
481
398
  // top: a tile below the fold is withheld from the weave (browser#167), so its layer goes
482
399
  // live late. Bring the block into view once, on join.
483
- if (this.o.ui && this.o.scrollIntoView) {
400
+ if (this.o.chrome) {
484
401
  try {
485
402
  this.container.scrollIntoView?.({ block: 'start', behavior: 'instant' });
486
403
  this._fitGrid();
@@ -489,7 +406,7 @@ class Call {
489
406
  }
490
407
  }
491
408
  this._startStats();
492
- this._pagehide = () => this.leave();
409
+ this._pagehide = () => this.leave('pagehide');
493
410
  globalThis.addEventListener?.('pagehide', this._pagehide);
494
411
  this.log('joined', { id: this.id, peers: this.tiles.size });
495
412
  this.emit('joined', { room: this.room, id: this.id });
@@ -546,7 +463,7 @@ class Call {
546
463
  tile.onChannelOpen();
547
464
  return;
548
465
  }
549
- const h = normalizeHello(msg);
466
+ const h = wire.normalizeHello(msg);
550
467
  if (h) return tile.setHello(h);
551
468
  const hint = normalizeHint(msg);
552
469
  if (hint) return tile.setHint(hint);
@@ -558,8 +475,8 @@ class Call {
558
475
 
559
476
  inviteLink() {
560
477
  if (!this.room) return null;
561
- const base = this.o.inviteBase || (globalThis.location ? location.href : '');
562
- return buildInviteLink(base, this.room);
478
+ const base = this.o.invite.base || (globalThis.location ? location.href : '');
479
+ return wire.buildInviteLink(base, this.room);
563
480
  }
564
481
 
565
482
  mute(on) {
@@ -571,7 +488,7 @@ class Call {
571
488
  return this.muted;
572
489
  }
573
490
 
574
- cameraOff(on) {
491
+ setCameraOff(on) {
575
492
  this.camOff = on === undefined ? !this.camOff : !!on;
576
493
  if (this.local?.videoTrack) this.local.videoTrack.enabled = !this.camOff;
577
494
  this.transport?.broadcast(this.stateMsg());
@@ -615,7 +532,7 @@ class Call {
615
532
  return true;
616
533
  }
617
534
 
618
- leave() {
535
+ leave(reason = 'left') {
619
536
  if (this.state === 'left') return;
620
537
  globalThis.removeEventListener?.('pagehide', this._pagehide);
621
538
  this.transport?.stop();
@@ -626,19 +543,20 @@ class Call {
626
543
  clearInterval(this._statsTimer);
627
544
  this._setSpeaker(null);
628
545
  if (this.local) {
546
+ this._camEndedOff?.();
547
+ this._camEndedOff = null;
629
548
  this.local.stream.getTracks().forEach((t) => t.stop());
630
- if (this.local.owned && this.local.camStream) this.local.camStream.getTracks().forEach((t) => t.stop());
549
+ // A camera THIS module opened is closed; a page's StereoCamera / MediaStream is left running.
550
+ if (this.local.owned && this.local.cam) this.local.cam.close();
551
+ else if (this.local.cam) this.self?.detach();
631
552
  this.local.videoTrack = null;
632
553
  this.local.audioTrack = null;
633
554
  }
634
- if (this.self) {
635
- this.self.unregister();
636
- this.self.route = null; // a rejoin re-registers it
637
- }
555
+ this.self?.detach(); // a rejoin re-attaches it
638
556
  this._unhookWall();
639
557
  this._setState('left');
640
558
  this._layout();
641
- this.emit('left', { room: this.room });
559
+ this.emit('left', { room: this.room, reason });
642
560
  }
643
561
 
644
562
  // ── the wall: session loss and recovery ───────────────────────────────────────────────
@@ -672,7 +590,7 @@ class Call {
672
590
  this.weaveLive = true; // nothing to wait on
673
591
  return;
674
592
  }
675
- const gate = createLiveGate();
593
+ const gate = wire.createLiveGate();
676
594
  const t0 = performance.now();
677
595
  const onFrame = (_t, f) => {
678
596
  if (w !== this.wall || !this.wallLive || this.state === 'left') return;
@@ -727,7 +645,7 @@ class Call {
727
645
  this.log('session-ended', { byPage: hook.pageClosed });
728
646
  for (const t of this.tiles.values()) t.onWallLost();
729
647
  this.self?.onWallLost();
730
- if (hook.pageClosed || !this.o.recoverSession || this.state === 'left') {
648
+ if (hook.pageClosed || this.state === 'left') {
731
649
  this._unhookWall();
732
650
  return;
733
651
  }
@@ -744,7 +662,7 @@ class Call {
744
662
  try {
745
663
  // The shared manager: if the page has already re-opened its own wall, join it rather than
746
664
  // opening a second session (rule 1); else this creates the document's new one.
747
- w = await sharedInline3D(this.o.wallOptions);
665
+ w = await sharedInline3D();
748
666
  } catch {
749
667
  w = null;
750
668
  }
@@ -755,7 +673,6 @@ class Call {
755
673
  this.log('session-recovered', { attempt });
756
674
  for (const t of this.tiles.values()) t.reroute(true);
757
675
  this.self?.reroute(true);
758
- this.emit('session', { wall: w });
759
676
  }, backoffMs(attempt, { baseMs: 1000, maxMs: 15000 }));
760
677
  }
761
678
 
@@ -765,7 +682,6 @@ class Call {
765
682
  let last = 0;
766
683
  const tick = (now) => {
767
684
  this._raf = requestAnimationFrame(tick);
768
- this.self?.paint();
769
685
  for (const t of this.tiles.values()) t.paint();
770
686
  if (last && this.liftPool && this.liftPool.anyWeb) this._watchFrames(now - last);
771
687
  last = now;
@@ -797,10 +713,10 @@ class Call {
797
713
  const lo = this.o.liftOptions;
798
714
  // liftCapabilities probes the same model source the lifts will use.
799
715
  const capsOpts = { webFallback: true, ...(lo && lo.models ? { models: lo.models } : {}) };
800
- this._liftP = resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
716
+ this._liftP = liftMod.resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
801
717
  this.liftApi = r;
802
718
  if (r.lift) {
803
- this.liftPool = createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
719
+ this.liftPool = liftMod.createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
804
720
  this.liftPool.setDepth(this.depth);
805
721
  this.liftPool.setSpeaker(this.speakerId);
806
722
  if (!r.caps && r.capabilities) {
@@ -865,14 +781,15 @@ class Call {
865
781
  /** What the lobby says a mono participant will look like here. */
866
782
  _liftHint() {
867
783
  if (!this.wallLive) return '';
868
- if (!this.mono3DOn) return 'Mono cameras: 2D (2D→3D is off).';
784
+ const t = this.t;
785
+ if (!this.mono3DOn) return t('liftOff');
869
786
  const r = this.liftApi;
870
- if (!r) return 'Mono cameras: checking 2D→3D…';
871
- if (!r.lift) return this.liftMissing ? 'Mono cameras: 2D→3D unavailable in this build.' : 'Mono cameras: 2D (no 2D→3D provider here).';
787
+ if (!r) return t('liftChecking');
788
+ if (!r.lift) return this.liftMissing ? t('liftMissing') : t('liftNoProvider');
872
789
  const c = r.caps;
873
- if (c && c.native) return `Mono cameras: 2D→3D (native${c.provider ? `, ${c.provider}` : ''}).`;
874
- if (c && c.webFallback && c.webFallback.webgpu) return 'Mono cameras: 2D→3D (in this page, WebGPU).';
875
- return this.liftProven ? 'Mono cameras: 2D→3D.' : 'Mono cameras: 2D→3D (confirmed on the first mono participant).';
790
+ if (c && c.native) return t('liftNative', { provider: c.provider ? `, ${c.provider}` : '' });
791
+ if (c && c.webFallback && c.webFallback.webgpu) return t('liftWeb');
792
+ return this.liftProven ? t('liftProven') : t('liftUnproven');
876
793
  }
877
794
 
878
795
  _mono3DInfo() {
@@ -989,7 +906,7 @@ class Call {
989
906
  */
990
907
  _fitGrid() {
991
908
  const ui = this.ui;
992
- if (!ui || !this.o.ui || typeof globalThis.innerHeight !== 'number') return;
909
+ if (!ui || !this.o.chrome || typeof globalThis.innerHeight !== 'number') return;
993
910
  const host = this.container.getBoundingClientRect?.();
994
911
  const grid = ui.grid.getBoundingClientRect?.();
995
912
  if (!host || !grid || !grid.height) return;
@@ -1010,55 +927,59 @@ class Call {
1010
927
 
1011
928
  _buildDom() {
1012
929
  const c = this.container;
930
+ const t = this.t;
1013
931
  injectCallStyle(c.ownerDocument || document);
1014
932
  c.classList.add('dxr-call-host');
1015
- if (this.o.accent) c.style.setProperty('--dxr-accent', this.o.accent);
933
+ // theme → CSS custom properties on the host (chrome only: no variable can reach a tile).
934
+ for (const [prop, v] of Object.entries(this.o.cssVars)) c.style.setProperty(prop, v);
1016
935
  const ui = (this.ui = {});
1017
- ui.banner = el('div', { class: 'dxr-call-banner dxr-call-hidden' }, [
1018
- el('span', { text: 'You are seeing this call in 2D.' }),
1019
- el('a', { href: this.o.browserUrl, target: '_blank', rel: 'noopener', text: 'View in 3D with DisplayXR Browser' }),
936
+ ui.banner = el('div', { class: 'dxr-call-banner dxr-call-hidden', part: 'banner' }, [
937
+ el('span', { text: t('banner2D') }),
938
+ el('a', { href: this.o.landing.browserUrl, target: '_blank', rel: 'noopener', text: t('bannerLink') }),
1020
939
  ]);
1021
- ui.grid = el('div', { class: 'dxr-call-grid', 'data-layout': this.o.layout, 'data-n': '0' });
1022
- ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
1023
- ui.selfSlot = el('div', { class: 'dxr-call-self' });
1024
- ui.bar = el('div', { class: 'dxr-call-bar' });
940
+ ui.grid = el('div', { class: 'dxr-call-grid', part: 'grid', 'data-layout': this.o.layout, 'data-n': '0' });
941
+ ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'lobby' });
942
+ ui.selfSlot = el('div', { class: 'dxr-call-self', part: 'self' });
943
+ ui.bar = el('div', { class: 'dxr-call-bar', part: 'bar' });
1025
944
  ui.foot = el('div', { class: 'dxr-call-foot' }, [ui.selfSlot, ui.bar]);
1026
- ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
1027
- if (!this.o.ui) {
945
+ ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'invite' });
946
+ if (!this.o.chrome) {
1028
947
  show(ui.bar, false);
1029
948
  }
1030
949
  c.append(ui.banner, ui.panel, ui.grid, ui.invite, ui.foot);
1031
950
  if (this.o.selfView) this.self = new SelfTile(this, ui.selfSlot);
1032
951
  else show(ui.selfSlot, false);
1033
- if (this.o.ui) this._buildBar();
952
+ if (this.o.chrome) this._buildBar();
1034
953
  }
1035
954
 
1036
955
  _buildBar() {
1037
956
  const ui = this.ui;
957
+ const t = this.t;
1038
958
  const ib = (label, icon, onclick, extra = '') => el('button', { class: `dxr-call-ib ${extra}`, type: 'button', 'aria-label': label, title: label, html: icon, onclick });
1039
- ui.mic = ib('Mute microphone', ICONS.mic, () => this.mute());
1040
- ui.cam = ib('Turn camera off', ICONS.cam, () => (this.local?.videoTrack ? this.cameraOff() : this.retryCamera().catch(() => {})));
1041
- ui.depth = el('input', { type: 'range', min: '-1', max: '1', step: '0.05', value: '0', 'aria-label': 'Depth' });
959
+ ui.mic = ib(t('mute'), ICONS.mic, () => this.mute());
960
+ ui.cam = ib(t('cameraOff'), ICONS.cam, () => (this.local?.videoTrack ? this.setCameraOff() : this.retryCamera().catch(() => {})));
961
+ ui.depth = el('input', { type: 'range', min: '-1', max: '1', step: '0.05', value: '0', 'aria-label': t('depth') });
1042
962
  ui.depth.addEventListener('input', () => this.setDepth(+ui.depth.value));
1043
963
  ui.depth.addEventListener('dblclick', () => this.setDepth(0));
1044
- ui.inviteBtn = ib('Invite', ICONS.link, () => {
964
+ ui.inviteBtn = ib(t('invite'), ICONS.link, () => {
1045
965
  this._showInvite = !this._showInvite;
1046
966
  this._layout();
1047
967
  });
1048
- ui.leave = ib('Leave call', ICONS.leave, () => this.leave(), 'dxr-call-ib--leave');
1049
- ui.bar.append(ui.mic, ui.cam, el('label', { class: 'dxr-call-depth' }, ['Depth', ui.depth]), ui.inviteBtn, ui.leave);
968
+ ui.leave = ib(t('leave'), ICONS.leave, () => this.leave(), 'dxr-call-ib--leave');
969
+ ui.bar.append(ui.mic, ui.cam, el('label', { class: 'dxr-call-depth' }, [t('depth'), ui.depth]), ui.inviteBtn, ui.leave);
1050
970
  }
1051
971
 
1052
972
  _refreshBar() {
1053
973
  const ui = this.ui;
1054
974
  if (!ui || !ui.mic) return;
975
+ const t = this.t;
1055
976
  ui.mic.innerHTML = this.muted ? ICONS.micOff : ICONS.mic;
1056
977
  ui.mic.setAttribute('aria-pressed', String(this.muted));
1057
- ui.mic.title = this.muted ? 'Unmute microphone' : 'Mute microphone';
978
+ ui.mic.title = this.muted ? t('unmute') : t('mute');
1058
979
  const noCam = !this.local?.videoTrack;
1059
980
  ui.cam.innerHTML = this.camOff || noCam ? ICONS.camOff : ICONS.cam;
1060
981
  ui.cam.setAttribute('aria-pressed', String(this.camOff || noCam));
1061
- ui.cam.title = noCam ? (this.camStatus === 'busy' ? `${PLATE_TEXT.cameraBusy} — click to retry` : 'No camera — click to retry') : this.camOff ? 'Turn camera on' : 'Turn camera off';
982
+ ui.cam.title = noCam ? (this.camStatus === 'busy' ? t('cameraBusyRetry', { cameraBusy: t('cameraBusy') }) : t('cameraRetry')) : this.camOff ? t('cameraOn') : t('cameraOff');
1062
983
  }
1063
984
 
1064
985
  _refreshSelf() {
@@ -1073,20 +994,21 @@ class Call {
1073
994
  target.replaceChildren();
1074
995
  const link = this.inviteLink();
1075
996
  if (!link) return;
1076
- const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': 'Invite link' });
997
+ const t = this.t;
998
+ const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': t('inviteLink') });
1077
999
  input.addEventListener('focus', () => input.select());
1078
- const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: 'Copy link' });
1000
+ const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: t('copyLink') });
1079
1001
  copy.addEventListener('click', async () => {
1080
1002
  try {
1081
1003
  await navigator.clipboard.writeText(link);
1082
- copy.textContent = 'Copied';
1004
+ copy.textContent = t('copied');
1083
1005
  } catch {
1084
1006
  input.select();
1085
- copy.textContent = 'Press Ctrl/Cmd+C';
1007
+ copy.textContent = t('pressCopy');
1086
1008
  }
1087
- setTimeout(() => (copy.textContent = 'Copy link'), 1800);
1009
+ setTimeout(() => (copy.textContent = t('copyLink')), 1800);
1088
1010
  });
1089
- const qr = el('canvas', { 'aria-label': 'QR code of the invite link', role: 'img' });
1011
+ const qr = el('canvas', { 'aria-label': t('qrLabel'), role: 'img' });
1090
1012
  try {
1091
1013
  drawQr(qr, link, { px: 4 });
1092
1014
  } catch {
@@ -1103,16 +1025,17 @@ class Call {
1103
1025
  const ui = this.ui;
1104
1026
  if (!ui) return;
1105
1027
  const c = this.container;
1106
- const lobby = this.o.ui && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
1028
+ const t = this.t;
1029
+ const lobby = this.o.chrome && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
1107
1030
  c.classList.toggle('dxr-call-host--lobby', lobby);
1108
- show(ui.banner, this.o.ui && !this.woven);
1031
+ show(ui.banner, this.o.chrome && !this.woven);
1109
1032
  const n = this.tiles.size;
1110
1033
  ui.grid.dataset.n = String(Math.min(n, 4));
1111
1034
  show(ui.grid, n > 0 && this.state !== 'left');
1112
- // Speaker layout: the active speaker (or the first tile) spans the row.
1035
+ // Speaker layout: the active speaker (or the first tile) spans the row. layout:'none' positions nothing.
1113
1036
  const main = this.o.layout === 'speaker' ? this.speakerId || [...this.tiles.keys()][0] : null;
1114
- for (const [id, t] of this.tiles) t.el.classList.toggle('dxr-call-tile--main', id === main);
1115
- if (!this.o.ui) return;
1037
+ for (const [id, tile] of this.tiles) tile.el.classList.toggle('dxr-call-tile--main', id === main);
1038
+ if (!this.o.chrome) return;
1116
1039
  this._refreshBar();
1117
1040
  queueMicrotask(() => this._fitGrid());
1118
1041
  const p = ui.panel;
@@ -1121,39 +1044,67 @@ class Call {
1121
1044
  p.replaceChildren();
1122
1045
  const l = this.local;
1123
1046
  const joining = !!this.room;
1124
- const title =
1125
- this.state === 'full' ? 'This call is full' : this.state === 'left' ? 'You left the call' : joining ? 'Join the 3D call' : 'Start a 3D call';
1126
- const kind = this.camStatus === 'busy' ? 'busy (joining audio-only)' : !l || !l.videoTrack ? 'none (joining audio-only)' : l.format === 'sbs' ? `sending 3D (side-by-side, ${l.width}×${l.height})` : `sending 2D (${l.width}×${l.height})`;
1047
+ const title = this.state === 'full' ? t('lobbyFullTitle') : this.state === 'left' ? t('lobbyLeftTitle') : joining ? t('lobbyJoinTitle') : t('lobbyStartTitle');
1048
+ const dims = { width: l?.width || 0, height: l?.height || 0 };
1049
+ const kind = this.camStatus === 'busy' ? t('kindBusy') : !l || !l.videoTrack ? t('kindNone') : l.format === 'sbs' ? t('kindSbs', dims) : t('kindMono', dims);
1127
1050
  const text =
1128
1051
  this.state === 'full'
1129
- ? `It already has ${this.o.maxPeers} participants.`
1130
- : `Check your framing below. Camera: ${l?.label || 'default'} — ${kind}.${this._liftHint() ? ` ${this._liftHint()}` : ''}`;
1052
+ ? t('lobbyFullText', { maxPeers: this.o.maxPeers })
1053
+ : `${t('lobbyText', { camera: l?.label || t('cameraDefault'), kind })}${this._liftHint() ? ` ${this._liftHint()}` : ''}`;
1131
1054
  const go = el('button', {
1132
1055
  class: 'dxr-call-btn dxr-call-btn--primary',
1133
1056
  type: 'button',
1134
- text: this.state === 'left' ? 'Rejoin' : joining ? 'Join call' : 'Start 3D call',
1057
+ text: this.state === 'left' ? t('rejoin') : joining ? t('join') : t('start'),
1135
1058
  });
1136
1059
  go.addEventListener('click', () => this.join().catch(() => {}));
1137
- const sel = el('select', { class: 'dxr-call-select', 'aria-label': 'Camera' });
1060
+ const sel = el('select', { class: 'dxr-call-select', 'aria-label': t('cameraSelect') });
1138
1061
  this._fillCameras(sel);
1139
1062
  sel.addEventListener('change', () => this.setCamera(sel.value).catch((e) => this.error('camera-failed', e.message, e)));
1140
- const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: 'Retry camera' }) : null;
1063
+ const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: t('retryCamera') }) : null;
1141
1064
  retry?.addEventListener('click', () => this.retryCamera().catch(() => {}));
1142
1065
  p.append(el('h3', { text: title }), el('p', { text }), el('div', { class: 'dxr-call-row' }, [this.state === 'full' ? null : go, retry, sel]));
1143
1066
  } else if (this.state === 'in-call' && n === 0) {
1144
1067
  show(p, true);
1145
- this._renderInvite(p, 'Waiting for others', 'Share this link (or scan the code). Anyone who opens it joins — keep it private.');
1068
+ this._renderInvite(p, t('waitingTitle'), t('waitingText'));
1146
1069
  } else if (this.state === 'joining') {
1147
1070
  show(p, true);
1148
- p.replaceChildren(el('p', { text: 'Joining…' }));
1071
+ p.replaceChildren(el('p', { text: t('joining') }));
1149
1072
  } else show(p, false);
1150
1073
  const inviteOpen = this.state === 'in-call' && n > 0 && this._showInvite;
1151
- if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, 'Invite', 'Anyone with this link can join.');
1074
+ if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, t('inviteTitle'), t('inviteText'));
1152
1075
  show(ui.invite, inviteOpen);
1153
1076
  if (!inviteOpen) ui.invite.replaceChildren();
1154
1077
  ui.inviteBtn?.setAttribute('aria-pressed', String(!!inviteOpen));
1155
1078
  }
1156
1079
 
1080
+ /**
1081
+ * Diagnostics (explicitly UNSTABLE — RFC 0003 §2): everything that used to sit on the public
1082
+ * handle and `CallPeer` as debug data. Shape free to change; read it in devtools, never in code.
1083
+ */
1084
+ diagnostics() {
1085
+ const l = this.local;
1086
+ return {
1087
+ sdk: wire.CALL_SDK,
1088
+ wire: wire.WIRE_VERSION,
1089
+ state: this.state,
1090
+ wall: this.wall,
1091
+ wallLive: this.wallLive,
1092
+ weaveLive: this.weaveLive,
1093
+ woven: this.woven,
1094
+ local: l
1095
+ ? { format: l.format, width: l.width, height: l.height, label: l.label, calibration: l.calibration, owned: l.owned, skipped: l.skipped, camera: l.cam || null, stream: l.stream }
1096
+ : null,
1097
+ hello: this.hello(),
1098
+ transport: this.transport,
1099
+ peers: [...this.tiles.values()].map((tile) => tile.diag()),
1100
+ mono3D: { ...this._mono3DInfo(), lift: this.liftApi, pool: this.liftPool ? this.liftPool.list() : [] },
1101
+ speaker: this.speakerId,
1102
+ depth: this.depth,
1103
+ options: this.o,
1104
+ kill: (id) => this.transport?._debugKill(id),
1105
+ };
1106
+ }
1107
+
1157
1108
  async _fillCameras(sel) {
1158
1109
  let cams = [];
1159
1110
  try {
@@ -1169,7 +1120,8 @@ class Call {
1169
1120
 
1170
1121
  _makeHandle() {
1171
1122
  const call = this;
1172
- return {
1123
+ const renamedEvents = { format: 'display', session: null };
1124
+ const handle = {
1173
1125
  get room() {
1174
1126
  return call.room;
1175
1127
  },
@@ -1179,15 +1131,16 @@ class Call {
1179
1131
  get state() {
1180
1132
  return call.state;
1181
1133
  },
1182
- get wall() {
1183
- return call.wall;
1184
- },
1185
- get format() {
1134
+ /** What YOU send: 'sbs' | 'mono'; null before a camera opened. */
1135
+ get localFormat() {
1186
1136
  return call.local ? call.local.format : null;
1187
1137
  },
1188
1138
  get muted() {
1189
1139
  return call.muted;
1190
1140
  },
1141
+ get cameraOff() {
1142
+ return call.camOff;
1143
+ },
1191
1144
  get depth() {
1192
1145
  return call.depth;
1193
1146
  },
@@ -1206,18 +1159,25 @@ class Call {
1206
1159
  get peers() {
1207
1160
  return Object.freeze([...call.tiles.values()].map((t) => t.info()));
1208
1161
  },
1209
- join: () => call.join(),
1162
+ join: () => call.join().then(() => undefined),
1210
1163
  inviteLink: () => call.inviteLink(),
1211
1164
  mute: (on) => call.mute(on),
1212
- cameraOff: (on) => call.cameraOff(on),
1213
- setCamera: (idOrStream, o) => call.setCamera(idOrStream, o),
1165
+ setCameraOff: (on) => call.setCameraOff(on),
1166
+ setCamera: (src, o) => call.setCamera(src, o),
1214
1167
  setDepth: (v) => call.setDepth(v),
1215
1168
  retryCamera: () => call.retryCamera(),
1216
- sendHint: (z) => call.sendHint(z),
1217
1169
  setMono3D: (on) => call.setMono3D(on),
1218
- leave: () => call.leave(),
1170
+ leave: () => call.leave('left'),
1171
+ /** A remote participant's tile element (for `layout: 'none'`), or null. */
1172
+ tile: (peerId) => call.tiles.get(peerId)?.el || null,
1173
+ diagnostics: () => call.diagnostics(),
1219
1174
  on(type, cb) {
1220
1175
  if (typeof cb !== 'function') throw new TypeError(`${TAG} on() takes a function`);
1176
+ if (type in renamedEvents && !call._eventWarned?.has(type)) {
1177
+ (call._eventWarned ||= new Set()).add(type);
1178
+ const to = renamedEvents[type];
1179
+ console.warn(`${TAG} the '${type}' event is deprecated (removed in 1.31)${to ? ` — listen for '${to}'` : ' — it is internal now; diagnostics() has the wall'}`);
1180
+ }
1221
1181
  if (!call.listeners.has(type)) call.listeners.set(type, new Set());
1222
1182
  call.listeners.get(type).add(cb);
1223
1183
  return () => call.listeners.get(type)?.delete(cb);
@@ -1225,14 +1185,28 @@ class Call {
1225
1185
  off(type, cb) {
1226
1186
  call.listeners.get(type)?.delete(cb);
1227
1187
  },
1228
- /** Diagnostics: the transport (mesh) and a way to simulate a dropped connection. */
1229
- _debug: {
1230
- get transport() {
1231
- return call.transport;
1232
- },
1233
- kill: (id) => call.transport?._debugKill(id),
1234
- },
1235
1188
  };
1189
+ // 1.29 handle members, one release of warnings (RFC 0003 §2): removed in 1.31.
1190
+ const legacy = {
1191
+ wall: ['diagnostics().wall (or the wall you passed / sharedInline3D())', () => call.wall],
1192
+ format: ['localFormat', () => handle.localFormat],
1193
+ sendHint: ['nothing — auto-convergence superseded it', () => (z) => call.sendHint(z)],
1194
+ _debug: ['diagnostics()', () => ({ transport: call.transport, kill: (id) => call.transport?._debugKill(id) })],
1195
+ };
1196
+ for (const [name, [to, get]] of Object.entries(legacy)) {
1197
+ Object.defineProperty(handle, name, {
1198
+ enumerable: false,
1199
+ configurable: true,
1200
+ get() {
1201
+ if (!call._handleWarned?.has(name)) {
1202
+ (call._handleWarned ||= new Set()).add(name);
1203
+ console.warn(`${TAG} handle.${name} is deprecated (removed in 1.31) — use ${to}`);
1204
+ }
1205
+ return get();
1206
+ },
1207
+ });
1208
+ }
1209
+ return handle;
1236
1210
  }
1237
1211
  }
1238
1212
 
@@ -1262,7 +1236,7 @@ function paintFlat(canvas, video, { eyeHalf = null, mirror = false } = {}) {
1262
1236
  const H = video.videoHeight;
1263
1237
  if (!W || !H || (video.readyState || 0) < 2) return;
1264
1238
  const eyeW = eyeHalf === null ? W : W / 2;
1265
- const r = eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
1239
+ const r = wire.eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
1266
1240
  g.save();
1267
1241
  if (mirror) {
1268
1242
  g.translate(canvas.width, 0);
@@ -1319,11 +1293,12 @@ class Tile {
1319
1293
  this.video.addEventListener('resize', () => this._maybeRoute());
1320
1294
  this.canvas = el('canvas');
1321
1295
  this.cover = el('div', { class: 'dxr-call-cover dxr-call-hidden' });
1322
- this.badge = el('div', { class: 'dxr-call-badge' });
1323
- this.plate = el('div', { class: 'dxr-call-state', text: 'Connecting…' });
1296
+ this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
1297
+ this.plate = el('div', { class: 'dxr-call-state', part: 'plate', text: call.t('connecting') });
1324
1298
  this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.cover, this.badge, this.plate]);
1325
1299
  this.stage.style.aspectRatio = String(call.o.tileAspect);
1326
- this.el = el('div', { class: 'dxr-call-tile', 'data-peer': id }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
1300
+ // `data-dxr-peer` is the documented hook (layout:'none', handle.tile()); `data-peer` is the 1.29 spelling.
1301
+ this.el = el('div', { class: 'dxr-call-tile', part: 'tile', 'data-dxr-peer': id, 'data-peer': id }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
1327
1302
  if (!call.o.ui) {
1328
1303
  show(this.badge, false);
1329
1304
  }
@@ -1331,15 +1306,29 @@ class Tile {
1331
1306
  this._renderBadge();
1332
1307
  }
1333
1308
 
1309
+ /** How this side shows the participant: `3D` (woven pair) | `2D→3D` (lifted) | `2D` (flat). */
1310
+ display() {
1311
+ return wire.badgeFor(this.route);
1312
+ }
1313
+
1314
+ /** The public `CallPeer` (RFC 0003 §2): what a page needs to run and observe. Debug data is `diag()`. */
1334
1315
  info() {
1335
1316
  return Object.freeze({
1336
1317
  id: this.id,
1337
1318
  format: this.format || 'mono',
1338
- route: this.route,
1319
+ display: this.display(),
1339
1320
  state: this.conn,
1340
1321
  muted: this.remote.muted,
1341
1322
  cameraOff: this.remote.cameraOff,
1342
1323
  speaking: this.speaking,
1324
+ });
1325
+ }
1326
+
1327
+ /** Everything else (UNSTABLE): `handle.diagnostics().peers[i]`. */
1328
+ diag() {
1329
+ return {
1330
+ ...this.info(),
1331
+ route: this.route,
1343
1332
  rectified: !!(this.hello && this.hello.rectified),
1344
1333
  hello: this.hello,
1345
1334
  quality: this.quality,
@@ -1354,14 +1343,15 @@ class Tile {
1354
1343
  via: this.autoStats.via,
1355
1344
  },
1356
1345
  lift: this.lifted
1357
- ? Object.freeze({
1346
+ ? {
1358
1347
  live: this.liftLive,
1359
1348
  native: !!(this.liftHandle && this.liftHandle.native),
1360
1349
  priority: this.call.liftPool ? this.call.liftPool.priority(this.id) : null,
1361
1350
  state: this.liftHandle ? this.liftHandle.state || null : 'pending',
1362
- })
1351
+ }
1363
1352
  : null,
1364
- });
1353
+ element: this.el,
1354
+ };
1365
1355
  }
1366
1356
 
1367
1357
  setStream(stream) {
@@ -1416,7 +1406,7 @@ class Tile {
1416
1406
 
1417
1407
  setConn(st) {
1418
1408
  if (st === 'unreachable' && this.conn !== 'unreachable') {
1419
- this.call.error('unreachable', `${PLATE_TEXT.unreachable}. Still retrying in the background.`, Object.assign(new Error('unreachable'), { peer: this.id }));
1409
+ this.call.error('unreachable', `${this.call.t('unreachable')}. Still retrying in the background.`, Object.assign(new Error('unreachable'), { peer: this.id }));
1420
1410
  }
1421
1411
  this.conn = st;
1422
1412
  if (st === 'connected' && this.leaveTimer) {
@@ -1427,26 +1417,28 @@ class Tile {
1427
1417
  }
1428
1418
 
1429
1419
  _renderPlate() {
1420
+ const s = this.call.t;
1430
1421
  const t =
1431
1422
  this.conn === 'left'
1432
- ? 'Left the call'
1423
+ ? s('leftCall')
1433
1424
  : this.conn === 'unreachable'
1434
- ? PLATE_TEXT.unreachable
1425
+ ? s('unreachable')
1435
1426
  : this.conn === 'reconnecting'
1436
- ? 'Reconnecting…'
1437
- : this.conn !== 'connected'
1438
- ? 'Connecting…'
1439
- : this.remote.cameraOff
1440
- ? 'Camera off'
1441
- : null;
1427
+ ? s('reconnecting')
1428
+ : this.conn !== 'connected'
1429
+ ? s('connecting')
1430
+ : this.remote.cameraOff
1431
+ ? s('cameraOffPlate')
1432
+ : null;
1442
1433
  this.plate.textContent = t || '';
1443
1434
  show(this.plate, !!t);
1444
1435
  }
1445
1436
 
1446
1437
  _renderBadge() {
1447
- const b = badgeFor(this.route);
1438
+ const s = this.call.t;
1439
+ const b = this.route === 'woven-sbs' ? s('badge3D') : this.route === 'lifted' ? s('badge2D3D') : s('badge2D');
1448
1440
  // A lifted tile whose lift is still loading reads "2D→3D…" (it is painted flat meanwhile).
1449
- const text = !this.route ? '…' : this.route === 'lifted' && !this.liftLive ? `${b}…` : b;
1441
+ const text = !this.route ? s('badgePending') : this.route === 'lifted' && !this.liftLive ? `${b}${s('badgePending')}` : b;
1450
1442
  this.badge.replaceChildren(el('b', { text }));
1451
1443
  this.badge.dataset.route = this.route || '';
1452
1444
  if (this.remote.muted) this.badge.insertAdjacentHTML('beforeend', ICONS.mutedSmall);
@@ -1464,7 +1456,7 @@ class Tile {
1464
1456
  const r =
1465
1457
  this.forceFlat && this.format === 'sbs'
1466
1458
  ? { route: 'flat-left' }
1467
- : routeFor({
1459
+ : wire.routeFor({
1468
1460
  format: this.format,
1469
1461
  woven: call.woven,
1470
1462
  mono3D: call.mono3DOn ? 'auto' : 'off',
@@ -1484,7 +1476,9 @@ class Tile {
1484
1476
  if (r.route === 'woven-sbs') this._registerWoven();
1485
1477
  else if (r.route === 'lifted') this._registerLifted();
1486
1478
  call.log('route', { peer: this.id, route: r.route, format: this.format, mono3d: r.mono3d || null });
1487
- call.emit('format', { id: this.id, format: this.format, route: r.route, mono3d: r.mono3d || null, hello: this.hello });
1479
+ call.emit('display', { id: this.id, display: this.display() });
1480
+ // 1.29's 'format' event (route / mono3d detail → diagnostics()): one release of compatibility.
1481
+ if (call.listeners.get('format')?.size) call.emit('format', { id: this.id, format: this.format, route: r.route, mono3d: r.mono3d || null, hello: this.hello });
1488
1482
  this._renderBadge();
1489
1483
  }
1490
1484
 
@@ -1669,7 +1663,7 @@ class Tile {
1669
1663
  const shift = this.conv.step(eyeW);
1670
1664
  const g = c.getContext('2d');
1671
1665
  for (const eye of [0, 1]) {
1672
- const r = eyeCropRect(eyeW, H, A, shift, eye);
1666
+ const r = wire.eyeCropRect(eyeW, H, A, shift, eye);
1673
1667
  g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
1674
1668
  }
1675
1669
  }
@@ -1795,106 +1789,197 @@ class Tile {
1795
1789
 
1796
1790
  // ── the self view ─────────────────────────────────────────────────────────────────────────
1797
1791
 
1792
+ /**
1793
+ * The self view IS an `addCameraView` of `@displayxr/inline3d/camera` (RFC 0003 §4): mirrored
1794
+ * correctly (each half mirrored AND the halves swapped — mirroring each half in place would invert
1795
+ * every disparity), woven on a 3D wall, flat elsewhere. The stream that is SENT is never touched.
1796
+ * This class owns the tile's chrome (badge, plate) and re-attaches the view when the camera or
1797
+ * the wall changes; the view owns the pixels.
1798
+ */
1798
1799
  class SelfTile {
1799
1800
  constructor(call, slot) {
1800
1801
  this.call = call;
1801
1802
  this.canvas = el('canvas');
1802
- this.badge = el('div', { class: 'dxr-call-badge' });
1803
- this.plate = el('div', { class: 'dxr-call-state dxr-call-hidden' });
1803
+ this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
1804
+ this.plate = el('div', { class: 'dxr-call-state dxr-call-hidden', part: 'plate' });
1804
1805
  this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.badge, this.plate]);
1805
1806
  this.stage.style.aspectRatio = String(call.o.tileAspect);
1806
- this.el = el('div', { class: 'dxr-call-tile dxr-call-tile--self' }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
1807
+ this.el = el('div', { class: 'dxr-call-tile dxr-call-tile--self', part: 'tile' }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
1807
1808
  slot.appendChild(this.el);
1808
- this.route = null;
1809
- this.handle = null;
1810
- this.mirror = null;
1809
+ this.view = null; // the CameraView over the current camera
1810
+ this.cam = null;
1811
+ }
1812
+
1813
+ /** 'woven-sbs' | 'flat-left' | 'flat' | null (no camera). */
1814
+ get route() {
1815
+ return this.view ? this.view.route : null;
1811
1816
  }
1812
1817
 
1813
1818
  update() {
1814
1819
  const l = this.call.local;
1820
+ const t = this.call.t;
1815
1821
  const noCam = !l || !l.videoTrack;
1816
- this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ? PLATE_TEXT.cameraBusy : this.call.camStatus === 'pending' ? '' : 'No camera') : this.call.camOff ? 'Camera off' : '';
1822
+ this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ? t('cameraBusy') : this.call.camStatus === 'pending' ? '' : t('noCamera')) : this.call.camOff ? t('cameraOffPlate') : '';
1817
1823
  show(this.plate, noCam || this.call.camOff);
1818
1824
  this.reroute(false);
1819
1825
  }
1820
1826
 
1821
1827
  reroute(force) {
1822
1828
  const l = this.call.local;
1823
- // Local SBS on a woven wall → woven mirror-and-swap preview; anything else is flat.
1824
- const route = l && l.format === 'sbs' && this.call.woven && l.videoTrack ? 'woven-sbs' : l && l.format === 'sbs' ? 'flat-left' : 'flat';
1825
- if (!force && route === this.route) return this._badge();
1826
- this.unregister();
1827
- this.route = route;
1828
- if (route === 'woven-sbs') {
1829
- if (!this.mirror) this.mirror = document.createElement('canvas');
1830
- this._paintMirror();
1831
- this.handle = this.call.wall.addImage(this.canvas, this.mirror);
1829
+ const cam = l && l.videoTrack ? l.cam : null;
1830
+ const wall = this.call.woven ? this.call.wall : { supported: false };
1831
+ if (!cam || cam.state !== 'live') {
1832
+ this.detach();
1833
+ return this._badge();
1834
+ }
1835
+ if (this.view && this.cam === cam) this.view._reroute(force, wall);
1836
+ else {
1837
+ this.detach();
1838
+ this.cam = cam;
1839
+ this.view = addCameraView(wall, this.canvas, cam, { mirror: true, autoConverge: false, aspect: this.call.o.tileAspect });
1832
1840
  }
1833
1841
  this._badge();
1834
1842
  }
1835
1843
 
1836
1844
  _badge() {
1837
- this.badge.replaceChildren(el('b', { text: `You · ${this.route === 'woven-sbs' ? '3D' : '2D'}` }));
1845
+ const t = this.call.t;
1846
+ this.badge.replaceChildren(el('b', { text: `${t('you')} · ${this.route === 'woven-sbs' ? t('badge3D') : t('badge2D')}` }));
1838
1847
  }
1839
1848
 
1840
1849
  onWallLost() {
1841
- this.handle = null;
1842
- this.reroute(true);
1850
+ this.view?._onWallLost();
1851
+ this._badge();
1843
1852
  }
1844
1853
 
1845
- unregister() {
1846
- if (this.handle) {
1847
- try {
1848
- this.handle.remove();
1849
- } catch {
1850
- /* ignore */
1851
- }
1852
- this.handle = null;
1853
- }
1854
+ /** Drop the view (leave(), a camera change). The canvas keeps its last frame; a rejoin re-attaches. */
1855
+ detach() {
1856
+ if (this.view) this.view.remove();
1857
+ this.view = null;
1858
+ this.cam = null;
1854
1859
  }
1855
1860
 
1856
1861
  setSpeaking(on) {
1857
1862
  this.el.classList.toggle('dxr-call-tile--speaking', on);
1858
1863
  }
1864
+ }
1859
1865
 
1860
- paint() {
1861
- const l = this.call.local;
1862
- if (!l || !l.video || this.call.state === 'left') return;
1863
- if (this.route === 'woven-sbs') this._paintMirror();
1864
- else paintFlat(this.canvas, l.video, { eyeHalf: this.route === 'flat-left' ? 0 : null, mirror: true });
1866
+ // ── 1.29 exports kept for ONE release (RFC 0003 §2 "Mechanics") ───────────────────────────
1867
+ //
1868
+ // Every helper below is INTERNAL: it exists so the module can be unit-tested, and it stays
1869
+ // importable by file path (`js/call/*.js`). It is no longer part of this entry's public surface
1870
+ // (call.d.ts marks each `@deprecated`; test/api-snapshot pins the list) and 1.31 removes it from
1871
+ // here. Until then a function or class warns once on its first call, an object on its first
1872
+ // property read; a primitive constant cannot warn (ESM bindings have no getter) and is simply
1873
+ // typed `@deprecated`. `peerjsCloud` left in 1.29 outright (Decision 12).
1874
+
1875
+ const REMOVED_IN = '1.31';
1876
+ const deprecatedWarned = new Set();
1877
+ function deprecate(name, from, value) {
1878
+ const warn = () => {
1879
+ if (deprecatedWarned.has(name)) return;
1880
+ deprecatedWarned.add(name);
1881
+ console.warn(`${TAG} \`${name}\` is internal and leaves '@displayxr/inline3d/call' in ${REMOVED_IN} — import it from ${from} if you must, or stop depending on it`);
1882
+ };
1883
+ if (typeof value === 'function') {
1884
+ return new Proxy(value, {
1885
+ apply(target, self, args) {
1886
+ warn();
1887
+ return Reflect.apply(target, self, args);
1888
+ },
1889
+ construct(target, args) {
1890
+ warn();
1891
+ return Reflect.construct(target, args);
1892
+ },
1893
+ });
1865
1894
  }
1866
-
1867
- /**
1868
- * The MIRRORED stereo preview: each half mirrored AND the halves swapped (wire.js
1869
- * mirrorSwapOps). Mirroring each half in place would invert every disparity. The stream that is
1870
- * SENT is never touched — only this canvas.
1871
- */
1872
- _paintMirror() {
1873
- const v = this.call.local.video;
1874
- const W = v.videoWidth;
1875
- const H = v.videoHeight;
1876
- if (!W || !H || (v.readyState || 0) < 2) return;
1877
- const A = this.call.o.tileAspect;
1878
- const eyeW = W / 2;
1879
- const { w: outW, h: outH } = eyeOutputSize(eyeW, H, A);
1880
- const c = this.mirror;
1881
- if (c.width !== 2 * outW || c.height !== outH) {
1882
- c.width = 2 * outW;
1883
- c.height = outH;
1884
- }
1885
- const g = c.getContext('2d');
1886
- const r = eyeCropRect(eyeW, H, A, 0, 0);
1887
- for (const op of mirrorSwapOps(W, H)) {
1888
- const dx = op.dx === 0 ? 0 : outW;
1889
- g.save();
1890
- g.translate(dx + outW, 0);
1891
- g.scale(-1, 1);
1892
- g.drawImage(v, op.sx + r.sx, r.sy, r.sw, r.sh, 0, 0, outW, outH);
1893
- g.restore();
1894
- }
1895
+ if (value && typeof value === 'object') {
1896
+ return new Proxy(value, {
1897
+ get(target, prop, receiver) {
1898
+ if (typeof prop === 'string') warn();
1899
+ return Reflect.get(target, prop, receiver);
1900
+ },
1901
+ });
1895
1902
  }
1903
+ return value;
1896
1904
  }
1897
1905
 
1906
+ /** @deprecated internal (js/call/options.js) — removed from this entry in 1.31 */
1907
+ export const normalizeCallOptions = deprecate('normalizeCallOptions', 'js/call/options.js', normalizeOptions);
1908
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1909
+ export const newRoomId = deprecate('newRoomId', 'js/call/wire.js', wire.newRoomId);
1910
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1911
+ export const isValidRoomId = deprecate('isValidRoomId', 'js/call/wire.js', wire.isValidRoomId);
1912
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `handle.inviteLink()` is the API */
1913
+ export const buildInviteLink = deprecate('buildInviteLink', 'js/call/wire.js', wire.buildInviteLink);
1914
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; the wire is documented in docs/call-wire.md */
1915
+ export const normalizeHello = deprecate('normalizeHello', 'js/call/wire.js', wire.normalizeHello);
1916
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1917
+ export const makeHello = deprecate('makeHello', 'js/call/wire.js', wire.makeHello);
1918
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `CallPeer.display` is the API */
1919
+ export const routeFor = deprecate('routeFor', 'js/call/wire.js', wire.routeFor);
1920
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1921
+ export const badgeFor = deprecate('badgeFor', 'js/call/wire.js', wire.badgeFor);
1922
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1923
+ export const createLiveGate = deprecate('createLiveGate', 'js/call/wire.js', wire.createLiveGate);
1924
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1925
+ export const convergenceShiftPx = deprecate('convergenceShiftPx', 'js/camera/converge.js', wire.convergenceShiftPx);
1926
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1927
+ export const lowPass = deprecate('lowPass', 'js/camera/converge.js', wire.lowPass);
1928
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1929
+ export const clampShift = deprecate('clampShift', 'js/camera/converge.js', wire.clampShift);
1930
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
1931
+ export const eyeCropRect = deprecate('eyeCropRect', 'js/camera/geometry.js', wire.eyeCropRect);
1932
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31; `addCameraView` of `/camera` mirrors a self view */
1933
+ export const mirrorSwapOps = deprecate('mirrorSwapOps', 'js/camera/geometry.js', wire.mirrorSwapOps);
1934
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
1935
+ export const mirrorSwapPixels = deprecate('mirrorSwapPixels', 'js/camera/geometry.js', wire.mirrorSwapPixels);
1936
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1937
+ export const maxBitrateKbps = deprecate('maxBitrateKbps', 'js/call/wire.js', wire.maxBitrateKbps);
1938
+ /** @deprecated internal (js/camera/disparity.js) — removed from this entry in 1.31; `autoConverge` of `/camera` and `/call` is the behaviour */
1939
+ export const measureFocusDisparity = deprecate('measureFocusDisparity', 'js/camera/disparity.js', disparity.measureFocusDisparity);
1940
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1941
+ export const preferVideoCodecs = deprecate('preferVideoCodecs', 'js/call/sdp.js', sdp.preferVideoCodecs);
1942
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1943
+ export const sortCodecCapabilities = deprecate('sortCodecCapabilities', 'js/call/sdp.js', sdp.sortCodecCapabilities);
1944
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1945
+ export const VIDEO_CODEC_ORDER = deprecate('VIDEO_CODEC_ORDER', 'js/call/sdp.js', sdp.VIDEO_CODEC_ORDER);
1946
+ /** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31; an SFU adapter will get a designed Transport seam */
1947
+ export const MeshTransport = deprecate('MeshTransport', 'js/call/transport.js', MeshTransportImpl);
1948
+ /** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31 */
1949
+ export const clampMaxPeers = deprecate('clampMaxPeers', 'js/call/transport.js', clampMaxPeersImpl);
1950
+ /** @deprecated internal (js/call/qr.js) — removed from this entry in 1.31 */
1951
+ export const qrEncode = deprecate('qrEncode', 'js/call/qr.js', qrMod.qrEncode);
1952
+ /** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; documented in signaling/README.md */
1953
+ export const roomKey = deprecate('roomKey', 'js/call/signaling.js', signalingMod.roomKey);
1954
+ /** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1955
+ export const SIGNAL_PROTOCOL = signalingMod.SIGNAL_PROTOCOL;
1956
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1957
+ export const WIRE_VERSION = wire.WIRE_VERSION;
1958
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1959
+ export const CALL_SDK = wire.CALL_SDK;
1960
+ /** @deprecated replaced by `theme.strings` (js/call/ui.js CALL_STRINGS) — removed from this entry in 1.31 */
1961
+ export const PLATE_TEXT = deprecate('PLATE_TEXT', 'js/call/ui.js (CALL_STRINGS)', Object.freeze({ unreachable: CALL_STRINGS.unreachable, cameraBusy: CALL_STRINGS.cameraBusy }));
1962
+ /** @deprecated folded into `theme.accent` (which takes the names) — removed from this entry in 1.31 */
1963
+ export const CALL_ACCENTS = deprecate('CALL_ACCENTS', 'js/call/ui.js', CALL_ACCENTS_IMPL);
1964
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1965
+ export const normalizeMono3D = deprecate('normalizeMono3D', 'js/call/lift.js', liftMod.normalizeMono3D);
1966
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1967
+ export const resolveLift = deprecate('resolveLift', 'js/call/lift.js', liftMod.resolveLift);
1968
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1969
+ export const createLiftPool = deprecate('createLiftPool', 'js/call/lift.js', liftMod.createLiftPool);
1970
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1971
+ export const createFrameWatch = deprecate('createFrameWatch', 'js/call/lift.js', liftMod.createFrameWatch);
1972
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1973
+ export const liftConvergenceFor = deprecate('liftConvergenceFor', 'js/call/lift.js', liftMod.liftConvergenceFor);
1974
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1975
+ export const liftPriorityFor = deprecate('liftPriorityFor', 'js/call/lift.js', liftMod.liftPriorityFor);
1976
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1977
+ export const setLiftPriority = deprecate('setLiftPriority', 'js/call/lift.js', liftMod.setLiftPriority);
1978
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1979
+ export const defaultLiftSpecifier = deprecate('defaultLiftSpecifier', 'js/call/lift.js', liftMod.defaultLiftSpecifier);
1980
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1981
+ export const LIFT_PRIORITY = deprecate('LIFT_PRIORITY', 'js/call/lift.js', liftMod.LIFT_PRIORITY);
1982
+
1898
1983
  // `<dxr-call>` — the one-line path as markup — is registered by importing this entry (RFC 0003 §1;
1899
1984
  // the ONE side effect of this module, declared in package.json `sideEffects`). A no-op where there
1900
1985
  // is no `customElements` registry (Node, workers) or the name is already defined.