@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 CHANGED
@@ -5,6 +5,230 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
5
5
  `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
6
6
  which tier they touch, because that is what tells you whether an upgrade can move your pixels.
7
7
 
8
+ ## 1.29.0 — 2026-09-30
9
+
10
+ Touches the **core** (additive: `sharedInline3D()`, `handle.rewoven()`) and the **preview tier**
11
+ (`./call` — RFC 0003 C1, the one-line path; `./player`, `./splat` `engine: 'playcanvas'`). One
12
+ **removal** in the preview tier: `peerjsCloud`. Otherwise additive; no change for a page that does
13
+ not call the new API.
14
+
15
+ ### `./call` — the one-line path (RFC 0003 §1, phase C1)
16
+
17
+ - **`mountCall(el, opts?)`** — `addCall` with every argument optional: the wall is the document's
18
+ shared manager (`sharedInline3D()`, below), signalling defaults to the hosted server, the camera
19
+ to `'auto'`, the SDK chrome to on. `addCall(wall, container, opts)` stays as the explicit form.
20
+ A call's session recovery now re-opens through the shared manager too, so it joins a wall the
21
+ page re-created rather than opening a second session.
22
+ - **`<dxr-call>`** — the same thing as markup, registered by importing `./call` (or `./call/full`,
23
+ or the CDN bundle): mounts on connect, `leave()`s on disconnect (a DOM move IS a teardown —
24
+ woven-canvas rule 2), re-dispatches every call event as a bubbling, composed `CustomEvent`
25
+ named `dxr-call:<event>` with the payload in `detail`, and exposes the handle as `el.call`
26
+ (plus a `ready` promise). Attributes are the string-typed options: `room`, `signaling` (a
27
+ URL), `key`, `camera`, `layout`, `accent`, `max-peers`, `no-ui`, `auto-join`, `mono3d="off"`,
28
+ `no-audio`, `no-self-view`, `no-auto-converge`, `tile-aspect`, `invite-base`, `browser-url`,
29
+ `debug`; anything else is `el.options = { … }`, set before connect (options win). Light DOM, no
30
+ shadow root, **no globals** (RFC 0003 Decision 11). `auto-join` joins during the mount, so the
31
+ element replays `joined` and one `peer` per participant already in the room once it can
32
+ subscribe. Exported: `DxrCallElement`, `defineCallElement`, `attrsToOpts`, `CALL_EVENT_PREFIX`.
33
+ - **`@displayxr/inline3d/call/full`** — `./call` with `mono3D` defaulted to a **statically
34
+ imported** `lift`, the one shape a bundler can follow; importing it also points `<dxr-call>` at
35
+ that `mountCall`. `liftBundled` says whether this copy of the SDK carries the module. **Until
36
+ `./lift` lands on `main`, `js/lift/index.js` is a placeholder** (`lift === null`): the entry
37
+ builds and behaves like `./call`, and `liftBundled` is `false`.
38
+ - **CDN bundle `dist/call.js`** — one pre-bundled ESM file (core + call + element, ~124 KB min,
39
+ no three/PlayCanvas), built by `npm run build:dist` (esbuild) and by `prepack` into every
40
+ published tarball, so jsDelivr serves it as `https://cdn.jsdelivr.net/npm/@displayxr/inline3d@1/dist/call.js`.
41
+ `<script type="module">` only; lift is a lazy sibling chunk (`dist/lift.js`) once the module
42
+ is present. `dist/` is not committed.
43
+ - **`warning` event, code `'lift-not-bundled'`** — a 2D participant is on a 3D display, `mono3D`
44
+ is `'auto'`, and the lift module could not be imported from this build (a bundler that could
45
+ not follow the computed import, or a copy without `./lift`). Emitted once per call with one
46
+ `console.warn` naming the fix (`mono3D: lift` or `./call/full`); the lobby reads "2D→3D
47
+ unavailable in this build". Previously a silent flat tile and an info line. A display that
48
+ honestly cannot lift is unchanged (`handle.mono3D.reason`).
49
+ - **`key`** — `dxrSignaling(url, { key })` / `CallOptions.key` / `<dxr-call key>`: a publishable
50
+ key for the hosted service (RFC 0003 §5a), sent as a query parameter on connect. Accepted and
51
+ forwarded now; the server ignores it until C3.
52
+ - **Removed: `peerjsCloud`** (RFC 0003 Decision 12 — no known external user, so no warning
53
+ release). The demo adapter over the public PeerJS broker lives on as
54
+ `samples/call/peerjs-cloud.js`; the sample's `?signal=peerjs` still works. `dxrSignaling()`
55
+ (the hosted server) has been the default since 1.28.
56
+ - **Types:** `mountCall`, `CallOptions.wall` / `.key`, `CallEvents.warning`, `DxrCallElement`,
57
+ `attrsToOpts`, `defineCallElement`, `DxrCallEventMap` (+ `HTMLElementTagNameMap` /
58
+ `HTMLElementEventMap` augmentations), `call-full.d.ts`.
59
+ - **Tests:** element unit tests (attribute→option table, events, disconnect = leave); a
60
+ headless Chrome ↔ Chrome end-to-end run of `<dxr-call>` (source and CDN bundle) over the
61
+ hosted server (`npm run test:e2e`); Vite/webpack/esbuild smoke builds of `./call/full` and
62
+ `./call` that also run the bundles (`npm run test:bundlers`). Neither is part of `npm test`.
63
+
64
+ ### Core — additive
65
+
66
+ - **`sharedInline3D(opts?)`** — the document's shared manager: the live `createInline3D()`
67
+ result if one exists, else a new one (concurrent callers share one creation; an unsupported
68
+ browser is never cached). Woven-canvas rule 1 as an API: a module that needs a wall it did not
69
+ create (`mountCall`, `<dxr-call>`) uses this and never opens a second session next to the
70
+ page's own; a page that made its wall first gets that same wall back here. Semver-covered.
71
+
72
+ - **`./splat` `setVideo` crossfade, band and on-screen rect** (#105, RFC 0001 A5.1-A5.3,
73
+ `engine: 'playcanvas'`). `transition: 'crossfade'` (`durationMs` 600, `easing`
74
+ 'easeInOutSine', the player's defaults) dissolves on the GPU from the video already on the plane:
75
+ its quad is frozen and drawn blended over the incoming one, the buffer opaque throughout; from the
76
+ splat and on `setVideo(null)` it stays a cut. `band: 2.39 | '2.39:1'` fits the picture into a
77
+ centred letterbox slot (`cover` fills the band and crops each eye about its centre). The result
78
+ gains `transition`, `band`, `rect` (the picture in CSS px, canvas-relative, clipped) and
79
+ `onRectChange(cb)`. `attachPlayer` now crossfades and takes `band` (controls in the bars) through
80
+ these instead of cutting / ignoring them. **Behaviour change for `attachPlayer(…, { skin: 'call' })`:**
81
+ that skin defaults `band` to 2.39, which surface mode used to ignore; the video now plays in a
82
+ 2.39 letterbox. Pass `band: null` to keep the full tile.
83
+ - **`handle.rewoven()`** — `firstWoven`, measured from the call, for a canvas that is already woven
84
+ but whose rect is about to change (fullscreen, a layout resize). Same `{ woven, confirmed,
85
+ reason, ms }` shape, never rejects; a change of the canvas's CSS size or devicePixelRatio while
86
+ pending restarts the hold (checked every frame, every window kind), and it settles anyway with
87
+ the new reason `'hold-capped'` four holds after the call if the size never stops changing;
88
+ before the first join it is `firstWoven`. Woven-canvas rule 11.
89
+ - **Player: `fullscreenCover`** (opt-in, default false). Entering or leaving fullscreen puts an
90
+ opaque cover over the canvas (on top, hard cut, below the controls) until `rewoven()` settles.
91
+ Off by default because a blind A/B on the Leia panel showed no raw side-by-side pair without it
92
+ and ~1.2 s of no picture with it; the flash that did show is browser-wide (rule 11).
93
+ - **Docs:** woven-canvas rules 9–11 (black goes under the canvas — previously only in the
94
+ authoring guide; no `backdrop-filter` over the weave; a resize is a fresh-canvas moment). The
95
+ authoring guide's short list now matches, 1–11.
96
+ - **`attachPlayer(splat, src, opts)`** — surface mode (RFC 0001 Addendum A): the player's
97
+ transport, playlist and events on an existing PlayCanvas splat handle's `setVideo`, with no
98
+ canvas of its own. `detach()` gives the slot back (`setVideo(null)`: scene, pose, lens and rig
99
+ as they were); `'detached'` `{ reason: 'superseded' | 'released' }` when something else takes or
100
+ empties the slot. Cut transitions only for now; `fullscreen` defaults off. Panel-checked on one
101
+ persistent canvas: attach, next/back, detach, scene state identical after.
102
+ - **`addPlayer` refactor:** one player core on a surface adapter. No behaviour change.
103
+ - **`./splat`:** read-only `handle.videoElement` (the `<video>` the plane shows, or null) and
104
+ `handle.canvas`.
105
+ - **Types:** `attachPlayer`, `AttachPlayerOptions`, `AttachedPlayerHandle`, `PlayerDetachedEvent`,
106
+ `'detached'` in `PlayerEvent`; `SplatHandle.videoElement` / `.canvas`.
107
+
108
+ ## 1.28.0 — 2026-09-27
109
+
110
+ Touches the **preview tier** (a new subpath, `./call`; the SBS store clamp below) and two **core**
111
+ fixes: image/video backing buffers are capped at the source's resolution, and every SBS store is
112
+ clamped to the device's GL limits.
113
+
114
+ ### Fixed — core + preview: SBS backing stores clamped to the device's GL limits
115
+
116
+ - A woven canvas wider than the device's WebGL limit was **silently** clamped by the browser, and
117
+ the SDK still laid the eyes out from `canvas.width / 2`. On an Android 3D tablet (Adreno 740,
118
+ DisplayXR Browser 154) `MAX_TEXTURE_SIZE` is 4096 (Chromium's
119
+ `webgl_or_caps_max_texture_size_limit_4096` workaround, browser-pvt#187) where Windows and Linux
120
+ report 16384. A full-screen SBS canvas at `renderScale` 1 asked for 5120×1348, got a 4096-wide
121
+ drawing buffer, and put the eye boundary at 62.5% of it while the weave split it at 50%: a large
122
+ double image on the panel, nothing logged.
123
+ - Every path that sizes an SBS store now clamps the request **before** setting `canvas.width/height`,
124
+ so 2 × eye width and the height stay within min(`MAX_TEXTURE_SIZE`, `MAX_RENDERBUFFER_SIZE`,
125
+ `MAX_VIEWPORT_DIMS`). Both axes shrink by one factor, so the eye keeps its aspect and the store
126
+ stays 2:1 of the per-eye size. Mono (the 2D tier) is clamped with one eye across. Limits are
127
+ queried once per GL context and cached. One shared helper (`js/inline3d-buffer-limit.js`) serves
128
+ the core (`addImage` / `addVideo`, explicit `{width, height}` included), `./viewer`, `./splat` on
129
+ both engines and `./model` on both engines.
130
+ - Belt and braces: if the drawing buffer still differs from `canvas.width/height` after a resize (a
131
+ browser clamp the query did not predict), the viewers map the layer's viewports onto the real
132
+ drawing buffer instead of `canvas.width / 2`, for live frames, replays and mono. An `addScene`
133
+ canvas is the page's to size; the core warns once if the browser clamped it.
134
+ - One warning per handle when the clamp bites, naming the limit and the numbers, e.g.
135
+ `[inline3d/splat] SBS buffer 5120×1348 exceeds MAX_TEXTURE_SIZE 4096 on this device; rendering at
136
+ 4096×1078 (renderScale 1 → 0.8)`.
137
+ - `handle.renderScale` (`./splat`, both engines; `./model`, both engines) now reports the scale **in
138
+ force** (0.8 in the case above); the new `handle.renderScaleRequested` is the request as given.
139
+ `setRenderScale` still accepts (0, 4]. `SceneViewer` gains `effectiveRenderScale`. Nothing changes
140
+ on a device whose limit holds the store.
141
+
142
+ ### Added — call: auto-convergence on the point between the remote person's eyes
143
+
144
+ - SBS tiles now converge by MEASUREMENT: the receiver finds the disparity of the point between the
145
+ remote person's eyes in the decoded frames and shifts each eye by half of it, so that person sits at
146
+ the display plane. No calibration, works for any SBS sender. Tracks a small template after the first
147
+ lock (~1 ms per measurement at 5 Hz). `autoConverge: false` restores the pair as sent; the depth
148
+ slider stays an offset on top; lifted (mono) tiles are untouched. `measureFocusDisparity` is
149
+ exported for pages that want to reuse it.
150
+
151
+ ### Fixed — core: image/video buffers capped at the source's per-eye resolution
152
+
153
+ - An image or video window's backing buffer was sized box x devicePixelRatio per eye regardless of
154
+ the source, so a large tile over a small clip re-drew many times the clip's pixels every frame.
155
+ Measured on an NP02J tablet (DisplayXR Browser 1.0.6), the player sample's 90vw tile got a
156
+ 4608x1296 buffer for a 640x360-per-eye clip: 25 fps, 47 of 97 video frames dropped. The buffer
157
+ is now capped at the source's per-eye size (box aspect kept, only ever shrinks), which on the same
158
+ page gives 1280x360, 58 fps, 0 dropped. The compositor scales the layer to the box either way, so
159
+ the picture is the same. An explicit `{width, height}` is never capped; `cornerRadius` / `feather`
160
+ keep their on-screen size; the buffer re-derives when a new title has a different size.
161
+
162
+ ### Docs — MP4 (H.264/AAC) plays in DisplayXR Browser 1.0.6+
163
+
164
+ - The codec guidance no longer says the DisplayXR Browser cannot play H.264/AAC. From **1.0.6** it
165
+ plays MP4 through the operating system's decoders (Windows GPU + Media Foundation, Android
166
+ MediaCodec). VP9/AV1 + Opus in WebM stays the recommended first source: it also plays in 1.0.5 and
167
+ earlier, stock Chromium, and on machines with no hardware H.264 decode (where `canPlayType` still
168
+ answers "probably" for H.264). Updated in the authoring guide (media traps, hosting video), the
169
+ PlayCanvas adapter's `setVideo`, and `pickSource`'s doc comment and fallback log text (text only,
170
+ no behaviour change). WebRTC calls are unaffected: the browser still ships no H.264 for WebRTC,
171
+ so `./call` keeps preferring VP9.
172
+
173
+ ### Added — call: mono→3D through `lift()` (P2a of RFC 0002)
174
+
175
+ - **Mono participants are lifted to 3D** on a 3D display: a mono peer on a woven wall is handed to
176
+ `lift()` (`@displayxr/inline3d/lift`) with `{ mode: 'live', wall }` — one lifted tile per peer,
177
+ in its own slot, lifted at once and re-created once when the weave session goes live (#172). `mono3D: 'auto'` (default) loads
178
+ the lift module lazily and falls back to flat, with one log line, when it is missing or there is
179
+ neither a native 2D→3D provider nor WebGPU; `'off'` keeps mono peers flat; a function injects your
180
+ own `lift`. `setMono3D(on)` switches at runtime; `liftOptions` passes `models` / `ort` /
181
+ `quality` through; `maxLifted` (default 4) caps concurrent lifts.
182
+ - **One depth control**: the depth slider is lift's convergence on lifted tiles (0 = `'auto'`) and
183
+ the crop shift on stereo tiles.
184
+ - **Priority**: the active speaker's lifted stream is `high`, the others `normal`, offscreen or
185
+ camera-off tiles `paused`, through `setLiftPriority()` — the hook for the native provider's
186
+ per-stream priority (a no-op on the web path).
187
+ - **Badges** `3D` / `2D→3D` / `2D`; the lobby says what a mono participant will look like
188
+ (`liftCapabilities()` when available); `handle.mono3D` reports the state. On the web provider a
189
+ sustained frame-rate drop emits a `quality` event with `lift: { degraded, frameMs, tiles }`.
190
+ - **Changed:** `badgeFor('lifted')` is now `'2D→3D'` (was `'3D'`); `routeFor()` takes
191
+ `overBudget` / `failed` and reports `mono3d: 'budget' | 'failed'`.
192
+
193
+ ### Added — call (`@displayxr/inline3d/call`, P1 of RFC 0002)
194
+
195
+ - **`addCall(wall, container, opts)`** — a 3D video call in any page. Full-mesh WebRTC, up to 4
196
+ participants (`maxPeers`, enforced on the client and the server), behind an internal `Transport`
197
+ seam so an SFU can replace the mesh later. VP9 > VP8 > AV1 (never H.264, which the DisplayXR
198
+ Browser does not have); senders hold resolution (`contentHint: 'detail'`,
199
+ `degradationPreference: 'maintain-resolution'`) with a `maxBitrate` that falls as the mesh grows.
200
+ A dropped pair walks ICE restart → rebuild with backoff; a peer that leaves shows "Left the call"
201
+ instead of a black tile.
202
+ - **Stereo from any side-by-side camera**: `camera: 'auto'` picks a device delivering > 2.5:1
203
+ frames (a raw 1280x480 stereo camera works — nothing is upscaled) and skips a camera held by an
204
+ eye tracker instead of failing. The pair is sent raw with `rectified: false`; `opts.rectify` is
205
+ the seam for a calibrated step (P2).
206
+ - **The receiver decides** (RFC §3): an SBS peer is woven with a convergence shift
207
+ (`f·B/(2Z)` from `hello` + `hint`, low-passed, clamped, plus the depth slider) on the DisplayXR
208
+ Browser and shown as its left eye elsewhere; a mono peer is flat (the `mono3D` hook is where
209
+ `lift()` lands in P2). Plain Chrome/Safari/Firefox participants are first-class. Built on
210
+ `wall.addImage` over a module-owned canvas, so the core still owns every woven buffer; a failed
211
+ layer is retried and an ended session re-opened without a reload.
212
+ - **Signalling is pluggable** (`SignalingAdapter`): `dxrSignaling(url)` speaks the new
213
+ `dxr-signal/1` protocol, with a reference Cloudflare Worker + Durable Object (template, not
214
+ deployed; mints short-lived TURN credentials server-side) and a zero-dependency Node dev server
215
+ in `signaling/`. `peerjsCloud()` is a demo-only zero-setup path. Rooms are 128-bit random ids
216
+ carried in the invite link's `#room=` fragment, which never reaches a server.
217
+ - **SDK chrome** (`ui: true`; `ui: false` for headless): lobby with a 3D self-preview, invite link
218
+ + QR code (a small built-in encoder, no dependency), bottom bar (mute, camera, depth, invite,
219
+ leave), per-tile `3D`/`2D` badge, a "View in 3D with DisplayXR Browser" banner on other browsers,
220
+ `grid` or `speaker` layout (active speaker from inbound audio levels). The mirrored stereo self
221
+ view mirrors each half AND swaps them; the sent stream is never mirrored.
222
+ - **Hardened after the first panel run**: woven tiles (self view included) register only once the
223
+ session delivers stereo frames, working around a browser bug where a tile registered before the
224
+ weave session is live is fed the whole side-by-side frame per eye (displayxr-browser-pvt#172). A
225
+ participant with no connection after ~10 s shows "Can't reach this participant — the network
226
+ needs a relay (TURN)" and emits `error` `unreachable` while retries continue. A camera held by
227
+ another app (e.g. eye tracking) gives `error` `camera-busy` and an audio-only join with
228
+ `retryCamera()`, and no 0x0 video track is ever sent. `peerjsCloud()` no longer counts stale
229
+ slots toward a full room and frees its slot on `pagehide`.
230
+ - Sample: [`samples/call/`](samples/call/). Types: `call.d.ts`.
231
+
8
232
  ## 1.27.0 — 2026-09-26
9
233
 
10
234
  Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only) and adds one optional field
package/README.md CHANGED
@@ -199,7 +199,8 @@ js/
199
199
  inline3d-splat-live.js setSource's live outgoing photo: its own camera, layer and target (PlayCanvas)
200
200
  from what the asset declares (you serve the decoder files — see the guide)
201
201
  inline3d-player.js preview: addPlayer() — a media player window built on addVideo (stereo
202
- paint, display modes and the fallback are inherited) + an SDK transport bar
202
+ paint, display modes and the fallback are inherited) + an SDK transport bar;
203
+ attachPlayer() — the same player on a PlayCanvas splat handle's setVideo
203
204
  docs/
204
205
  authoring-inline-3d.md the authoring guide
205
206
  authoring-motion-and-effects.md
@@ -207,8 +208,10 @@ docs/
207
208
  that is not the API
208
209
  porting-three-js-apps.md porting an existing three.js app (WebXR or plain) to inline 3D —
209
210
  the WebXR→inline-3d mapping table and the whole render loop
210
- woven-canvas-rules.md never show a raw side-by-side frame: the join, the eight rules,
211
- handle.firstWoven, reading the browser's `withheld` log line
211
+ woven-canvas-rules.md never show a raw side-by-side frame: the join, the rules,
212
+ handle.firstWoven / rewoven(), reading the browser's `withheld` log line
213
+ call.md 3D video calls: the <dxr-call> / mountCall() one-line embed, options,
214
+ events, headless, theming, React, self-hosting, troubleshooting
212
215
  proposals/ browser-side asks the SDK is waiting on
213
216
  ```
214
217
 
package/call-full.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Type definitions for @displayxr/inline3d/call/full — the call entry with 2D→3D bundled.
2
+ // PREVIEW tier — not covered by the 1.x semver promise. See docs/sdk-stability.md.
3
+ //
4
+ // Same surface as `./call` (re-exported), except `mountCall` / `addCall` default `mono3D` to a
5
+ // STATICALLY imported `lift` (`./lift`), which is what lets a bundler ship it. Use this entry in a
6
+ // bundled app that wants 2D participants lifted to 3D; use `./call` when you do not want the
7
+ // depth-model plumbing (it then warns `lift-not-bundled` the first time a 2D peer would have been
8
+ // lifted on a 3D display). Importing this entry also points `<dxr-call>` at this `mountCall`.
9
+
10
+ import type { CallHandle, CallOptions } from './call.js';
11
+
12
+ export * from './call.js';
13
+
14
+ /** True when this copy of the SDK carries the lift module; false on a build where `./lift` has not landed yet. */
15
+ export const liftBundled: boolean;
16
+
17
+ /** `mountCall` of `./call` with `mono3D` defaulted to the bundled `lift` (unless you pass your own). */
18
+ export function mountCall(el: HTMLElement, opts?: CallOptions): Promise<CallHandle>;
19
+
20
+ /** `addCall` of `./call` with `mono3D` defaulted to the bundled `lift` (unless you pass your own). */
21
+ export function addCall(wall: unknown, container: HTMLElement, opts?: CallOptions): Promise<CallHandle>;