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