@displayxr/inline3d 1.47.6 → 1.48.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 +81 -1
- package/call.d.ts +98 -8
- package/camera.d.ts +34 -5
- package/dist/call.js +8 -6
- package/docs/woven-canvas-rules.md +25 -13
- package/js/call/element.js +7 -3
- package/js/call/options.js +20 -3
- package/js/call/transport.js +11 -1
- package/js/call/ui.js +8 -1
- package/js/call/wire.js +163 -6
- package/js/camera/capture.js +107 -20
- package/js/camera/conv-meter.js +152 -0
- package/js/inline3d-call.js +392 -71
- package/js/inline3d-camera.js +29 -6
- package/js/inline3d-player.js +8 -4
- package/package.json +8 -1
- package/signaling/README.md +289 -0
- package/signaling/dev-server.mjs +320 -0
- package/signaling/gate.mjs +112 -0
- package/signaling/keys.mjs +123 -0
- package/signaling/limits.mjs +221 -0
- package/signaling/room.mjs +322 -0
- package/signaling/turn-budget.mjs +254 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// camera/conv-meter.js — the frame grab and the measuring loop behind auto-convergence, shared by
|
|
2
|
+
// a call's SENDER (it measures its own raw, pre-encode frames and sends the result to every peer:
|
|
3
|
+
// docs/call.md "Auto-convergence is measured by the sender") and a receiver's FALLBACK (an older
|
|
4
|
+
// sender that sends nothing; the depth cursor's copy). Internal: importable by path, not part of
|
|
5
|
+
// any entry. The measurement itself is camera/disparity.js.
|
|
6
|
+
|
|
7
|
+
import { lumaFromRgba, createDisparityTrack, createFocusTracker, downsampleLuma } from './disparity.js';
|
|
8
|
+
|
|
9
|
+
/** Sampling interval and the per-eye width of the measured copy: enough for sub-pixel disparity on a face, a few ms per measurement. */
|
|
10
|
+
export const CONV_METER_INTERVAL_MS = 200;
|
|
11
|
+
export const CONV_METER_EYE_WIDTH = 240;
|
|
12
|
+
|
|
13
|
+
/** WebCodecs pixel formats whose plane 0 is luma (Y): read directly, no colour conversion. */
|
|
14
|
+
export const Y_PLANE_FORMATS = new Set(['I420', 'I420A', 'I422', 'I444', 'NV12']);
|
|
15
|
+
|
|
16
|
+
const nowMs = () => (globalThis.performance ? performance.now() : Date.now());
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A small grayscale copy of `video`'s current frame, per-eye width at most `eyeMax`.
|
|
20
|
+
*
|
|
21
|
+
* Preferred: WebCodecs. `new VideoFrame(video)` + an ASYNC `copyTo()`, reading the luma (Y) plane
|
|
22
|
+
* directly: no synchronous GPU->CPU canvas readback on the paint path (that readback, not the
|
|
23
|
+
* matching, was ~25 ms of a ~30 ms measurement). Otherwise a small canvas (`scratch`, reused) +
|
|
24
|
+
* `getImageData`. Resolves null for a frame that cannot be read (not decoded yet, tainted, a
|
|
25
|
+
* failed copy). Never rejects.
|
|
26
|
+
*
|
|
27
|
+
* `scale` = copy px per source px; `readMs` = main-thread time spent getting the pixels.
|
|
28
|
+
*
|
|
29
|
+
* @param {HTMLVideoElement} video
|
|
30
|
+
* @param {{eyeMax?: number, scratch?: HTMLCanvasElement|null}} [o]
|
|
31
|
+
* @returns {Promise<{img: Uint8Array, w: number, h: number, scale: number, via: 'videoframe'|'canvas', readMs: number}|null>}
|
|
32
|
+
*/
|
|
33
|
+
export async function grabLumaFrame(video, { eyeMax = CONV_METER_EYE_WIDTH, scratch = null } = {}) {
|
|
34
|
+
const W = video ? video.videoWidth : 0;
|
|
35
|
+
const H = video ? video.videoHeight : 0;
|
|
36
|
+
if (!W || !H || (video.readyState || 0) < 2) return null;
|
|
37
|
+
if (typeof VideoFrame === 'function') {
|
|
38
|
+
let frame = null;
|
|
39
|
+
try {
|
|
40
|
+
frame = new VideoFrame(video);
|
|
41
|
+
} catch {
|
|
42
|
+
frame = null;
|
|
43
|
+
}
|
|
44
|
+
if (frame && Y_PLANE_FORMATS.has(frame.format)) {
|
|
45
|
+
try {
|
|
46
|
+
const fw = frame.visibleRect ? frame.visibleRect.width : frame.displayWidth;
|
|
47
|
+
const fh = frame.visibleRect ? frame.visibleRect.height : frame.displayHeight;
|
|
48
|
+
const buf = new Uint8Array(frame.allocationSize());
|
|
49
|
+
const layout = await frame.copyTo(buf);
|
|
50
|
+
const t0 = nowMs();
|
|
51
|
+
const f = Math.max(1, Math.ceil(fw / 2 / eyeMax));
|
|
52
|
+
const ds = downsampleLuma(buf, layout[0].offset, layout[0].stride, fw, fh, f);
|
|
53
|
+
return { img: ds.img, w: ds.w, h: ds.h, scale: ds.w / fw, via: 'videoframe', readMs: nowMs() - t0 };
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
} finally {
|
|
57
|
+
frame.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (frame) frame.close();
|
|
61
|
+
}
|
|
62
|
+
// Fallback: a small canvas + getImageData (synchronous readback).
|
|
63
|
+
const t0 = nowMs();
|
|
64
|
+
const s = Math.min(1, eyeMax / (W / 2));
|
|
65
|
+
const w = Math.max(2, Math.round((W * s) / 2) * 2);
|
|
66
|
+
const h = Math.max(2, Math.round(H * s));
|
|
67
|
+
const c = scratch || (globalThis.document ? document.createElement('canvas') : null);
|
|
68
|
+
if (!c) return null;
|
|
69
|
+
if (c.width !== w || c.height !== h) {
|
|
70
|
+
c.width = w;
|
|
71
|
+
c.height = h;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const g = c.getContext('2d', { willReadFrequently: true });
|
|
75
|
+
g.drawImage(video, 0, 0, w, h);
|
|
76
|
+
const img = lumaFromRgba(g.getImageData(0, 0, w, h).data, w * h);
|
|
77
|
+
return { img, w, h, scale: w / W, via: 'canvas', readMs: nowMs() - t0 };
|
|
78
|
+
} catch {
|
|
79
|
+
return null; // a frame that cannot be read (tainted, not decoded yet): skip it
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The measuring loop: grab a copy at most every `intervalMs`, find the disparity of the point
|
|
85
|
+
* between the subject's eyes (camera/disparity.js focus tracker), median-of-3 it. A failed grab or
|
|
86
|
+
* measurement HOLDS the last value (a blink, a hand over the face). `reset()` forgets everything,
|
|
87
|
+
* including a grab still in flight.
|
|
88
|
+
*
|
|
89
|
+
* `sample(video, at)` resolves null when it did not sample (too soon, still busy, or reset while
|
|
90
|
+
* in flight), else `{ d, frac, luma, ncc, method, via, ms }`: `d` the held disparity in SOURCE px
|
|
91
|
+
* (left x − right x; null until a first lock), `frac = d / (videoWidth / 2)` (resolution-free, what
|
|
92
|
+
* goes on the wire), `luma` the copy (null if the grab failed), `ncc`/`method` of THIS measurement
|
|
93
|
+
* (null when it failed), `ms` = read + match time.
|
|
94
|
+
*
|
|
95
|
+
* @param {{intervalMs?: number, eyeMax?: number, grab?: typeof grabLumaFrame, now?: () => number}} [o]
|
|
96
|
+
*/
|
|
97
|
+
export function createConvergenceMeter({ intervalMs = CONV_METER_INTERVAL_MS, eyeMax = CONV_METER_EYE_WIDTH, grab = grabLumaFrame, now = nowMs } = {}) {
|
|
98
|
+
const focus = createFocusTracker();
|
|
99
|
+
const track = createDisparityTrack();
|
|
100
|
+
let lastAt = -Infinity;
|
|
101
|
+
let busy = false;
|
|
102
|
+
let gen = 0;
|
|
103
|
+
let scratch = null;
|
|
104
|
+
const meter = {
|
|
105
|
+
get busy() {
|
|
106
|
+
return busy;
|
|
107
|
+
},
|
|
108
|
+
/** The held disparity (source px), null until a first lock. */
|
|
109
|
+
get d() {
|
|
110
|
+
return track.value;
|
|
111
|
+
},
|
|
112
|
+
async sample(video, at = now()) {
|
|
113
|
+
if (busy || at - lastAt < intervalMs) return null;
|
|
114
|
+
lastAt = at;
|
|
115
|
+
busy = true;
|
|
116
|
+
const g0 = gen;
|
|
117
|
+
try {
|
|
118
|
+
if (!scratch && globalThis.document && typeof document.createElement === 'function') scratch = document.createElement('canvas');
|
|
119
|
+
let got = null;
|
|
120
|
+
try {
|
|
121
|
+
got = await grab(video, { eyeMax, scratch });
|
|
122
|
+
} catch {
|
|
123
|
+
got = null;
|
|
124
|
+
}
|
|
125
|
+
if (g0 !== gen) return null; // reset() while the grab was in flight: a stale frame
|
|
126
|
+
const t = now();
|
|
127
|
+
const m = got ? focus.measure(got.img, got.w, got.h, at) : null;
|
|
128
|
+
const d = track.push(m ? m.d / got.scale : null);
|
|
129
|
+
const W = video && video.videoWidth;
|
|
130
|
+
return {
|
|
131
|
+
d,
|
|
132
|
+
frac: d !== null && W > 0 ? d / (W / 2) : null,
|
|
133
|
+
luma: got ? { img: got.img, w: got.w, h: got.h, scale: got.scale } : null,
|
|
134
|
+
ncc: m ? m.c : null,
|
|
135
|
+
method: m ? m.method : null,
|
|
136
|
+
via: got ? got.via : null,
|
|
137
|
+
ms: (got ? got.readMs : 0) + (now() - t),
|
|
138
|
+
};
|
|
139
|
+
} finally {
|
|
140
|
+
if (g0 === gen) busy = false;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
reset() {
|
|
144
|
+
gen++;
|
|
145
|
+
busy = false;
|
|
146
|
+
lastAt = -Infinity;
|
|
147
|
+
focus.reset();
|
|
148
|
+
track.reset();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return meter;
|
|
152
|
+
}
|