@displayxr/inline3d 1.28.0 → 1.29.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 +109 -0
- package/README.md +6 -3
- package/call-full.d.ts +21 -0
- package/call.d.ts +103 -23
- package/dist/call.js +81 -0
- package/index.d.ts +27 -1
- package/js/call/capture.js +47 -2
- package/js/call/disparity.js +432 -0
- package/js/call/element.js +217 -0
- package/js/call/lift.js +9 -0
- package/js/call/signaling.js +7 -202
- package/js/call/wire.js +14 -8
- package/js/inline3d-call-full.js +48 -0
- package/js/inline3d-call.js +222 -16
- package/js/inline3d-player.js +646 -216
- package/js/inline3d-splat-playcanvas.js +46 -3
- package/js/inline3d-splat-video.js +328 -29
- package/js/inline3d.js +130 -1
- package/js/lift/index.js +15 -0
- package/package.json +21 -4
- package/player.d.ts +46 -2
- package/splat.d.ts +38 -0
package/js/inline3d-call.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
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 and the one-line path of RFC 0003 §1 (C1).
|
|
5
5
|
//
|
|
6
|
-
// import {
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// const wall = await createInline3D();
|
|
10
|
-
// const call = await addCall(wall, document.getElementById('call'), {
|
|
11
|
-
// signaling: dxrSignaling(), // hosted DisplayXR server; or dxrSignaling('wss://your.server')
|
|
12
|
-
// });
|
|
6
|
+
// import { mountCall } from '@displayxr/inline3d/call';
|
|
7
|
+
// const call = await mountCall(document.getElementById('call')); // hosted signalling, auto camera, SDK UI
|
|
13
8
|
// call.on('peer', ({ id }) => console.log('joined', id));
|
|
14
9
|
//
|
|
10
|
+
// `mountCall(el, opts?)` is `addCall` with every argument optional: the wall is the document's
|
|
11
|
+
// shared manager (`sharedInline3D()` — never a second session next to the page's own), signalling
|
|
12
|
+
// defaults to the hosted server, the camera to 'auto', the chrome to on. `addCall(wall, container,
|
|
13
|
+
// opts)` stays as the explicit form for a page that manages its own wall. `<dxr-call>`
|
|
14
|
+
// (call/element.js) is the same thing as markup. A bundled app that wants 2D→3D imports
|
|
15
|
+
// `@displayxr/inline3d/call/full` (lift statically imported) or passes `mono3D: lift` itself —
|
|
16
|
+
// on the plain `/call` entry a mono peer on a 3D display raises `warning { code:
|
|
17
|
+
// 'lift-not-bundled' }` instead of silently staying flat.
|
|
18
|
+
//
|
|
15
19
|
// WHAT IT OWNS. Unlike the other modules this one takes a CONTAINER, not a canvas: a call has a
|
|
16
20
|
// variable number of participants, so it creates one persistent canvas per remote participant
|
|
17
21
|
// (plus the self view) inside `container`, lays them out, and draws its own chrome around them.
|
|
@@ -67,7 +71,7 @@
|
|
|
67
71
|
// SDK's teardown synchronously, so no listener can distinguish them afterwards); `leave()`
|
|
68
72
|
// restores it.
|
|
69
73
|
|
|
70
|
-
import {
|
|
74
|
+
import { sharedInline3D } from './inline3d.js';
|
|
71
75
|
import {
|
|
72
76
|
WIRE_VERSION,
|
|
73
77
|
CALL_SDK,
|
|
@@ -97,9 +101,12 @@ import { openCamera, openMic } from './call/capture.js';
|
|
|
97
101
|
import { drawQr } from './call/qr.js';
|
|
98
102
|
import { injectCallStyle, ICONS, el, show, resolveCallAccent, CALL_ACCENTS } from './call/ui.js';
|
|
99
103
|
import { normalizeMono3D, resolveLift, createLiftPool, createFrameWatch } from './call/lift.js';
|
|
104
|
+
import { defineCallElement } from './call/element.js';
|
|
105
|
+
export { DxrCallElement, defineCallElement, attrsToOpts, CALL_EVENT_PREFIX } from './call/element.js';
|
|
100
106
|
|
|
101
107
|
import { dxrSignaling } from './call/signaling.js';
|
|
102
|
-
|
|
108
|
+
import { lumaFromRgba, createDisparityTrack, createFocusTracker, downsampleLuma } from './call/disparity.js';
|
|
109
|
+
export { dxrSignaling, SIGNAL_PROTOCOL, roomKey, DXR_SIGNAL_DEFAULT } from './call/signaling.js';
|
|
103
110
|
export {
|
|
104
111
|
WIRE_VERSION,
|
|
105
112
|
CALL_SDK,
|
|
@@ -114,6 +121,7 @@ export {
|
|
|
114
121
|
CALL_ACCENTS,
|
|
115
122
|
};
|
|
116
123
|
export { createLiveGate } from './call/wire.js';
|
|
124
|
+
export { measureFocusDisparity } from './call/disparity.js';
|
|
117
125
|
export { convergenceShiftPx, lowPass, clampShift, eyeCropRect, mirrorSwapOps, mirrorSwapPixels, maxBitrateKbps } from './call/wire.js';
|
|
118
126
|
export { preferVideoCodecs, sortCodecCapabilities, VIDEO_CODEC_ORDER } from './call/sdp.js';
|
|
119
127
|
export { MeshTransport, clampMaxPeers } from './call/transport.js';
|
|
@@ -137,6 +145,12 @@ const STATS_TICK_MS = 500;
|
|
|
137
145
|
const SPEAKING_LEVEL = 0.02;
|
|
138
146
|
const LAYER_RETRIES = 4;
|
|
139
147
|
const LIFT_RETRIES = 2;
|
|
148
|
+
// Auto-convergence sampling: ~5 Hz on a copy whose eye is at most this wide (enough for sub-pixel
|
|
149
|
+
// disparity on a face; small enough to stay a few ms per measurement).
|
|
150
|
+
const AUTO_CONV_INTERVAL_MS = 200;
|
|
151
|
+
const AUTO_CONV_EYE_WIDTH = 240;
|
|
152
|
+
// WebCodecs pixel formats whose plane 0 is luma (Y): read directly, no colour conversion.
|
|
153
|
+
const Y_PLANE_FORMATS = new Set(['I420', 'I420A', 'I422', 'I444', 'NV12']);
|
|
140
154
|
const DEFAULT_BROWSER_URL = 'https://github.com/DisplayXR/displayxr-browser';
|
|
141
155
|
export const PLATE_TEXT = Object.freeze({
|
|
142
156
|
unreachable: "Can't reach this participant — the network needs a relay (TURN)",
|
|
@@ -158,14 +172,19 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
158
172
|
}
|
|
159
173
|
return {
|
|
160
174
|
room,
|
|
161
|
-
// Default: the hosted DisplayXR signalling server (which also mints TURN credentials).
|
|
162
|
-
|
|
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 } : {}),
|
|
163
178
|
iceServers: Array.isArray(opts.iceServers) ? opts.iceServers : undefined,
|
|
164
179
|
camera: opts.camera === undefined ? 'auto' : opts.camera,
|
|
165
180
|
format: opts.format === 'sbs' ? 'sbs' : opts.format === 'mono' ? 'mono' : undefined,
|
|
166
181
|
calibration: opts.calibration && typeof opts.calibration === 'object' ? { ...opts.calibration } : {},
|
|
167
182
|
rectify: typeof opts.rectify === 'function' ? opts.rectify : null,
|
|
168
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,
|
|
169
188
|
mono3D: normalizeMono3D(opts.mono3D),
|
|
170
189
|
maxPeers: clampMaxPeers(opts.maxPeers === undefined ? DEFAULT_MAX_PEERS : opts.maxPeers),
|
|
171
190
|
// Extra lift() options for lifted tiles (models, ort, quality, providers). The call's own
|
|
@@ -186,6 +205,8 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
186
205
|
scrollIntoView: opts.scrollIntoView === undefined ? true : !!opts.scrollIntoView,
|
|
187
206
|
wallOptions: opts.wallOptions && typeof opts.wallOptions === 'object' ? opts.wallOptions : {},
|
|
188
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,
|
|
189
210
|
};
|
|
190
211
|
}
|
|
191
212
|
|
|
@@ -201,13 +222,30 @@ export async function addCall(wall, container, opts = {}) {
|
|
|
201
222
|
if (!container || typeof container.appendChild !== 'function') throw new TypeError('@displayxr/inline3d/call: addCall(wall, container, opts) needs a container element');
|
|
202
223
|
const o = normalizeCallOptions(opts);
|
|
203
224
|
if (!o.signaling || typeof o.signaling.join !== 'function') {
|
|
204
|
-
throw new TypeError('@displayxr/inline3d/call: opts.signaling must be a SignalingAdapter — dxrSignaling([url])
|
|
225
|
+
throw new TypeError('@displayxr/inline3d/call: opts.signaling must be a SignalingAdapter — dxrSignaling([url]) or your own');
|
|
205
226
|
}
|
|
206
227
|
const call = new Call(wall, container, o);
|
|
207
228
|
await call._init();
|
|
208
229
|
return call.handle;
|
|
209
230
|
}
|
|
210
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The one-line path (RFC 0003 §1): `addCall` with every argument optional. The wall is
|
|
234
|
+
* `opts.wall` if given, else the document's shared manager — `sharedInline3D(opts.wallOptions)`,
|
|
235
|
+
* which returns the page's existing session rather than opening a second one (woven-canvas
|
|
236
|
+
* rule 1). Everything else defaults as `addCall` does: hosted signalling, `camera: 'auto'`,
|
|
237
|
+
* `ui: true`.
|
|
238
|
+
*
|
|
239
|
+
* @param {HTMLElement} el the container (a `<dxr-call>`, a `<div>`, …)
|
|
240
|
+
* @param {object} [opts] see call.d.ts `CallOptions`
|
|
241
|
+
*/
|
|
242
|
+
export async function mountCall(el, opts = {}) {
|
|
243
|
+
if (!el || typeof el.appendChild !== 'function') throw new TypeError('@displayxr/inline3d/call: mountCall(el, opts) needs a container element');
|
|
244
|
+
const { wall: given, ...rest } = opts;
|
|
245
|
+
const wall = given || (await sharedInline3D(rest.wallOptions));
|
|
246
|
+
return addCall(wall, el, rest);
|
|
247
|
+
}
|
|
248
|
+
|
|
211
249
|
class Call {
|
|
212
250
|
constructor(wall, container, o) {
|
|
213
251
|
this.o = o;
|
|
@@ -238,6 +276,8 @@ class Call {
|
|
|
238
276
|
this.liftPool = null;
|
|
239
277
|
this._liftP = null;
|
|
240
278
|
this.liftProven = false;
|
|
279
|
+
this.liftMissing = false; // the lift MODULE failed to import (vs. a display that can't lift)
|
|
280
|
+
this._liftWarned = false;
|
|
241
281
|
this._frameWatch = createFrameWatch();
|
|
242
282
|
this.handle = this._makeHandle();
|
|
243
283
|
if (this.wallLive) this._hookWall(this.wall);
|
|
@@ -272,6 +312,13 @@ class Call {
|
|
|
272
312
|
this.emit('error', { code, message, error: err || null });
|
|
273
313
|
}
|
|
274
314
|
|
|
315
|
+
/** Degraded, not broken (RFC 0003 §2): one console.warn (always — the page must see it) + an event. */
|
|
316
|
+
warning(code, message) {
|
|
317
|
+
this.log('warning', { code, message });
|
|
318
|
+
console.warn(`${TAG} ${code}: ${message}`);
|
|
319
|
+
this.emit('warning', { code, message });
|
|
320
|
+
}
|
|
321
|
+
|
|
275
322
|
// ── setup ────────────────────────────────────────────────────────────────────────────────
|
|
276
323
|
|
|
277
324
|
async _init() {
|
|
@@ -695,7 +742,9 @@ class Call {
|
|
|
695
742
|
if (this.state === 'left') return;
|
|
696
743
|
let w = null;
|
|
697
744
|
try {
|
|
698
|
-
|
|
745
|
+
// The shared manager: if the page has already re-opened its own wall, join it rather than
|
|
746
|
+
// opening a second session (rule 1); else this creates the document's new one.
|
|
747
|
+
w = await sharedInline3D(this.o.wallOptions);
|
|
699
748
|
} catch {
|
|
700
749
|
w = null;
|
|
701
750
|
}
|
|
@@ -748,7 +797,7 @@ class Call {
|
|
|
748
797
|
const lo = this.o.liftOptions;
|
|
749
798
|
// liftCapabilities probes the same model source the lifts will use.
|
|
750
799
|
const capsOpts = { webFallback: true, ...(lo && lo.models ? { models: lo.models } : {}) };
|
|
751
|
-
this._liftP = resolveLift(this.mono3D, { log, capsOpts }).then((r) => {
|
|
800
|
+
this._liftP = resolveLift(this.mono3D, { log, capsOpts, ...(this.o._liftImporter ? { importer: this.o._liftImporter } : {}) }).then((r) => {
|
|
752
801
|
this.liftApi = r;
|
|
753
802
|
if (r.lift) {
|
|
754
803
|
this.liftPool = createLiftPool({ lift: r.lift, max: this.o.maxLifted, log, options: this.o.liftOptions });
|
|
@@ -763,6 +812,12 @@ class Call {
|
|
|
763
812
|
})
|
|
764
813
|
.catch(() => {});
|
|
765
814
|
}
|
|
815
|
+
} else if (r.reason === 'import-failed' || r.reason === 'no-lift-export') {
|
|
816
|
+
// The MODULE is missing (a bundled app whose bundler could not follow the computed
|
|
817
|
+
// import, or a copy of the SDK built without lift) — not a display that can't lift. Said
|
|
818
|
+
// loudly, once, when a mono peer actually arrives (_warnLiftMissing): a developer testing
|
|
819
|
+
// in the DisplayXR Browser sees it the first time a 2D caller joins.
|
|
820
|
+
this.liftMissing = true;
|
|
766
821
|
} else if (r.reason !== 'off') {
|
|
767
822
|
console.info(`${TAG} mono→3D unavailable (${r.reason}) — mono participants are shown flat`);
|
|
768
823
|
}
|
|
@@ -774,6 +829,21 @@ class Call {
|
|
|
774
829
|
return this._liftP;
|
|
775
830
|
}
|
|
776
831
|
|
|
832
|
+
/**
|
|
833
|
+
* `warning { code: 'lift-not-bundled' }`, once per call: a mono peer is on a 3D display, lift
|
|
834
|
+
* was asked for ('auto'), and the lift module could not be imported. Names the fix.
|
|
835
|
+
*/
|
|
836
|
+
_warnLiftMissing() {
|
|
837
|
+
if (this._liftWarned || !this.liftMissing || !this.mono3DOn) return;
|
|
838
|
+
this._liftWarned = true;
|
|
839
|
+
this.warning(
|
|
840
|
+
'lift-not-bundled',
|
|
841
|
+
'a 2D participant is on a 3D display but the 2D→3D module could not be imported from this build, so they stay flat. ' +
|
|
842
|
+
"Import it explicitly — `import { lift } from '@displayxr/inline3d/lift'; mountCall(el, { mono3D: lift })` — " +
|
|
843
|
+
"or use `@displayxr/inline3d/call/full`, which bundles it."
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
777
847
|
/** Re-run the routing table for every mono tile (lift resolved, a slot freed, setMono3D). */
|
|
778
848
|
_rerouteMono() {
|
|
779
849
|
for (const t of this.tiles.values()) if (t.format && t.format !== 'sbs') t.reroute(false);
|
|
@@ -798,7 +868,7 @@ class Call {
|
|
|
798
868
|
if (!this.mono3DOn) return 'Mono cameras: 2D (2D→3D is off).';
|
|
799
869
|
const r = this.liftApi;
|
|
800
870
|
if (!r) return 'Mono cameras: checking 2D→3D…';
|
|
801
|
-
if (!r.lift) return 'Mono cameras: 2D (no 2D→3D provider here).';
|
|
871
|
+
if (!r.lift) return this.liftMissing ? 'Mono cameras: 2D→3D unavailable in this build.' : 'Mono cameras: 2D (no 2D→3D provider here).';
|
|
802
872
|
const c = r.caps;
|
|
803
873
|
if (c && c.native) return `Mono cameras: 2D→3D (native${c.provider ? `, ${c.provider}` : ''}).`;
|
|
804
874
|
if (c && c.webFallback && c.webFallback.webgpu) return 'Mono cameras: 2D→3D (in this page, WebGPU).';
|
|
@@ -1219,6 +1289,17 @@ class Tile {
|
|
|
1219
1289
|
this.speaking = false;
|
|
1220
1290
|
this.quality = null;
|
|
1221
1291
|
this.conv = createConvergence();
|
|
1292
|
+
this.autoTrack = createDisparityTrack();
|
|
1293
|
+
this.autoFocus = createFocusTracker();
|
|
1294
|
+
this.autoAt = 0;
|
|
1295
|
+
this.autoCanvas = null;
|
|
1296
|
+
this.autoLocked = false;
|
|
1297
|
+
this.autoStats = { n: 0, ms: 0, readMs: 0, method: null, via: null };
|
|
1298
|
+
this.autoPending = false;
|
|
1299
|
+
// Bumped on every stream change and on destroy(): an async copyTo() that resolves after either
|
|
1300
|
+
// belongs to a stream this tile no longer shows, and is dropped (review of #92).
|
|
1301
|
+
this.autoGen = 0;
|
|
1302
|
+
this.destroyed = false;
|
|
1222
1303
|
this.conv.depth = call.depth;
|
|
1223
1304
|
this.hintGate = rateGate(HINT_MAX_HZ);
|
|
1224
1305
|
this.layerFails = 0;
|
|
@@ -1263,6 +1344,15 @@ class Tile {
|
|
|
1263
1344
|
hello: this.hello,
|
|
1264
1345
|
quality: this.quality,
|
|
1265
1346
|
convergencePx: this.conv.current,
|
|
1347
|
+
// Auto-convergence: the measured disparity (source px, null until a lock) and the mean cost of
|
|
1348
|
+
// one measurement including the frame readback.
|
|
1349
|
+
autoConverge: {
|
|
1350
|
+
disparityPx: this.conv.measuredPx,
|
|
1351
|
+
ms: this.autoStats.n ? this.autoStats.ms / this.autoStats.n : null,
|
|
1352
|
+
readMs: this.autoStats.n ? this.autoStats.readMs / this.autoStats.n : null,
|
|
1353
|
+
method: this.autoStats.method,
|
|
1354
|
+
via: this.autoStats.via,
|
|
1355
|
+
},
|
|
1266
1356
|
lift: this.lifted
|
|
1267
1357
|
? Object.freeze({
|
|
1268
1358
|
live: this.liftLive,
|
|
@@ -1278,6 +1368,7 @@ class Tile {
|
|
|
1278
1368
|
if (this.video.srcObject !== stream) {
|
|
1279
1369
|
this.video.srcObject = stream;
|
|
1280
1370
|
this.audio.srcObject = stream;
|
|
1371
|
+
this._resetAutoConverge(); // a new stream may be a new camera: measure afresh
|
|
1281
1372
|
}
|
|
1282
1373
|
this.video.play().catch(() => {});
|
|
1283
1374
|
this.audio.play().catch(() => {});
|
|
@@ -1383,6 +1474,8 @@ class Tile {
|
|
|
1383
1474
|
});
|
|
1384
1475
|
// First mono peer on a woven wall and lift not asked for yet (e.g. setMono3D(true) later).
|
|
1385
1476
|
if (this.format !== 'sbs' && call.woven && call.mono3DOn && !call._liftP) call._ensureLift();
|
|
1477
|
+
// A mono peer on a 3D display that stays flat because the lift MODULE is missing: say so.
|
|
1478
|
+
if (this.format !== 'sbs' && call.woven && call.liftMissing) call._warnLiftMissing();
|
|
1386
1479
|
const key = `${r.route}|${this.format}`;
|
|
1387
1480
|
if (!force && key === this.routedKey) return;
|
|
1388
1481
|
this.routedKey = key;
|
|
@@ -1572,6 +1665,7 @@ class Tile {
|
|
|
1572
1665
|
c.width = 2 * outW;
|
|
1573
1666
|
c.height = outH;
|
|
1574
1667
|
}
|
|
1668
|
+
if (this.call.o.autoConverge) this._sampleDisparity(v, W, H);
|
|
1575
1669
|
const shift = this.conv.step(eyeW);
|
|
1576
1670
|
const g = c.getContext('2d');
|
|
1577
1671
|
for (const eye of [0, 1]) {
|
|
@@ -1580,7 +1674,114 @@ class Tile {
|
|
|
1580
1674
|
}
|
|
1581
1675
|
}
|
|
1582
1676
|
|
|
1677
|
+
/**
|
|
1678
|
+
* Auto-convergence: a few times a second, measure the disparity of the point between the remote
|
|
1679
|
+
* person's eyes on a small grayscale copy of the SOURCE frame (before our shift, so there is no
|
|
1680
|
+
* feedback loop) and hand it to the convergence state, which halves it per eye and low-passes it.
|
|
1681
|
+
* A failed measurement holds the last good value.
|
|
1682
|
+
*/
|
|
1683
|
+
_sampleDisparity(v, W, H) {
|
|
1684
|
+
const now = performance.now();
|
|
1685
|
+
if (this.autoPending || now - this.autoAt < AUTO_CONV_INTERVAL_MS) return;
|
|
1686
|
+
this.autoAt = now;
|
|
1687
|
+
// Preferred: WebCodecs. `new VideoFrame(video)` + an ASYNC copyTo() of the decoded frame, reading
|
|
1688
|
+
// the luma (Y) plane directly: no synchronous GPU->CPU canvas readback on the paint path (that
|
|
1689
|
+
// readback, not the matching, was ~25 ms of a ~30 ms measurement).
|
|
1690
|
+
if (typeof VideoFrame === 'function') {
|
|
1691
|
+
let frame = null;
|
|
1692
|
+
try {
|
|
1693
|
+
frame = new VideoFrame(v);
|
|
1694
|
+
} catch {
|
|
1695
|
+
frame = null;
|
|
1696
|
+
}
|
|
1697
|
+
if (frame && Y_PLANE_FORMATS.has(frame.format)) {
|
|
1698
|
+
this.autoPending = true;
|
|
1699
|
+
const gen = this.autoGen;
|
|
1700
|
+
const fw = frame.visibleRect ? frame.visibleRect.width : frame.displayWidth;
|
|
1701
|
+
const fh = frame.visibleRect ? frame.visibleRect.height : frame.displayHeight;
|
|
1702
|
+
const buf = new Uint8Array(frame.allocationSize());
|
|
1703
|
+
frame
|
|
1704
|
+
.copyTo(buf)
|
|
1705
|
+
.then((layout) => {
|
|
1706
|
+
if (gen !== this.autoGen) return; // the stream changed or the tile is gone: stale frame
|
|
1707
|
+
const t0 = performance.now();
|
|
1708
|
+
const f = Math.max(1, Math.ceil(fw / 2 / AUTO_CONV_EYE_WIDTH));
|
|
1709
|
+
const ds = downsampleLuma(buf, layout[0].offset, layout[0].stride, fw, fh, f);
|
|
1710
|
+
this._applyMeasurement(ds.img, ds.w, ds.h, ds.w / fw, now, performance.now() - t0, 'videoframe');
|
|
1711
|
+
})
|
|
1712
|
+
.catch(() => {
|
|
1713
|
+
if (gen === this.autoGen) this._applyMeasurement(null, 0, 0, 1, now, 0, 'videoframe');
|
|
1714
|
+
})
|
|
1715
|
+
.finally(() => {
|
|
1716
|
+
frame.close();
|
|
1717
|
+
this.autoPending = false;
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (frame) frame.close();
|
|
1722
|
+
}
|
|
1723
|
+
// Fallback: a small canvas + getImageData (synchronous readback).
|
|
1724
|
+
const s = Math.min(1, AUTO_CONV_EYE_WIDTH / (W / 2));
|
|
1725
|
+
const w = Math.max(2, Math.round((W * s) / 2) * 2);
|
|
1726
|
+
const h = Math.max(2, Math.round(H * s));
|
|
1727
|
+
if (!this.autoCanvas) this.autoCanvas = document.createElement('canvas');
|
|
1728
|
+
const c = this.autoCanvas;
|
|
1729
|
+
if (c.width !== w || c.height !== h) {
|
|
1730
|
+
c.width = w;
|
|
1731
|
+
c.height = h;
|
|
1732
|
+
}
|
|
1733
|
+
const g = c.getContext('2d', { willReadFrequently: true });
|
|
1734
|
+
let img = null;
|
|
1735
|
+
try {
|
|
1736
|
+
g.drawImage(v, 0, 0, w, h);
|
|
1737
|
+
img = lumaFromRgba(g.getImageData(0, 0, w, h).data, w * h);
|
|
1738
|
+
} catch {
|
|
1739
|
+
img = null; // a frame that cannot be read (tainted, not decoded yet): skip it
|
|
1740
|
+
}
|
|
1741
|
+
this._applyMeasurement(img, w, h, w / W, now, performance.now() - now, 'canvas');
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Measure one grabbed frame and hand the disparity to the convergence state (which halves it per
|
|
1746
|
+
* eye and low-passes it). A failed measurement holds the last good value. `readMs` = main-thread
|
|
1747
|
+
* time spent getting the pixels.
|
|
1748
|
+
*/
|
|
1749
|
+
_applyMeasurement(img, w, h, s, at, readMs, via) {
|
|
1750
|
+
if (this.destroyed) return;
|
|
1751
|
+
const t = performance.now();
|
|
1752
|
+
const m = img ? this.autoFocus.measure(img, w, h, at) : null;
|
|
1753
|
+
const d = this.autoTrack.push(m ? m.d / s : null);
|
|
1754
|
+
this.conv.measuredPx = d;
|
|
1755
|
+
this.autoStats.n++;
|
|
1756
|
+
this.autoStats.readMs += readMs;
|
|
1757
|
+
this.autoStats.ms += readMs + (performance.now() - t);
|
|
1758
|
+
this.autoStats.method = m ? m.method : null;
|
|
1759
|
+
this.autoStats.via = via;
|
|
1760
|
+
if (m && !this.autoLocked) {
|
|
1761
|
+
this.autoLocked = true;
|
|
1762
|
+
this.call.log('auto-converge', {
|
|
1763
|
+
peer: this.id,
|
|
1764
|
+
disparityPx: Math.round(d * 10) / 10,
|
|
1765
|
+
at: { x: Math.round(m.x / s), y: Math.round(m.y / s) },
|
|
1766
|
+
ncc: Math.round(m.c * 100) / 100,
|
|
1767
|
+
method: m.method,
|
|
1768
|
+
via,
|
|
1769
|
+
ms: Math.round(readMs + (performance.now() - t)),
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
_resetAutoConverge() {
|
|
1775
|
+
this.autoGen++;
|
|
1776
|
+
this.autoTrack.reset();
|
|
1777
|
+
this.autoFocus.reset();
|
|
1778
|
+
this.conv.measuredPx = null;
|
|
1779
|
+
this.autoLocked = false;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1583
1782
|
destroy() {
|
|
1783
|
+
this.destroyed = true;
|
|
1784
|
+
this.autoGen++;
|
|
1584
1785
|
clearTimeout(this.helloTimer);
|
|
1585
1786
|
clearTimeout(this.layerTimer);
|
|
1586
1787
|
clearTimeout(this.leaveTimer);
|
|
@@ -1693,3 +1894,8 @@ class SelfTile {
|
|
|
1693
1894
|
}
|
|
1694
1895
|
}
|
|
1695
1896
|
}
|
|
1897
|
+
|
|
1898
|
+
// `<dxr-call>` — the one-line path as markup — is registered by importing this entry (RFC 0003 §1;
|
|
1899
|
+
// the ONE side effect of this module, declared in package.json `sideEffects`). A no-op where there
|
|
1900
|
+
// is no `customElements` registry (Node, workers) or the name is already defined.
|
|
1901
|
+
defineCallElement();
|