@displayxr/inline3d 1.27.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 +224 -0
- package/README.md +6 -3
- package/call-full.d.ts +21 -0
- package/call.d.ts +454 -0
- package/dist/call.js +81 -0
- package/index.d.ts +34 -2
- package/js/call/capture.js +212 -0
- package/js/call/disparity.js +432 -0
- package/js/call/element.js +217 -0
- package/js/call/lift.js +323 -0
- package/js/call/qr.js +283 -0
- package/js/call/sdp.js +86 -0
- package/js/call/signaling.js +203 -0
- package/js/call/transport.js +589 -0
- package/js/call/ui.js +146 -0
- package/js/call/wire.js +434 -0
- package/js/inline3d-buffer-limit.js +198 -0
- package/js/inline3d-call-full.js +48 -0
- package/js/inline3d-call.js +1901 -0
- package/js/inline3d-model-playcanvas.js +6 -0
- package/js/inline3d-model.js +6 -0
- package/js/inline3d-player.js +649 -218
- package/js/inline3d-splat-playcanvas.js +134 -18
- package/js/inline3d-splat-video.js +328 -29
- package/js/inline3d-splat.js +5 -0
- package/js/inline3d-viewer.js +86 -10
- package/js/inline3d.js +257 -15
- package/js/lift/index.js +15 -0
- package/model.d.ts +10 -1
- package/package.json +29 -4
- package/player.d.ts +49 -4
- package/splat.d.ts +52 -3
- package/viewer.d.ts +12 -1
package/js/inline3d.js
CHANGED
|
@@ -35,6 +35,10 @@ export { undock, undockAvailable, undockUrl, tileScreenRect };
|
|
|
35
35
|
// The eased 2D<->3D transition. A pure state machine (no DOM, no WebXR) ported from the native
|
|
36
36
|
// `dxr::ModeSwitch`, so the browser eases the disparity around a mode switch the same way — and in
|
|
37
37
|
// the same ORDER — as the native apps and the demos. See _requestRenderingModeEased.
|
|
38
|
+
// The device-limit clamp on a backing store (MAX_TEXTURE_SIZE & co.), shared with ./viewer,
|
|
39
|
+
// ./splat and ./model. Dependency-free.
|
|
40
|
+
import { probeBufferLimits, clampEyeBuffer, clampWarning, bufferScale, mismatchWarning } from './inline3d-buffer-limit.js';
|
|
41
|
+
|
|
38
42
|
import {
|
|
39
43
|
ModeSwitch,
|
|
40
44
|
MODE_SWITCH_DEFAULT_DURATION_MS,
|
|
@@ -589,6 +593,39 @@ export async function createInline3D(opts = {}) {
|
|
|
589
593
|
return new Inline3D(session, refSpace, { lazy, rootMargin, autoChrome, modeSwitch, untrackedFallback });
|
|
590
594
|
}
|
|
591
595
|
|
|
596
|
+
// The one shared creation in flight, so two callers racing before the first session resolves
|
|
597
|
+
// (a <dxr-call> and a player both mounting on load) share ONE createInline3D() rather than
|
|
598
|
+
// opening two sessions. Cleared once it settles: from then on `liveManager` is the answer.
|
|
599
|
+
let sharedPending = null;
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* The document's shared manager: the live `createInline3D()` result if one exists, else a new one
|
|
603
|
+
* (registered as the shared one by construction, since every manager is). Woven-canvas rule 1 —
|
|
604
|
+
* one inline-3D session per document — is what this exists for: a module that needs a wall it
|
|
605
|
+
* did not create (`mountCall`, `<dxr-call>`) must not open a second session next to the page's
|
|
606
|
+
* own, and a page that made its wall first and then adds such a module gets that same wall back.
|
|
607
|
+
*
|
|
608
|
+
* `opts` are `createInline3D`'s and apply only when this call creates the manager; a manager
|
|
609
|
+
* that already exists is returned as it was configured. An unsupported browser resolves to the
|
|
610
|
+
* same `{ supported: false }` shape as `createInline3D`, and it is NOT cached: nothing was
|
|
611
|
+
* created, and a later call re-probes (the weave service can bind after page load).
|
|
612
|
+
*
|
|
613
|
+
* Closing: whoever `close()`s the shared manager ends the document's session for everyone using
|
|
614
|
+
* it; the next `sharedInline3D()` opens a fresh one. Modules that borrow the wall must not close
|
|
615
|
+
* it (the call module never does; it only closes a wall it created — which it no longer does).
|
|
616
|
+
*
|
|
617
|
+
* @param {object} [opts] see {@link createInline3D}
|
|
618
|
+
* @returns {Promise<Inline3D | {supported:false, trackingState:'unknown', error?:Error}>}
|
|
619
|
+
*/
|
|
620
|
+
export function sharedInline3D(opts = {}) {
|
|
621
|
+
if (liveManager && liveManager._running) return Promise.resolve(liveManager);
|
|
622
|
+
if (sharedPending) return sharedPending;
|
|
623
|
+
sharedPending = createInline3D(opts).finally(() => {
|
|
624
|
+
sharedPending = null;
|
|
625
|
+
});
|
|
626
|
+
return sharedPending;
|
|
627
|
+
}
|
|
628
|
+
|
|
592
629
|
/**
|
|
593
630
|
* Back-compatible single-scene helper: open a session, weave one canvas, drive a render
|
|
594
631
|
* callback with the two eye views each frame. Equivalent to
|
|
@@ -637,6 +674,15 @@ function chromeTextPlates(root) {
|
|
|
637
674
|
return plates;
|
|
638
675
|
}
|
|
639
676
|
|
|
677
|
+
/** A pending rewoven() settles anyway ('hold-capped') after this many holds from the call. */
|
|
678
|
+
const REWOVEN_MAX_HOLDS = 4;
|
|
679
|
+
|
|
680
|
+
/** The CSS size + dpr of a window's canvas: what a pending rewoven() restarts on. */
|
|
681
|
+
function boxKeyOf(canvas) {
|
|
682
|
+
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
|
|
683
|
+
return `${canvas.clientWidth}x${canvas.clientHeight}@${dpr}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
640
686
|
class Inline3D {
|
|
641
687
|
constructor(
|
|
642
688
|
session,
|
|
@@ -1175,6 +1221,20 @@ class Inline3D {
|
|
|
1175
1221
|
live = false;
|
|
1176
1222
|
};
|
|
1177
1223
|
},
|
|
1224
|
+
/**
|
|
1225
|
+
* {@link firstWoven}, measured from NOW: resolves once this canvas, as it is at the call,
|
|
1226
|
+
* has drawn a stereo frame and then held for `firstWovenHoldMs`. For a canvas that is
|
|
1227
|
+
* already woven but whose rect is about to change (fullscreen, a layout resize): the
|
|
1228
|
+
* browser re-registers the moved rect and the same identity gap as a fresh canvas
|
|
1229
|
+
* applies, so cover the canvas across the change and release on this. A change of the
|
|
1230
|
+
* canvas's CSS size or devicePixelRatio while it is pending restarts the hold (checked every
|
|
1231
|
+
* frame, for every window kind; a move without a resize is not detected), and it settles
|
|
1232
|
+
* anyway with `reason: 'hold-capped'` four holds after the call, so a size that never stops
|
|
1233
|
+
* animating cannot keep a cover up. Calling it again while pending returns the same
|
|
1234
|
+
* promise, restarted. Before the first join it IS `firstWoven`; on a window that will not
|
|
1235
|
+
* weave it resolves that `woven: false` result. Same shape, never rejects.
|
|
1236
|
+
*/
|
|
1237
|
+
rewoven: () => this._rewoven(win),
|
|
1178
1238
|
};
|
|
1179
1239
|
}
|
|
1180
1240
|
|
|
@@ -1953,6 +2013,10 @@ class Inline3D {
|
|
|
1953
2013
|
fwRegAt: nowMs(),
|
|
1954
2014
|
fwLayerAt: null,
|
|
1955
2015
|
fwStereo: false,
|
|
2016
|
+
// handle.rewoven(): null, or the pending { promise, resolve, calledAt, at, stereo, box } —
|
|
2017
|
+
// firstWoven's two halves again, counted from the call (and from each later box change).
|
|
2018
|
+
rw: null,
|
|
2019
|
+
rwGone: null, // the reason this window will never weave again, once one arrived
|
|
1956
2020
|
};
|
|
1957
2021
|
win.fwPromise = new Promise((resolve) => {
|
|
1958
2022
|
win.fwResolve = resolve;
|
|
@@ -2047,6 +2111,7 @@ class Inline3D {
|
|
|
2047
2111
|
win.fwLayerAt = nowMs();
|
|
2048
2112
|
win.fwStereo = false;
|
|
2049
2113
|
}
|
|
2114
|
+
this._restartRewoven(win);
|
|
2050
2115
|
win.layerLostSent = false; // a live layer again: a future loss is worth reporting again
|
|
2051
2116
|
// Nothing about the hardware state is re-asserted here, and that is the point: the panel's
|
|
2052
2117
|
// mode is the DISPLAY's, it survives a tile scrolling away, and this SDK never requests it
|
|
@@ -2340,17 +2405,108 @@ class Inline3D {
|
|
|
2340
2405
|
win.autoExcluded.clear();
|
|
2341
2406
|
}
|
|
2342
2407
|
|
|
2343
|
-
/**
|
|
2344
|
-
|
|
2408
|
+
/**
|
|
2409
|
+
* The per-eye buffer size this window should have right now: explicit, or box × dpr — capped
|
|
2410
|
+
* at the SOURCE's own per-eye resolution for image and video windows.
|
|
2411
|
+
*
|
|
2412
|
+
* WHY THE CAP (measured, NP02J tablet, DisplayXR Browser 1.0.6): the player sample's 90vw
|
|
2413
|
+
* tile at dpr 2 asked for a 4608×1296 backing store for a 640×360-per-eye clip — 13× the
|
|
2414
|
+
* source's pixels, re-drawn every frame. The page fell to 25 fps and dropped 47 of 97 video
|
|
2415
|
+
* frames; the same clip in a 45vw tile (2304×648) ran at 57 fps with 0 dropped. Drawing a
|
|
2416
|
+
* source into a buffer larger than itself adds no detail: the compositor scales the layer to
|
|
2417
|
+
* the box either way, so upscaling here only spends GPU fill-rate. The cap keeps the BOX
|
|
2418
|
+
* aspect (the eyes stay correctly shaped) and only shrinks, never grows past box × dpr.
|
|
2419
|
+
* An explicit {width, height} is the page's call and is never capped by the source.
|
|
2420
|
+
*
|
|
2421
|
+
* THEN THE DEVICE LIMIT, on every window including an explicit size: the whole store (2 × eye
|
|
2422
|
+
* wide in SBS) must fit min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), or
|
|
2423
|
+
* the weave cannot take it as one texture. An Android 3D tablet (Adreno 740) reports 4096 where
|
|
2424
|
+
* desktops report 16384. Both axes shrink by one factor (./inline3d-buffer-limit.js), warned
|
|
2425
|
+
* once per window. `win.bufScale` records the total shrink so buffer-px decoration
|
|
2426
|
+
* (cornerRadius, feather) keeps its on-screen size.
|
|
2427
|
+
*/
|
|
2428
|
+
_eyeSize(win, sbs = true) {
|
|
2345
2429
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2430
|
+
let w = win.reqW || Math.round((win.canvas.clientWidth || 256) * dpr);
|
|
2431
|
+
let h = win.reqH || Math.round((win.canvas.clientHeight || 256) * dpr);
|
|
2432
|
+
let scale = 1;
|
|
2433
|
+
const eye = !win.reqW && !win.reqH && this._sourceEyeSize(win);
|
|
2434
|
+
if (eye) {
|
|
2435
|
+
// The smallest buffer that still holds every source pixel along the axis that fills the box.
|
|
2436
|
+
const s = Math.max(eye.w / w, eye.h / h);
|
|
2437
|
+
if (s < 1) {
|
|
2438
|
+
scale = s;
|
|
2439
|
+
w = Math.max(1, Math.round(w * s));
|
|
2440
|
+
h = Math.max(1, Math.round(h * s));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const c = clampEyeBuffer(w, h, probeBufferLimits(), { cols: sbs ? 2 : 1 });
|
|
2444
|
+
if (c.clamped) {
|
|
2445
|
+
scale *= c.scale;
|
|
2446
|
+
w = c.eyeW;
|
|
2447
|
+
h = c.eyeH;
|
|
2448
|
+
if (!win.warnedBufClamp) {
|
|
2449
|
+
win.warnedBufClamp = true;
|
|
2450
|
+
console.warn(clampWarning('[inline3d]', c));
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
win.bufScale = scale;
|
|
2454
|
+
return { w, h };
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
/** One eye of an image/video window's source (an SBS pair: left eye = left half), or null. */
|
|
2458
|
+
_sourceEyeSize(win) {
|
|
2459
|
+
if (win.kind !== 'video' && win.kind !== 'image') return null;
|
|
2460
|
+
const src = win.kind === 'video' ? win.video : win.img;
|
|
2461
|
+
if (!src) return null;
|
|
2462
|
+
const sw = src.videoWidth || src.naturalWidth || src.width;
|
|
2463
|
+
const sh = src.videoHeight || src.naturalHeight || src.height;
|
|
2464
|
+
if (!(sw > 1) || !(sh > 0)) return null;
|
|
2465
|
+
return { w: sw / 2, h: sh };
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/**
|
|
2469
|
+
* A scene canvas is the app's to size, so the core cannot clamp it — but it can SEE the browser
|
|
2470
|
+
* clamp it. Where the store exceeds the device's MAX_TEXTURE_SIZE (4096 on an Adreno 740 tablet)
|
|
2471
|
+
* the drawing buffer silently comes out smaller than canvas.width, while getViewport() keeps
|
|
2472
|
+
* splitting canvas.width: the eye boundary lands off-centre and the panel shows a double image.
|
|
2473
|
+
* Say so once. ./viewer, ./splat and ./model clamp before sizing and never trip this.
|
|
2474
|
+
*
|
|
2475
|
+
* Only called after the app has drawn a stereo frame, so the canvas already HAS its context:
|
|
2476
|
+
* getContext() of the same type returns it, of another type returns null — it never creates one.
|
|
2477
|
+
*/
|
|
2478
|
+
_checkSceneBuffer(win) {
|
|
2479
|
+
const c = win.canvas;
|
|
2480
|
+
if (!c || typeof c.getContext !== 'function') return;
|
|
2481
|
+
if (win.sceneGl === undefined) {
|
|
2482
|
+
let gl = null;
|
|
2483
|
+
try {
|
|
2484
|
+
gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
2485
|
+
} catch {
|
|
2486
|
+
gl = null;
|
|
2487
|
+
}
|
|
2488
|
+
win.sceneGl = gl && typeof gl.drawingBufferWidth === 'number' ? gl : null;
|
|
2489
|
+
}
|
|
2490
|
+
if (!win.sceneGl) return;
|
|
2491
|
+
const b = bufferScale(c, win.sceneGl);
|
|
2492
|
+
if (b.mismatch) {
|
|
2493
|
+
win.warnedBufMismatch = true;
|
|
2494
|
+
console.warn(
|
|
2495
|
+
mismatchWarning(
|
|
2496
|
+
'[inline3d]',
|
|
2497
|
+
c,
|
|
2498
|
+
b,
|
|
2499
|
+
'getViewport() splits canvas.width, so the eyes are off-centre in the woven buffer. This ' +
|
|
2500
|
+
'scene canvas is sized by the page: keep 2 × eye width and the height within ' +
|
|
2501
|
+
'MAX_TEXTURE_SIZE / MAX_RENDERBUFFER_SIZE / MAX_VIEWPORT_DIMS, or map getViewport() onto ' +
|
|
2502
|
+
'gl.drawingBufferWidth/Height.',
|
|
2503
|
+
),
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2350
2506
|
}
|
|
2351
2507
|
|
|
2352
2508
|
_sizeBuffer(win, sbs) {
|
|
2353
|
-
const { w: boxW, h: boxH } = this._eyeSize(win);
|
|
2509
|
+
const { w: boxW, h: boxH } = this._eyeSize(win, sbs);
|
|
2354
2510
|
win.eyeW = boxW;
|
|
2355
2511
|
win.eyeH = boxH;
|
|
2356
2512
|
win.canvas.width = sbs ? boxW * 2 : boxW; // SBS = two eye tiles wide
|
|
@@ -2397,8 +2553,12 @@ class Inline3D {
|
|
|
2397
2553
|
if (!win.resizePending) return;
|
|
2398
2554
|
win.resizePending = false;
|
|
2399
2555
|
if (!win.layer || !win.ownsBuffer) return;
|
|
2556
|
+
// The on-screen box and the buffer are separate questions since the source cap (#88): a
|
|
2557
|
+
// box that moves (fullscreen) re-joins the weave at a new rect even when a capped buffer
|
|
2558
|
+
// stays the same size, so a pending rewoven() restarts on the BOX, not on the buffer.
|
|
2559
|
+
this._noteRewovenBox(win);
|
|
2400
2560
|
const { w, h } = this._eyeSize(win);
|
|
2401
|
-
if (w === win.eyeW && h === win.eyeH) return; // observer fired,
|
|
2561
|
+
if (w === win.eyeW && h === win.eyeH) return; // observer fired, the buffer didn't move
|
|
2402
2562
|
this._sizeBuffer(win, /*sbs*/ true);
|
|
2403
2563
|
this._paint(win, null); // repaint NOW: setting canvas.width cleared the buffer
|
|
2404
2564
|
};
|
|
@@ -2473,10 +2633,25 @@ class Inline3D {
|
|
|
2473
2633
|
const srcW = src.videoWidth || src.naturalWidth || src.width;
|
|
2474
2634
|
const srcH = src.videoHeight || src.naturalHeight || src.height;
|
|
2475
2635
|
if (!srcW || !srcH) return;
|
|
2636
|
+
// The buffer is capped at the source's per-eye resolution (_eyeSize), and a source's size is
|
|
2637
|
+
// only known once it has decoded — or changes on a new title. Re-derive on a change; the
|
|
2638
|
+
// resize clears the buffer, and the paint right below refills it.
|
|
2639
|
+
const srcKey = srcW + 'x' + srcH;
|
|
2640
|
+
if (win.srcKey !== srcKey) {
|
|
2641
|
+
win.srcKey = srcKey;
|
|
2642
|
+
if (win.ownsBuffer && !(win.reqW && win.reqH)) {
|
|
2643
|
+
const { w, h } = this._eyeSize(win, win.sbs);
|
|
2644
|
+
if (w !== win.eyeW || h !== win.eyeH) this._sizeBuffer(win, win.sbs);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
// cornerRadius / feather are buffer px at the UNCAPPED size; keep their on-screen size.
|
|
2648
|
+
const k = win.bufScale || 1;
|
|
2649
|
+
const radius = win.cornerRadius * k;
|
|
2650
|
+
const feather = win.feather * k;
|
|
2476
2651
|
ctx.clearRect(0, 0, c.width, c.height);
|
|
2477
2652
|
if (!win.sbs) {
|
|
2478
2653
|
// Flat fallback: left eye only, stretched to the square buffer.
|
|
2479
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height,
|
|
2654
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height, radius, feather);
|
|
2480
2655
|
return;
|
|
2481
2656
|
}
|
|
2482
2657
|
const halfDst = c.width / 2;
|
|
@@ -2487,16 +2662,16 @@ class Inline3D {
|
|
|
2487
2662
|
// as a squeezed pair). See _trackBakedStereo.
|
|
2488
2663
|
const stereo = Math.min(this._stereoFactor, this._bakedStereo);
|
|
2489
2664
|
if (stereo < 1) {
|
|
2490
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height,
|
|
2491
|
-
drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo,
|
|
2665
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
|
|
2666
|
+
drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo, radius, feather); // L+R
|
|
2492
2667
|
return;
|
|
2493
2668
|
}
|
|
2494
2669
|
// A single stretched draw maps SBS source → SBS buffer (left→left, right→right); the
|
|
2495
2670
|
// per-eye path is only needed to bake decoration (rounded corners / edge feather), which
|
|
2496
2671
|
// MUST be applied to each eye separately — see drawEye/featherEye.
|
|
2497
|
-
if (
|
|
2498
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height,
|
|
2499
|
-
drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height,
|
|
2672
|
+
if (radius > 0 || feather > 0) {
|
|
2673
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
|
|
2674
|
+
drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height, radius, feather); // R
|
|
2500
2675
|
} else {
|
|
2501
2676
|
ctx.drawImage(src, 0, 0, srcW, srcH, 0, 0, c.width, c.height);
|
|
2502
2677
|
}
|
|
@@ -2656,7 +2831,11 @@ class Inline3D {
|
|
|
2656
2831
|
win.onFrame(views, win.layer, f);
|
|
2657
2832
|
// A stereo frame the page drew without throwing. A short view list is the load
|
|
2658
2833
|
// fallback (a mono frame), which is not what a poster is waiting for.
|
|
2659
|
-
if (views.length >= 2)
|
|
2834
|
+
if (views.length >= 2) {
|
|
2835
|
+
win.fwStereo = true;
|
|
2836
|
+
if (!win.warnedBufMismatch) this._checkSceneBuffer(win);
|
|
2837
|
+
if (win.rw) win.rw.stereo = true;
|
|
2838
|
+
}
|
|
2660
2839
|
} catch (err) {
|
|
2661
2840
|
if (!win.frameThrewWarned) {
|
|
2662
2841
|
win.frameThrewWarned = true;
|
|
@@ -2679,6 +2858,7 @@ class Inline3D {
|
|
|
2679
2858
|
// video has never had a frame) the tile holds nothing worth revealing yet.
|
|
2680
2859
|
if (win.sbs && (win.kind === 'video' ? ((win.video && win.video.readyState) || 0) >= 2 : !!win.img)) {
|
|
2681
2860
|
win.fwStereo = true;
|
|
2861
|
+
if (win.rw) win.rw.stereo = true;
|
|
2682
2862
|
}
|
|
2683
2863
|
}
|
|
2684
2864
|
this._tickFirstWoven(win);
|
|
@@ -2696,6 +2876,20 @@ class Inline3D {
|
|
|
2696
2876
|
* which knows nothing about whether the compositor has matched this canvas yet.
|
|
2697
2877
|
*/
|
|
2698
2878
|
_tickFirstWoven(win) {
|
|
2879
|
+
if (win.rw) {
|
|
2880
|
+
// Every frame, for every window kind: the ResizeObserver only watches SDK-sized buffers, and
|
|
2881
|
+
// no observer sees a dpr-only change.
|
|
2882
|
+
this._noteRewovenBox(win);
|
|
2883
|
+
const rw = win.rw;
|
|
2884
|
+
const t = nowMs();
|
|
2885
|
+
if (rw.stereo && t - rw.at >= win.fwHoldMs) this._settleRewoven(win, true, 'hold-elapsed');
|
|
2886
|
+
// A box that never stops changing (a long CSS transition on width) would restart the hold
|
|
2887
|
+
// forever and leave a cover up for good: once it has restarted, give up waiting
|
|
2888
|
+
// REWOVEN_MAX_HOLDS holds after the call.
|
|
2889
|
+
else if (rw.at > rw.calledAt && t - rw.calledAt >= REWOVEN_MAX_HOLDS * win.fwHoldMs) {
|
|
2890
|
+
this._settleRewoven(win, true, 'hold-capped');
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2699
2893
|
if (win.fwResult || !win.fwStereo || win.fwLayerAt === null) return;
|
|
2700
2894
|
if (nowMs() - win.fwLayerAt < win.fwHoldMs) return;
|
|
2701
2895
|
this._settleFirstWoven(win, true, 'hold-elapsed');
|
|
@@ -2703,12 +2897,60 @@ class Inline3D {
|
|
|
2703
2897
|
|
|
2704
2898
|
/** One-shot: the first call wins, later ones are ignored. */
|
|
2705
2899
|
_settleFirstWoven(win, woven, reason) {
|
|
2900
|
+
// Every "will not weave" path lands here, so a pending rewoven() is released with it, and a
|
|
2901
|
+
// later one answers at once (firstWoven may have settled woven:true long before).
|
|
2902
|
+
if (!woven) {
|
|
2903
|
+
if (!win.rwGone) win.rwGone = reason;
|
|
2904
|
+
this._settleRewoven(win, false, reason);
|
|
2905
|
+
}
|
|
2706
2906
|
if (win.fwResult) return;
|
|
2707
2907
|
win.fwResult = Object.freeze({ woven, confirmed: false, reason, ms: Math.round(nowMs() - win.fwRegAt) });
|
|
2708
2908
|
win.fwResolve(win.fwResult);
|
|
2709
2909
|
win.fwResolve = null;
|
|
2710
2910
|
}
|
|
2711
2911
|
|
|
2912
|
+
/** handle.rewoven(): see the handle's doc comment. */
|
|
2913
|
+
_rewoven(win) {
|
|
2914
|
+
if (!win.fwResult) return win.fwPromise; // still on the first join: the same question
|
|
2915
|
+
if (!win.fwResult.woven) return Promise.resolve(win.fwResult);
|
|
2916
|
+
if (win.rwGone) return Promise.resolve(Object.freeze({ woven: false, confirmed: false, reason: win.rwGone, ms: 0 }));
|
|
2917
|
+
if (win.rw) {
|
|
2918
|
+
this._restartRewoven(win);
|
|
2919
|
+
return win.rw.promise;
|
|
2920
|
+
}
|
|
2921
|
+
let resolve;
|
|
2922
|
+
const promise = new Promise((r) => {
|
|
2923
|
+
resolve = r;
|
|
2924
|
+
});
|
|
2925
|
+
const t = nowMs();
|
|
2926
|
+
win.rw = { promise, resolve, calledAt: t, at: t, stereo: false, box: boxKeyOf(win.canvas) };
|
|
2927
|
+
return promise;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
/** A pending rewoven() restarts when the canvas's CSS size or dpr differs from the last look. */
|
|
2931
|
+
_noteRewovenBox(win) {
|
|
2932
|
+
const rw = win.rw;
|
|
2933
|
+
if (!rw) return;
|
|
2934
|
+
const box = boxKeyOf(win.canvas);
|
|
2935
|
+
if (box === rw.box) return;
|
|
2936
|
+
rw.box = box;
|
|
2937
|
+
this._restartRewoven(win);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
/** The canvas moved again (a box change, a new layer): the hold starts over from here. */
|
|
2941
|
+
_restartRewoven(win) {
|
|
2942
|
+
if (!win.rw) return;
|
|
2943
|
+
win.rw.at = nowMs();
|
|
2944
|
+
win.rw.stereo = false;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
_settleRewoven(win, woven, reason) {
|
|
2948
|
+
const rw = win.rw;
|
|
2949
|
+
if (!rw) return;
|
|
2950
|
+
win.rw = null;
|
|
2951
|
+
rw.resolve(Object.freeze({ woven, confirmed: false, reason, ms: Math.round(nowMs() - rw.calledAt) }));
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2712
2954
|
// ── page lifecycle: bfcache, freeze, restore (browser#87) ───────────────────────────
|
|
2713
2955
|
//
|
|
2714
2956
|
// A weaved window's rect reaches the compositor from the session's own rAF: every frame the
|
package/js/lift/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// js/lift/index.js — PLACEHOLDER for the lift module (`@displayxr/inline3d/lift`), which is on
|
|
2
|
+
// the `feat/lift` branch and not yet on main. That branch replaces this file wholesale (resolve
|
|
3
|
+
// any add/add conflict by taking feat/lift's file).
|
|
4
|
+
//
|
|
5
|
+
// It exists so `./call/full` (js/inline3d-call-full.js) can import lift STATICALLY today — the
|
|
6
|
+
// one shape a bundler can follow — and so the `./call` entry's computed import of this path
|
|
7
|
+
// resolves to "a lift module with no `lift` export" (reason `no-lift-export`) rather than a
|
|
8
|
+
// network 404. Either way the call raises `warning { code: 'lift-not-bundled' }` when a 2D
|
|
9
|
+
// participant would have been lifted, and the lobby reads "2D→3D unavailable in this build".
|
|
10
|
+
//
|
|
11
|
+
// `lift` is exported as null (not omitted): webpack turns a namespace access to a MISSING export
|
|
12
|
+
// into a build error, and the point of this file is that `/call/full` builds everywhere today.
|
|
13
|
+
|
|
14
|
+
export const LIFT_PLACEHOLDER = true;
|
|
15
|
+
export const lift = null;
|
package/model.d.ts
CHANGED
|
@@ -34,7 +34,11 @@ export interface ModelOptions {
|
|
|
34
34
|
* so a long subject still fits once the turntable turns it (default true).
|
|
35
35
|
*/
|
|
36
36
|
fitSweep?: boolean;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Per-eye buffer scale; 0.5–0.7 is usually free (default 1). Clamped to the device's GL limits: the whole store (2 × eye wide in 3D) is kept within
|
|
39
|
+
* min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), both axes shrunk by one factor,
|
|
40
|
+
* with a one-time console warning. The handle's `renderScale` reports the value in force.
|
|
41
|
+
*/
|
|
38
42
|
renderScale?: number;
|
|
39
43
|
feather?: number;
|
|
40
44
|
/**
|
|
@@ -176,6 +180,11 @@ export interface ModelHandle {
|
|
|
176
180
|
/** The last pose the page set (a copy), or null. */
|
|
177
181
|
getCameraPose(): SplatCameraPose | null;
|
|
178
182
|
|
|
183
|
+
/** The per-eye buffer scale in force: the `renderScale` option times the device-limit clamp. */
|
|
184
|
+
readonly renderScale: number;
|
|
185
|
+
/** The `renderScale` option as given. */
|
|
186
|
+
readonly renderScaleRequested: number;
|
|
187
|
+
|
|
179
188
|
remove(): void;
|
|
180
189
|
exclude(el: Element): void;
|
|
181
190
|
unexclude(el: Element): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -36,16 +36,29 @@
|
|
|
36
36
|
"types": "./player.d.ts",
|
|
37
37
|
"import": "./js/inline3d-player.js"
|
|
38
38
|
},
|
|
39
|
+
"./call": {
|
|
40
|
+
"types": "./call.d.ts",
|
|
41
|
+
"import": "./js/inline3d-call.js"
|
|
42
|
+
},
|
|
43
|
+
"./call/full": {
|
|
44
|
+
"types": "./call-full.d.ts",
|
|
45
|
+
"import": "./js/inline3d-call-full.js"
|
|
46
|
+
},
|
|
39
47
|
"./undock": {
|
|
40
48
|
"types": "./index.d.ts",
|
|
41
49
|
"import": "./js/inline3d-undock.js"
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
|
-
"sideEffects":
|
|
52
|
+
"sideEffects": [
|
|
53
|
+
"./js/inline3d-call.js",
|
|
54
|
+
"./js/inline3d-call-full.js",
|
|
55
|
+
"./dist/*.js"
|
|
56
|
+
],
|
|
45
57
|
"files": [
|
|
46
58
|
"js/inline3d.js",
|
|
47
59
|
"js/inline3d-undock.js",
|
|
48
60
|
"js/inline3d-mode-switch.js",
|
|
61
|
+
"js/inline3d-buffer-limit.js",
|
|
49
62
|
"js/inline3d-three.js",
|
|
50
63
|
"js/inline3d-viewer.js",
|
|
51
64
|
"js/inline3d-splat.js",
|
|
@@ -66,12 +79,19 @@
|
|
|
66
79
|
"js/inline3d-pc-look.js",
|
|
67
80
|
"js/inline3d-playcanvas-engine.js",
|
|
68
81
|
"js/inline3d-player.js",
|
|
82
|
+
"js/inline3d-call.js",
|
|
83
|
+
"js/inline3d-call-full.js",
|
|
84
|
+
"js/call/",
|
|
85
|
+
"js/lift/",
|
|
86
|
+
"dist/",
|
|
69
87
|
"index.d.ts",
|
|
70
88
|
"three.d.ts",
|
|
71
89
|
"viewer.d.ts",
|
|
72
90
|
"splat.d.ts",
|
|
73
91
|
"model.d.ts",
|
|
74
92
|
"player.d.ts",
|
|
93
|
+
"call.d.ts",
|
|
94
|
+
"call-full.d.ts",
|
|
75
95
|
"CHANGELOG.md",
|
|
76
96
|
"README.md",
|
|
77
97
|
"LICENSE",
|
|
@@ -125,10 +145,15 @@
|
|
|
125
145
|
},
|
|
126
146
|
"scripts": {
|
|
127
147
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
128
|
-
"test": "tsc --noEmit -p tsconfig.json && node --test \"test/*.test.mjs\""
|
|
148
|
+
"test": "tsc --noEmit -p tsconfig.json && node --test \"test/*.test.mjs\"",
|
|
149
|
+
"build:dist": "node tools/dist/build.mjs",
|
|
150
|
+
"prepack": "node tools/dist/build.mjs",
|
|
151
|
+
"test:e2e": "node test/e2e/call-element.e2e.mjs",
|
|
152
|
+
"test:bundlers": "node test/bundlers/run.mjs"
|
|
129
153
|
},
|
|
130
154
|
"devDependencies": {
|
|
131
155
|
"typescript": "^5.4.0",
|
|
132
|
-
"@types/webxr": "^0.5.0"
|
|
156
|
+
"@types/webxr": "^0.5.0",
|
|
157
|
+
"esbuild": "^0.25.0"
|
|
133
158
|
}
|
|
134
159
|
}
|
package/player.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface PlayerOptions {
|
|
|
91
91
|
* tile (e.g. `2.39` or `'2.39:1'` for a scope band in a 16:9 tile); the rest stays clear.
|
|
92
92
|
* Implies `fit: 'contain'` unless `fit` says otherwise.
|
|
93
93
|
*/
|
|
94
|
-
band?: number | string;
|
|
94
|
+
band?: number | string | null;
|
|
95
95
|
/**
|
|
96
96
|
* What the poster image is: `'mono'` (one image for both eyes, the default), or a stereo still
|
|
97
97
|
* laid out like the video (`'sbs'` / `'tb'`), painted eye by eye so it is 3D before the first frame.
|
|
@@ -123,6 +123,12 @@ export interface PlayerOptions {
|
|
|
123
123
|
* Fullscreen API.
|
|
124
124
|
*/
|
|
125
125
|
fullscreen?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Cover the canvas with black across a fullscreen change until `handle.rewoven()` settles
|
|
128
|
+
* (woven-canvas rule 11). Default false: on the Leia panel fullscreen showed no raw side-by-side
|
|
129
|
+
* pair without it, and the cover costs ~1.2 s with no picture.
|
|
130
|
+
*/
|
|
131
|
+
fullscreenCover?: boolean;
|
|
126
132
|
/** Default: `'anonymous'` iff `src` is a cross-origin URL; unset (browser default) otherwise. */
|
|
127
133
|
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
128
134
|
/** Per-eye buffer resolution in px. `'sbs'` + supported wall only — see {@link TileOptions}. */
|
|
@@ -135,7 +141,7 @@ export interface PlayerOptions {
|
|
|
135
141
|
observe?: Element;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
export type PlayerEvent = 'play' | 'pause' | 'ended' | 'timeupdate' | 'ready' | 'error' | 'titlechange';
|
|
144
|
+
export type PlayerEvent = 'play' | 'pause' | 'ended' | 'timeupdate' | 'ready' | 'error' | 'titlechange' | 'detached';
|
|
139
145
|
|
|
140
146
|
/** One playlist entry (RFC 0001 Addendum A4). */
|
|
141
147
|
export interface PlayerTitle {
|
|
@@ -224,6 +230,44 @@ export function addPlayer(
|
|
|
224
230
|
opts?: PlayerOptions,
|
|
225
231
|
): PlayerHandle;
|
|
226
232
|
|
|
233
|
+
/** {@link attachPlayer}'s options: the player's, plus where its controls go. */
|
|
234
|
+
export interface AttachPlayerOptions extends PlayerOptions {
|
|
235
|
+
/** Where the SDK controls go. Default: the handle's canvas's parent element. */
|
|
236
|
+
chromeContainer?: Element;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `'detached'`'s payload: something else took (`'superseded'`) or emptied (`'released'`) the slot. */
|
|
240
|
+
export interface PlayerDetachedEvent {
|
|
241
|
+
reason: 'superseded' | 'released';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** What {@link attachPlayer} returns: a {@link PlayerHandle} that can give its slot back. */
|
|
245
|
+
export interface AttachedPlayerHandle extends PlayerHandle {
|
|
246
|
+
/**
|
|
247
|
+
* Give the slot back: `setVideo(null)`, so the splat, pose, lens and declared rig are exactly as
|
|
248
|
+
* they were, then tear down the controls and release the elements. Resolves once restored. Leave
|
|
249
|
+
* a Watch screen with `await player.detach()` before the handle's own `setSource`.
|
|
250
|
+
*/
|
|
251
|
+
detach(): Promise<void>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* PREVIEW. Surface mode (RFC 0001 Addendum A): the same transport, playlist and events as
|
|
256
|
+
* {@link addPlayer}, drawn through an EXISTING `addSplat(…, { engine: 'playcanvas' })` handle's
|
|
257
|
+
* `setVideo` — no canvas, layer or session of its own. The scene stays on screen until the first
|
|
258
|
+
* frame (it is the poster); a source swap lands at the next title's first frame, as a cut or, with
|
|
259
|
+
* `transition: 'crossfade'`, `./splat` setVideo's GPU crossfade. Differences from `addPlayer`:
|
|
260
|
+
* `fullscreen` defaults to false; `band` goes to setVideo's letterbox slot; `poster`, `posterFormat` and the tile options are ignored
|
|
261
|
+
* (warns once). If another `setVideo` takes the slot, the player stops and emits `'detached'`
|
|
262
|
+
* ({@link PlayerDetachedEvent}); it never fights for it.
|
|
263
|
+
*/
|
|
264
|
+
export function attachPlayer(
|
|
265
|
+
splat: { setVideo: (...args: any[]) => Promise<unknown>; canvas?: HTMLCanvasElement; videoElement?: HTMLVideoElement | null },
|
|
266
|
+
/** `null` with `opts.titles` loads the first title. */
|
|
267
|
+
src: PlayerSource | null,
|
|
268
|
+
opts?: AttachPlayerOptions,
|
|
269
|
+
): AttachedPlayerHandle;
|
|
270
|
+
|
|
227
271
|
/** The `setSource()` transitions a video can mean. */
|
|
228
272
|
export type PlayerTransition = 'cut' | 'crossfade';
|
|
229
273
|
|
|
@@ -267,7 +311,8 @@ export type PlayerAccent = 'azure' | 'violet' | 'magenta' | 'sunset' | 'amber' |
|
|
|
267
311
|
|
|
268
312
|
/**
|
|
269
313
|
* Pick the first source this browser can play from candidates listed best first; each is a URL, or
|
|
270
|
-
* `{ src, type }` with a FULL `canPlayType` string (`'video/webm; codecs="vp9, opus"'`).
|
|
271
|
-
* DisplayXR Browser
|
|
314
|
+
* `{ src, type }` with a FULL `canPlayType` string (`'video/webm; codecs="vp9, opus"'`). Only
|
|
315
|
+
* DisplayXR Browser 1.0.6+ plays H.264/AAC (via the OS decoders), and a codec-less `'video/mp4'`
|
|
316
|
+
* still answers 'maybe' — so list a VP9 WebM first when you have one.
|
|
272
317
|
*/
|
|
273
318
|
export function pickSource(candidates: PlayerSource, canPlayType?: (type: string) => string): string | Blob | undefined;
|