@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.js
CHANGED
|
@@ -593,6 +593,39 @@ export async function createInline3D(opts = {}) {
|
|
|
593
593
|
return new Inline3D(session, refSpace, { lazy, rootMargin, autoChrome, modeSwitch, untrackedFallback });
|
|
594
594
|
}
|
|
595
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
|
+
|
|
596
629
|
/**
|
|
597
630
|
* Back-compatible single-scene helper: open a session, weave one canvas, drive a render
|
|
598
631
|
* callback with the two eye views each frame. Equivalent to
|
|
@@ -641,6 +674,15 @@ function chromeTextPlates(root) {
|
|
|
641
674
|
return plates;
|
|
642
675
|
}
|
|
643
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
|
+
|
|
644
686
|
class Inline3D {
|
|
645
687
|
constructor(
|
|
646
688
|
session,
|
|
@@ -1179,6 +1221,20 @@ class Inline3D {
|
|
|
1179
1221
|
live = false;
|
|
1180
1222
|
};
|
|
1181
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),
|
|
1182
1238
|
};
|
|
1183
1239
|
}
|
|
1184
1240
|
|
|
@@ -1957,6 +2013,10 @@ class Inline3D {
|
|
|
1957
2013
|
fwRegAt: nowMs(),
|
|
1958
2014
|
fwLayerAt: null,
|
|
1959
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
|
|
1960
2020
|
};
|
|
1961
2021
|
win.fwPromise = new Promise((resolve) => {
|
|
1962
2022
|
win.fwResolve = resolve;
|
|
@@ -2051,6 +2111,7 @@ class Inline3D {
|
|
|
2051
2111
|
win.fwLayerAt = nowMs();
|
|
2052
2112
|
win.fwStereo = false;
|
|
2053
2113
|
}
|
|
2114
|
+
this._restartRewoven(win);
|
|
2054
2115
|
win.layerLostSent = false; // a live layer again: a future loss is worth reporting again
|
|
2055
2116
|
// Nothing about the hardware state is re-asserted here, and that is the point: the panel's
|
|
2056
2117
|
// mode is the DISPLAY's, it survives a tile scrolling away, and this SDK never requests it
|
|
@@ -2492,8 +2553,12 @@ class Inline3D {
|
|
|
2492
2553
|
if (!win.resizePending) return;
|
|
2493
2554
|
win.resizePending = false;
|
|
2494
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);
|
|
2495
2560
|
const { w, h } = this._eyeSize(win);
|
|
2496
|
-
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
|
|
2497
2562
|
this._sizeBuffer(win, /*sbs*/ true);
|
|
2498
2563
|
this._paint(win, null); // repaint NOW: setting canvas.width cleared the buffer
|
|
2499
2564
|
};
|
|
@@ -2769,6 +2834,7 @@ class Inline3D {
|
|
|
2769
2834
|
if (views.length >= 2) {
|
|
2770
2835
|
win.fwStereo = true;
|
|
2771
2836
|
if (!win.warnedBufMismatch) this._checkSceneBuffer(win);
|
|
2837
|
+
if (win.rw) win.rw.stereo = true;
|
|
2772
2838
|
}
|
|
2773
2839
|
} catch (err) {
|
|
2774
2840
|
if (!win.frameThrewWarned) {
|
|
@@ -2792,6 +2858,7 @@ class Inline3D {
|
|
|
2792
2858
|
// video has never had a frame) the tile holds nothing worth revealing yet.
|
|
2793
2859
|
if (win.sbs && (win.kind === 'video' ? ((win.video && win.video.readyState) || 0) >= 2 : !!win.img)) {
|
|
2794
2860
|
win.fwStereo = true;
|
|
2861
|
+
if (win.rw) win.rw.stereo = true;
|
|
2795
2862
|
}
|
|
2796
2863
|
}
|
|
2797
2864
|
this._tickFirstWoven(win);
|
|
@@ -2809,6 +2876,20 @@ class Inline3D {
|
|
|
2809
2876
|
* which knows nothing about whether the compositor has matched this canvas yet.
|
|
2810
2877
|
*/
|
|
2811
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
|
+
}
|
|
2812
2893
|
if (win.fwResult || !win.fwStereo || win.fwLayerAt === null) return;
|
|
2813
2894
|
if (nowMs() - win.fwLayerAt < win.fwHoldMs) return;
|
|
2814
2895
|
this._settleFirstWoven(win, true, 'hold-elapsed');
|
|
@@ -2816,12 +2897,60 @@ class Inline3D {
|
|
|
2816
2897
|
|
|
2817
2898
|
/** One-shot: the first call wins, later ones are ignored. */
|
|
2818
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
|
+
}
|
|
2819
2906
|
if (win.fwResult) return;
|
|
2820
2907
|
win.fwResult = Object.freeze({ woven, confirmed: false, reason, ms: Math.round(nowMs() - win.fwRegAt) });
|
|
2821
2908
|
win.fwResolve(win.fwResult);
|
|
2822
2909
|
win.fwResolve = null;
|
|
2823
2910
|
}
|
|
2824
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
|
+
|
|
2825
2954
|
// ── page lifecycle: bfcache, freeze, restore (browser#87) ───────────────────────────
|
|
2826
2955
|
//
|
|
2827
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/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",
|
|
@@ -40,12 +40,20 @@
|
|
|
40
40
|
"types": "./call.d.ts",
|
|
41
41
|
"import": "./js/inline3d-call.js"
|
|
42
42
|
},
|
|
43
|
+
"./call/full": {
|
|
44
|
+
"types": "./call-full.d.ts",
|
|
45
|
+
"import": "./js/inline3d-call-full.js"
|
|
46
|
+
},
|
|
43
47
|
"./undock": {
|
|
44
48
|
"types": "./index.d.ts",
|
|
45
49
|
"import": "./js/inline3d-undock.js"
|
|
46
50
|
}
|
|
47
51
|
},
|
|
48
|
-
"sideEffects":
|
|
52
|
+
"sideEffects": [
|
|
53
|
+
"./js/inline3d-call.js",
|
|
54
|
+
"./js/inline3d-call-full.js",
|
|
55
|
+
"./dist/*.js"
|
|
56
|
+
],
|
|
49
57
|
"files": [
|
|
50
58
|
"js/inline3d.js",
|
|
51
59
|
"js/inline3d-undock.js",
|
|
@@ -72,7 +80,10 @@
|
|
|
72
80
|
"js/inline3d-playcanvas-engine.js",
|
|
73
81
|
"js/inline3d-player.js",
|
|
74
82
|
"js/inline3d-call.js",
|
|
83
|
+
"js/inline3d-call-full.js",
|
|
75
84
|
"js/call/",
|
|
85
|
+
"js/lift/",
|
|
86
|
+
"dist/",
|
|
76
87
|
"index.d.ts",
|
|
77
88
|
"three.d.ts",
|
|
78
89
|
"viewer.d.ts",
|
|
@@ -80,6 +91,7 @@
|
|
|
80
91
|
"model.d.ts",
|
|
81
92
|
"player.d.ts",
|
|
82
93
|
"call.d.ts",
|
|
94
|
+
"call-full.d.ts",
|
|
83
95
|
"CHANGELOG.md",
|
|
84
96
|
"README.md",
|
|
85
97
|
"LICENSE",
|
|
@@ -133,10 +145,15 @@
|
|
|
133
145
|
},
|
|
134
146
|
"scripts": {
|
|
135
147
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
136
|
-
"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"
|
|
137
153
|
},
|
|
138
154
|
"devDependencies": {
|
|
139
155
|
"typescript": "^5.4.0",
|
|
140
|
-
"@types/webxr": "^0.5.0"
|
|
156
|
+
"@types/webxr": "^0.5.0",
|
|
157
|
+
"esbuild": "^0.25.0"
|
|
141
158
|
}
|
|
142
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
|
|
package/splat.d.ts
CHANGED
|
@@ -271,6 +271,25 @@ export interface SplatVideoOptions {
|
|
|
271
271
|
muted?: boolean;
|
|
272
272
|
/** Default true for a URL (the SDK's own element), false for an element you pass. */
|
|
273
273
|
autoplay?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* `'cut'` (default) or `'crossfade'`: a GPU dissolve from the video already on the plane (its
|
|
276
|
+
* last frame, frozen) to this one. From the splat, and on `setVideo(null)`, it is always a cut.
|
|
277
|
+
* The same vocabulary as `./player`'s `setSource`; `./splat`'s gaussian transitions throw here.
|
|
278
|
+
*/
|
|
279
|
+
transition?: 'cut' | 'crossfade';
|
|
280
|
+
/** Crossfade length. Default 600 (the player's). 0 = a cut. */
|
|
281
|
+
durationMs?: number;
|
|
282
|
+
/** A named easing ({@link SplatEasing}) or `(x) => y` on [0, 1]. Default `'easeInOutSine'`. */
|
|
283
|
+
easing?: SplatEasing | ((x: number) => number);
|
|
284
|
+
/** Only `'frozen'`: the outgoing video dissolves from its last frame. */
|
|
285
|
+
outgoing?: 'frozen';
|
|
286
|
+
/**
|
|
287
|
+
* A letterbox slot: the picture is fitted into a centred band of this aspect inside the tile
|
|
288
|
+
* (`2.39` or `'2.39:1'` for a scope band in a 16:9 tile), the rest of the element left clear —
|
|
289
|
+
* `./player`'s `band`, on the plane. `fit: 'cover'` fills the band and crops the eye image about
|
|
290
|
+
* its centre.
|
|
291
|
+
*/
|
|
292
|
+
band?: number | string | null;
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
/** What `handle.setVideo(src)` resolves to. */
|
|
@@ -279,6 +298,18 @@ export interface SplatVideo {
|
|
|
279
298
|
readonly video: HTMLVideoElement;
|
|
280
299
|
readonly format: 'sbs' | 'tb' | 'mono';
|
|
281
300
|
readonly fit: 'contain' | 'cover';
|
|
301
|
+
/** The band's aspect, or null. */
|
|
302
|
+
readonly band: number | null;
|
|
303
|
+
/**
|
|
304
|
+
* Where the picture is on screen, in CSS px from the canvas's top-left, clipped to the canvas;
|
|
305
|
+
* the letterbox bars are the canvas minus this. Null before the first drawn frame and once
|
|
306
|
+
* another `setVideo` replaced this one. Place controls against the picture with it.
|
|
307
|
+
*/
|
|
308
|
+
readonly rect: { readonly x: number; readonly y: number; readonly width: number; readonly height: number } | null;
|
|
309
|
+
/** `cb(rect)` whenever the rect changes (a resize, a new size, a band): a microtask right after the draw, same frame, before paint. Returns an unsubscribe. */
|
|
310
|
+
onRectChange(cb: (rect: { x: number; y: number; width: number; height: number }) => void): () => void;
|
|
311
|
+
/** What the swap did: `'crossfade'` only when it replaced a video on screen and asked to. Resolved at the swap, before the fade ends. */
|
|
312
|
+
readonly transition: 'cut' | 'crossfade';
|
|
282
313
|
/** `handle.setVideo(null)`, or a no-op if another setVideo has replaced this one. */
|
|
283
314
|
remove(): Promise<null>;
|
|
284
315
|
/** Frames drawn with the plane up, and texture uploads (new video frames). */
|
|
@@ -1024,6 +1055,13 @@ export interface SplatHandle {
|
|
|
1024
1055
|
readonly renderScale: number;
|
|
1025
1056
|
/** The per-eye buffer scale as requested (the option, or the last `setRenderScale`). Both engines. */
|
|
1026
1057
|
readonly renderScaleRequested: number;
|
|
1058
|
+
/**
|
|
1059
|
+
* `engine: 'playcanvas'` only. The `<video>` `setVideo`'s plane is showing now, or null (none, or
|
|
1060
|
+
* one still waiting for its first frame). Read-only; no event reports it changing.
|
|
1061
|
+
*/
|
|
1062
|
+
readonly videoElement?: HTMLVideoElement | null;
|
|
1063
|
+
/** `engine: 'playcanvas'` only. The tile's canvas (page chrome sits beside it). */
|
|
1064
|
+
readonly canvas?: HTMLCanvasElement;
|
|
1027
1065
|
/**
|
|
1028
1066
|
* `engine: 'playcanvas'` only. The 2D tier's EYE offset — a head-parallax analogue for a flat
|
|
1029
1067
|
* screen with no tracked eyes (e.g. a phone's tilt). `{x, y}` is normalised in the mono camera's
|