@displayxr/inline3d 1.28.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +233 -0
- package/README.md +10 -3
- package/call-full.d.ts +21 -0
- package/call.d.ts +328 -217
- package/camera.d.ts +257 -0
- package/dist/call.js +86 -0
- package/index.d.ts +27 -1
- package/js/call/capture.js +4 -167
- package/js/call/disparity.js +4 -0
- package/js/call/element.js +221 -0
- package/js/call/lift.js +9 -0
- package/js/call/options.js +132 -0
- package/js/call/signaling.js +7 -202
- package/js/call/ui.js +106 -9
- package/js/call/wire.js +8 -147
- package/js/camera/capture.js +220 -0
- package/js/camera/converge.js +82 -0
- package/js/camera/disparity.js +432 -0
- package/js/camera/geometry.js +80 -0
- package/js/camera/metadata.js +398 -0
- package/js/inline3d-call-full.js +48 -0
- package/js/inline3d-call.js +609 -318
- package/js/inline3d-camera.js +678 -0
- package/js/inline3d-player.js +646 -216
- package/js/inline3d-splat-deferred.js +153 -0
- package/js/inline3d-splat-pc-entry.js +45 -0
- package/js/inline3d-splat-playcanvas.js +46 -3
- package/js/inline3d-splat-video.js +328 -29
- package/js/inline3d-splat.js +2 -145
- package/js/inline3d.js +130 -1
- package/js/lift/index.js +15 -0
- package/package.json +39 -4
- package/player.d.ts +46 -2
- package/splat-playcanvas.d.ts +4 -0
- package/splat.d.ts +38 -0
package/js/inline3d-call.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
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
|
|
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
|
-
// import {
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// const wall = await createInline3D();
|
|
10
|
-
// const call = await addCall(wall, document.getElementById('call'), {
|
|
11
|
-
// signaling: dxrSignaling(), // hosted DisplayXR server; or dxrSignaling('wss://your.server')
|
|
12
|
-
// });
|
|
7
|
+
// import { mountCall } from '@displayxr/inline3d/call';
|
|
8
|
+
// const call = await mountCall(document.getElementById('call')); // hosted signalling, auto camera, SDK UI
|
|
13
9
|
// call.on('peer', ({ id }) => console.log('joined', id));
|
|
14
10
|
//
|
|
11
|
+
// `mountCall(el, opts?)` is `addCall` with every argument optional: the wall is the document's
|
|
12
|
+
// shared manager (`sharedInline3D()` — never a second session next to the page's own), signalling
|
|
13
|
+
// defaults to the hosted server, the camera to 'auto', the chrome to on. `addCall(wall, container,
|
|
14
|
+
// opts)` stays as the explicit form for a page that manages its own wall. `<dxr-call>`
|
|
15
|
+
// (call/element.js) is the same thing as markup. A bundled app that wants 2D→3D imports
|
|
16
|
+
// `@displayxr/inline3d/call/full` (lift statically imported) or passes `mono3D: lift` itself —
|
|
17
|
+
// on the plain `/call` entry a mono peer on a 3D display raises `warning { code:
|
|
18
|
+
// 'lift-not-bundled' }` instead of silently staying flat.
|
|
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
|
+
//
|
|
15
31
|
// WHAT IT OWNS. Unlike the other modules this one takes a CONTAINER, not a canvas: a call has a
|
|
16
32
|
// variable number of participants, so it creates one persistent canvas per remote participant
|
|
17
33
|
// (plus the self view) inside `container`, lays them out, and draws its own chrome around them.
|
|
@@ -67,68 +83,28 @@
|
|
|
67
83
|
// SDK's teardown synchronously, so no listener can distinguish them afterwards); `leave()`
|
|
68
84
|
// restores it.
|
|
69
85
|
|
|
70
|
-
import {
|
|
71
|
-
import
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
newRoomId,
|
|
77
|
-
newPeerId,
|
|
78
|
-
isValidRoomId,
|
|
79
|
-
parseInviteLink,
|
|
80
|
-
buildInviteLink,
|
|
81
|
-
normalizeHello,
|
|
82
|
-
makeHello,
|
|
83
|
-
normalizeHint,
|
|
84
|
-
normalizeState,
|
|
85
|
-
rateGate,
|
|
86
|
-
routeFor,
|
|
87
|
-
badgeFor,
|
|
88
|
-
createConvergence,
|
|
89
|
-
eyeCropRect,
|
|
90
|
-
eyeOutputSize,
|
|
91
|
-
mirrorSwapOps,
|
|
92
|
-
backoffMs,
|
|
93
|
-
createLiveGate,
|
|
94
|
-
} from './call/wire.js';
|
|
95
|
-
import { MeshTransport, clampMaxPeers } from './call/transport.js';
|
|
96
|
-
import { openCamera, openMic } from './call/capture.js';
|
|
86
|
+
import { sharedInline3D } from './inline3d.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';
|
|
97
92
|
import { drawQr } from './call/qr.js';
|
|
98
|
-
import
|
|
99
|
-
import
|
|
100
|
-
|
|
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';
|
|
100
|
+
import { defineCallElement } from './call/element.js';
|
|
101
|
+
import { lumaFromRgba, createDisparityTrack, createFocusTracker, downsampleLuma } from './call/disparity.js';
|
|
101
102
|
import { dxrSignaling } from './call/signaling.js';
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
isValidRoomId,
|
|
108
|
-
parseInviteLink,
|
|
109
|
-
buildInviteLink,
|
|
110
|
-
normalizeHello,
|
|
111
|
-
makeHello,
|
|
112
|
-
routeFor,
|
|
113
|
-
badgeFor,
|
|
114
|
-
CALL_ACCENTS,
|
|
115
|
-
};
|
|
116
|
-
export { createLiveGate } from './call/wire.js';
|
|
117
|
-
export { convergenceShiftPx, lowPass, clampShift, eyeCropRect, mirrorSwapOps, mirrorSwapPixels, maxBitrateKbps } from './call/wire.js';
|
|
118
|
-
export { preferVideoCodecs, sortCodecCapabilities, VIDEO_CODEC_ORDER } from './call/sdp.js';
|
|
119
|
-
export { MeshTransport, clampMaxPeers } from './call/transport.js';
|
|
120
|
-
export { qrEncode } from './call/qr.js';
|
|
121
|
-
export {
|
|
122
|
-
normalizeMono3D,
|
|
123
|
-
resolveLift,
|
|
124
|
-
createLiftPool,
|
|
125
|
-
createFrameWatch,
|
|
126
|
-
liftConvergenceFor,
|
|
127
|
-
liftPriorityFor,
|
|
128
|
-
setLiftPriority,
|
|
129
|
-
defaultLiftSpecifier,
|
|
130
|
-
LIFT_PRIORITY,
|
|
131
|
-
} from './call/lift.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 };
|
|
132
108
|
|
|
133
109
|
const TAG = '[inline3d/call]';
|
|
134
110
|
const HELLO_WAIT_MS = 2000;
|
|
@@ -137,58 +113,12 @@ const STATS_TICK_MS = 500;
|
|
|
137
113
|
const SPEAKING_LEVEL = 0.02;
|
|
138
114
|
const LAYER_RETRIES = 4;
|
|
139
115
|
const LIFT_RETRIES = 2;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Apply defaults and validate. Pure (no DOM, no network) — see test/call.test.mjs.
|
|
148
|
-
* @param {object} [opts]
|
|
149
|
-
*/
|
|
150
|
-
export function normalizeCallOptions(opts = {}) {
|
|
151
|
-
const ui = opts.ui === undefined ? true : !!opts.ui;
|
|
152
|
-
const aspect = typeof opts.tileAspect === 'number' && opts.tileAspect > 0.3 && opts.tileAspect < 4 ? opts.tileAspect : 16 / 9;
|
|
153
|
-
let room = opts.room === undefined || opts.room === null ? 'auto' : opts.room;
|
|
154
|
-
if (room !== 'auto' && !isValidRoomId(room)) {
|
|
155
|
-
const fromLink = parseInviteLink(room);
|
|
156
|
-
if (!fromLink) throw new Error(`@displayxr/inline3d/call: room "${room}" is not a valid room id (16-64 base64url chars) or invite link`);
|
|
157
|
-
room = fromLink;
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
room,
|
|
161
|
-
// Default: the hosted DisplayXR signalling server (which also mints TURN credentials).
|
|
162
|
-
signaling: opts.signaling || dxrSignaling(),
|
|
163
|
-
iceServers: Array.isArray(opts.iceServers) ? opts.iceServers : undefined,
|
|
164
|
-
camera: opts.camera === undefined ? 'auto' : opts.camera,
|
|
165
|
-
format: opts.format === 'sbs' ? 'sbs' : opts.format === 'mono' ? 'mono' : undefined,
|
|
166
|
-
calibration: opts.calibration && typeof opts.calibration === 'object' ? { ...opts.calibration } : {},
|
|
167
|
-
rectify: typeof opts.rectify === 'function' ? opts.rectify : null,
|
|
168
|
-
audio: opts.audio === undefined ? true : !!opts.audio,
|
|
169
|
-
mono3D: normalizeMono3D(opts.mono3D),
|
|
170
|
-
maxPeers: clampMaxPeers(opts.maxPeers === undefined ? DEFAULT_MAX_PEERS : opts.maxPeers),
|
|
171
|
-
// Extra lift() options for lifted tiles (models, ort, quality, providers). The call's own
|
|
172
|
-
// keys (mode, wall, ui, convergence, priority) always win.
|
|
173
|
-
liftOptions: opts.liftOptions && typeof opts.liftOptions === 'object' ? { ...opts.liftOptions } : null,
|
|
174
|
-
// Concurrent lifted tiles. Default = maxPeers (4): every mono peer can be lifted.
|
|
175
|
-
maxLifted: Number.isFinite(+opts.maxLifted) && opts.maxLifted !== null ? Math.max(0, Math.min(DEFAULT_MAX_PEERS, Math.floor(+opts.maxLifted))) : DEFAULT_MAX_PEERS,
|
|
176
|
-
layout: opts.layout === 'speaker' ? 'speaker' : 'grid',
|
|
177
|
-
ui,
|
|
178
|
-
autoJoin: opts.autoJoin === undefined ? !ui : !!opts.autoJoin,
|
|
179
|
-
selfView: opts.selfView === undefined ? true : !!opts.selfView,
|
|
180
|
-
tileAspect: aspect,
|
|
181
|
-
accent: typeof opts.accent === 'string' && opts.accent ? resolveCallAccent(opts.accent) : null,
|
|
182
|
-
inviteBase: typeof opts.inviteBase === 'string' ? opts.inviteBase : null,
|
|
183
|
-
updateUrl: opts.updateUrl === undefined ? ui : !!opts.updateUrl,
|
|
184
|
-
browserUrl: typeof opts.browserUrl === 'string' ? opts.browserUrl : DEFAULT_BROWSER_URL,
|
|
185
|
-
recoverSession: opts.recoverSession === undefined ? true : !!opts.recoverSession,
|
|
186
|
-
scrollIntoView: opts.scrollIntoView === undefined ? true : !!opts.scrollIntoView,
|
|
187
|
-
wallOptions: opts.wallOptions && typeof opts.wallOptions === 'object' ? opts.wallOptions : {},
|
|
188
|
-
log: typeof opts.log === 'function' ? opts.log : opts.debug ? (tag, obj) => console.log(`${TAG} ${tag} ${JSON.stringify(obj)}`) : null,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
116
|
+
// Auto-convergence sampling: ~5 Hz on a copy whose eye is at most this wide (enough for sub-pixel
|
|
117
|
+
// disparity on a face; small enough to stay a few ms per measurement).
|
|
118
|
+
const AUTO_CONV_INTERVAL_MS = 200;
|
|
119
|
+
const AUTO_CONV_EYE_WIDTH = 240;
|
|
120
|
+
// WebCodecs pixel formats whose plane 0 is luma (Y): read directly, no colour conversion.
|
|
121
|
+
const Y_PLANE_FORMATS = new Set(['I420', 'I420A', 'I422', 'I444', 'NV12']);
|
|
192
122
|
/**
|
|
193
123
|
* Put a 3D video call in `container`. Resolves once the camera is open and (with `ui:true`) the
|
|
194
124
|
* lobby is showing; with `ui:false` (or `autoJoin:true`) once the call is joined.
|
|
@@ -199,18 +129,36 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
199
129
|
*/
|
|
200
130
|
export async function addCall(wall, container, opts = {}) {
|
|
201
131
|
if (!container || typeof container.appendChild !== 'function') throw new TypeError('@displayxr/inline3d/call: addCall(wall, container, opts) needs a container element');
|
|
202
|
-
const o =
|
|
132
|
+
const o = normalizeOptions(opts);
|
|
203
133
|
if (!o.signaling || typeof o.signaling.join !== 'function') {
|
|
204
|
-
throw new TypeError('@displayxr/inline3d/call: opts.signaling must be a SignalingAdapter — dxrSignaling([url])
|
|
134
|
+
throw new TypeError('@displayxr/inline3d/call: opts.signaling must be a SignalingAdapter — dxrSignaling([url]) or your own');
|
|
205
135
|
}
|
|
206
136
|
const call = new Call(wall, container, o);
|
|
207
137
|
await call._init();
|
|
208
138
|
return call.handle;
|
|
209
139
|
}
|
|
210
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The one-line path (RFC 0003 §1): `addCall` with every argument optional. The wall is
|
|
143
|
+
* `opts.wall` if given, else the document's shared manager — `sharedInline3D(opts.wallOptions)`,
|
|
144
|
+
* which returns the page's existing session rather than opening a second one (woven-canvas
|
|
145
|
+
* rule 1). Everything else defaults as `addCall` does: hosted signalling, `camera: 'auto'`,
|
|
146
|
+
* `ui: true`.
|
|
147
|
+
*
|
|
148
|
+
* @param {HTMLElement} el the container (a `<dxr-call>`, a `<div>`, …)
|
|
149
|
+
* @param {object} [opts] see call.d.ts `CallOptions`
|
|
150
|
+
*/
|
|
151
|
+
export async function mountCall(el, opts = {}) {
|
|
152
|
+
if (!el || typeof el.appendChild !== 'function') throw new TypeError('@displayxr/inline3d/call: mountCall(el, opts) needs a container element');
|
|
153
|
+
const { wall: given, ...rest } = opts;
|
|
154
|
+
const wall = given || (await sharedInline3D(rest.wallOptions));
|
|
155
|
+
return addCall(wall, el, rest);
|
|
156
|
+
}
|
|
157
|
+
|
|
211
158
|
class Call {
|
|
212
159
|
constructor(wall, container, o) {
|
|
213
160
|
this.o = o;
|
|
161
|
+
this.t = createStrings(o.strings); // every chrome string, overridable via theme.strings
|
|
214
162
|
this.container = container;
|
|
215
163
|
this.wall = wall || null;
|
|
216
164
|
this.wallLive = !!(wall && wall.supported);
|
|
@@ -238,7 +186,9 @@ class Call {
|
|
|
238
186
|
this.liftPool = null;
|
|
239
187
|
this._liftP = null;
|
|
240
188
|
this.liftProven = false;
|
|
241
|
-
this.
|
|
189
|
+
this.liftMissing = false; // the lift MODULE failed to import (vs. a display that can't lift)
|
|
190
|
+
this._liftWarned = false;
|
|
191
|
+
this._frameWatch = liftMod.createFrameWatch();
|
|
242
192
|
this.handle = this._makeHandle();
|
|
243
193
|
if (this.wallLive) this._hookWall(this.wall);
|
|
244
194
|
}
|
|
@@ -272,6 +222,13 @@ class Call {
|
|
|
272
222
|
this.emit('error', { code, message, error: err || null });
|
|
273
223
|
}
|
|
274
224
|
|
|
225
|
+
/** Degraded, not broken (RFC 0003 §2): one console.warn (always — the page must see it) + an event. */
|
|
226
|
+
warning(code, message) {
|
|
227
|
+
this.log('warning', { code, message });
|
|
228
|
+
console.warn(`${TAG} ${code}: ${message}`);
|
|
229
|
+
this.emit('warning', { code, message });
|
|
230
|
+
}
|
|
231
|
+
|
|
275
232
|
// ── setup ────────────────────────────────────────────────────────────────────────────────
|
|
276
233
|
|
|
277
234
|
async _init() {
|
|
@@ -285,35 +242,31 @@ class Call {
|
|
|
285
242
|
else this._setState('lobby');
|
|
286
243
|
}
|
|
287
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
|
+
*/
|
|
288
251
|
async _openMedia(want, format, { keepOnFail = false } = {}) {
|
|
289
252
|
const log = (t, x) => this.log(t, x);
|
|
290
253
|
let cam = null;
|
|
291
254
|
let camError = null;
|
|
255
|
+
const given = isStereoCamera(want) ? want : null;
|
|
292
256
|
try {
|
|
293
|
-
cam = await openCamera(want,
|
|
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: [] });
|
|
294
259
|
} catch (err) {
|
|
295
260
|
camError = err;
|
|
296
261
|
// No camera is never fatal: the call goes on audio-only, the self view and every receiver
|
|
297
262
|
// say why, and the user can retry (the tracker may let go) or pick another camera.
|
|
298
263
|
const busy = err.code === 'camera-busy';
|
|
299
|
-
this.error(busy ? 'camera-busy' : err.code || 'no-camera', busy ? `${
|
|
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);
|
|
300
265
|
if (keepOnFail && this.local && this.local.videoTrack) return this.local; // keep the working one
|
|
301
266
|
this.camStatus = busy ? 'busy' : 'none';
|
|
302
267
|
}
|
|
303
268
|
if (cam) this.camStatus = 'ok';
|
|
304
|
-
if (cam && cam.
|
|
305
|
-
// P2 seam: a calibrated rectification step (plug-in or runtime supplied). Its output is a
|
|
306
|
-
// rectified SBS stream; the hello then says so.
|
|
307
|
-
try {
|
|
308
|
-
const out = await this.o.rectify(cam.stream, { width: cam.width, height: cam.height, deviceId: cam.deviceId, label: cam.label });
|
|
309
|
-
if (out && typeof out.getVideoTracks === 'function') {
|
|
310
|
-
cam.stream = out;
|
|
311
|
-
cam.calibration = { ...cam.calibration, rectified: true };
|
|
312
|
-
}
|
|
313
|
-
} catch (err) {
|
|
314
|
-
this.error('rectify-failed', `rectify() threw — sending the raw pair: ${err.message}`, err);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
269
|
+
if (cam && cam.rectifyError) this.error('rectify-failed', `rectify() threw — sending the raw pair: ${cam.rectifyError.message}`, cam.rectifyError);
|
|
317
270
|
let audioTrack = this.local ? this.local.audioTrack : null;
|
|
318
271
|
if (this.o.audio && !audioTrack) audioTrack = await openMic({ log });
|
|
319
272
|
if (audioTrack) audioTrack.enabled = !this.muted;
|
|
@@ -328,28 +281,39 @@ class Call {
|
|
|
328
281
|
videoTrack.enabled = !this.camOff;
|
|
329
282
|
}
|
|
330
283
|
const stream = new MediaStream([videoTrack, audioTrack].filter(Boolean));
|
|
331
|
-
const video = this.local?.video || Object.assign(document.createElement('video'), { muted: true, playsInline: true, autoplay: true });
|
|
332
|
-
video.srcObject = cam ? cam.stream : null;
|
|
333
|
-
video.play().catch(() => {});
|
|
334
284
|
const prev = this.local;
|
|
335
285
|
this.local = {
|
|
336
286
|
stream,
|
|
287
|
+
cam, // the StereoCamera (null = audio-only)
|
|
337
288
|
camStream: cam ? cam.stream : null,
|
|
338
|
-
video,
|
|
289
|
+
video: cam ? cam.video : null,
|
|
339
290
|
videoTrack,
|
|
340
291
|
audioTrack,
|
|
341
292
|
format: cam ? cam.format : 'mono',
|
|
342
293
|
width: cam ? cam.width : 0,
|
|
343
294
|
height: cam ? cam.height : 0,
|
|
344
295
|
calibration: cam ? cam.calibration : {},
|
|
345
|
-
owned: cam ? cam.owned : false,
|
|
296
|
+
owned: cam ? cam.owned && cam !== given : false,
|
|
346
297
|
label: cam ? cam.label : '',
|
|
347
298
|
// Kept on the error path too: which cameras were held, and why, is the one clue a
|
|
348
299
|
// 'camera-busy' / 'no-camera' report carries.
|
|
349
300
|
skipped: cam ? cam.skipped : camError?.skipped || [],
|
|
350
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
|
+
}
|
|
351
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 });
|
|
352
|
-
if (prev && prev.owned && prev.
|
|
316
|
+
if (prev && prev.owned && prev.cam && prev.cam !== cam) prev.cam.close();
|
|
353
317
|
this._refreshSelf();
|
|
354
318
|
this._refreshLobby();
|
|
355
319
|
return this.local;
|
|
@@ -357,7 +321,7 @@ class Call {
|
|
|
357
321
|
|
|
358
322
|
hello() {
|
|
359
323
|
const l = this.local || {};
|
|
360
|
-
return makeHello({
|
|
324
|
+
return wire.makeHello({
|
|
361
325
|
format: l.format,
|
|
362
326
|
width: l.width,
|
|
363
327
|
height: l.height,
|
|
@@ -379,18 +343,18 @@ class Call {
|
|
|
379
343
|
if (this.state === 'joining' || this.state === 'in-call') return this.handle;
|
|
380
344
|
if (this.state === 'left' && !this.local?.videoTrack) await this._openMedia(this.o.camera);
|
|
381
345
|
if (!this.room) {
|
|
382
|
-
this.room = newRoomId();
|
|
346
|
+
this.room = wire.newRoomId();
|
|
383
347
|
this.log('room-created', {});
|
|
384
348
|
}
|
|
385
|
-
if (this.o.updateUrl && globalThis.history && globalThis.location) {
|
|
349
|
+
if (this.o.invite.updateUrl && globalThis.history && globalThis.location) {
|
|
386
350
|
try {
|
|
387
|
-
history.replaceState(history.state, '', buildInviteLink(location.href, this.room));
|
|
351
|
+
history.replaceState(history.state, '', wire.buildInviteLink(location.href, this.room));
|
|
388
352
|
} catch {
|
|
389
353
|
/* sandboxed iframe */
|
|
390
354
|
}
|
|
391
355
|
}
|
|
392
356
|
this._setState('joining');
|
|
393
|
-
const t = new
|
|
357
|
+
const t = new MeshTransportImpl({
|
|
394
358
|
signaling: this.o.signaling,
|
|
395
359
|
id: this.id,
|
|
396
360
|
maxPeers: this.o.maxPeers,
|
|
@@ -433,7 +397,7 @@ class Call {
|
|
|
433
397
|
// The grid is sized so the whole call block fits ONE viewport, but only if it starts at the
|
|
434
398
|
// top: a tile below the fold is withheld from the weave (browser#167), so its layer goes
|
|
435
399
|
// live late. Bring the block into view once, on join.
|
|
436
|
-
if (this.o.
|
|
400
|
+
if (this.o.chrome) {
|
|
437
401
|
try {
|
|
438
402
|
this.container.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
|
439
403
|
this._fitGrid();
|
|
@@ -442,7 +406,7 @@ class Call {
|
|
|
442
406
|
}
|
|
443
407
|
}
|
|
444
408
|
this._startStats();
|
|
445
|
-
this._pagehide = () => this.leave();
|
|
409
|
+
this._pagehide = () => this.leave('pagehide');
|
|
446
410
|
globalThis.addEventListener?.('pagehide', this._pagehide);
|
|
447
411
|
this.log('joined', { id: this.id, peers: this.tiles.size });
|
|
448
412
|
this.emit('joined', { room: this.room, id: this.id });
|
|
@@ -499,7 +463,7 @@ class Call {
|
|
|
499
463
|
tile.onChannelOpen();
|
|
500
464
|
return;
|
|
501
465
|
}
|
|
502
|
-
const h = normalizeHello(msg);
|
|
466
|
+
const h = wire.normalizeHello(msg);
|
|
503
467
|
if (h) return tile.setHello(h);
|
|
504
468
|
const hint = normalizeHint(msg);
|
|
505
469
|
if (hint) return tile.setHint(hint);
|
|
@@ -511,8 +475,8 @@ class Call {
|
|
|
511
475
|
|
|
512
476
|
inviteLink() {
|
|
513
477
|
if (!this.room) return null;
|
|
514
|
-
const base = this.o.
|
|
515
|
-
return buildInviteLink(base, this.room);
|
|
478
|
+
const base = this.o.invite.base || (globalThis.location ? location.href : '');
|
|
479
|
+
return wire.buildInviteLink(base, this.room);
|
|
516
480
|
}
|
|
517
481
|
|
|
518
482
|
mute(on) {
|
|
@@ -524,7 +488,7 @@ class Call {
|
|
|
524
488
|
return this.muted;
|
|
525
489
|
}
|
|
526
490
|
|
|
527
|
-
|
|
491
|
+
setCameraOff(on) {
|
|
528
492
|
this.camOff = on === undefined ? !this.camOff : !!on;
|
|
529
493
|
if (this.local?.videoTrack) this.local.videoTrack.enabled = !this.camOff;
|
|
530
494
|
this.transport?.broadcast(this.stateMsg());
|
|
@@ -568,7 +532,7 @@ class Call {
|
|
|
568
532
|
return true;
|
|
569
533
|
}
|
|
570
534
|
|
|
571
|
-
leave() {
|
|
535
|
+
leave(reason = 'left') {
|
|
572
536
|
if (this.state === 'left') return;
|
|
573
537
|
globalThis.removeEventListener?.('pagehide', this._pagehide);
|
|
574
538
|
this.transport?.stop();
|
|
@@ -579,19 +543,20 @@ class Call {
|
|
|
579
543
|
clearInterval(this._statsTimer);
|
|
580
544
|
this._setSpeaker(null);
|
|
581
545
|
if (this.local) {
|
|
546
|
+
this._camEndedOff?.();
|
|
547
|
+
this._camEndedOff = null;
|
|
582
548
|
this.local.stream.getTracks().forEach((t) => t.stop());
|
|
583
|
-
|
|
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();
|
|
584
552
|
this.local.videoTrack = null;
|
|
585
553
|
this.local.audioTrack = null;
|
|
586
554
|
}
|
|
587
|
-
|
|
588
|
-
this.self.unregister();
|
|
589
|
-
this.self.route = null; // a rejoin re-registers it
|
|
590
|
-
}
|
|
555
|
+
this.self?.detach(); // a rejoin re-attaches it
|
|
591
556
|
this._unhookWall();
|
|
592
557
|
this._setState('left');
|
|
593
558
|
this._layout();
|
|
594
|
-
this.emit('left', { room: this.room });
|
|
559
|
+
this.emit('left', { room: this.room, reason });
|
|
595
560
|
}
|
|
596
561
|
|
|
597
562
|
// ── the wall: session loss and recovery ───────────────────────────────────────────────
|
|
@@ -625,7 +590,7 @@ class Call {
|
|
|
625
590
|
this.weaveLive = true; // nothing to wait on
|
|
626
591
|
return;
|
|
627
592
|
}
|
|
628
|
-
const gate = createLiveGate();
|
|
593
|
+
const gate = wire.createLiveGate();
|
|
629
594
|
const t0 = performance.now();
|
|
630
595
|
const onFrame = (_t, f) => {
|
|
631
596
|
if (w !== this.wall || !this.wallLive || this.state === 'left') return;
|
|
@@ -680,7 +645,7 @@ class Call {
|
|
|
680
645
|
this.log('session-ended', { byPage: hook.pageClosed });
|
|
681
646
|
for (const t of this.tiles.values()) t.onWallLost();
|
|
682
647
|
this.self?.onWallLost();
|
|
683
|
-
if (hook.pageClosed ||
|
|
648
|
+
if (hook.pageClosed || this.state === 'left') {
|
|
684
649
|
this._unhookWall();
|
|
685
650
|
return;
|
|
686
651
|
}
|
|
@@ -695,7 +660,9 @@ class Call {
|
|
|
695
660
|
if (this.state === 'left') return;
|
|
696
661
|
let w = null;
|
|
697
662
|
try {
|
|
698
|
-
|
|
663
|
+
// The shared manager: if the page has already re-opened its own wall, join it rather than
|
|
664
|
+
// opening a second session (rule 1); else this creates the document's new one.
|
|
665
|
+
w = await sharedInline3D();
|
|
699
666
|
} catch {
|
|
700
667
|
w = null;
|
|
701
668
|
}
|
|
@@ -706,7 +673,6 @@ class Call {
|
|
|
706
673
|
this.log('session-recovered', { attempt });
|
|
707
674
|
for (const t of this.tiles.values()) t.reroute(true);
|
|
708
675
|
this.self?.reroute(true);
|
|
709
|
-
this.emit('session', { wall: w });
|
|
710
676
|
}, backoffMs(attempt, { baseMs: 1000, maxMs: 15000 }));
|
|
711
677
|
}
|
|
712
678
|
|
|
@@ -716,7 +682,6 @@ class Call {
|
|
|
716
682
|
let last = 0;
|
|
717
683
|
const tick = (now) => {
|
|
718
684
|
this._raf = requestAnimationFrame(tick);
|
|
719
|
-
this.self?.paint();
|
|
720
685
|
for (const t of this.tiles.values()) t.paint();
|
|
721
686
|
if (last && this.liftPool && this.liftPool.anyWeb) this._watchFrames(now - last);
|
|
722
687
|
last = now;
|
|
@@ -748,10 +713,10 @@ class Call {
|
|
|
748
713
|
const lo = this.o.liftOptions;
|
|
749
714
|
// liftCapabilities probes the same model source the lifts will use.
|
|
750
715
|
const capsOpts = { webFallback: true, ...(lo && lo.models ? { models: lo.models } : {}) };
|
|
751
|
-
this._liftP = resolveLift(this.mono3D, { log, capsOpts }).then((r) => {
|
|
716
|
+
this._liftP = liftMod.resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
|
|
752
717
|
this.liftApi = r;
|
|
753
718
|
if (r.lift) {
|
|
754
|
-
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 });
|
|
755
720
|
this.liftPool.setDepth(this.depth);
|
|
756
721
|
this.liftPool.setSpeaker(this.speakerId);
|
|
757
722
|
if (!r.caps && r.capabilities) {
|
|
@@ -763,6 +728,12 @@ class Call {
|
|
|
763
728
|
})
|
|
764
729
|
.catch(() => {});
|
|
765
730
|
}
|
|
731
|
+
} else if (r.reason === 'import-failed' || r.reason === 'no-lift-export') {
|
|
732
|
+
// The MODULE is missing (a bundled app whose bundler could not follow the computed
|
|
733
|
+
// import, or a copy of the SDK built without lift) — not a display that can't lift. Said
|
|
734
|
+
// loudly, once, when a mono peer actually arrives (_warnLiftMissing): a developer testing
|
|
735
|
+
// in the DisplayXR Browser sees it the first time a 2D caller joins.
|
|
736
|
+
this.liftMissing = true;
|
|
766
737
|
} else if (r.reason !== 'off') {
|
|
767
738
|
console.info(`${TAG} mono→3D unavailable (${r.reason}) — mono participants are shown flat`);
|
|
768
739
|
}
|
|
@@ -774,6 +745,21 @@ class Call {
|
|
|
774
745
|
return this._liftP;
|
|
775
746
|
}
|
|
776
747
|
|
|
748
|
+
/**
|
|
749
|
+
* `warning { code: 'lift-not-bundled' }`, once per call: a mono peer is on a 3D display, lift
|
|
750
|
+
* was asked for ('auto'), and the lift module could not be imported. Names the fix.
|
|
751
|
+
*/
|
|
752
|
+
_warnLiftMissing() {
|
|
753
|
+
if (this._liftWarned || !this.liftMissing || !this.mono3DOn) return;
|
|
754
|
+
this._liftWarned = true;
|
|
755
|
+
this.warning(
|
|
756
|
+
'lift-not-bundled',
|
|
757
|
+
'a 2D participant is on a 3D display but the 2D→3D module could not be imported from this build, so they stay flat. ' +
|
|
758
|
+
"Import it explicitly — `import { lift } from '@displayxr/inline3d/lift'; mountCall(el, { mono3D: lift })` — " +
|
|
759
|
+
"or use `@displayxr/inline3d/call/full`, which bundles it."
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
777
763
|
/** Re-run the routing table for every mono tile (lift resolved, a slot freed, setMono3D). */
|
|
778
764
|
_rerouteMono() {
|
|
779
765
|
for (const t of this.tiles.values()) if (t.format && t.format !== 'sbs') t.reroute(false);
|
|
@@ -795,14 +781,15 @@ class Call {
|
|
|
795
781
|
/** What the lobby says a mono participant will look like here. */
|
|
796
782
|
_liftHint() {
|
|
797
783
|
if (!this.wallLive) return '';
|
|
798
|
-
|
|
784
|
+
const t = this.t;
|
|
785
|
+
if (!this.mono3DOn) return t('liftOff');
|
|
799
786
|
const r = this.liftApi;
|
|
800
|
-
if (!r) return '
|
|
801
|
-
if (!r.lift) return '
|
|
787
|
+
if (!r) return t('liftChecking');
|
|
788
|
+
if (!r.lift) return this.liftMissing ? t('liftMissing') : t('liftNoProvider');
|
|
802
789
|
const c = r.caps;
|
|
803
|
-
if (c && c.native) return
|
|
804
|
-
if (c && c.webFallback && c.webFallback.webgpu) return '
|
|
805
|
-
return this.liftProven ? '
|
|
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');
|
|
806
793
|
}
|
|
807
794
|
|
|
808
795
|
_mono3DInfo() {
|
|
@@ -919,7 +906,7 @@ class Call {
|
|
|
919
906
|
*/
|
|
920
907
|
_fitGrid() {
|
|
921
908
|
const ui = this.ui;
|
|
922
|
-
if (!ui || !this.o.
|
|
909
|
+
if (!ui || !this.o.chrome || typeof globalThis.innerHeight !== 'number') return;
|
|
923
910
|
const host = this.container.getBoundingClientRect?.();
|
|
924
911
|
const grid = ui.grid.getBoundingClientRect?.();
|
|
925
912
|
if (!host || !grid || !grid.height) return;
|
|
@@ -940,55 +927,59 @@ class Call {
|
|
|
940
927
|
|
|
941
928
|
_buildDom() {
|
|
942
929
|
const c = this.container;
|
|
930
|
+
const t = this.t;
|
|
943
931
|
injectCallStyle(c.ownerDocument || document);
|
|
944
932
|
c.classList.add('dxr-call-host');
|
|
945
|
-
|
|
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);
|
|
946
935
|
const ui = (this.ui = {});
|
|
947
|
-
ui.banner = el('div', { class: 'dxr-call-banner dxr-call-hidden' }, [
|
|
948
|
-
el('span', { text: '
|
|
949
|
-
el('a', { href: this.o.browserUrl, target: '_blank', rel: 'noopener', text: '
|
|
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') }),
|
|
950
939
|
]);
|
|
951
|
-
ui.grid = el('div', { class: 'dxr-call-grid', 'data-layout': this.o.layout, 'data-n': '0' });
|
|
952
|
-
ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
|
|
953
|
-
ui.selfSlot = el('div', { class: 'dxr-call-self' });
|
|
954
|
-
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' });
|
|
955
944
|
ui.foot = el('div', { class: 'dxr-call-foot' }, [ui.selfSlot, ui.bar]);
|
|
956
|
-
ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
|
|
957
|
-
if (!this.o.
|
|
945
|
+
ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'invite' });
|
|
946
|
+
if (!this.o.chrome) {
|
|
958
947
|
show(ui.bar, false);
|
|
959
948
|
}
|
|
960
949
|
c.append(ui.banner, ui.panel, ui.grid, ui.invite, ui.foot);
|
|
961
950
|
if (this.o.selfView) this.self = new SelfTile(this, ui.selfSlot);
|
|
962
951
|
else show(ui.selfSlot, false);
|
|
963
|
-
if (this.o.
|
|
952
|
+
if (this.o.chrome) this._buildBar();
|
|
964
953
|
}
|
|
965
954
|
|
|
966
955
|
_buildBar() {
|
|
967
956
|
const ui = this.ui;
|
|
957
|
+
const t = this.t;
|
|
968
958
|
const ib = (label, icon, onclick, extra = '') => el('button', { class: `dxr-call-ib ${extra}`, type: 'button', 'aria-label': label, title: label, html: icon, onclick });
|
|
969
|
-
ui.mic = ib('
|
|
970
|
-
ui.cam = ib('
|
|
971
|
-
ui.depth = el('input', { type: 'range', min: '-1', max: '1', step: '0.05', value: '0', 'aria-label': '
|
|
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') });
|
|
972
962
|
ui.depth.addEventListener('input', () => this.setDepth(+ui.depth.value));
|
|
973
963
|
ui.depth.addEventListener('dblclick', () => this.setDepth(0));
|
|
974
|
-
ui.inviteBtn = ib('
|
|
964
|
+
ui.inviteBtn = ib(t('invite'), ICONS.link, () => {
|
|
975
965
|
this._showInvite = !this._showInvite;
|
|
976
966
|
this._layout();
|
|
977
967
|
});
|
|
978
|
-
ui.leave = ib('
|
|
979
|
-
ui.bar.append(ui.mic, ui.cam, el('label', { class: 'dxr-call-depth' }, ['
|
|
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);
|
|
980
970
|
}
|
|
981
971
|
|
|
982
972
|
_refreshBar() {
|
|
983
973
|
const ui = this.ui;
|
|
984
974
|
if (!ui || !ui.mic) return;
|
|
975
|
+
const t = this.t;
|
|
985
976
|
ui.mic.innerHTML = this.muted ? ICONS.micOff : ICONS.mic;
|
|
986
977
|
ui.mic.setAttribute('aria-pressed', String(this.muted));
|
|
987
|
-
ui.mic.title = this.muted ? '
|
|
978
|
+
ui.mic.title = this.muted ? t('unmute') : t('mute');
|
|
988
979
|
const noCam = !this.local?.videoTrack;
|
|
989
980
|
ui.cam.innerHTML = this.camOff || noCam ? ICONS.camOff : ICONS.cam;
|
|
990
981
|
ui.cam.setAttribute('aria-pressed', String(this.camOff || noCam));
|
|
991
|
-
ui.cam.title = noCam ? (this.camStatus === 'busy' ?
|
|
982
|
+
ui.cam.title = noCam ? (this.camStatus === 'busy' ? t('cameraBusyRetry', { cameraBusy: t('cameraBusy') }) : t('cameraRetry')) : this.camOff ? t('cameraOn') : t('cameraOff');
|
|
992
983
|
}
|
|
993
984
|
|
|
994
985
|
_refreshSelf() {
|
|
@@ -1003,20 +994,21 @@ class Call {
|
|
|
1003
994
|
target.replaceChildren();
|
|
1004
995
|
const link = this.inviteLink();
|
|
1005
996
|
if (!link) return;
|
|
1006
|
-
const
|
|
997
|
+
const t = this.t;
|
|
998
|
+
const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': t('inviteLink') });
|
|
1007
999
|
input.addEventListener('focus', () => input.select());
|
|
1008
|
-
const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: '
|
|
1000
|
+
const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: t('copyLink') });
|
|
1009
1001
|
copy.addEventListener('click', async () => {
|
|
1010
1002
|
try {
|
|
1011
1003
|
await navigator.clipboard.writeText(link);
|
|
1012
|
-
copy.textContent = '
|
|
1004
|
+
copy.textContent = t('copied');
|
|
1013
1005
|
} catch {
|
|
1014
1006
|
input.select();
|
|
1015
|
-
copy.textContent = '
|
|
1007
|
+
copy.textContent = t('pressCopy');
|
|
1016
1008
|
}
|
|
1017
|
-
setTimeout(() => (copy.textContent = '
|
|
1009
|
+
setTimeout(() => (copy.textContent = t('copyLink')), 1800);
|
|
1018
1010
|
});
|
|
1019
|
-
const qr = el('canvas', { 'aria-label': '
|
|
1011
|
+
const qr = el('canvas', { 'aria-label': t('qrLabel'), role: 'img' });
|
|
1020
1012
|
try {
|
|
1021
1013
|
drawQr(qr, link, { px: 4 });
|
|
1022
1014
|
} catch {
|
|
@@ -1033,16 +1025,17 @@ class Call {
|
|
|
1033
1025
|
const ui = this.ui;
|
|
1034
1026
|
if (!ui) return;
|
|
1035
1027
|
const c = this.container;
|
|
1036
|
-
const
|
|
1028
|
+
const t = this.t;
|
|
1029
|
+
const lobby = this.o.chrome && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
|
|
1037
1030
|
c.classList.toggle('dxr-call-host--lobby', lobby);
|
|
1038
|
-
show(ui.banner, this.o.
|
|
1031
|
+
show(ui.banner, this.o.chrome && !this.woven);
|
|
1039
1032
|
const n = this.tiles.size;
|
|
1040
1033
|
ui.grid.dataset.n = String(Math.min(n, 4));
|
|
1041
1034
|
show(ui.grid, n > 0 && this.state !== 'left');
|
|
1042
|
-
// 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.
|
|
1043
1036
|
const main = this.o.layout === 'speaker' ? this.speakerId || [...this.tiles.keys()][0] : null;
|
|
1044
|
-
for (const [id,
|
|
1045
|
-
if (!this.o.
|
|
1037
|
+
for (const [id, tile] of this.tiles) tile.el.classList.toggle('dxr-call-tile--main', id === main);
|
|
1038
|
+
if (!this.o.chrome) return;
|
|
1046
1039
|
this._refreshBar();
|
|
1047
1040
|
queueMicrotask(() => this._fitGrid());
|
|
1048
1041
|
const p = ui.panel;
|
|
@@ -1051,39 +1044,67 @@ class Call {
|
|
|
1051
1044
|
p.replaceChildren();
|
|
1052
1045
|
const l = this.local;
|
|
1053
1046
|
const joining = !!this.room;
|
|
1054
|
-
const title =
|
|
1055
|
-
|
|
1056
|
-
const kind = this.camStatus === 'busy' ? '
|
|
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);
|
|
1057
1050
|
const text =
|
|
1058
1051
|
this.state === 'full'
|
|
1059
|
-
?
|
|
1060
|
-
:
|
|
1052
|
+
? t('lobbyFullText', { maxPeers: this.o.maxPeers })
|
|
1053
|
+
: `${t('lobbyText', { camera: l?.label || t('cameraDefault'), kind })}${this._liftHint() ? ` ${this._liftHint()}` : ''}`;
|
|
1061
1054
|
const go = el('button', {
|
|
1062
1055
|
class: 'dxr-call-btn dxr-call-btn--primary',
|
|
1063
1056
|
type: 'button',
|
|
1064
|
-
text: this.state === 'left' ? '
|
|
1057
|
+
text: this.state === 'left' ? t('rejoin') : joining ? t('join') : t('start'),
|
|
1065
1058
|
});
|
|
1066
1059
|
go.addEventListener('click', () => this.join().catch(() => {}));
|
|
1067
|
-
const sel = el('select', { class: 'dxr-call-select', 'aria-label': '
|
|
1060
|
+
const sel = el('select', { class: 'dxr-call-select', 'aria-label': t('cameraSelect') });
|
|
1068
1061
|
this._fillCameras(sel);
|
|
1069
1062
|
sel.addEventListener('change', () => this.setCamera(sel.value).catch((e) => this.error('camera-failed', e.message, e)));
|
|
1070
|
-
const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: '
|
|
1063
|
+
const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: t('retryCamera') }) : null;
|
|
1071
1064
|
retry?.addEventListener('click', () => this.retryCamera().catch(() => {}));
|
|
1072
1065
|
p.append(el('h3', { text: title }), el('p', { text }), el('div', { class: 'dxr-call-row' }, [this.state === 'full' ? null : go, retry, sel]));
|
|
1073
1066
|
} else if (this.state === 'in-call' && n === 0) {
|
|
1074
1067
|
show(p, true);
|
|
1075
|
-
this._renderInvite(p, '
|
|
1068
|
+
this._renderInvite(p, t('waitingTitle'), t('waitingText'));
|
|
1076
1069
|
} else if (this.state === 'joining') {
|
|
1077
1070
|
show(p, true);
|
|
1078
|
-
p.replaceChildren(el('p', { text: '
|
|
1071
|
+
p.replaceChildren(el('p', { text: t('joining') }));
|
|
1079
1072
|
} else show(p, false);
|
|
1080
1073
|
const inviteOpen = this.state === 'in-call' && n > 0 && this._showInvite;
|
|
1081
|
-
if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, '
|
|
1074
|
+
if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, t('inviteTitle'), t('inviteText'));
|
|
1082
1075
|
show(ui.invite, inviteOpen);
|
|
1083
1076
|
if (!inviteOpen) ui.invite.replaceChildren();
|
|
1084
1077
|
ui.inviteBtn?.setAttribute('aria-pressed', String(!!inviteOpen));
|
|
1085
1078
|
}
|
|
1086
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
|
+
|
|
1087
1108
|
async _fillCameras(sel) {
|
|
1088
1109
|
let cams = [];
|
|
1089
1110
|
try {
|
|
@@ -1099,7 +1120,8 @@ class Call {
|
|
|
1099
1120
|
|
|
1100
1121
|
_makeHandle() {
|
|
1101
1122
|
const call = this;
|
|
1102
|
-
|
|
1123
|
+
const renamedEvents = { format: 'display', session: null };
|
|
1124
|
+
const handle = {
|
|
1103
1125
|
get room() {
|
|
1104
1126
|
return call.room;
|
|
1105
1127
|
},
|
|
@@ -1109,15 +1131,16 @@ class Call {
|
|
|
1109
1131
|
get state() {
|
|
1110
1132
|
return call.state;
|
|
1111
1133
|
},
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
},
|
|
1115
|
-
get format() {
|
|
1134
|
+
/** What YOU send: 'sbs' | 'mono'; null before a camera opened. */
|
|
1135
|
+
get localFormat() {
|
|
1116
1136
|
return call.local ? call.local.format : null;
|
|
1117
1137
|
},
|
|
1118
1138
|
get muted() {
|
|
1119
1139
|
return call.muted;
|
|
1120
1140
|
},
|
|
1141
|
+
get cameraOff() {
|
|
1142
|
+
return call.camOff;
|
|
1143
|
+
},
|
|
1121
1144
|
get depth() {
|
|
1122
1145
|
return call.depth;
|
|
1123
1146
|
},
|
|
@@ -1136,18 +1159,25 @@ class Call {
|
|
|
1136
1159
|
get peers() {
|
|
1137
1160
|
return Object.freeze([...call.tiles.values()].map((t) => t.info()));
|
|
1138
1161
|
},
|
|
1139
|
-
join: () => call.join(),
|
|
1162
|
+
join: () => call.join().then(() => undefined),
|
|
1140
1163
|
inviteLink: () => call.inviteLink(),
|
|
1141
1164
|
mute: (on) => call.mute(on),
|
|
1142
|
-
|
|
1143
|
-
setCamera: (
|
|
1165
|
+
setCameraOff: (on) => call.setCameraOff(on),
|
|
1166
|
+
setCamera: (src, o) => call.setCamera(src, o),
|
|
1144
1167
|
setDepth: (v) => call.setDepth(v),
|
|
1145
1168
|
retryCamera: () => call.retryCamera(),
|
|
1146
|
-
sendHint: (z) => call.sendHint(z),
|
|
1147
1169
|
setMono3D: (on) => call.setMono3D(on),
|
|
1148
|
-
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(),
|
|
1149
1174
|
on(type, cb) {
|
|
1150
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
|
+
}
|
|
1151
1181
|
if (!call.listeners.has(type)) call.listeners.set(type, new Set());
|
|
1152
1182
|
call.listeners.get(type).add(cb);
|
|
1153
1183
|
return () => call.listeners.get(type)?.delete(cb);
|
|
@@ -1155,14 +1185,28 @@ class Call {
|
|
|
1155
1185
|
off(type, cb) {
|
|
1156
1186
|
call.listeners.get(type)?.delete(cb);
|
|
1157
1187
|
},
|
|
1158
|
-
/** Diagnostics: the transport (mesh) and a way to simulate a dropped connection. */
|
|
1159
|
-
_debug: {
|
|
1160
|
-
get transport() {
|
|
1161
|
-
return call.transport;
|
|
1162
|
-
},
|
|
1163
|
-
kill: (id) => call.transport?._debugKill(id),
|
|
1164
|
-
},
|
|
1165
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;
|
|
1166
1210
|
}
|
|
1167
1211
|
}
|
|
1168
1212
|
|
|
@@ -1192,7 +1236,7 @@ function paintFlat(canvas, video, { eyeHalf = null, mirror = false } = {}) {
|
|
|
1192
1236
|
const H = video.videoHeight;
|
|
1193
1237
|
if (!W || !H || (video.readyState || 0) < 2) return;
|
|
1194
1238
|
const eyeW = eyeHalf === null ? W : W / 2;
|
|
1195
|
-
const r = eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
|
|
1239
|
+
const r = wire.eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
|
|
1196
1240
|
g.save();
|
|
1197
1241
|
if (mirror) {
|
|
1198
1242
|
g.translate(canvas.width, 0);
|
|
@@ -1219,6 +1263,17 @@ class Tile {
|
|
|
1219
1263
|
this.speaking = false;
|
|
1220
1264
|
this.quality = null;
|
|
1221
1265
|
this.conv = createConvergence();
|
|
1266
|
+
this.autoTrack = createDisparityTrack();
|
|
1267
|
+
this.autoFocus = createFocusTracker();
|
|
1268
|
+
this.autoAt = 0;
|
|
1269
|
+
this.autoCanvas = null;
|
|
1270
|
+
this.autoLocked = false;
|
|
1271
|
+
this.autoStats = { n: 0, ms: 0, readMs: 0, method: null, via: null };
|
|
1272
|
+
this.autoPending = false;
|
|
1273
|
+
// Bumped on every stream change and on destroy(): an async copyTo() that resolves after either
|
|
1274
|
+
// belongs to a stream this tile no longer shows, and is dropped (review of #92).
|
|
1275
|
+
this.autoGen = 0;
|
|
1276
|
+
this.destroyed = false;
|
|
1222
1277
|
this.conv.depth = call.depth;
|
|
1223
1278
|
this.hintGate = rateGate(HINT_MAX_HZ);
|
|
1224
1279
|
this.layerFails = 0;
|
|
@@ -1238,11 +1293,12 @@ class Tile {
|
|
|
1238
1293
|
this.video.addEventListener('resize', () => this._maybeRoute());
|
|
1239
1294
|
this.canvas = el('canvas');
|
|
1240
1295
|
this.cover = el('div', { class: 'dxr-call-cover dxr-call-hidden' });
|
|
1241
|
-
this.badge = el('div', { class: 'dxr-call-badge' });
|
|
1242
|
-
this.plate = el('div', { class: 'dxr-call-state', text: '
|
|
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') });
|
|
1243
1298
|
this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.cover, this.badge, this.plate]);
|
|
1244
1299
|
this.stage.style.aspectRatio = String(call.o.tileAspect);
|
|
1245
|
-
|
|
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' })]);
|
|
1246
1302
|
if (!call.o.ui) {
|
|
1247
1303
|
show(this.badge, false);
|
|
1248
1304
|
}
|
|
@@ -1250,34 +1306,59 @@ class Tile {
|
|
|
1250
1306
|
this._renderBadge();
|
|
1251
1307
|
}
|
|
1252
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()`. */
|
|
1253
1315
|
info() {
|
|
1254
1316
|
return Object.freeze({
|
|
1255
1317
|
id: this.id,
|
|
1256
1318
|
format: this.format || 'mono',
|
|
1257
|
-
|
|
1319
|
+
display: this.display(),
|
|
1258
1320
|
state: this.conn,
|
|
1259
1321
|
muted: this.remote.muted,
|
|
1260
1322
|
cameraOff: this.remote.cameraOff,
|
|
1261
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,
|
|
1262
1332
|
rectified: !!(this.hello && this.hello.rectified),
|
|
1263
1333
|
hello: this.hello,
|
|
1264
1334
|
quality: this.quality,
|
|
1265
1335
|
convergencePx: this.conv.current,
|
|
1336
|
+
// Auto-convergence: the measured disparity (source px, null until a lock) and the mean cost of
|
|
1337
|
+
// one measurement including the frame readback.
|
|
1338
|
+
autoConverge: {
|
|
1339
|
+
disparityPx: this.conv.measuredPx,
|
|
1340
|
+
ms: this.autoStats.n ? this.autoStats.ms / this.autoStats.n : null,
|
|
1341
|
+
readMs: this.autoStats.n ? this.autoStats.readMs / this.autoStats.n : null,
|
|
1342
|
+
method: this.autoStats.method,
|
|
1343
|
+
via: this.autoStats.via,
|
|
1344
|
+
},
|
|
1266
1345
|
lift: this.lifted
|
|
1267
|
-
?
|
|
1346
|
+
? {
|
|
1268
1347
|
live: this.liftLive,
|
|
1269
1348
|
native: !!(this.liftHandle && this.liftHandle.native),
|
|
1270
1349
|
priority: this.call.liftPool ? this.call.liftPool.priority(this.id) : null,
|
|
1271
1350
|
state: this.liftHandle ? this.liftHandle.state || null : 'pending',
|
|
1272
|
-
}
|
|
1351
|
+
}
|
|
1273
1352
|
: null,
|
|
1274
|
-
|
|
1353
|
+
element: this.el,
|
|
1354
|
+
};
|
|
1275
1355
|
}
|
|
1276
1356
|
|
|
1277
1357
|
setStream(stream) {
|
|
1278
1358
|
if (this.video.srcObject !== stream) {
|
|
1279
1359
|
this.video.srcObject = stream;
|
|
1280
1360
|
this.audio.srcObject = stream;
|
|
1361
|
+
this._resetAutoConverge(); // a new stream may be a new camera: measure afresh
|
|
1281
1362
|
}
|
|
1282
1363
|
this.video.play().catch(() => {});
|
|
1283
1364
|
this.audio.play().catch(() => {});
|
|
@@ -1325,7 +1406,7 @@ class Tile {
|
|
|
1325
1406
|
|
|
1326
1407
|
setConn(st) {
|
|
1327
1408
|
if (st === 'unreachable' && this.conn !== 'unreachable') {
|
|
1328
|
-
this.call.error('unreachable', `${
|
|
1409
|
+
this.call.error('unreachable', `${this.call.t('unreachable')}. Still retrying in the background.`, Object.assign(new Error('unreachable'), { peer: this.id }));
|
|
1329
1410
|
}
|
|
1330
1411
|
this.conn = st;
|
|
1331
1412
|
if (st === 'connected' && this.leaveTimer) {
|
|
@@ -1336,26 +1417,28 @@ class Tile {
|
|
|
1336
1417
|
}
|
|
1337
1418
|
|
|
1338
1419
|
_renderPlate() {
|
|
1420
|
+
const s = this.call.t;
|
|
1339
1421
|
const t =
|
|
1340
1422
|
this.conn === 'left'
|
|
1341
|
-
? '
|
|
1423
|
+
? s('leftCall')
|
|
1342
1424
|
: this.conn === 'unreachable'
|
|
1343
|
-
?
|
|
1425
|
+
? s('unreachable')
|
|
1344
1426
|
: this.conn === 'reconnecting'
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1427
|
+
? s('reconnecting')
|
|
1428
|
+
: this.conn !== 'connected'
|
|
1429
|
+
? s('connecting')
|
|
1430
|
+
: this.remote.cameraOff
|
|
1431
|
+
? s('cameraOffPlate')
|
|
1432
|
+
: null;
|
|
1351
1433
|
this.plate.textContent = t || '';
|
|
1352
1434
|
show(this.plate, !!t);
|
|
1353
1435
|
}
|
|
1354
1436
|
|
|
1355
1437
|
_renderBadge() {
|
|
1356
|
-
const
|
|
1438
|
+
const s = this.call.t;
|
|
1439
|
+
const b = this.route === 'woven-sbs' ? s('badge3D') : this.route === 'lifted' ? s('badge2D3D') : s('badge2D');
|
|
1357
1440
|
// A lifted tile whose lift is still loading reads "2D→3D…" (it is painted flat meanwhile).
|
|
1358
|
-
const text = !this.route ? '
|
|
1441
|
+
const text = !this.route ? s('badgePending') : this.route === 'lifted' && !this.liftLive ? `${b}${s('badgePending')}` : b;
|
|
1359
1442
|
this.badge.replaceChildren(el('b', { text }));
|
|
1360
1443
|
this.badge.dataset.route = this.route || '';
|
|
1361
1444
|
if (this.remote.muted) this.badge.insertAdjacentHTML('beforeend', ICONS.mutedSmall);
|
|
@@ -1373,7 +1456,7 @@ class Tile {
|
|
|
1373
1456
|
const r =
|
|
1374
1457
|
this.forceFlat && this.format === 'sbs'
|
|
1375
1458
|
? { route: 'flat-left' }
|
|
1376
|
-
: routeFor({
|
|
1459
|
+
: wire.routeFor({
|
|
1377
1460
|
format: this.format,
|
|
1378
1461
|
woven: call.woven,
|
|
1379
1462
|
mono3D: call.mono3DOn ? 'auto' : 'off',
|
|
@@ -1383,6 +1466,8 @@ class Tile {
|
|
|
1383
1466
|
});
|
|
1384
1467
|
// First mono peer on a woven wall and lift not asked for yet (e.g. setMono3D(true) later).
|
|
1385
1468
|
if (this.format !== 'sbs' && call.woven && call.mono3DOn && !call._liftP) call._ensureLift();
|
|
1469
|
+
// A mono peer on a 3D display that stays flat because the lift MODULE is missing: say so.
|
|
1470
|
+
if (this.format !== 'sbs' && call.woven && call.liftMissing) call._warnLiftMissing();
|
|
1386
1471
|
const key = `${r.route}|${this.format}`;
|
|
1387
1472
|
if (!force && key === this.routedKey) return;
|
|
1388
1473
|
this.routedKey = key;
|
|
@@ -1391,7 +1476,9 @@ class Tile {
|
|
|
1391
1476
|
if (r.route === 'woven-sbs') this._registerWoven();
|
|
1392
1477
|
else if (r.route === 'lifted') this._registerLifted();
|
|
1393
1478
|
call.log('route', { peer: this.id, route: r.route, format: this.format, mono3d: r.mono3d || null });
|
|
1394
|
-
call.emit('
|
|
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 });
|
|
1395
1482
|
this._renderBadge();
|
|
1396
1483
|
}
|
|
1397
1484
|
|
|
@@ -1572,15 +1659,123 @@ class Tile {
|
|
|
1572
1659
|
c.width = 2 * outW;
|
|
1573
1660
|
c.height = outH;
|
|
1574
1661
|
}
|
|
1662
|
+
if (this.call.o.autoConverge) this._sampleDisparity(v, W, H);
|
|
1575
1663
|
const shift = this.conv.step(eyeW);
|
|
1576
1664
|
const g = c.getContext('2d');
|
|
1577
1665
|
for (const eye of [0, 1]) {
|
|
1578
|
-
const r = eyeCropRect(eyeW, H, A, shift, eye);
|
|
1666
|
+
const r = wire.eyeCropRect(eyeW, H, A, shift, eye);
|
|
1579
1667
|
g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
|
|
1580
1668
|
}
|
|
1581
1669
|
}
|
|
1582
1670
|
|
|
1671
|
+
/**
|
|
1672
|
+
* Auto-convergence: a few times a second, measure the disparity of the point between the remote
|
|
1673
|
+
* person's eyes on a small grayscale copy of the SOURCE frame (before our shift, so there is no
|
|
1674
|
+
* feedback loop) and hand it to the convergence state, which halves it per eye and low-passes it.
|
|
1675
|
+
* A failed measurement holds the last good value.
|
|
1676
|
+
*/
|
|
1677
|
+
_sampleDisparity(v, W, H) {
|
|
1678
|
+
const now = performance.now();
|
|
1679
|
+
if (this.autoPending || now - this.autoAt < AUTO_CONV_INTERVAL_MS) return;
|
|
1680
|
+
this.autoAt = now;
|
|
1681
|
+
// Preferred: WebCodecs. `new VideoFrame(video)` + an ASYNC copyTo() of the decoded frame, reading
|
|
1682
|
+
// the luma (Y) plane directly: no synchronous GPU->CPU canvas readback on the paint path (that
|
|
1683
|
+
// readback, not the matching, was ~25 ms of a ~30 ms measurement).
|
|
1684
|
+
if (typeof VideoFrame === 'function') {
|
|
1685
|
+
let frame = null;
|
|
1686
|
+
try {
|
|
1687
|
+
frame = new VideoFrame(v);
|
|
1688
|
+
} catch {
|
|
1689
|
+
frame = null;
|
|
1690
|
+
}
|
|
1691
|
+
if (frame && Y_PLANE_FORMATS.has(frame.format)) {
|
|
1692
|
+
this.autoPending = true;
|
|
1693
|
+
const gen = this.autoGen;
|
|
1694
|
+
const fw = frame.visibleRect ? frame.visibleRect.width : frame.displayWidth;
|
|
1695
|
+
const fh = frame.visibleRect ? frame.visibleRect.height : frame.displayHeight;
|
|
1696
|
+
const buf = new Uint8Array(frame.allocationSize());
|
|
1697
|
+
frame
|
|
1698
|
+
.copyTo(buf)
|
|
1699
|
+
.then((layout) => {
|
|
1700
|
+
if (gen !== this.autoGen) return; // the stream changed or the tile is gone: stale frame
|
|
1701
|
+
const t0 = performance.now();
|
|
1702
|
+
const f = Math.max(1, Math.ceil(fw / 2 / AUTO_CONV_EYE_WIDTH));
|
|
1703
|
+
const ds = downsampleLuma(buf, layout[0].offset, layout[0].stride, fw, fh, f);
|
|
1704
|
+
this._applyMeasurement(ds.img, ds.w, ds.h, ds.w / fw, now, performance.now() - t0, 'videoframe');
|
|
1705
|
+
})
|
|
1706
|
+
.catch(() => {
|
|
1707
|
+
if (gen === this.autoGen) this._applyMeasurement(null, 0, 0, 1, now, 0, 'videoframe');
|
|
1708
|
+
})
|
|
1709
|
+
.finally(() => {
|
|
1710
|
+
frame.close();
|
|
1711
|
+
this.autoPending = false;
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (frame) frame.close();
|
|
1716
|
+
}
|
|
1717
|
+
// Fallback: a small canvas + getImageData (synchronous readback).
|
|
1718
|
+
const s = Math.min(1, AUTO_CONV_EYE_WIDTH / (W / 2));
|
|
1719
|
+
const w = Math.max(2, Math.round((W * s) / 2) * 2);
|
|
1720
|
+
const h = Math.max(2, Math.round(H * s));
|
|
1721
|
+
if (!this.autoCanvas) this.autoCanvas = document.createElement('canvas');
|
|
1722
|
+
const c = this.autoCanvas;
|
|
1723
|
+
if (c.width !== w || c.height !== h) {
|
|
1724
|
+
c.width = w;
|
|
1725
|
+
c.height = h;
|
|
1726
|
+
}
|
|
1727
|
+
const g = c.getContext('2d', { willReadFrequently: true });
|
|
1728
|
+
let img = null;
|
|
1729
|
+
try {
|
|
1730
|
+
g.drawImage(v, 0, 0, w, h);
|
|
1731
|
+
img = lumaFromRgba(g.getImageData(0, 0, w, h).data, w * h);
|
|
1732
|
+
} catch {
|
|
1733
|
+
img = null; // a frame that cannot be read (tainted, not decoded yet): skip it
|
|
1734
|
+
}
|
|
1735
|
+
this._applyMeasurement(img, w, h, w / W, now, performance.now() - now, 'canvas');
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Measure one grabbed frame and hand the disparity to the convergence state (which halves it per
|
|
1740
|
+
* eye and low-passes it). A failed measurement holds the last good value. `readMs` = main-thread
|
|
1741
|
+
* time spent getting the pixels.
|
|
1742
|
+
*/
|
|
1743
|
+
_applyMeasurement(img, w, h, s, at, readMs, via) {
|
|
1744
|
+
if (this.destroyed) return;
|
|
1745
|
+
const t = performance.now();
|
|
1746
|
+
const m = img ? this.autoFocus.measure(img, w, h, at) : null;
|
|
1747
|
+
const d = this.autoTrack.push(m ? m.d / s : null);
|
|
1748
|
+
this.conv.measuredPx = d;
|
|
1749
|
+
this.autoStats.n++;
|
|
1750
|
+
this.autoStats.readMs += readMs;
|
|
1751
|
+
this.autoStats.ms += readMs + (performance.now() - t);
|
|
1752
|
+
this.autoStats.method = m ? m.method : null;
|
|
1753
|
+
this.autoStats.via = via;
|
|
1754
|
+
if (m && !this.autoLocked) {
|
|
1755
|
+
this.autoLocked = true;
|
|
1756
|
+
this.call.log('auto-converge', {
|
|
1757
|
+
peer: this.id,
|
|
1758
|
+
disparityPx: Math.round(d * 10) / 10,
|
|
1759
|
+
at: { x: Math.round(m.x / s), y: Math.round(m.y / s) },
|
|
1760
|
+
ncc: Math.round(m.c * 100) / 100,
|
|
1761
|
+
method: m.method,
|
|
1762
|
+
via,
|
|
1763
|
+
ms: Math.round(readMs + (performance.now() - t)),
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
_resetAutoConverge() {
|
|
1769
|
+
this.autoGen++;
|
|
1770
|
+
this.autoTrack.reset();
|
|
1771
|
+
this.autoFocus.reset();
|
|
1772
|
+
this.conv.measuredPx = null;
|
|
1773
|
+
this.autoLocked = false;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1583
1776
|
destroy() {
|
|
1777
|
+
this.destroyed = true;
|
|
1778
|
+
this.autoGen++;
|
|
1584
1779
|
clearTimeout(this.helloTimer);
|
|
1585
1780
|
clearTimeout(this.layerTimer);
|
|
1586
1781
|
clearTimeout(this.leaveTimer);
|
|
@@ -1594,102 +1789,198 @@ class Tile {
|
|
|
1594
1789
|
|
|
1595
1790
|
// ── the self view ─────────────────────────────────────────────────────────────────────────
|
|
1596
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
|
+
*/
|
|
1597
1799
|
class SelfTile {
|
|
1598
1800
|
constructor(call, slot) {
|
|
1599
1801
|
this.call = call;
|
|
1600
1802
|
this.canvas = el('canvas');
|
|
1601
|
-
this.badge = el('div', { class: 'dxr-call-badge' });
|
|
1602
|
-
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' });
|
|
1603
1805
|
this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.badge, this.plate]);
|
|
1604
1806
|
this.stage.style.aspectRatio = String(call.o.tileAspect);
|
|
1605
|
-
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' })]);
|
|
1606
1808
|
slot.appendChild(this.el);
|
|
1607
|
-
this.
|
|
1608
|
-
this.
|
|
1609
|
-
|
|
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;
|
|
1610
1816
|
}
|
|
1611
1817
|
|
|
1612
1818
|
update() {
|
|
1613
1819
|
const l = this.call.local;
|
|
1820
|
+
const t = this.call.t;
|
|
1614
1821
|
const noCam = !l || !l.videoTrack;
|
|
1615
|
-
this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ?
|
|
1822
|
+
this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ? t('cameraBusy') : this.call.camStatus === 'pending' ? '' : t('noCamera')) : this.call.camOff ? t('cameraOffPlate') : '';
|
|
1616
1823
|
show(this.plate, noCam || this.call.camOff);
|
|
1617
1824
|
this.reroute(false);
|
|
1618
1825
|
}
|
|
1619
1826
|
|
|
1620
1827
|
reroute(force) {
|
|
1621
1828
|
const l = this.call.local;
|
|
1622
|
-
|
|
1623
|
-
const
|
|
1624
|
-
if (!
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
this.
|
|
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 });
|
|
1631
1840
|
}
|
|
1632
1841
|
this._badge();
|
|
1633
1842
|
}
|
|
1634
1843
|
|
|
1635
1844
|
_badge() {
|
|
1636
|
-
|
|
1845
|
+
const t = this.call.t;
|
|
1846
|
+
this.badge.replaceChildren(el('b', { text: `${t('you')} · ${this.route === 'woven-sbs' ? t('badge3D') : t('badge2D')}` }));
|
|
1637
1847
|
}
|
|
1638
1848
|
|
|
1639
1849
|
onWallLost() {
|
|
1640
|
-
this.
|
|
1641
|
-
this.
|
|
1850
|
+
this.view?._onWallLost();
|
|
1851
|
+
this._badge();
|
|
1642
1852
|
}
|
|
1643
1853
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
/* ignore */
|
|
1650
|
-
}
|
|
1651
|
-
this.handle = null;
|
|
1652
|
-
}
|
|
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;
|
|
1653
1859
|
}
|
|
1654
1860
|
|
|
1655
1861
|
setSpeaking(on) {
|
|
1656
1862
|
this.el.classList.toggle('dxr-call-tile--speaking', on);
|
|
1657
1863
|
}
|
|
1864
|
+
}
|
|
1658
1865
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
+
});
|
|
1664
1894
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
const v = this.call.local.video;
|
|
1673
|
-
const W = v.videoWidth;
|
|
1674
|
-
const H = v.videoHeight;
|
|
1675
|
-
if (!W || !H || (v.readyState || 0) < 2) return;
|
|
1676
|
-
const A = this.call.o.tileAspect;
|
|
1677
|
-
const eyeW = W / 2;
|
|
1678
|
-
const { w: outW, h: outH } = eyeOutputSize(eyeW, H, A);
|
|
1679
|
-
const c = this.mirror;
|
|
1680
|
-
if (c.width !== 2 * outW || c.height !== outH) {
|
|
1681
|
-
c.width = 2 * outW;
|
|
1682
|
-
c.height = outH;
|
|
1683
|
-
}
|
|
1684
|
-
const g = c.getContext('2d');
|
|
1685
|
-
const r = eyeCropRect(eyeW, H, A, 0, 0);
|
|
1686
|
-
for (const op of mirrorSwapOps(W, H)) {
|
|
1687
|
-
const dx = op.dx === 0 ? 0 : outW;
|
|
1688
|
-
g.save();
|
|
1689
|
-
g.translate(dx + outW, 0);
|
|
1690
|
-
g.scale(-1, 1);
|
|
1691
|
-
g.drawImage(v, op.sx + r.sx, r.sy, r.sw, r.sh, 0, 0, outW, outH);
|
|
1692
|
-
g.restore();
|
|
1693
|
-
}
|
|
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
|
+
});
|
|
1694
1902
|
}
|
|
1903
|
+
return value;
|
|
1695
1904
|
}
|
|
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
|
+
|
|
1983
|
+
// `<dxr-call>` — the one-line path as markup — is registered by importing this entry (RFC 0003 §1;
|
|
1984
|
+
// the ONE side effect of this module, declared in package.json `sideEffects`). A no-op where there
|
|
1985
|
+
// is no `customElements` registry (Node, workers) or the name is already defined.
|
|
1986
|
+
defineCallElement();
|