@displayxr/inline3d 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +4 -0
- package/call.d.ts +269 -238
- package/camera.d.ts +257 -0
- package/dist/call.js +17 -12
- package/js/call/capture.js +4 -212
- package/js/call/disparity.js +4 -432
- package/js/call/element.js +14 -10
- package/js/call/options.js +132 -0
- package/js/call/ui.js +106 -9
- package/js/call/wire.js +8 -153
- 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.js +404 -319
- package/js/inline3d-camera.js +678 -0
- package/js/inline3d-splat-deferred.js +153 -0
- package/js/inline3d-splat-pc-entry.js +45 -0
- package/js/inline3d-splat.js +2 -145
- package/package.json +19 -1
- package/splat-playcanvas.d.ts +4 -0
package/js/inline3d-call.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
103
|
-
import
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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,
|
|
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 ? `${
|
|
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.
|
|
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.
|
|
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
|
|
357
|
+
const t = new MeshTransportImpl({
|
|
441
358
|
signaling: this.o.signaling,
|
|
442
359
|
id: this.id,
|
|
443
360
|
maxPeers: this.o.maxPeers,
|
|
@@ -480,7 +397,7 @@ class Call {
|
|
|
480
397
|
// The grid is sized so the whole call block fits ONE viewport, but only if it starts at the
|
|
481
398
|
// top: a tile below the fold is withheld from the weave (browser#167), so its layer goes
|
|
482
399
|
// live late. Bring the block into view once, on join.
|
|
483
|
-
if (this.o.
|
|
400
|
+
if (this.o.chrome) {
|
|
484
401
|
try {
|
|
485
402
|
this.container.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
|
486
403
|
this._fitGrid();
|
|
@@ -489,7 +406,7 @@ class Call {
|
|
|
489
406
|
}
|
|
490
407
|
}
|
|
491
408
|
this._startStats();
|
|
492
|
-
this._pagehide = () => this.leave();
|
|
409
|
+
this._pagehide = () => this.leave('pagehide');
|
|
493
410
|
globalThis.addEventListener?.('pagehide', this._pagehide);
|
|
494
411
|
this.log('joined', { id: this.id, peers: this.tiles.size });
|
|
495
412
|
this.emit('joined', { room: this.room, id: this.id });
|
|
@@ -546,7 +463,7 @@ class Call {
|
|
|
546
463
|
tile.onChannelOpen();
|
|
547
464
|
return;
|
|
548
465
|
}
|
|
549
|
-
const h = normalizeHello(msg);
|
|
466
|
+
const h = wire.normalizeHello(msg);
|
|
550
467
|
if (h) return tile.setHello(h);
|
|
551
468
|
const hint = normalizeHint(msg);
|
|
552
469
|
if (hint) return tile.setHint(hint);
|
|
@@ -558,8 +475,8 @@ class Call {
|
|
|
558
475
|
|
|
559
476
|
inviteLink() {
|
|
560
477
|
if (!this.room) return null;
|
|
561
|
-
const base = this.o.
|
|
562
|
-
return buildInviteLink(base, this.room);
|
|
478
|
+
const base = this.o.invite.base || (globalThis.location ? location.href : '');
|
|
479
|
+
return wire.buildInviteLink(base, this.room);
|
|
563
480
|
}
|
|
564
481
|
|
|
565
482
|
mute(on) {
|
|
@@ -571,7 +488,7 @@ class Call {
|
|
|
571
488
|
return this.muted;
|
|
572
489
|
}
|
|
573
490
|
|
|
574
|
-
|
|
491
|
+
setCameraOff(on) {
|
|
575
492
|
this.camOff = on === undefined ? !this.camOff : !!on;
|
|
576
493
|
if (this.local?.videoTrack) this.local.videoTrack.enabled = !this.camOff;
|
|
577
494
|
this.transport?.broadcast(this.stateMsg());
|
|
@@ -615,7 +532,7 @@ class Call {
|
|
|
615
532
|
return true;
|
|
616
533
|
}
|
|
617
534
|
|
|
618
|
-
leave() {
|
|
535
|
+
leave(reason = 'left') {
|
|
619
536
|
if (this.state === 'left') return;
|
|
620
537
|
globalThis.removeEventListener?.('pagehide', this._pagehide);
|
|
621
538
|
this.transport?.stop();
|
|
@@ -626,19 +543,20 @@ class Call {
|
|
|
626
543
|
clearInterval(this._statsTimer);
|
|
627
544
|
this._setSpeaker(null);
|
|
628
545
|
if (this.local) {
|
|
546
|
+
this._camEndedOff?.();
|
|
547
|
+
this._camEndedOff = null;
|
|
629
548
|
this.local.stream.getTracks().forEach((t) => t.stop());
|
|
630
|
-
|
|
549
|
+
// A camera THIS module opened is closed; a page's StereoCamera / MediaStream is left running.
|
|
550
|
+
if (this.local.owned && this.local.cam) this.local.cam.close();
|
|
551
|
+
else if (this.local.cam) this.self?.detach();
|
|
631
552
|
this.local.videoTrack = null;
|
|
632
553
|
this.local.audioTrack = null;
|
|
633
554
|
}
|
|
634
|
-
|
|
635
|
-
this.self.unregister();
|
|
636
|
-
this.self.route = null; // a rejoin re-registers it
|
|
637
|
-
}
|
|
555
|
+
this.self?.detach(); // a rejoin re-attaches it
|
|
638
556
|
this._unhookWall();
|
|
639
557
|
this._setState('left');
|
|
640
558
|
this._layout();
|
|
641
|
-
this.emit('left', { room: this.room });
|
|
559
|
+
this.emit('left', { room: this.room, reason });
|
|
642
560
|
}
|
|
643
561
|
|
|
644
562
|
// ── the wall: session loss and recovery ───────────────────────────────────────────────
|
|
@@ -672,7 +590,7 @@ class Call {
|
|
|
672
590
|
this.weaveLive = true; // nothing to wait on
|
|
673
591
|
return;
|
|
674
592
|
}
|
|
675
|
-
const gate = createLiveGate();
|
|
593
|
+
const gate = wire.createLiveGate();
|
|
676
594
|
const t0 = performance.now();
|
|
677
595
|
const onFrame = (_t, f) => {
|
|
678
596
|
if (w !== this.wall || !this.wallLive || this.state === 'left') return;
|
|
@@ -727,7 +645,7 @@ class Call {
|
|
|
727
645
|
this.log('session-ended', { byPage: hook.pageClosed });
|
|
728
646
|
for (const t of this.tiles.values()) t.onWallLost();
|
|
729
647
|
this.self?.onWallLost();
|
|
730
|
-
if (hook.pageClosed ||
|
|
648
|
+
if (hook.pageClosed || this.state === 'left') {
|
|
731
649
|
this._unhookWall();
|
|
732
650
|
return;
|
|
733
651
|
}
|
|
@@ -744,7 +662,7 @@ class Call {
|
|
|
744
662
|
try {
|
|
745
663
|
// The shared manager: if the page has already re-opened its own wall, join it rather than
|
|
746
664
|
// opening a second session (rule 1); else this creates the document's new one.
|
|
747
|
-
w = await sharedInline3D(
|
|
665
|
+
w = await sharedInline3D();
|
|
748
666
|
} catch {
|
|
749
667
|
w = null;
|
|
750
668
|
}
|
|
@@ -755,7 +673,6 @@ class Call {
|
|
|
755
673
|
this.log('session-recovered', { attempt });
|
|
756
674
|
for (const t of this.tiles.values()) t.reroute(true);
|
|
757
675
|
this.self?.reroute(true);
|
|
758
|
-
this.emit('session', { wall: w });
|
|
759
676
|
}, backoffMs(attempt, { baseMs: 1000, maxMs: 15000 }));
|
|
760
677
|
}
|
|
761
678
|
|
|
@@ -765,7 +682,6 @@ class Call {
|
|
|
765
682
|
let last = 0;
|
|
766
683
|
const tick = (now) => {
|
|
767
684
|
this._raf = requestAnimationFrame(tick);
|
|
768
|
-
this.self?.paint();
|
|
769
685
|
for (const t of this.tiles.values()) t.paint();
|
|
770
686
|
if (last && this.liftPool && this.liftPool.anyWeb) this._watchFrames(now - last);
|
|
771
687
|
last = now;
|
|
@@ -797,10 +713,10 @@ class Call {
|
|
|
797
713
|
const lo = this.o.liftOptions;
|
|
798
714
|
// liftCapabilities probes the same model source the lifts will use.
|
|
799
715
|
const capsOpts = { webFallback: true, ...(lo && lo.models ? { models: lo.models } : {}) };
|
|
800
|
-
this._liftP = resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
|
|
716
|
+
this._liftP = liftMod.resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
|
|
801
717
|
this.liftApi = r;
|
|
802
718
|
if (r.lift) {
|
|
803
|
-
this.liftPool = createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
|
|
719
|
+
this.liftPool = liftMod.createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
|
|
804
720
|
this.liftPool.setDepth(this.depth);
|
|
805
721
|
this.liftPool.setSpeaker(this.speakerId);
|
|
806
722
|
if (!r.caps && r.capabilities) {
|
|
@@ -865,14 +781,15 @@ class Call {
|
|
|
865
781
|
/** What the lobby says a mono participant will look like here. */
|
|
866
782
|
_liftHint() {
|
|
867
783
|
if (!this.wallLive) return '';
|
|
868
|
-
|
|
784
|
+
const t = this.t;
|
|
785
|
+
if (!this.mono3DOn) return t('liftOff');
|
|
869
786
|
const r = this.liftApi;
|
|
870
|
-
if (!r) return '
|
|
871
|
-
if (!r.lift) return this.liftMissing ? '
|
|
787
|
+
if (!r) return t('liftChecking');
|
|
788
|
+
if (!r.lift) return this.liftMissing ? t('liftMissing') : t('liftNoProvider');
|
|
872
789
|
const c = r.caps;
|
|
873
|
-
if (c && c.native) return
|
|
874
|
-
if (c && c.webFallback && c.webFallback.webgpu) return '
|
|
875
|
-
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');
|
|
876
793
|
}
|
|
877
794
|
|
|
878
795
|
_mono3DInfo() {
|
|
@@ -989,7 +906,7 @@ class Call {
|
|
|
989
906
|
*/
|
|
990
907
|
_fitGrid() {
|
|
991
908
|
const ui = this.ui;
|
|
992
|
-
if (!ui || !this.o.
|
|
909
|
+
if (!ui || !this.o.chrome || typeof globalThis.innerHeight !== 'number') return;
|
|
993
910
|
const host = this.container.getBoundingClientRect?.();
|
|
994
911
|
const grid = ui.grid.getBoundingClientRect?.();
|
|
995
912
|
if (!host || !grid || !grid.height) return;
|
|
@@ -1010,55 +927,59 @@ class Call {
|
|
|
1010
927
|
|
|
1011
928
|
_buildDom() {
|
|
1012
929
|
const c = this.container;
|
|
930
|
+
const t = this.t;
|
|
1013
931
|
injectCallStyle(c.ownerDocument || document);
|
|
1014
932
|
c.classList.add('dxr-call-host');
|
|
1015
|
-
|
|
933
|
+
// theme → CSS custom properties on the host (chrome only: no variable can reach a tile).
|
|
934
|
+
for (const [prop, v] of Object.entries(this.o.cssVars)) c.style.setProperty(prop, v);
|
|
1016
935
|
const ui = (this.ui = {});
|
|
1017
|
-
ui.banner = el('div', { class: 'dxr-call-banner dxr-call-hidden' }, [
|
|
1018
|
-
el('span', { text: '
|
|
1019
|
-
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') }),
|
|
1020
939
|
]);
|
|
1021
|
-
ui.grid = el('div', { class: 'dxr-call-grid', 'data-layout': this.o.layout, 'data-n': '0' });
|
|
1022
|
-
ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
|
|
1023
|
-
ui.selfSlot = el('div', { class: 'dxr-call-self' });
|
|
1024
|
-
ui.bar = el('div', { class: 'dxr-call-bar' });
|
|
940
|
+
ui.grid = el('div', { class: 'dxr-call-grid', part: 'grid', 'data-layout': this.o.layout, 'data-n': '0' });
|
|
941
|
+
ui.panel = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'lobby' });
|
|
942
|
+
ui.selfSlot = el('div', { class: 'dxr-call-self', part: 'self' });
|
|
943
|
+
ui.bar = el('div', { class: 'dxr-call-bar', part: 'bar' });
|
|
1025
944
|
ui.foot = el('div', { class: 'dxr-call-foot' }, [ui.selfSlot, ui.bar]);
|
|
1026
|
-
ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden' });
|
|
1027
|
-
if (!this.o.
|
|
945
|
+
ui.invite = el('div', { class: 'dxr-call-panel dxr-call-hidden', part: 'invite' });
|
|
946
|
+
if (!this.o.chrome) {
|
|
1028
947
|
show(ui.bar, false);
|
|
1029
948
|
}
|
|
1030
949
|
c.append(ui.banner, ui.panel, ui.grid, ui.invite, ui.foot);
|
|
1031
950
|
if (this.o.selfView) this.self = new SelfTile(this, ui.selfSlot);
|
|
1032
951
|
else show(ui.selfSlot, false);
|
|
1033
|
-
if (this.o.
|
|
952
|
+
if (this.o.chrome) this._buildBar();
|
|
1034
953
|
}
|
|
1035
954
|
|
|
1036
955
|
_buildBar() {
|
|
1037
956
|
const ui = this.ui;
|
|
957
|
+
const t = this.t;
|
|
1038
958
|
const ib = (label, icon, onclick, extra = '') => el('button', { class: `dxr-call-ib ${extra}`, type: 'button', 'aria-label': label, title: label, html: icon, onclick });
|
|
1039
|
-
ui.mic = ib('
|
|
1040
|
-
ui.cam = ib('
|
|
1041
|
-
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') });
|
|
1042
962
|
ui.depth.addEventListener('input', () => this.setDepth(+ui.depth.value));
|
|
1043
963
|
ui.depth.addEventListener('dblclick', () => this.setDepth(0));
|
|
1044
|
-
ui.inviteBtn = ib('
|
|
964
|
+
ui.inviteBtn = ib(t('invite'), ICONS.link, () => {
|
|
1045
965
|
this._showInvite = !this._showInvite;
|
|
1046
966
|
this._layout();
|
|
1047
967
|
});
|
|
1048
|
-
ui.leave = ib('
|
|
1049
|
-
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);
|
|
1050
970
|
}
|
|
1051
971
|
|
|
1052
972
|
_refreshBar() {
|
|
1053
973
|
const ui = this.ui;
|
|
1054
974
|
if (!ui || !ui.mic) return;
|
|
975
|
+
const t = this.t;
|
|
1055
976
|
ui.mic.innerHTML = this.muted ? ICONS.micOff : ICONS.mic;
|
|
1056
977
|
ui.mic.setAttribute('aria-pressed', String(this.muted));
|
|
1057
|
-
ui.mic.title = this.muted ? '
|
|
978
|
+
ui.mic.title = this.muted ? t('unmute') : t('mute');
|
|
1058
979
|
const noCam = !this.local?.videoTrack;
|
|
1059
980
|
ui.cam.innerHTML = this.camOff || noCam ? ICONS.camOff : ICONS.cam;
|
|
1060
981
|
ui.cam.setAttribute('aria-pressed', String(this.camOff || noCam));
|
|
1061
|
-
ui.cam.title = noCam ? (this.camStatus === 'busy' ?
|
|
982
|
+
ui.cam.title = noCam ? (this.camStatus === 'busy' ? t('cameraBusyRetry', { cameraBusy: t('cameraBusy') }) : t('cameraRetry')) : this.camOff ? t('cameraOn') : t('cameraOff');
|
|
1062
983
|
}
|
|
1063
984
|
|
|
1064
985
|
_refreshSelf() {
|
|
@@ -1073,20 +994,21 @@ class Call {
|
|
|
1073
994
|
target.replaceChildren();
|
|
1074
995
|
const link = this.inviteLink();
|
|
1075
996
|
if (!link) return;
|
|
1076
|
-
const
|
|
997
|
+
const t = this.t;
|
|
998
|
+
const input = el('input', { type: 'text', readOnly: true, value: link, 'aria-label': t('inviteLink') });
|
|
1077
999
|
input.addEventListener('focus', () => input.select());
|
|
1078
|
-
const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: '
|
|
1000
|
+
const copy = el('button', { class: 'dxr-call-btn dxr-call-btn--primary', type: 'button', text: t('copyLink') });
|
|
1079
1001
|
copy.addEventListener('click', async () => {
|
|
1080
1002
|
try {
|
|
1081
1003
|
await navigator.clipboard.writeText(link);
|
|
1082
|
-
copy.textContent = '
|
|
1004
|
+
copy.textContent = t('copied');
|
|
1083
1005
|
} catch {
|
|
1084
1006
|
input.select();
|
|
1085
|
-
copy.textContent = '
|
|
1007
|
+
copy.textContent = t('pressCopy');
|
|
1086
1008
|
}
|
|
1087
|
-
setTimeout(() => (copy.textContent = '
|
|
1009
|
+
setTimeout(() => (copy.textContent = t('copyLink')), 1800);
|
|
1088
1010
|
});
|
|
1089
|
-
const qr = el('canvas', { 'aria-label': '
|
|
1011
|
+
const qr = el('canvas', { 'aria-label': t('qrLabel'), role: 'img' });
|
|
1090
1012
|
try {
|
|
1091
1013
|
drawQr(qr, link, { px: 4 });
|
|
1092
1014
|
} catch {
|
|
@@ -1103,16 +1025,17 @@ class Call {
|
|
|
1103
1025
|
const ui = this.ui;
|
|
1104
1026
|
if (!ui) return;
|
|
1105
1027
|
const c = this.container;
|
|
1106
|
-
const
|
|
1028
|
+
const t = this.t;
|
|
1029
|
+
const lobby = this.o.chrome && (this.state === 'lobby' || this.state === 'idle' || this.state === 'full' || this.state === 'left');
|
|
1107
1030
|
c.classList.toggle('dxr-call-host--lobby', lobby);
|
|
1108
|
-
show(ui.banner, this.o.
|
|
1031
|
+
show(ui.banner, this.o.chrome && !this.woven);
|
|
1109
1032
|
const n = this.tiles.size;
|
|
1110
1033
|
ui.grid.dataset.n = String(Math.min(n, 4));
|
|
1111
1034
|
show(ui.grid, n > 0 && this.state !== 'left');
|
|
1112
|
-
// Speaker layout: the active speaker (or the first tile) spans the row.
|
|
1035
|
+
// Speaker layout: the active speaker (or the first tile) spans the row. layout:'none' positions nothing.
|
|
1113
1036
|
const main = this.o.layout === 'speaker' ? this.speakerId || [...this.tiles.keys()][0] : null;
|
|
1114
|
-
for (const [id,
|
|
1115
|
-
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;
|
|
1116
1039
|
this._refreshBar();
|
|
1117
1040
|
queueMicrotask(() => this._fitGrid());
|
|
1118
1041
|
const p = ui.panel;
|
|
@@ -1121,39 +1044,67 @@ class Call {
|
|
|
1121
1044
|
p.replaceChildren();
|
|
1122
1045
|
const l = this.local;
|
|
1123
1046
|
const joining = !!this.room;
|
|
1124
|
-
const title =
|
|
1125
|
-
|
|
1126
|
-
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);
|
|
1127
1050
|
const text =
|
|
1128
1051
|
this.state === 'full'
|
|
1129
|
-
?
|
|
1130
|
-
:
|
|
1052
|
+
? t('lobbyFullText', { maxPeers: this.o.maxPeers })
|
|
1053
|
+
: `${t('lobbyText', { camera: l?.label || t('cameraDefault'), kind })}${this._liftHint() ? ` ${this._liftHint()}` : ''}`;
|
|
1131
1054
|
const go = el('button', {
|
|
1132
1055
|
class: 'dxr-call-btn dxr-call-btn--primary',
|
|
1133
1056
|
type: 'button',
|
|
1134
|
-
text: this.state === 'left' ? '
|
|
1057
|
+
text: this.state === 'left' ? t('rejoin') : joining ? t('join') : t('start'),
|
|
1135
1058
|
});
|
|
1136
1059
|
go.addEventListener('click', () => this.join().catch(() => {}));
|
|
1137
|
-
const sel = el('select', { class: 'dxr-call-select', 'aria-label': '
|
|
1060
|
+
const sel = el('select', { class: 'dxr-call-select', 'aria-label': t('cameraSelect') });
|
|
1138
1061
|
this._fillCameras(sel);
|
|
1139
1062
|
sel.addEventListener('change', () => this.setCamera(sel.value).catch((e) => this.error('camera-failed', e.message, e)));
|
|
1140
|
-
const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: '
|
|
1063
|
+
const retry = !l || !l.videoTrack ? el('button', { class: 'dxr-call-btn', type: 'button', text: t('retryCamera') }) : null;
|
|
1141
1064
|
retry?.addEventListener('click', () => this.retryCamera().catch(() => {}));
|
|
1142
1065
|
p.append(el('h3', { text: title }), el('p', { text }), el('div', { class: 'dxr-call-row' }, [this.state === 'full' ? null : go, retry, sel]));
|
|
1143
1066
|
} else if (this.state === 'in-call' && n === 0) {
|
|
1144
1067
|
show(p, true);
|
|
1145
|
-
this._renderInvite(p, '
|
|
1068
|
+
this._renderInvite(p, t('waitingTitle'), t('waitingText'));
|
|
1146
1069
|
} else if (this.state === 'joining') {
|
|
1147
1070
|
show(p, true);
|
|
1148
|
-
p.replaceChildren(el('p', { text: '
|
|
1071
|
+
p.replaceChildren(el('p', { text: t('joining') }));
|
|
1149
1072
|
} else show(p, false);
|
|
1150
1073
|
const inviteOpen = this.state === 'in-call' && n > 0 && this._showInvite;
|
|
1151
|
-
if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, '
|
|
1074
|
+
if (inviteOpen && !ui.invite.childElementCount) this._renderInvite(ui.invite, t('inviteTitle'), t('inviteText'));
|
|
1152
1075
|
show(ui.invite, inviteOpen);
|
|
1153
1076
|
if (!inviteOpen) ui.invite.replaceChildren();
|
|
1154
1077
|
ui.inviteBtn?.setAttribute('aria-pressed', String(!!inviteOpen));
|
|
1155
1078
|
}
|
|
1156
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Diagnostics (explicitly UNSTABLE — RFC 0003 §2): everything that used to sit on the public
|
|
1082
|
+
* handle and `CallPeer` as debug data. Shape free to change; read it in devtools, never in code.
|
|
1083
|
+
*/
|
|
1084
|
+
diagnostics() {
|
|
1085
|
+
const l = this.local;
|
|
1086
|
+
return {
|
|
1087
|
+
sdk: wire.CALL_SDK,
|
|
1088
|
+
wire: wire.WIRE_VERSION,
|
|
1089
|
+
state: this.state,
|
|
1090
|
+
wall: this.wall,
|
|
1091
|
+
wallLive: this.wallLive,
|
|
1092
|
+
weaveLive: this.weaveLive,
|
|
1093
|
+
woven: this.woven,
|
|
1094
|
+
local: l
|
|
1095
|
+
? { format: l.format, width: l.width, height: l.height, label: l.label, calibration: l.calibration, owned: l.owned, skipped: l.skipped, camera: l.cam || null, stream: l.stream }
|
|
1096
|
+
: null,
|
|
1097
|
+
hello: this.hello(),
|
|
1098
|
+
transport: this.transport,
|
|
1099
|
+
peers: [...this.tiles.values()].map((tile) => tile.diag()),
|
|
1100
|
+
mono3D: { ...this._mono3DInfo(), lift: this.liftApi, pool: this.liftPool ? this.liftPool.list() : [] },
|
|
1101
|
+
speaker: this.speakerId,
|
|
1102
|
+
depth: this.depth,
|
|
1103
|
+
options: this.o,
|
|
1104
|
+
kill: (id) => this.transport?._debugKill(id),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1157
1108
|
async _fillCameras(sel) {
|
|
1158
1109
|
let cams = [];
|
|
1159
1110
|
try {
|
|
@@ -1169,7 +1120,8 @@ class Call {
|
|
|
1169
1120
|
|
|
1170
1121
|
_makeHandle() {
|
|
1171
1122
|
const call = this;
|
|
1172
|
-
|
|
1123
|
+
const renamedEvents = { format: 'display', session: null };
|
|
1124
|
+
const handle = {
|
|
1173
1125
|
get room() {
|
|
1174
1126
|
return call.room;
|
|
1175
1127
|
},
|
|
@@ -1179,15 +1131,16 @@ class Call {
|
|
|
1179
1131
|
get state() {
|
|
1180
1132
|
return call.state;
|
|
1181
1133
|
},
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
},
|
|
1185
|
-
get format() {
|
|
1134
|
+
/** What YOU send: 'sbs' | 'mono'; null before a camera opened. */
|
|
1135
|
+
get localFormat() {
|
|
1186
1136
|
return call.local ? call.local.format : null;
|
|
1187
1137
|
},
|
|
1188
1138
|
get muted() {
|
|
1189
1139
|
return call.muted;
|
|
1190
1140
|
},
|
|
1141
|
+
get cameraOff() {
|
|
1142
|
+
return call.camOff;
|
|
1143
|
+
},
|
|
1191
1144
|
get depth() {
|
|
1192
1145
|
return call.depth;
|
|
1193
1146
|
},
|
|
@@ -1206,18 +1159,25 @@ class Call {
|
|
|
1206
1159
|
get peers() {
|
|
1207
1160
|
return Object.freeze([...call.tiles.values()].map((t) => t.info()));
|
|
1208
1161
|
},
|
|
1209
|
-
join: () => call.join(),
|
|
1162
|
+
join: () => call.join().then(() => undefined),
|
|
1210
1163
|
inviteLink: () => call.inviteLink(),
|
|
1211
1164
|
mute: (on) => call.mute(on),
|
|
1212
|
-
|
|
1213
|
-
setCamera: (
|
|
1165
|
+
setCameraOff: (on) => call.setCameraOff(on),
|
|
1166
|
+
setCamera: (src, o) => call.setCamera(src, o),
|
|
1214
1167
|
setDepth: (v) => call.setDepth(v),
|
|
1215
1168
|
retryCamera: () => call.retryCamera(),
|
|
1216
|
-
sendHint: (z) => call.sendHint(z),
|
|
1217
1169
|
setMono3D: (on) => call.setMono3D(on),
|
|
1218
|
-
leave: () => call.leave(),
|
|
1170
|
+
leave: () => call.leave('left'),
|
|
1171
|
+
/** A remote participant's tile element (for `layout: 'none'`), or null. */
|
|
1172
|
+
tile: (peerId) => call.tiles.get(peerId)?.el || null,
|
|
1173
|
+
diagnostics: () => call.diagnostics(),
|
|
1219
1174
|
on(type, cb) {
|
|
1220
1175
|
if (typeof cb !== 'function') throw new TypeError(`${TAG} on() takes a function`);
|
|
1176
|
+
if (type in renamedEvents && !call._eventWarned?.has(type)) {
|
|
1177
|
+
(call._eventWarned ||= new Set()).add(type);
|
|
1178
|
+
const to = renamedEvents[type];
|
|
1179
|
+
console.warn(`${TAG} the '${type}' event is deprecated (removed in 1.31)${to ? ` — listen for '${to}'` : ' — it is internal now; diagnostics() has the wall'}`);
|
|
1180
|
+
}
|
|
1221
1181
|
if (!call.listeners.has(type)) call.listeners.set(type, new Set());
|
|
1222
1182
|
call.listeners.get(type).add(cb);
|
|
1223
1183
|
return () => call.listeners.get(type)?.delete(cb);
|
|
@@ -1225,14 +1185,28 @@ class Call {
|
|
|
1225
1185
|
off(type, cb) {
|
|
1226
1186
|
call.listeners.get(type)?.delete(cb);
|
|
1227
1187
|
},
|
|
1228
|
-
/** Diagnostics: the transport (mesh) and a way to simulate a dropped connection. */
|
|
1229
|
-
_debug: {
|
|
1230
|
-
get transport() {
|
|
1231
|
-
return call.transport;
|
|
1232
|
-
},
|
|
1233
|
-
kill: (id) => call.transport?._debugKill(id),
|
|
1234
|
-
},
|
|
1235
1188
|
};
|
|
1189
|
+
// 1.29 handle members, one release of warnings (RFC 0003 §2): removed in 1.31.
|
|
1190
|
+
const legacy = {
|
|
1191
|
+
wall: ['diagnostics().wall (or the wall you passed / sharedInline3D())', () => call.wall],
|
|
1192
|
+
format: ['localFormat', () => handle.localFormat],
|
|
1193
|
+
sendHint: ['nothing — auto-convergence superseded it', () => (z) => call.sendHint(z)],
|
|
1194
|
+
_debug: ['diagnostics()', () => ({ transport: call.transport, kill: (id) => call.transport?._debugKill(id) })],
|
|
1195
|
+
};
|
|
1196
|
+
for (const [name, [to, get]] of Object.entries(legacy)) {
|
|
1197
|
+
Object.defineProperty(handle, name, {
|
|
1198
|
+
enumerable: false,
|
|
1199
|
+
configurable: true,
|
|
1200
|
+
get() {
|
|
1201
|
+
if (!call._handleWarned?.has(name)) {
|
|
1202
|
+
(call._handleWarned ||= new Set()).add(name);
|
|
1203
|
+
console.warn(`${TAG} handle.${name} is deprecated (removed in 1.31) — use ${to}`);
|
|
1204
|
+
}
|
|
1205
|
+
return get();
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return handle;
|
|
1236
1210
|
}
|
|
1237
1211
|
}
|
|
1238
1212
|
|
|
@@ -1262,7 +1236,7 @@ function paintFlat(canvas, video, { eyeHalf = null, mirror = false } = {}) {
|
|
|
1262
1236
|
const H = video.videoHeight;
|
|
1263
1237
|
if (!W || !H || (video.readyState || 0) < 2) return;
|
|
1264
1238
|
const eyeW = eyeHalf === null ? W : W / 2;
|
|
1265
|
-
const r = eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
|
|
1239
|
+
const r = wire.eyeCropRect(eyeW, H, canvas.width / canvas.height, 0, 0);
|
|
1266
1240
|
g.save();
|
|
1267
1241
|
if (mirror) {
|
|
1268
1242
|
g.translate(canvas.width, 0);
|
|
@@ -1319,11 +1293,12 @@ class Tile {
|
|
|
1319
1293
|
this.video.addEventListener('resize', () => this._maybeRoute());
|
|
1320
1294
|
this.canvas = el('canvas');
|
|
1321
1295
|
this.cover = el('div', { class: 'dxr-call-cover dxr-call-hidden' });
|
|
1322
|
-
this.badge = el('div', { class: 'dxr-call-badge' });
|
|
1323
|
-
this.plate = el('div', { class: 'dxr-call-state', text: '
|
|
1296
|
+
this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
|
|
1297
|
+
this.plate = el('div', { class: 'dxr-call-state', part: 'plate', text: call.t('connecting') });
|
|
1324
1298
|
this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.cover, this.badge, this.plate]);
|
|
1325
1299
|
this.stage.style.aspectRatio = String(call.o.tileAspect);
|
|
1326
|
-
|
|
1300
|
+
// `data-dxr-peer` is the documented hook (layout:'none', handle.tile()); `data-peer` is the 1.29 spelling.
|
|
1301
|
+
this.el = el('div', { class: 'dxr-call-tile', part: 'tile', 'data-dxr-peer': id, 'data-peer': id }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
|
|
1327
1302
|
if (!call.o.ui) {
|
|
1328
1303
|
show(this.badge, false);
|
|
1329
1304
|
}
|
|
@@ -1331,15 +1306,29 @@ class Tile {
|
|
|
1331
1306
|
this._renderBadge();
|
|
1332
1307
|
}
|
|
1333
1308
|
|
|
1309
|
+
/** How this side shows the participant: `3D` (woven pair) | `2D→3D` (lifted) | `2D` (flat). */
|
|
1310
|
+
display() {
|
|
1311
|
+
return wire.badgeFor(this.route);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/** The public `CallPeer` (RFC 0003 §2): what a page needs to run and observe. Debug data is `diag()`. */
|
|
1334
1315
|
info() {
|
|
1335
1316
|
return Object.freeze({
|
|
1336
1317
|
id: this.id,
|
|
1337
1318
|
format: this.format || 'mono',
|
|
1338
|
-
|
|
1319
|
+
display: this.display(),
|
|
1339
1320
|
state: this.conn,
|
|
1340
1321
|
muted: this.remote.muted,
|
|
1341
1322
|
cameraOff: this.remote.cameraOff,
|
|
1342
1323
|
speaking: this.speaking,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** Everything else (UNSTABLE): `handle.diagnostics().peers[i]`. */
|
|
1328
|
+
diag() {
|
|
1329
|
+
return {
|
|
1330
|
+
...this.info(),
|
|
1331
|
+
route: this.route,
|
|
1343
1332
|
rectified: !!(this.hello && this.hello.rectified),
|
|
1344
1333
|
hello: this.hello,
|
|
1345
1334
|
quality: this.quality,
|
|
@@ -1354,14 +1343,15 @@ class Tile {
|
|
|
1354
1343
|
via: this.autoStats.via,
|
|
1355
1344
|
},
|
|
1356
1345
|
lift: this.lifted
|
|
1357
|
-
?
|
|
1346
|
+
? {
|
|
1358
1347
|
live: this.liftLive,
|
|
1359
1348
|
native: !!(this.liftHandle && this.liftHandle.native),
|
|
1360
1349
|
priority: this.call.liftPool ? this.call.liftPool.priority(this.id) : null,
|
|
1361
1350
|
state: this.liftHandle ? this.liftHandle.state || null : 'pending',
|
|
1362
|
-
}
|
|
1351
|
+
}
|
|
1363
1352
|
: null,
|
|
1364
|
-
|
|
1353
|
+
element: this.el,
|
|
1354
|
+
};
|
|
1365
1355
|
}
|
|
1366
1356
|
|
|
1367
1357
|
setStream(stream) {
|
|
@@ -1416,7 +1406,7 @@ class Tile {
|
|
|
1416
1406
|
|
|
1417
1407
|
setConn(st) {
|
|
1418
1408
|
if (st === 'unreachable' && this.conn !== 'unreachable') {
|
|
1419
|
-
this.call.error('unreachable', `${
|
|
1409
|
+
this.call.error('unreachable', `${this.call.t('unreachable')}. Still retrying in the background.`, Object.assign(new Error('unreachable'), { peer: this.id }));
|
|
1420
1410
|
}
|
|
1421
1411
|
this.conn = st;
|
|
1422
1412
|
if (st === 'connected' && this.leaveTimer) {
|
|
@@ -1427,26 +1417,28 @@ class Tile {
|
|
|
1427
1417
|
}
|
|
1428
1418
|
|
|
1429
1419
|
_renderPlate() {
|
|
1420
|
+
const s = this.call.t;
|
|
1430
1421
|
const t =
|
|
1431
1422
|
this.conn === 'left'
|
|
1432
|
-
? '
|
|
1423
|
+
? s('leftCall')
|
|
1433
1424
|
: this.conn === 'unreachable'
|
|
1434
|
-
?
|
|
1425
|
+
? s('unreachable')
|
|
1435
1426
|
: this.conn === 'reconnecting'
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1427
|
+
? s('reconnecting')
|
|
1428
|
+
: this.conn !== 'connected'
|
|
1429
|
+
? s('connecting')
|
|
1430
|
+
: this.remote.cameraOff
|
|
1431
|
+
? s('cameraOffPlate')
|
|
1432
|
+
: null;
|
|
1442
1433
|
this.plate.textContent = t || '';
|
|
1443
1434
|
show(this.plate, !!t);
|
|
1444
1435
|
}
|
|
1445
1436
|
|
|
1446
1437
|
_renderBadge() {
|
|
1447
|
-
const
|
|
1438
|
+
const s = this.call.t;
|
|
1439
|
+
const b = this.route === 'woven-sbs' ? s('badge3D') : this.route === 'lifted' ? s('badge2D3D') : s('badge2D');
|
|
1448
1440
|
// A lifted tile whose lift is still loading reads "2D→3D…" (it is painted flat meanwhile).
|
|
1449
|
-
const text = !this.route ? '
|
|
1441
|
+
const text = !this.route ? s('badgePending') : this.route === 'lifted' && !this.liftLive ? `${b}${s('badgePending')}` : b;
|
|
1450
1442
|
this.badge.replaceChildren(el('b', { text }));
|
|
1451
1443
|
this.badge.dataset.route = this.route || '';
|
|
1452
1444
|
if (this.remote.muted) this.badge.insertAdjacentHTML('beforeend', ICONS.mutedSmall);
|
|
@@ -1464,7 +1456,7 @@ class Tile {
|
|
|
1464
1456
|
const r =
|
|
1465
1457
|
this.forceFlat && this.format === 'sbs'
|
|
1466
1458
|
? { route: 'flat-left' }
|
|
1467
|
-
: routeFor({
|
|
1459
|
+
: wire.routeFor({
|
|
1468
1460
|
format: this.format,
|
|
1469
1461
|
woven: call.woven,
|
|
1470
1462
|
mono3D: call.mono3DOn ? 'auto' : 'off',
|
|
@@ -1484,7 +1476,9 @@ class Tile {
|
|
|
1484
1476
|
if (r.route === 'woven-sbs') this._registerWoven();
|
|
1485
1477
|
else if (r.route === 'lifted') this._registerLifted();
|
|
1486
1478
|
call.log('route', { peer: this.id, route: r.route, format: this.format, mono3d: r.mono3d || null });
|
|
1487
|
-
call.emit('
|
|
1479
|
+
call.emit('display', { id: this.id, display: this.display() });
|
|
1480
|
+
// 1.29's 'format' event (route / mono3d detail → diagnostics()): one release of compatibility.
|
|
1481
|
+
if (call.listeners.get('format')?.size) call.emit('format', { id: this.id, format: this.format, route: r.route, mono3d: r.mono3d || null, hello: this.hello });
|
|
1488
1482
|
this._renderBadge();
|
|
1489
1483
|
}
|
|
1490
1484
|
|
|
@@ -1669,7 +1663,7 @@ class Tile {
|
|
|
1669
1663
|
const shift = this.conv.step(eyeW);
|
|
1670
1664
|
const g = c.getContext('2d');
|
|
1671
1665
|
for (const eye of [0, 1]) {
|
|
1672
|
-
const r = eyeCropRect(eyeW, H, A, shift, eye);
|
|
1666
|
+
const r = wire.eyeCropRect(eyeW, H, A, shift, eye);
|
|
1673
1667
|
g.drawImage(v, eye * eyeW + r.sx, r.sy, r.sw, r.sh, eye * outW, 0, outW, outH);
|
|
1674
1668
|
}
|
|
1675
1669
|
}
|
|
@@ -1795,106 +1789,197 @@ class Tile {
|
|
|
1795
1789
|
|
|
1796
1790
|
// ── the self view ─────────────────────────────────────────────────────────────────────────
|
|
1797
1791
|
|
|
1792
|
+
/**
|
|
1793
|
+
* The self view IS an `addCameraView` of `@displayxr/inline3d/camera` (RFC 0003 §4): mirrored
|
|
1794
|
+
* correctly (each half mirrored AND the halves swapped — mirroring each half in place would invert
|
|
1795
|
+
* every disparity), woven on a 3D wall, flat elsewhere. The stream that is SENT is never touched.
|
|
1796
|
+
* This class owns the tile's chrome (badge, plate) and re-attaches the view when the camera or
|
|
1797
|
+
* the wall changes; the view owns the pixels.
|
|
1798
|
+
*/
|
|
1798
1799
|
class SelfTile {
|
|
1799
1800
|
constructor(call, slot) {
|
|
1800
1801
|
this.call = call;
|
|
1801
1802
|
this.canvas = el('canvas');
|
|
1802
|
-
this.badge = el('div', { class: 'dxr-call-badge' });
|
|
1803
|
-
this.plate = el('div', { class: 'dxr-call-state dxr-call-hidden' });
|
|
1803
|
+
this.badge = el('div', { class: 'dxr-call-badge', part: 'badge' });
|
|
1804
|
+
this.plate = el('div', { class: 'dxr-call-state dxr-call-hidden', part: 'plate' });
|
|
1804
1805
|
this.stage = el('div', { class: 'dxr-call-stage' }, [this.canvas, this.badge, this.plate]);
|
|
1805
1806
|
this.stage.style.aspectRatio = String(call.o.tileAspect);
|
|
1806
|
-
this.el = el('div', { class: 'dxr-call-tile dxr-call-tile--self' }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
|
|
1807
|
+
this.el = el('div', { class: 'dxr-call-tile dxr-call-tile--self', part: 'tile' }, [this.stage, el('div', { class: 'dxr-call-talk' })]);
|
|
1807
1808
|
slot.appendChild(this.el);
|
|
1808
|
-
this.
|
|
1809
|
-
this.
|
|
1810
|
-
|
|
1809
|
+
this.view = null; // the CameraView over the current camera
|
|
1810
|
+
this.cam = null;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/** 'woven-sbs' | 'flat-left' | 'flat' | null (no camera). */
|
|
1814
|
+
get route() {
|
|
1815
|
+
return this.view ? this.view.route : null;
|
|
1811
1816
|
}
|
|
1812
1817
|
|
|
1813
1818
|
update() {
|
|
1814
1819
|
const l = this.call.local;
|
|
1820
|
+
const t = this.call.t;
|
|
1815
1821
|
const noCam = !l || !l.videoTrack;
|
|
1816
|
-
this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ?
|
|
1822
|
+
this.plate.textContent = noCam ? (this.call.camStatus === 'busy' ? t('cameraBusy') : this.call.camStatus === 'pending' ? '' : t('noCamera')) : this.call.camOff ? t('cameraOffPlate') : '';
|
|
1817
1823
|
show(this.plate, noCam || this.call.camOff);
|
|
1818
1824
|
this.reroute(false);
|
|
1819
1825
|
}
|
|
1820
1826
|
|
|
1821
1827
|
reroute(force) {
|
|
1822
1828
|
const l = this.call.local;
|
|
1823
|
-
|
|
1824
|
-
const
|
|
1825
|
-
if (!
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
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 });
|
|
1832
1840
|
}
|
|
1833
1841
|
this._badge();
|
|
1834
1842
|
}
|
|
1835
1843
|
|
|
1836
1844
|
_badge() {
|
|
1837
|
-
|
|
1845
|
+
const t = this.call.t;
|
|
1846
|
+
this.badge.replaceChildren(el('b', { text: `${t('you')} · ${this.route === 'woven-sbs' ? t('badge3D') : t('badge2D')}` }));
|
|
1838
1847
|
}
|
|
1839
1848
|
|
|
1840
1849
|
onWallLost() {
|
|
1841
|
-
this.
|
|
1842
|
-
this.
|
|
1850
|
+
this.view?._onWallLost();
|
|
1851
|
+
this._badge();
|
|
1843
1852
|
}
|
|
1844
1853
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
/* ignore */
|
|
1851
|
-
}
|
|
1852
|
-
this.handle = null;
|
|
1853
|
-
}
|
|
1854
|
+
/** Drop the view (leave(), a camera change). The canvas keeps its last frame; a rejoin re-attaches. */
|
|
1855
|
+
detach() {
|
|
1856
|
+
if (this.view) this.view.remove();
|
|
1857
|
+
this.view = null;
|
|
1858
|
+
this.cam = null;
|
|
1854
1859
|
}
|
|
1855
1860
|
|
|
1856
1861
|
setSpeaking(on) {
|
|
1857
1862
|
this.el.classList.toggle('dxr-call-tile--speaking', on);
|
|
1858
1863
|
}
|
|
1864
|
+
}
|
|
1859
1865
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1866
|
+
// ── 1.29 exports kept for ONE release (RFC 0003 §2 "Mechanics") ───────────────────────────
|
|
1867
|
+
//
|
|
1868
|
+
// Every helper below is INTERNAL: it exists so the module can be unit-tested, and it stays
|
|
1869
|
+
// importable by file path (`js/call/*.js`). It is no longer part of this entry's public surface
|
|
1870
|
+
// (call.d.ts marks each `@deprecated`; test/api-snapshot pins the list) and 1.31 removes it from
|
|
1871
|
+
// here. Until then a function or class warns once on its first call, an object on its first
|
|
1872
|
+
// property read; a primitive constant cannot warn (ESM bindings have no getter) and is simply
|
|
1873
|
+
// typed `@deprecated`. `peerjsCloud` left in 1.29 outright (Decision 12).
|
|
1874
|
+
|
|
1875
|
+
const REMOVED_IN = '1.31';
|
|
1876
|
+
const deprecatedWarned = new Set();
|
|
1877
|
+
function deprecate(name, from, value) {
|
|
1878
|
+
const warn = () => {
|
|
1879
|
+
if (deprecatedWarned.has(name)) return;
|
|
1880
|
+
deprecatedWarned.add(name);
|
|
1881
|
+
console.warn(`${TAG} \`${name}\` is internal and leaves '@displayxr/inline3d/call' in ${REMOVED_IN} — import it from ${from} if you must, or stop depending on it`);
|
|
1882
|
+
};
|
|
1883
|
+
if (typeof value === 'function') {
|
|
1884
|
+
return new Proxy(value, {
|
|
1885
|
+
apply(target, self, args) {
|
|
1886
|
+
warn();
|
|
1887
|
+
return Reflect.apply(target, self, args);
|
|
1888
|
+
},
|
|
1889
|
+
construct(target, args) {
|
|
1890
|
+
warn();
|
|
1891
|
+
return Reflect.construct(target, args);
|
|
1892
|
+
},
|
|
1893
|
+
});
|
|
1865
1894
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
const v = this.call.local.video;
|
|
1874
|
-
const W = v.videoWidth;
|
|
1875
|
-
const H = v.videoHeight;
|
|
1876
|
-
if (!W || !H || (v.readyState || 0) < 2) return;
|
|
1877
|
-
const A = this.call.o.tileAspect;
|
|
1878
|
-
const eyeW = W / 2;
|
|
1879
|
-
const { w: outW, h: outH } = eyeOutputSize(eyeW, H, A);
|
|
1880
|
-
const c = this.mirror;
|
|
1881
|
-
if (c.width !== 2 * outW || c.height !== outH) {
|
|
1882
|
-
c.width = 2 * outW;
|
|
1883
|
-
c.height = outH;
|
|
1884
|
-
}
|
|
1885
|
-
const g = c.getContext('2d');
|
|
1886
|
-
const r = eyeCropRect(eyeW, H, A, 0, 0);
|
|
1887
|
-
for (const op of mirrorSwapOps(W, H)) {
|
|
1888
|
-
const dx = op.dx === 0 ? 0 : outW;
|
|
1889
|
-
g.save();
|
|
1890
|
-
g.translate(dx + outW, 0);
|
|
1891
|
-
g.scale(-1, 1);
|
|
1892
|
-
g.drawImage(v, op.sx + r.sx, r.sy, r.sw, r.sh, 0, 0, outW, outH);
|
|
1893
|
-
g.restore();
|
|
1894
|
-
}
|
|
1895
|
+
if (value && typeof value === 'object') {
|
|
1896
|
+
return new Proxy(value, {
|
|
1897
|
+
get(target, prop, receiver) {
|
|
1898
|
+
if (typeof prop === 'string') warn();
|
|
1899
|
+
return Reflect.get(target, prop, receiver);
|
|
1900
|
+
},
|
|
1901
|
+
});
|
|
1895
1902
|
}
|
|
1903
|
+
return value;
|
|
1896
1904
|
}
|
|
1897
1905
|
|
|
1906
|
+
/** @deprecated internal (js/call/options.js) — removed from this entry in 1.31 */
|
|
1907
|
+
export const normalizeCallOptions = deprecate('normalizeCallOptions', 'js/call/options.js', normalizeOptions);
|
|
1908
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1909
|
+
export const newRoomId = deprecate('newRoomId', 'js/call/wire.js', wire.newRoomId);
|
|
1910
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1911
|
+
export const isValidRoomId = deprecate('isValidRoomId', 'js/call/wire.js', wire.isValidRoomId);
|
|
1912
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `handle.inviteLink()` is the API */
|
|
1913
|
+
export const buildInviteLink = deprecate('buildInviteLink', 'js/call/wire.js', wire.buildInviteLink);
|
|
1914
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; the wire is documented in docs/call-wire.md */
|
|
1915
|
+
export const normalizeHello = deprecate('normalizeHello', 'js/call/wire.js', wire.normalizeHello);
|
|
1916
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1917
|
+
export const makeHello = deprecate('makeHello', 'js/call/wire.js', wire.makeHello);
|
|
1918
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; `CallPeer.display` is the API */
|
|
1919
|
+
export const routeFor = deprecate('routeFor', 'js/call/wire.js', wire.routeFor);
|
|
1920
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1921
|
+
export const badgeFor = deprecate('badgeFor', 'js/call/wire.js', wire.badgeFor);
|
|
1922
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1923
|
+
export const createLiveGate = deprecate('createLiveGate', 'js/call/wire.js', wire.createLiveGate);
|
|
1924
|
+
/** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
|
|
1925
|
+
export const convergenceShiftPx = deprecate('convergenceShiftPx', 'js/camera/converge.js', wire.convergenceShiftPx);
|
|
1926
|
+
/** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
|
|
1927
|
+
export const lowPass = deprecate('lowPass', 'js/camera/converge.js', wire.lowPass);
|
|
1928
|
+
/** @deprecated internal (js/camera/converge.js) — removed from this entry in 1.31 */
|
|
1929
|
+
export const clampShift = deprecate('clampShift', 'js/camera/converge.js', wire.clampShift);
|
|
1930
|
+
/** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
|
|
1931
|
+
export const eyeCropRect = deprecate('eyeCropRect', 'js/camera/geometry.js', wire.eyeCropRect);
|
|
1932
|
+
/** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31; `addCameraView` of `/camera` mirrors a self view */
|
|
1933
|
+
export const mirrorSwapOps = deprecate('mirrorSwapOps', 'js/camera/geometry.js', wire.mirrorSwapOps);
|
|
1934
|
+
/** @deprecated internal (js/camera/geometry.js) — removed from this entry in 1.31 */
|
|
1935
|
+
export const mirrorSwapPixels = deprecate('mirrorSwapPixels', 'js/camera/geometry.js', wire.mirrorSwapPixels);
|
|
1936
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31 */
|
|
1937
|
+
export const maxBitrateKbps = deprecate('maxBitrateKbps', 'js/call/wire.js', wire.maxBitrateKbps);
|
|
1938
|
+
/** @deprecated internal (js/camera/disparity.js) — removed from this entry in 1.31; `autoConverge` of `/camera` and `/call` is the behaviour */
|
|
1939
|
+
export const measureFocusDisparity = deprecate('measureFocusDisparity', 'js/camera/disparity.js', disparity.measureFocusDisparity);
|
|
1940
|
+
/** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
|
|
1941
|
+
export const preferVideoCodecs = deprecate('preferVideoCodecs', 'js/call/sdp.js', sdp.preferVideoCodecs);
|
|
1942
|
+
/** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
|
|
1943
|
+
export const sortCodecCapabilities = deprecate('sortCodecCapabilities', 'js/call/sdp.js', sdp.sortCodecCapabilities);
|
|
1944
|
+
/** @deprecated internal (js/call/sdp.js) — removed from this entry in 1.31 */
|
|
1945
|
+
export const VIDEO_CODEC_ORDER = deprecate('VIDEO_CODEC_ORDER', 'js/call/sdp.js', sdp.VIDEO_CODEC_ORDER);
|
|
1946
|
+
/** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31; an SFU adapter will get a designed Transport seam */
|
|
1947
|
+
export const MeshTransport = deprecate('MeshTransport', 'js/call/transport.js', MeshTransportImpl);
|
|
1948
|
+
/** @deprecated internal (js/call/transport.js) — removed from this entry in 1.31 */
|
|
1949
|
+
export const clampMaxPeers = deprecate('clampMaxPeers', 'js/call/transport.js', clampMaxPeersImpl);
|
|
1950
|
+
/** @deprecated internal (js/call/qr.js) — removed from this entry in 1.31 */
|
|
1951
|
+
export const qrEncode = deprecate('qrEncode', 'js/call/qr.js', qrMod.qrEncode);
|
|
1952
|
+
/** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; documented in signaling/README.md */
|
|
1953
|
+
export const roomKey = deprecate('roomKey', 'js/call/signaling.js', signalingMod.roomKey);
|
|
1954
|
+
/** @deprecated internal (js/call/signaling.js) — removed from this entry in 1.31; a primitive: no runtime warning */
|
|
1955
|
+
export const SIGNAL_PROTOCOL = signalingMod.SIGNAL_PROTOCOL;
|
|
1956
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
|
|
1957
|
+
export const WIRE_VERSION = wire.WIRE_VERSION;
|
|
1958
|
+
/** @deprecated internal (js/call/wire.js) — removed from this entry in 1.31; a primitive: no runtime warning */
|
|
1959
|
+
export const CALL_SDK = wire.CALL_SDK;
|
|
1960
|
+
/** @deprecated replaced by `theme.strings` (js/call/ui.js CALL_STRINGS) — removed from this entry in 1.31 */
|
|
1961
|
+
export const PLATE_TEXT = deprecate('PLATE_TEXT', 'js/call/ui.js (CALL_STRINGS)', Object.freeze({ unreachable: CALL_STRINGS.unreachable, cameraBusy: CALL_STRINGS.cameraBusy }));
|
|
1962
|
+
/** @deprecated folded into `theme.accent` (which takes the names) — removed from this entry in 1.31 */
|
|
1963
|
+
export const CALL_ACCENTS = deprecate('CALL_ACCENTS', 'js/call/ui.js', CALL_ACCENTS_IMPL);
|
|
1964
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1965
|
+
export const normalizeMono3D = deprecate('normalizeMono3D', 'js/call/lift.js', liftMod.normalizeMono3D);
|
|
1966
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1967
|
+
export const resolveLift = deprecate('resolveLift', 'js/call/lift.js', liftMod.resolveLift);
|
|
1968
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1969
|
+
export const createLiftPool = deprecate('createLiftPool', 'js/call/lift.js', liftMod.createLiftPool);
|
|
1970
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1971
|
+
export const createFrameWatch = deprecate('createFrameWatch', 'js/call/lift.js', liftMod.createFrameWatch);
|
|
1972
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1973
|
+
export const liftConvergenceFor = deprecate('liftConvergenceFor', 'js/call/lift.js', liftMod.liftConvergenceFor);
|
|
1974
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1975
|
+
export const liftPriorityFor = deprecate('liftPriorityFor', 'js/call/lift.js', liftMod.liftPriorityFor);
|
|
1976
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1977
|
+
export const setLiftPriority = deprecate('setLiftPriority', 'js/call/lift.js', liftMod.setLiftPriority);
|
|
1978
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1979
|
+
export const defaultLiftSpecifier = deprecate('defaultLiftSpecifier', 'js/call/lift.js', liftMod.defaultLiftSpecifier);
|
|
1980
|
+
/** @deprecated internal (js/call/lift.js) — removed from this entry in 1.31 */
|
|
1981
|
+
export const LIFT_PRIORITY = deprecate('LIFT_PRIORITY', 'js/call/lift.js', liftMod.LIFT_PRIORITY);
|
|
1982
|
+
|
|
1898
1983
|
// `<dxr-call>` — the one-line path as markup — is registered by importing this entry (RFC 0003 §1;
|
|
1899
1984
|
// the ONE side effect of this module, declared in package.json `sideEffects`). A no-op where there
|
|
1900
1985
|
// is no `customElements` registry (Node, workers) or the name is already defined.
|