@displayxr/inline3d 1.29.0 → 1.31.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,
@@ -445,8 +362,9 @@ class Call {
445
362
  log: (tag, obj) => this.log(tag, obj),
446
363
  });
447
364
  this.transport = t;
365
+ let session = null;
448
366
  try {
449
- await t.start({
367
+ session = await t.start({
450
368
  room: this.room,
451
369
  localStream: this.local.stream,
452
370
  sendFormat: this.local.format,
@@ -480,7 +398,7 @@ class Call {
480
398
  // The grid is sized so the whole call block fits ONE viewport, but only if it starts at the
481
399
  // top: a tile below the fold is withheld from the weave (browser#167), so its layer goes
482
400
  // live late. Bring the block into view once, on join.
483
- if (this.o.ui && this.o.scrollIntoView) {
401
+ if (this.o.chrome) {
484
402
  try {
485
403
  this.container.scrollIntoView?.({ block: 'start', behavior: 'instant' });
486
404
  this._fitGrid();
@@ -489,13 +407,42 @@ class Call {
489
407
  }
490
408
  }
491
409
  this._startStats();
492
- this._pagehide = () => this.leave();
410
+ this._pagehide = () => this.leave('pagehide');
493
411
  globalThis.addEventListener?.('pagehide', this._pagehide);
494
412
  this.log('joined', { id: this.id, peers: this.tiles.size });
495
413
  this.emit('joined', { room: this.room, id: this.id });
414
+ // The TURN decision is reported on the next task, after join() / mountCall() have resolved:
415
+ // a page (or <dxr-call>) subscribes once it has the handle, and an event raised mid-join
416
+ // would reach nobody.
417
+ setTimeout(() => {
418
+ if (this.transport === t) this._reportTurn(session);
419
+ }, 0);
496
420
  return this.handle;
497
421
  }
498
422
 
423
+ /**
424
+ * The signalling server's TURN decision for this session (hosted service, RFC 0003 §5c), once
425
+ * per join. `turn-cap` (an `error`): the service's monthly relay budget is spent, nobody gets
426
+ * TURN — direct connections still work. `turn-shed` (a `warning`): only THIS session has no
427
+ * TURN (an anonymous session near the budget, or too many credential requests from this
428
+ * address). A server with no TURN at all (self-hosted) says nothing here: that is a setup
429
+ * choice, documented in docs/call.md.
430
+ */
431
+ _reportTurn(session) {
432
+ const turn = session && session.turn;
433
+ if (!turn || typeof turn !== 'object') return;
434
+ if (turn.status === 'off' && turn.reason === 'cap') {
435
+ this.error('turn-cap', 'relay (TURN) capacity for this month is used up; direct connections still work, but a participant behind a strict NAT may be unreachable', null);
436
+ } else if (turn.reason === 'budget' || turn.reason === 'rate') {
437
+ this.warning(
438
+ 'turn-shed',
439
+ turn.reason === 'rate'
440
+ ? 'no relay (TURN) for this session: too many credential requests from this address this hour'
441
+ : 'no relay (TURN) for this anonymous session: the hosted service is near its monthly relay budget (a publishable key keeps TURN)'
442
+ );
443
+ }
444
+ }
445
+
499
446
  _onPeer(id) {
500
447
  if (this.tiles.has(id)) return;
501
448
  const tile = new Tile(this, id);
@@ -546,7 +493,7 @@ class Call {
546
493
  tile.onChannelOpen();
547
494
  return;
548
495
  }
549
- const h = normalizeHello(msg);
496
+ const h = wire.normalizeHello(msg);
550
497
  if (h) return tile.setHello(h);
551
498
  const hint = normalizeHint(msg);
552
499
  if (hint) return tile.setHint(hint);
@@ -558,8 +505,8 @@ class Call {
558
505
 
559
506
  inviteLink() {
560
507
  if (!this.room) return null;
561
- const base = this.o.inviteBase || (globalThis.location ? location.href : '');
562
- return buildInviteLink(base, this.room);
508
+ const base = this.o.invite.base || (globalThis.location ? location.href : '');
509
+ return wire.buildInviteLink(base, this.room);
563
510
  }
564
511
 
565
512
  mute(on) {
@@ -571,7 +518,7 @@ class Call {
571
518
  return this.muted;
572
519
  }
573
520
 
574
- cameraOff(on) {
521
+ setCameraOff(on) {
575
522
  this.camOff = on === undefined ? !this.camOff : !!on;
576
523
  if (this.local?.videoTrack) this.local.videoTrack.enabled = !this.camOff;
577
524
  this.transport?.broadcast(this.stateMsg());
@@ -615,7 +562,7 @@ class Call {
615
562
  return true;
616
563
  }
617
564
 
618
- leave() {
565
+ leave(reason = 'left') {
619
566
  if (this.state === 'left') return;
620
567
  globalThis.removeEventListener?.('pagehide', this._pagehide);
621
568
  this.transport?.stop();
@@ -626,19 +573,20 @@ class Call {
626
573
  clearInterval(this._statsTimer);
627
574
  this._setSpeaker(null);
628
575
  if (this.local) {
576
+ this._camEndedOff?.();
577
+ this._camEndedOff = null;
629
578
  this.local.stream.getTracks().forEach((t) => t.stop());
630
- if (this.local.owned && this.local.camStream) this.local.camStream.getTracks().forEach((t) => t.stop());
579
+ // A camera THIS module opened is closed; a page's StereoCamera / MediaStream is left running.
580
+ if (this.local.owned && this.local.cam) this.local.cam.close();
581
+ else if (this.local.cam) this.self?.detach();
631
582
  this.local.videoTrack = null;
632
583
  this.local.audioTrack = null;
633
584
  }
634
- if (this.self) {
635
- this.self.unregister();
636
- this.self.route = null; // a rejoin re-registers it
637
- }
585
+ this.self?.detach(); // a rejoin re-attaches it
638
586
  this._unhookWall();
639
587
  this._setState('left');
640
588
  this._layout();
641
- this.emit('left', { room: this.room });
589
+ this.emit('left', { room: this.room, reason });
642
590
  }
643
591
 
644
592
  // ── the wall: session loss and recovery ───────────────────────────────────────────────
@@ -672,7 +620,7 @@ class Call {
672
620
  this.weaveLive = true; // nothing to wait on
673
621
  return;
674
622
  }
675
- const gate = createLiveGate();
623
+ const gate = wire.createLiveGate();
676
624
  const t0 = performance.now();
677
625
  const onFrame = (_t, f) => {
678
626
  if (w !== this.wall || !this.wallLive || this.state === 'left') return;
@@ -727,7 +675,7 @@ class Call {
727
675
  this.log('session-ended', { byPage: hook.pageClosed });
728
676
  for (const t of this.tiles.values()) t.onWallLost();
729
677
  this.self?.onWallLost();
730
- if (hook.pageClosed || !this.o.recoverSession || this.state === 'left') {
678
+ if (hook.pageClosed || this.state === 'left') {
731
679
  this._unhookWall();
732
680
  return;
733
681
  }
@@ -744,7 +692,7 @@ class Call {
744
692
  try {
745
693
  // The shared manager: if the page has already re-opened its own wall, join it rather than
746
694
  // opening a second session (rule 1); else this creates the document's new one.
747
- w = await sharedInline3D(this.o.wallOptions);
695
+ w = await sharedInline3D();
748
696
  } catch {
749
697
  w = null;
750
698
  }
@@ -755,7 +703,6 @@ class Call {
755
703
  this.log('session-recovered', { attempt });
756
704
  for (const t of this.tiles.values()) t.reroute(true);
757
705
  this.self?.reroute(true);
758
- this.emit('session', { wall: w });
759
706
  }, backoffMs(attempt, { baseMs: 1000, maxMs: 15000 }));
760
707
  }
761
708
 
@@ -765,7 +712,6 @@ class Call {
765
712
  let last = 0;
766
713
  const tick = (now) => {
767
714
  this._raf = requestAnimationFrame(tick);
768
- this.self?.paint();
769
715
  for (const t of this.tiles.values()) t.paint();
770
716
  if (last && this.liftPool && this.liftPool.anyWeb) this._watchFrames(now - last);
771
717
  last = now;
@@ -797,10 +743,10 @@ class Call {
797
743
  const lo = this.o.liftOptions;
798
744
  // liftCapabilities probes the same model source the lifts will use.
799
745
  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) => {
746
+ this._liftP = liftMod.resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
801
747
  this.liftApi = r;
802
748
  if (r.lift) {
803
- this.liftPool = createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
749
+ this.liftPool = liftMod.createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
804
750
  this.liftPool.setDepth(this.depth);
805
751
  this.liftPool.setSpeaker(this.speakerId);
806
752
  if (!r.caps && r.capabilities) {
@@ -865,14 +811,15 @@ class Call {
865
811
  /** What the lobby says a mono participant will look like here. */
866
812
  _liftHint() {
867
813
  if (!this.wallLive) return '';
868
- if (!this.mono3DOn) return 'Mono cameras: 2D (2D→3D is off).';
814
+ const t = this.t;
815
+ if (!this.mono3DOn) return t('liftOff');
869
816
  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).';
817
+ if (!r) return t('liftChecking');
818
+ if (!r.lift) return this.liftMissing ? t('liftMissing') : t('liftNoProvider');
872
819
  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).';
820
+ if (c && c.native) return t('liftNative', { provider: c.provider ? `, ${c.provider}` : '' });
821
+ if (c && c.webFallback && c.webFallback.webgpu) return t('liftWeb');
822
+ return this.liftProven ? t('liftProven') : t('liftUnproven');
876
823
  }
877
824
 
878
825
  _mono3DInfo() {
@@ -989,7 +936,7 @@ class Call {
989
936
  */
990
937
  _fitGrid() {
991
938
  const ui = this.ui;
992
- if (!ui || !this.o.ui || typeof globalThis.innerHeight !== 'number') return;
939
+ if (!ui || !this.o.chrome || typeof globalThis.innerHeight !== 'number') return;
993
940
  const host = this.container.getBoundingClientRect?.();
994
941
  const grid = ui.grid.getBoundingClientRect?.();
995
942
  if (!host || !grid || !grid.height) return;
@@ -1010,55 +957,59 @@ class Call {
1010
957
 
1011
958
  _buildDom() {
1012
959
  const c = this.container;
960
+ const t = this.t;
1013
961
  injectCallStyle(c.ownerDocument || document);
1014
962
  c.classList.add('dxr-call-host');
1015
- if (this.o.accent) c.style.setProperty('--dxr-accent', this.o.accent);
963
+ // theme → CSS custom properties on the host (chrome only: no variable can reach a tile).
964
+ for (const [prop, v] of Object.entries(this.o.cssVars)) c.style.setProperty(prop, v);
1016
965
  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' }),
966
+ ui.banner = el('div', { class: 'dxr-call-banner dxr-call-hidden', part: 'banner' }, [
967
+ el('span', { text: t('banner2D') }),
968
+ el('a', { href: this.o.landing.browserUrl, target: '_blank', rel: 'noopener', text: t('bannerLink') }),
1020
969
  ]);
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' });
970
+ ui.grid = el('div', { class: 'dxr-call-grid', part: 'grid', 'data-layout': this.o.layout, 'data-n': '0' });
971
+ ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'lobby' });
972
+ ui.selfSlot = el('div', { class: 'dxr-call-self', part: 'self' });
973
+ ui.bar = el('div', { class: 'dxr-call-bar', part: 'bar' });
1025
974
  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) {
975
+ ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'invite' });
976
+ if (!this.o.chrome) {
1028
977
  show(ui.bar, false);
1029
978
  }
1030
979
  c.append(ui.banner, ui.panel, ui.grid, ui.invite, ui.foot);
1031
980
  if (this.o.selfView) this.self = new SelfTile(this, ui.selfSlot);
1032
981
  else show(ui.selfSlot, false);
1033
- if (this.o.ui) this._buildBar();
982
+ if (this.o.chrome) this._buildBar();
1034
983
  }
1035
984
 
1036
985
  _buildBar() {
1037
986
  const ui = this.ui;
987
+ const t = this.t;
1038
988
  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' });
989
+ ui.mic = ib(t('mute'), ICONS.mic, () => this.mute());
990
+ ui.cam = ib(t('cameraOff'), ICONS.cam, () => (this.local?.videoTrack ? this.setCameraOff() : this.retryCamera().catch(() => {})));
991
+ ui.depth = el('input', { type: 'range', min: '-1', max: '1', step: '0.05', value: '0', 'aria-label': t('depth') });
1042
992
  ui.depth.addEventListener('input', () => this.setDepth(+ui.depth.value));
1043
993
  ui.depth.addEventListener('dblclick', () => this.setDepth(0));
1044
- ui.inviteBtn = ib('Invite', ICONS.link, () => {
994
+ ui.inviteBtn = ib(t('invite'), ICONS.link, () => {
1045
995
  this._showInvite = !this._showInvite;
1046
996
  this._layout();
1047
997
  });
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);
998
+ ui.leave = ib(t('leave'), ICONS.leave, () => this.leave(), 'dxr-call-ib--leave');
999
+ ui.bar.append(ui.mic, ui.cam, el('label', { class: 'dxr-call-depth' }, [t('depth'), ui.depth]), ui.inviteBtn, ui.leave);
1050
1000
  }
1051
1001
 
1052
1002
  _refreshBar() {
1053
1003
  const ui = this.ui;
1054
1004
  if (!ui || !ui.mic) return;
1005
+ const t = this.t;
1055
1006
  ui.mic.innerHTML = this.muted ? ICONS.micOff : ICONS.mic;
1056
1007
  ui.mic.setAttribute('aria-pressed', String(this.muted));
1057
- ui.mic.title = this.muted ? 'Unmute microphone' : 'Mute microphone';
1008
+ ui.mic.title = this.muted ? t('unmute') : t('mute');
1058
1009
  const noCam = !this.local?.videoTrack;
1059
1010
  ui.cam.innerHTML = this.camOff || noCam ? ICONS.camOff : ICONS.cam;
1060
1011
  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';
1012
+ ui.cam.title = noCam ? (this.camStatus === 'busy' ? t('cameraBusyRetry', { cameraBusy: t('cameraBusy') }) : t('cameraRetry')) : this.camOff ? t('cameraOn') : t('cameraOff');
1062
1013
  }
1063
1014
 
1064
1015
  _refreshSelf() {
@@ -1073,20 +1024,21 @@ class Call {
1073
1024
  target.replaceChildren();
1074
1025
  const link = this.inviteLink();
1075
1026
  if (!link) return;
1076
- const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': 'Invite link' });
1027
+ const t = this.t;
1028
+ const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': t('inviteLink') });
1077
1029
  input.addEventListener('focus', () => input.select());
1078
- const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: 'Copy link' });
1030
+ const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: t('copyLink') });
1079
1031
  copy.addEventListener('click', async () => {
1080
1032
  try {
1081
1033
  await navigator.clipboard.writeText(link);
1082
- copy.textContent = 'Copied';
1034
+ copy.textContent = t('copied');
1083
1035
  } catch {
1084
1036
  input.select();
1085
- copy.textContent = 'Press Ctrl/Cmd+C';
1037
+ copy.textContent = t('pressCopy');
1086
1038
  }
1087
- setTimeout(() => (copy.textContent = 'Copy link'), 1800);
1039
+ setTimeout(() => (copy.textContent = t('copyLink')), 1800);
1088
1040
  });
1089
- const qr = el('canvas', { 'aria-label': 'QR code of the invite link', role: 'img' });
1041
+ const qr = el('canvas', { 'aria-label': t('qrLabel'), role: 'img' });
1090
1042
  try {
1091
1043
  drawQr(qr, link, { px: 4 });
1092
1044
  } catch {
@@ -1103,16 +1055,17 @@ class Call {
1103
1055
  const ui = this.ui;
1104
1056
  if (!ui) return;
1105
1057
  const c = this.container;
1106
- const lobby = this.o.ui && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
1058
+ const t = this.t;
1059
+ const lobby = this.o.chrome && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
1107
1060
  c.classList.toggle('dxr-call-host--lobby', lobby);
1108
- show(ui.banner, this.o.ui && !this.woven);
1061
+ show(ui.banner, this.o.chrome && !this.woven);
1109
1062
  const n = this.tiles.size;
1110
1063
  ui.grid.dataset.n = String(Math.min(n, 4));
1111
1064
  show(ui.grid, n > 0 && this.state !== 'left');
1112
- // Speaker layout: the active speaker (or the first tile) spans the row.
1065
+ // Speaker layout: the active speaker (or the first tile) spans the row. layout:'none' positions nothing.
1113
1066
  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;
1067
+ for (const [id, tile] of this.tiles) tile.el.classList.toggle('dxr-call-tile--main', id === main);
1068
+ if (!this.o.chrome) return;
1116
1069
  this._refreshBar();
1117
1070
  queueMicrotask(() => this._fitGrid());
1118
1071
  const p = ui.panel;
@@ -1121,39 +1074,67 @@ class Call {
1121
1074
  p.replaceChildren();
1122
1075
  const l = this.local;
1123
1076
  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})`;
1077
+ const title = this.state === 'full' ? t('lobbyFullTitle') : this.state === 'left' ? t('lobbyLeftTitle') : joining ? t('lobbyJoinTitle') : t('lobbyStartTitle');
1078
+ const dims = { width: l?.width || 0, height: l?.height || 0 };
1079
+ const kind = this.camStatus === 'busy' ? t('kindBusy') : !l || !l.videoTrack ? t('kindNone') : l.format === 'sbs' ? t('kindSbs', dims) : t('kindMono', dims);
1127
1080
  const text =
1128
1081
  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()}` : ''}`;
1082
+ ? t('lobbyFullText', { maxPeers: this.o.maxPeers })
1083
+ : `${t('lobbyText', { camera: l?.label || t('cameraDefault'), kind })}${this._liftHint() ? ` ${this._liftHint()}` : ''}`;
1131
1084
  const go = el('button', {
1132
1085
  class: 'dxr-call-btn dxr-call-btn--primary',
1133
1086
  type: 'button',
1134
- text: this.state === 'left' ? 'Rejoin' : joining ? 'Join call' : 'Start 3D call',
1087
+ text: this.state === 'left' ? t('rejoin') : joining ? t('join') : t('start'),
1135
1088
  });
1136
1089
  go.addEventListener('click', () => this.join().catch(() => {}));
1137
- const sel = el('select', { class: 'dxr-call-select', 'aria-label': 'Camera' });
1090
+ const sel = el('select', { class: 'dxr-call-select', 'aria-label': t('cameraSelect') });
1138
1091
  this._fillCameras(sel);
1139
1092
  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;
1093
+ const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: t('retryCamera') }) : null;
1141
1094
  retry?.addEventListener('click', () => this.retryCamera().catch(() => {}));
1142
1095
  p.append(el('h3', { text: title }), el('p', { text }), el('div', { class: 'dxr-call-row' }, [this.state === 'full' ? null : go, retry, sel]));
1143
1096
  } else if (this.state === 'in-call' && n === 0) {
1144
1097
  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.');
1098
+ this._renderInvite(p, t('waitingTitle'), t('waitingText'));
1146
1099
  } else if (this.state === 'joining') {
1147
1100
  show(p, true);
1148
- p.replaceChildren(el('p', { text: 'Joining…' }));
1101
+ p.replaceChildren(el('p', { text: t('joining') }));
1149
1102
  } else show(p, false);
1150
1103
  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.');
1104
+ if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, t('inviteTitle'), t('inviteText'));
1152
1105
  show(ui.invite, inviteOpen);
1153
1106
  if (!inviteOpen) ui.invite.replaceChildren();
1154
1107
  ui.inviteBtn?.setAttribute('aria-pressed', String(!!inviteOpen));
1155
1108
  }
1156
1109
 
1110
+ /**
1111
+ * Diagnostics (explicitly UNSTABLE — RFC 0003 §2): everything that used to sit on the public
1112
+ * handle and `CallPeer` as debug data. Shape free to change; read it in devtools, never in code.
1113
+ */
1114
+ diagnostics() {
1115
+ const l = this.local;
1116
+ return {
1117
+ sdk: wire.CALL_SDK,
1118
+ wire: wire.WIRE_VERSION,
1119
+ state: this.state,
1120
+ wall: this.wall,
1121
+ wallLive: this.wallLive,
1122
+ weaveLive: this.weaveLive,
1123
+ woven: this.woven,
1124
+ local: l
1125
+ ? { 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 }
1126
+ : null,
1127
+ hello: this.hello(),
1128
+ transport: this.transport,
1129
+ peers: [...this.tiles.values()].map((tile) => tile.diag()),
1130
+ mono3D: { ...this._mono3DInfo(), lift: this.liftApi, pool: this.liftPool ? this.liftPool.list() : [] },
1131
+ speaker: this.speakerId,
1132
+ depth: this.depth,
1133
+ options: this.o,
1134
+ kill: (id) => this.transport?._debugKill(id),
1135
+ };
1136
+ }
1137
+
1157
1138
  async _fillCameras(sel) {
1158
1139
  let cams = [];
1159
1140
  try {
@@ -1169,7 +1150,8 @@ class Call {
1169
1150
 
1170
1151
  _makeHandle() {
1171
1152
  const call = this;
1172
- return {
1153
+ const renamedEvents = { format: 'display', session: null };
1154
+ const handle = {
1173
1155
  get room() {
1174
1156
  return call.room;
1175
1157
  },
@@ -1179,15 +1161,16 @@ class Call {
1179
1161
  get state() {
1180
1162
  return call.state;
1181
1163
  },
1182
- get wall() {
1183
- return call.wall;
1184
- },
1185
- get format() {
1164
+ /** What YOU send: 'sbs' | 'mono'; null before a camera opened. */
1165
+ get localFormat() {
1186
1166
  return call.local ? call.local.format : null;
1187
1167
  },
1188
1168
  get muted() {
1189
1169
  return call.muted;
1190
1170
  },
1171
+ get cameraOff() {
1172
+ return call.camOff;
1173
+ },
1191
1174
  get depth() {
1192
1175
  return call.depth;
1193
1176
  },
@@ -1206,18 +1189,25 @@ class Call {
1206
1189
  get peers() {
1207
1190
  return Object.freeze([...call.tiles.values()].map((t) => t.info()));
1208
1191
  },
1209
- join: () => call.join(),
1192
+ join: () => call.join().then(() => undefined),
1210
1193
  inviteLink: () => call.inviteLink(),
1211
1194
  mute: (on) => call.mute(on),
1212
- cameraOff: (on) => call.cameraOff(on),
1213
- setCamera: (idOrStream, o) => call.setCamera(idOrStream, o),
1195
+ setCameraOff: (on) => call.setCameraOff(on),
1196
+ setCamera: (src, o) => call.setCamera(src, o),
1214
1197
  setDepth: (v) => call.setDepth(v),
1215
1198
  retryCamera: () => call.retryCamera(),
1216
- sendHint: (z) => call.sendHint(z),
1217
1199
  setMono3D: (on) => call.setMono3D(on),
1218
- leave: () => call.leave(),
1200
+ leave: () => call.leave('left'),
1201
+ /** A remote participant's tile element (for `layout: 'none'`), or null. */
1202
+ tile: (peerId) => call.tiles.get(peerId)?.el || null,
1203
+ diagnostics: () => call.diagnostics(),
1219
1204
  on(type, cb) {
1220
1205
  if (typeof cb !== 'function') throw new TypeError(`${TAG} on() takes a function`);
1206
+ if (type in renamedEvents && !call._eventWarned?.has(type)) {
1207
+ (call._eventWarned ||= new Set()).add(type);
1208
+ const to = renamedEvents[type];
1209
+ console.warn(`${TAG} the '${type}' event is deprecated (removed in 1.31)${to ? ` — listen for '${to}'` : ' — it is internal now; diagnostics() has the wall'}`);
1210
+ }
1221
1211
  if (!call.listeners.has(type)) call.listeners.set(type, new Set());
1222
1212
  call.listeners.get(type).add(cb);
1223
1213
  return () => call.listeners.get(type)?.delete(cb);
@@ -1225,14 +1215,28 @@ class Call {
1225
1215
  off(type, cb) {
1226
1216
  call.listeners.get(type)?.delete(cb);
1227
1217
  },
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
1218
  };
1219
+ // 1.29 handle members, one release of warnings (RFC 0003 §2): removed in 1.31.
1220
+ const legacy = {
1221
+ wall: ['diagnostics().wall (or the wall you passed / sharedInline3D())', () => call.wall],
1222
+ format: ['localFormat', () => handle.localFormat],
1223
+ sendHint: ['nothing — auto-convergence superseded it', () => (z) => call.sendHint(z)],
1224
+ _debug: ['diagnostics()', () => ({ transport: call.transport, kill: (id) => call.transport?._debugKill(id) })],
1225
+ };
1226
+ for (const [name, [to, get]] of Object.entries(legacy)) {
1227
+ Object.defineProperty(handle, name, {
1228
+ enumerable: false,
1229
+ configurable: true,
1230
+ get() {
1231
+ if (!call._handleWarned?.has(name)) {
1232
+ (call._handleWarned ||= new Set()).add(name);
1233
+ console.warn(`${TAG} handle.${name} is deprecated (removed in 1.31) — use ${to}`);
1234
+ }
1235
+ return get();
1236
+ },
1237
+ });
1238
+ }
1239
+ return handle;
1236
1240
  }
1237
1241
  }
1238
1242
 
@@ -1262,7 +1266,7 @@ function paintFlat(canvas, video, { eyeHalf = null, mirror = false } = {}) {
1262
1266
  const H = video.videoHeight;
1263
1267
  if (!W || !H || (video.readyState || 0) < 2) return;
1264
1268
  const eyeW = eyeHalf === null ? W : W / 2;
1265
- const r = eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
1269
+ const r = wire.eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
1266
1270
  g.save();
1267
1271
  if (mirror) {
1268
1272
  g.translate(canvas.width, 0);
@@ -1319,11 +1323,12 @@ class Tile {
1319
1323
  this.video.addEventListener('resize', () => this._maybeRoute());
1320
1324
  this.canvas = el('canvas');
1321
1325
  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…' });
1326
+ this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
1327
+ this.plate = el('div', { class: 'dxr-call-state', part: 'plate', text: call.t('connecting') });
1324
1328
  this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.cover, this.badge, this.plate]);
1325
1329
  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' })]);
1330
+ // `data-dxr-peer` is the documented hook (layout:'none', handle.tile()); `data-peer` is the 1.29 spelling.
1331
+ 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
1332
  if (!call.o.ui) {
1328
1333
  show(this.badge, false);
1329
1334
  }
@@ -1331,15 +1336,29 @@ class Tile {
1331
1336
  this._renderBadge();
1332
1337
  }
1333
1338
 
1339
+ /** How this side shows the participant: `3D` (woven pair) | `2D→3D` (lifted) | `2D` (flat). */
1340
+ display() {
1341
+ return wire.badgeFor(this.route);
1342
+ }
1343
+
1344
+ /** The public `CallPeer` (RFC 0003 §2): what a page needs to run and observe. Debug data is `diag()`. */
1334
1345
  info() {
1335
1346
  return Object.freeze({
1336
1347
  id: this.id,
1337
1348
  format: this.format || 'mono',
1338
- route: this.route,
1349
+ display: this.display(),
1339
1350
  state: this.conn,
1340
1351
  muted: this.remote.muted,
1341
1352
  cameraOff: this.remote.cameraOff,
1342
1353
  speaking: this.speaking,
1354
+ });
1355
+ }
1356
+
1357
+ /** Everything else (UNSTABLE): `handle.diagnostics().peers[i]`. */
1358
+ diag() {
1359
+ return {
1360
+ ...this.info(),
1361
+ route: this.route,
1343
1362
  rectified: !!(this.hello && this.hello.rectified),
1344
1363
  hello: this.hello,
1345
1364
  quality: this.quality,
@@ -1354,14 +1373,15 @@ class Tile {
1354
1373
  via: this.autoStats.via,
1355
1374
  },
1356
1375
  lift: this.lifted
1357
- ? Object.freeze({
1376
+ ? {
1358
1377
  live: this.liftLive,
1359
1378
  native: !!(this.liftHandle && this.liftHandle.native),
1360
1379
  priority: this.call.liftPool ? this.call.liftPool.priority(this.id) : null,
1361
1380
  state: this.liftHandle ? this.liftHandle.state || null : 'pending',
1362
- })
1381
+ }
1363
1382
  : null,
1364
- });
1383
+ element: this.el,
1384
+ };
1365
1385
  }
1366
1386
 
1367
1387
  setStream(stream) {
@@ -1416,7 +1436,7 @@ class Tile {
1416
1436
 
1417
1437
  setConn(st) {
1418
1438
  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 }));
1439
+ this.call.error('unreachable', `${this.call.t('unreachable')}. Still retrying in the background.`, Object.assign(new Error('unreachable'), { peer: this.id }));
1420
1440
  }
1421
1441
  this.conn = st;
1422
1442
  if (st === 'connected' && this.leaveTimer) {
@@ -1427,26 +1447,28 @@ class Tile {
1427
1447
  }
1428
1448
 
1429
1449
  _renderPlate() {
1450
+ const s = this.call.t;
1430
1451
  const t =
1431
1452
  this.conn === 'left'
1432
- ? 'Left the call'
1453
+ ? s('leftCall')
1433
1454
  : this.conn === 'unreachable'
1434
- ? PLATE_TEXT.unreachable
1455
+ ? s('unreachable')
1435
1456
  : this.conn === 'reconnecting'
1436
- ? 'Reconnecting…'
1437
- : this.conn !== 'connected'
1438
- ? 'Connecting…'
1439
- : this.remote.cameraOff
1440
- ? 'Camera off'
1441
- : null;
1457
+ ? s('reconnecting')
1458
+ : this.conn !== 'connected'
1459
+ ? s('connecting')
1460
+ : this.remote.cameraOff
1461
+ ? s('cameraOffPlate')
1462
+ : null;
1442
1463
  this.plate.textContent = t || '';
1443
1464
  show(this.plate, !!t);
1444
1465
  }
1445
1466
 
1446
1467
  _renderBadge() {
1447
- const b = badgeFor(this.route);
1468
+ const s = this.call.t;
1469
+ const b = this.route === 'woven-sbs' ? s('badge3D') : this.route === 'lifted' ? s('badge2D3D') : s('badge2D');
1448
1470
  // 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;
1471
+ const text = !this.route ? s('badgePending') : this.route === 'lifted' && !this.liftLive ? `${b}${s('badgePending')}` : b;
1450
1472
  this.badge.replaceChildren(el('b', { text }));
1451
1473
  this.badge.dataset.route = this.route || '';
1452
1474
  if (this.remote.muted) this.badge.insertAdjacentHTML('beforeend', ICONS.mutedSmall);
@@ -1464,7 +1486,7 @@ class Tile {
1464
1486
  const r =
1465
1487
  this.forceFlat && this.format === 'sbs'
1466
1488
  ? { route: 'flat-left' }
1467
- : routeFor({
1489
+ : wire.routeFor({
1468
1490
  format: this.format,
1469
1491
  woven: call.woven,
1470
1492
  mono3D: call.mono3DOn ? 'auto' : 'off',
@@ -1484,7 +1506,9 @@ class Tile {
1484
1506
  if (r.route === 'woven-sbs') this._registerWoven();
1485
1507
  else if (r.route === 'lifted') this._registerLifted();
1486
1508
  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 });
1509
+ call.emit('display', { id: this.id, display: this.display() });
1510
+ // 1.29's 'format' event (route / mono3d detail → diagnostics()): one release of compatibility.
1511
+ 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
1512
  this._renderBadge();
1489
1513
  }
1490
1514
 
@@ -1669,7 +1693,7 @@ class Tile {
1669
1693
  const shift = this.conv.step(eyeW);
1670
1694
  const g = c.getContext('2d');
1671
1695
  for (const eye of [0, 1]) {
1672
- const r = eyeCropRect(eyeW, H, A, shift, eye);
1696
+ const r = wire.eyeCropRect(eyeW, H, A, shift, eye);
1673
1697
  g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
1674
1698
  }
1675
1699
  }
@@ -1795,106 +1819,197 @@ class Tile {
1795
1819
 
1796
1820
  // ── the self view ─────────────────────────────────────────────────────────────────────────
1797
1821
 
1822
+ /**
1823
+ * The self view IS an `addCameraView` of `@displayxr/inline3d/camera` (RFC 0003 §4): mirrored
1824
+ * correctly (each half mirrored AND the halves swapped — mirroring each half in place would invert
1825
+ * every disparity), woven on a 3D wall, flat elsewhere. The stream that is SENT is never touched.
1826
+ * This class owns the tile's chrome (badge, plate) and re-attaches the view when the camera or
1827
+ * the wall changes; the view owns the pixels.
1828
+ */
1798
1829
  class SelfTile {
1799
1830
  constructor(call, slot) {
1800
1831
  this.call = call;
1801
1832
  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' });
1833
+ this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
1834
+ this.plate = el('div', { class: 'dxr-call-state dxr-call-hidden', part: 'plate' });
1804
1835
  this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.badge, this.plate]);
1805
1836
  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' })]);
1837
+ this.el = el('div', { class: 'dxr-call-tile dxr-call-tile--self', part: 'tile' }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
1807
1838
  slot.appendChild(this.el);
1808
- this.route = null;
1809
- this.handle = null;
1810
- this.mirror = null;
1839
+ this.view = null; // the CameraView over the current camera
1840
+ this.cam = null;
1841
+ }
1842
+
1843
+ /** 'woven-sbs' | 'flat-left' | 'flat' | null (no camera). */
1844
+ get route() {
1845
+ return this.view ? this.view.route : null;
1811
1846
  }
1812
1847
 
1813
1848
  update() {
1814
1849
  const l = this.call.local;
1850
+ const t = this.call.t;
1815
1851
  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' : '';
1852
+ this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ? t('cameraBusy') : this.call.camStatus === 'pending' ? '' : t('noCamera')) : this.call.camOff ? t('cameraOffPlate') : '';
1817
1853
  show(this.plate, noCam || this.call.camOff);
1818
1854
  this.reroute(false);
1819
1855
  }
1820
1856
 
1821
1857
  reroute(force) {
1822
1858
  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);
1859
+ const cam = l && l.videoTrack ? l.cam : null;
1860
+ const wall = this.call.woven ? this.call.wall : { supported: false };
1861
+ if (!cam || cam.state !== 'live') {
1862
+ this.detach();
1863
+ return this._badge();
1864
+ }
1865
+ if (this.view && this.cam === cam) this.view._reroute(force, wall);
1866
+ else {
1867
+ this.detach();
1868
+ this.cam = cam;
1869
+ this.view = addCameraView(wall, this.canvas, cam, { mirror: true, autoConverge: false, aspect: this.call.o.tileAspect });
1832
1870
  }
1833
1871
  this._badge();
1834
1872
  }
1835
1873
 
1836
1874
  _badge() {
1837
- this.badge.replaceChildren(el('b', { text: `You · ${this.route === 'woven-sbs' ? '3D' : '2D'}` }));
1875
+ const t = this.call.t;
1876
+ this.badge.replaceChildren(el('b', { text: `${t('you')} · ${this.route === 'woven-sbs' ? t('badge3D') : t('badge2D')}` }));
1838
1877
  }
1839
1878
 
1840
1879
  onWallLost() {
1841
- this.handle = null;
1842
- this.reroute(true);
1880
+ this.view?._onWallLost();
1881
+ this._badge();
1843
1882
  }
1844
1883
 
1845
- unregister() {
1846
- if (this.handle) {
1847
- try {
1848
- this.handle.remove();
1849
- } catch {
1850
- /* ignore */
1851
- }
1852
- this.handle = null;
1853
- }
1884
+ /** Drop the view (leave(), a camera change). The canvas keeps its last frame; a rejoin re-attaches. */
1885
+ detach() {
1886
+ if (this.view) this.view.remove();
1887
+ this.view = null;
1888
+ this.cam = null;
1854
1889
  }
1855
1890
 
1856
1891
  setSpeaking(on) {
1857
1892
  this.el.classList.toggle('dxr-call-tile--speaking', on);
1858
1893
  }
1894
+ }
1859
1895
 
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 });
1896
+ // ── 1.29 exports kept for ONE release (RFC 0003 §2 "Mechanics") ───────────────────────────
1897
+ //
1898
+ // Every helper below is INTERNAL: it exists so the module can be unit-tested, and it stays
1899
+ // importable by file path (`js/call/*.js`). It is no longer part of this entry's public surface
1900
+ // (call.d.ts marks each `@deprecated`; test/api-snapshot pins the list) and 1.31 removes it from
1901
+ // here. Until then a function or class warns once on its first call, an object on its first
1902
+ // property read; a primitive constant cannot warn (ESM bindings have no getter) and is simply
1903
+ // typed `@deprecated`. `peerjsCloud` left in 1.29 outright (Decision 12).
1904
+
1905
+ const REMOVED_IN = '1.31';
1906
+ const deprecatedWarned = new Set();
1907
+ function deprecate(name, from, value) {
1908
+ const warn = () => {
1909
+ if (deprecatedWarned.has(name)) return;
1910
+ deprecatedWarned.add(name);
1911
+ 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`);
1912
+ };
1913
+ if (typeof value === 'function') {
1914
+ return new Proxy(value, {
1915
+ apply(target, self, args) {
1916
+ warn();
1917
+ return Reflect.apply(target, self, args);
1918
+ },
1919
+ construct(target, args) {
1920
+ warn();
1921
+ return Reflect.construct(target, args);
1922
+ },
1923
+ });
1865
1924
  }
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
- }
1925
+ if (value && typeof value === 'object') {
1926
+ return new Proxy(value, {
1927
+ get(target, prop, receiver) {
1928
+ if (typeof prop === 'string') warn();
1929
+ return Reflect.get(target, prop, receiver);
1930
+ },
1931
+ });
1895
1932
  }
1933
+ return value;
1896
1934
  }
1897
1935
 
1936
+ /** @deprecated internal (js/call/options.js) — removed from this entry in 1.31 */
1937
+ export const normalizeCallOptions = deprecate('normalizeCallOptions', 'js/call/options.js', normalizeOptions);
1938
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1939
+ export const newRoomId = deprecate('newRoomId', 'js/call/wire.js', wire.newRoomId);
1940
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1941
+ export const isValidRoomId = deprecate('isValidRoomId', 'js/call/wire.js', wire.isValidRoomId);
1942
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `handle.inviteLink()` is the API */
1943
+ export const buildInviteLink = deprecate('buildInviteLink', 'js/call/wire.js', wire.buildInviteLink);
1944
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; the wire is documented in docs/call-wire.md */
1945
+ export const normalizeHello = deprecate('normalizeHello', 'js/call/wire.js', wire.normalizeHello);
1946
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1947
+ export const makeHello = deprecate('makeHello', 'js/call/wire.js', wire.makeHello);
1948
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `CallPeer.display` is the API */
1949
+ export const routeFor = deprecate('routeFor', 'js/call/wire.js', wire.routeFor);
1950
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1951
+ export const badgeFor = deprecate('badgeFor', 'js/call/wire.js', wire.badgeFor);
1952
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1953
+ export const createLiveGate = deprecate('createLiveGate', 'js/call/wire.js', wire.createLiveGate);
1954
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1955
+ export const convergenceShiftPx = deprecate('convergenceShiftPx', 'js/camera/converge.js', wire.convergenceShiftPx);
1956
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1957
+ export const lowPass = deprecate('lowPass', 'js/camera/converge.js', wire.lowPass);
1958
+ /** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
1959
+ export const clampShift = deprecate('clampShift', 'js/camera/converge.js', wire.clampShift);
1960
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
1961
+ export const eyeCropRect = deprecate('eyeCropRect', 'js/camera/geometry.js', wire.eyeCropRect);
1962
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31; `addCameraView` of `/camera` mirrors a self view */
1963
+ export const mirrorSwapOps = deprecate('mirrorSwapOps', 'js/camera/geometry.js', wire.mirrorSwapOps);
1964
+ /** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
1965
+ export const mirrorSwapPixels = deprecate('mirrorSwapPixels', 'js/camera/geometry.js', wire.mirrorSwapPixels);
1966
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
1967
+ export const maxBitrateKbps = deprecate('maxBitrateKbps', 'js/call/wire.js', wire.maxBitrateKbps);
1968
+ /** @deprecated internal (js/camera/disparity.js) — removed from this entry in 1.31; `autoConverge` of `/camera` and `/call` is the behaviour */
1969
+ export const measureFocusDisparity = deprecate('measureFocusDisparity', 'js/camera/disparity.js', disparity.measureFocusDisparity);
1970
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1971
+ export const preferVideoCodecs = deprecate('preferVideoCodecs', 'js/call/sdp.js', sdp.preferVideoCodecs);
1972
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1973
+ export const sortCodecCapabilities = deprecate('sortCodecCapabilities', 'js/call/sdp.js', sdp.sortCodecCapabilities);
1974
+ /** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
1975
+ export const VIDEO_CODEC_ORDER = deprecate('VIDEO_CODEC_ORDER', 'js/call/sdp.js', sdp.VIDEO_CODEC_ORDER);
1976
+ /** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31; an SFU adapter will get a designed Transport seam */
1977
+ export const MeshTransport = deprecate('MeshTransport', 'js/call/transport.js', MeshTransportImpl);
1978
+ /** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31 */
1979
+ export const clampMaxPeers = deprecate('clampMaxPeers', 'js/call/transport.js', clampMaxPeersImpl);
1980
+ /** @deprecated internal (js/call/qr.js) — removed from this entry in 1.31 */
1981
+ export const qrEncode = deprecate('qrEncode', 'js/call/qr.js', qrMod.qrEncode);
1982
+ /** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; documented in signaling/README.md */
1983
+ export const roomKey = deprecate('roomKey', 'js/call/signaling.js', signalingMod.roomKey);
1984
+ /** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1985
+ export const SIGNAL_PROTOCOL = signalingMod.SIGNAL_PROTOCOL;
1986
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1987
+ export const WIRE_VERSION = wire.WIRE_VERSION;
1988
+ /** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
1989
+ export const CALL_SDK = wire.CALL_SDK;
1990
+ /** @deprecated replaced by `theme.strings` (js/call/ui.js CALL_STRINGS) — removed from this entry in 1.31 */
1991
+ export const PLATE_TEXT = deprecate('PLATE_TEXT', 'js/call/ui.js (CALL_STRINGS)', Object.freeze({ unreachable: CALL_STRINGS.unreachable, cameraBusy: CALL_STRINGS.cameraBusy }));
1992
+ /** @deprecated folded into `theme.accent` (which takes the names) — removed from this entry in 1.31 */
1993
+ export const CALL_ACCENTS = deprecate('CALL_ACCENTS', 'js/call/ui.js', CALL_ACCENTS_IMPL);
1994
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1995
+ export const normalizeMono3D = deprecate('normalizeMono3D', 'js/call/lift.js', liftMod.normalizeMono3D);
1996
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1997
+ export const resolveLift = deprecate('resolveLift', 'js/call/lift.js', liftMod.resolveLift);
1998
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
1999
+ export const createLiftPool = deprecate('createLiftPool', 'js/call/lift.js', liftMod.createLiftPool);
2000
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2001
+ export const createFrameWatch = deprecate('createFrameWatch', 'js/call/lift.js', liftMod.createFrameWatch);
2002
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2003
+ export const liftConvergenceFor = deprecate('liftConvergenceFor', 'js/call/lift.js', liftMod.liftConvergenceFor);
2004
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2005
+ export const liftPriorityFor = deprecate('liftPriorityFor', 'js/call/lift.js', liftMod.liftPriorityFor);
2006
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2007
+ export const setLiftPriority = deprecate('setLiftPriority', 'js/call/lift.js', liftMod.setLiftPriority);
2008
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2009
+ export const defaultLiftSpecifier = deprecate('defaultLiftSpecifier', 'js/call/lift.js', liftMod.defaultLiftSpecifier);
2010
+ /** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
2011
+ export const LIFT_PRIORITY = deprecate('LIFT_PRIORITY', 'js/call/lift.js', liftMod.LIFT_PRIORITY);
2012
+
1898
2013
  // `<dxr-call>` — the one-line path as markup — is registered by importing this entry (RFC 0003 §1;
1899
2014
  // the ONE side effect of this module, declared in package.json `sideEffects`). A no-op where there
1900
2015
  // is no `customElements` registry (Node, workers) or the name is already defined.