three-vr-player 0.8.0 → 0.9.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/dist/Player.d.ts +4 -0
- package/dist/VideoSource-BXMPMJzW.js +722 -0
- package/dist/VideoSource-BXMPMJzW.js.map +1 -0
- package/dist/core/StereoScene.d.ts +50 -0
- package/dist/core/VRControls.d.ts +30 -1
- package/dist/core/projections.d.ts +1 -1
- package/dist/core/vr-panel-layout.d.ts +2 -1
- package/dist/core.js +1 -1
- package/dist/three-vr-player.js +42 -34
- package/dist/three-vr-player.js.map +1 -1
- package/dist/three-vr-player.standalone.js +176 -157
- package/dist/three-vr-player.standalone.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/package.json +3 -2
- package/dist/VideoSource-Cx7XPpvZ.js +0 -528
- package/dist/VideoSource-Cx7XPpvZ.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Projection = '180-sbs' | '180-mono' | '360-mono' | '360-sbs' | '360-tb' | 'flat-2d' | 'flat-sbs-full' | 'flat-sbs-half';
|
|
1
|
+
export type Projection = '180-sbs' | '180-mono' | '360-mono' | '360-sbs' | '360-tb' | 'fisheye190-sbs' | 'fisheye190-mono' | 'flat-2d' | 'flat-sbs-full' | 'flat-sbs-half';
|
|
2
2
|
export interface PlayerOptions {
|
|
3
3
|
/** Initial source URL. If omitted, call `player.load(src)` later. */
|
|
4
4
|
src?: string;
|
|
@@ -30,6 +30,11 @@ export interface PlayerOptions {
|
|
|
30
30
|
vrButton?: boolean;
|
|
31
31
|
/** Title shown on the in-VR control panel. Optional. */
|
|
32
32
|
title?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Content carries a DeoVR-style packed alpha matte (fisheye passthrough). When omitted, it's
|
|
35
|
+
* inferred from a `_ALPHA` marker in the source filename. Only affects fisheye projections.
|
|
36
|
+
*/
|
|
37
|
+
alpha?: boolean;
|
|
33
38
|
/**
|
|
34
39
|
* If a cross-origin source can't be used as a WebGL texture (CORS-tainted) and
|
|
35
40
|
* isn't proxied, fall back to plain 2D `<video>` playback instead of erroring.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-vr-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Embeddable web player for 180/360 and side-by-side/top-bottom 3D video, undistorted on desktop and in WebXR. Built on three.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"build": "vite build && vite build --config vite.standalone.config.ts",
|
|
49
49
|
"build:demo": "vite build --config vite.demo.config.ts",
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"test": "vitest run"
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"prepublishOnly": "npm run build"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"three": ">=0.160.0 <1"
|
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
import * as l from "three";
|
|
2
|
-
function k(a) {
|
|
3
|
-
let e = a;
|
|
4
|
-
try {
|
|
5
|
-
e = new URL(a).pathname;
|
|
6
|
-
} catch {
|
|
7
|
-
}
|
|
8
|
-
const t = e.toLowerCase();
|
|
9
|
-
return t.endsWith(".m3u8") ? "hls" : t.endsWith(".mpd") ? "dash" : "progressive";
|
|
10
|
-
}
|
|
11
|
-
const F = {
|
|
12
|
-
progressive: "/proxy/stream",
|
|
13
|
-
hls: "/proxy/hls/manifest.m3u8",
|
|
14
|
-
dash: "/proxy/mpd/manifest.m3u8"
|
|
15
|
-
};
|
|
16
|
-
function _(a, e) {
|
|
17
|
-
const t = k(a);
|
|
18
|
-
if (!e || !e.url) return { url: a, format: t };
|
|
19
|
-
const s = e.url.replace(/\/+$/, ""), i = new URLSearchParams();
|
|
20
|
-
i.set("d", a), e.apiPassword && i.set("api_password", e.apiPassword);
|
|
21
|
-
for (const [r, n] of Object.entries(e.headers ?? {}))
|
|
22
|
-
n != null && n !== "" && i.set(`h_${r}`, n);
|
|
23
|
-
return { url: `${s}${F[t]}?${i.toString()}`, format: t };
|
|
24
|
-
}
|
|
25
|
-
const m = {
|
|
26
|
-
"180-sbs": { geom: "sphere180", split: "sbs", stereo: !0 },
|
|
27
|
-
"180-mono": { geom: "sphere180", split: "mono", stereo: !1 },
|
|
28
|
-
"360-mono": { geom: "sphere360", split: "mono", stereo: !1 },
|
|
29
|
-
"360-sbs": { geom: "sphere360", split: "sbs", stereo: !0 },
|
|
30
|
-
"360-tb": { geom: "sphere360", split: "tb", stereo: !0 },
|
|
31
|
-
"flat-2d": { geom: "plane", split: "mono", stereo: !1, flat: !0, aspect: "full" },
|
|
32
|
-
"flat-sbs-full": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "per-eye" },
|
|
33
|
-
"flat-sbs-half": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "full" }
|
|
34
|
-
}, z = [
|
|
35
|
-
{ value: "180-sbs", label: "180° SBS (VR180)" },
|
|
36
|
-
{ value: "180-mono", label: "180° Mono" },
|
|
37
|
-
{ value: "360-mono", label: "360° Mono" },
|
|
38
|
-
{ value: "360-sbs", label: "360° SBS" },
|
|
39
|
-
{ value: "360-tb", label: "360° Top-Bottom" },
|
|
40
|
-
{ value: "flat-2d", label: "Flat 2D" },
|
|
41
|
-
{ value: "flat-sbs-full", label: "Flat 3D — Full SBS" },
|
|
42
|
-
{ value: "flat-sbs-half", label: "Flat 3D — Half SBS" }
|
|
43
|
-
];
|
|
44
|
-
function H(a) {
|
|
45
|
-
var e;
|
|
46
|
-
return !!((e = m[a]) != null && e.flat);
|
|
47
|
-
}
|
|
48
|
-
function U(a) {
|
|
49
|
-
const e = String(a).toLowerCase(), t = /(^|[^a-z])(tb|ou|top.?bottom|over.?under)([^a-z]|$)/.test(e), s = /(sbs|side.?by.?side)/.test(e);
|
|
50
|
-
return e.includes("360") ? t ? "360-tb" : s ? "360-sbs" : "360-mono" : e.includes("180") || e.includes("vr180") ? e.includes("mono") ? "180-mono" : "180-sbs" : s ? e.includes("half") ? "flat-sbs-half" : "flat-sbs-full" : null;
|
|
51
|
-
}
|
|
52
|
-
function R(a) {
|
|
53
|
-
if (!Number.isFinite(a) || a < 0) return "0:00";
|
|
54
|
-
const e = Math.floor(a % 60), t = Math.floor(a / 60 % 60), s = Math.floor(a / 3600), i = String(e).padStart(2, "0");
|
|
55
|
-
return s > 0 ? `${s}:${String(t).padStart(2, "0")}:${i}` : `${t}:${i}`;
|
|
56
|
-
}
|
|
57
|
-
const v = 1024, w = 340;
|
|
58
|
-
function A() {
|
|
59
|
-
const a = v, e = w, t = 48;
|
|
60
|
-
return {
|
|
61
|
-
width: a,
|
|
62
|
-
height: e,
|
|
63
|
-
recenter: { x: 30, y: 26, w: 160, h: 52 },
|
|
64
|
-
exit: { x: a - 190, y: 26, w: 160, h: 52 },
|
|
65
|
-
title: { x: 0, y: 92, w: a, h: 44 },
|
|
66
|
-
play: { x: a / 2 - 44, y: 150, w: 88, h: 88 },
|
|
67
|
-
volIcon: { x: t, y: 178, w: 44, h: 44 },
|
|
68
|
-
volBar: { x: t + 60, y: 192, w: 200, h: 16 },
|
|
69
|
-
seekBar: { x: t, y: 286, w: a - t * 2, h: 14 },
|
|
70
|
-
timeCur: { x: t, y: 262 },
|
|
71
|
-
timeDur: { x: a - t, y: 262 }
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const T = (a) => a < 0 ? 0 : a > 1 ? 1 : a;
|
|
75
|
-
function x(a, e, t, s = 0, i = 0) {
|
|
76
|
-
return e >= a.x - s && e <= a.x + a.w + s && t >= a.y - i && t <= a.y + a.h + i;
|
|
77
|
-
}
|
|
78
|
-
function C(a, e, t = A()) {
|
|
79
|
-
return x(t.recenter, a, e) ? { region: "recenter" } : x(t.exit, a, e) ? { region: "exit" } : x(t.play, a, e) ? { region: "play" } : x(t.volIcon, a, e) ? { region: "volume" } : x(t.volBar, a, e, 12, 26) ? { region: "volume", value: T((a - t.volBar.x) / t.volBar.w) } : x(t.seekBar, a, e, 12, 26) ? { region: "seek", value: T((a - t.seekBar.x) / t.seekBar.w) } : null;
|
|
80
|
-
}
|
|
81
|
-
const S = "#4f8cff", y = "#e8eaed", E = "#aab2c0", B = [4, 5], M = 8e3, I = 0.05, V = 0.02, O = 1500;
|
|
82
|
-
class W {
|
|
83
|
-
// re-lock the panel to the head pose for a few frames after summon
|
|
84
|
-
constructor(e) {
|
|
85
|
-
this.layout = A(), this.raycaster = new l.Raycaster(), this.tmpMatrix = new l.Matrix4(), this.tmpVec = new l.Vector3(), this.tmpQuat = new l.Quaternion(), this.prevQuat = [], this.prevPos = [], this.revealCooldownUntil = 0, this.controllers = [], this.lasers = [], this.connected = [], this.cleanups = [], this.visible = !1, this.hover = null, this.paintKey = "", this.idleAt = 0, this.togglePrev = !1, this.placeFrames = 0, this.renderer = e.renderer, this.scene = e.scene, this.actions = e.actions, this.canvas = document.createElement("canvas"), this.canvas.width = v, this.canvas.height = w, this.ctx = this.canvas.getContext("2d"), this.texture = new l.CanvasTexture(this.canvas), this.texture.colorSpace = l.SRGBColorSpace, this.texture.minFilter = l.LinearFilter;
|
|
86
|
-
const t = 1, s = t * w / v;
|
|
87
|
-
this.panel = new l.Mesh(
|
|
88
|
-
new l.PlaneGeometry(t, s),
|
|
89
|
-
new l.MeshBasicMaterial({ map: this.texture, transparent: !0, depthTest: !1, side: l.DoubleSide })
|
|
90
|
-
), this.panel.renderOrder = 10, this.panel.frustumCulled = !1, this.panel.visible = !1, this.scene.add(this.panel), this.cursor = new l.Mesh(
|
|
91
|
-
new l.SphereGeometry(7e-3, 16, 12),
|
|
92
|
-
new l.MeshBasicMaterial({ color: 16777215, depthTest: !1 })
|
|
93
|
-
), this.cursor.renderOrder = 11, this.cursor.frustumCulled = !1, this.cursor.visible = !1, this.scene.add(this.cursor);
|
|
94
|
-
const i = this.renderer.xr.getSession(), r = i ? Array.from(i.inputSources).filter((n) => n.targetRayMode === "tracked-pointer").length : 0;
|
|
95
|
-
for (let n = 0; n < 2; n++) {
|
|
96
|
-
const o = this.renderer.xr.getController(n), h = this.makeLaser();
|
|
97
|
-
h.visible = !1, o.add(h), this.scene.add(o), this.controllers.push(o), this.lasers.push(h), this.connected.push(n < r);
|
|
98
|
-
const u = () => this.onSelect(n), c = () => this.toggle(), p = (f) => {
|
|
99
|
-
this.connected[n] = !(f != null && f.data) || f.data.targetRayMode === "tracked-pointer";
|
|
100
|
-
}, g = () => {
|
|
101
|
-
this.connected[n] = !1;
|
|
102
|
-
}, d = o;
|
|
103
|
-
d.addEventListener("selectstart", u), d.addEventListener("squeezestart", c), d.addEventListener("connected", p), d.addEventListener("disconnected", g), this.cleanups.push(() => {
|
|
104
|
-
d.removeEventListener("selectstart", u), d.removeEventListener("squeezestart", c), d.removeEventListener("connected", p), d.removeEventListener("disconnected", g);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
this.paint(!0);
|
|
108
|
-
}
|
|
109
|
-
makeLaser() {
|
|
110
|
-
const e = new l.BufferGeometry().setFromPoints([new l.Vector3(0, 0, 0), new l.Vector3(0, 0, -1)]), t = new l.LineBasicMaterial({ color: 5213439, transparent: !0, opacity: 0.8 });
|
|
111
|
-
return new l.Line(e, t);
|
|
112
|
-
}
|
|
113
|
-
/** Called once per frame from the render loop (before renderer.render). */
|
|
114
|
-
update(e) {
|
|
115
|
-
if (!this.renderer.xr.getSession()) return;
|
|
116
|
-
if (this.pollToggle(), this.maybeRevealOnMotion(e), !this.visible) {
|
|
117
|
-
for (const i of this.lasers) i.visible = !1;
|
|
118
|
-
this.cursor.visible = !1;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (this.placeFrames > 0 && (this.place(), this.placeFrames--), this.idleAt && e > this.idleAt) {
|
|
122
|
-
this.hide();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
let t = null, s = null;
|
|
126
|
-
for (let i = 0; i < this.controllers.length; i++) {
|
|
127
|
-
const r = this.lasers[i], n = this.controllers[i], o = this.tmpVec.setFromMatrixPosition(n.matrixWorld).lengthSq() > 1e-6;
|
|
128
|
-
if (!this.connected[i] || !o) {
|
|
129
|
-
r.visible = !1;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
const h = this.rayHit(n);
|
|
133
|
-
h.point ? (r.visible = !0, r.scale.z = h.distance, s = h.point, h.hit && (t = h.hit.region)) : r.visible = !1;
|
|
134
|
-
}
|
|
135
|
-
s ? (this.cursor.position.copy(s), this.cursor.visible = !0) : this.cursor.visible = !1, t && (this.idleAt = e + M), this.hover = t, this.paint(!1);
|
|
136
|
-
}
|
|
137
|
-
/** Ray from a controller against the panel: the region under it (if any) and
|
|
138
|
-
* how far the laser reaches. */
|
|
139
|
-
rayHit(e) {
|
|
140
|
-
this.tmpMatrix.identity().extractRotation(e.matrixWorld), this.raycaster.ray.origin.setFromMatrixPosition(e.matrixWorld), this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(this.tmpMatrix);
|
|
141
|
-
const t = this.raycaster.intersectObject(this.panel, !1);
|
|
142
|
-
if (!t.length) return { hit: null, distance: 5, point: null };
|
|
143
|
-
const s = t[0];
|
|
144
|
-
return { hit: s.uv ? C(s.uv.x * v, (1 - s.uv.y) * w, this.layout) : null, distance: s.distance, point: s.point };
|
|
145
|
-
}
|
|
146
|
-
onSelect(e) {
|
|
147
|
-
if (!this.visible || !this.connected[e]) return;
|
|
148
|
-
const { hit: t } = this.rayHit(this.controllers[e]);
|
|
149
|
-
if (t) {
|
|
150
|
-
switch (this.idleAt = P() + M, t.region) {
|
|
151
|
-
case "play":
|
|
152
|
-
this.actions.togglePlay();
|
|
153
|
-
break;
|
|
154
|
-
case "exit":
|
|
155
|
-
this.actions.exitVR();
|
|
156
|
-
break;
|
|
157
|
-
case "recenter":
|
|
158
|
-
this.actions.recenter(), this.placeFrames = 12;
|
|
159
|
-
break;
|
|
160
|
-
case "seek":
|
|
161
|
-
t.value !== void 0 && this.actions.seekFraction(t.value);
|
|
162
|
-
break;
|
|
163
|
-
case "volume":
|
|
164
|
-
t.value !== void 0 ? this.actions.setVolume(t.value) : this.actions.toggleMute();
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
this.paint(!0);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
/** Re-summon the panel when a connected controller is deliberately moved (and
|
|
171
|
-
* we're past the post-hide cooldown). Tracks each controller's pose per frame. */
|
|
172
|
-
maybeRevealOnMotion(e) {
|
|
173
|
-
for (let t = 0; t < this.controllers.length; t++) {
|
|
174
|
-
const s = this.controllers[t], i = this.tmpVec.setFromMatrixPosition(s.matrixWorld);
|
|
175
|
-
if (!this.connected[t] || i.lengthSq() <= 1e-6) continue;
|
|
176
|
-
const r = this.tmpQuat.setFromRotationMatrix(s.matrixWorld), n = this.prevQuat[t], o = this.prevPos[t];
|
|
177
|
-
n && o && !this.visible && e > this.revealCooldownUntil && (r.angleTo(n) > I || i.distanceTo(o) > V) && this.show(), this.prevQuat[t] || (this.prevQuat[t] = new l.Quaternion()), this.prevPos[t] || (this.prevPos[t] = new l.Vector3()), this.prevQuat[t].copy(r), this.prevPos[t].copy(i);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
pollToggle() {
|
|
181
|
-
const e = this.renderer.xr.getSession();
|
|
182
|
-
let t = !1;
|
|
183
|
-
for (const s of (e == null ? void 0 : e.inputSources) ?? []) {
|
|
184
|
-
const i = s.gamepad;
|
|
185
|
-
i && B.some((r) => {
|
|
186
|
-
var n;
|
|
187
|
-
return (n = i.buttons[r]) == null ? void 0 : n.pressed;
|
|
188
|
-
}) && (t = !0);
|
|
189
|
-
}
|
|
190
|
-
t && !this.togglePrev && this.toggle(), this.togglePrev = t;
|
|
191
|
-
}
|
|
192
|
-
toggle() {
|
|
193
|
-
this.visible ? this.hide() : this.show();
|
|
194
|
-
}
|
|
195
|
-
show() {
|
|
196
|
-
this.place(), this.placeFrames = 12, this.visible = !0, this.panel.visible = !0, this.idleAt = P() + M, this.paint(!0);
|
|
197
|
-
}
|
|
198
|
-
hide() {
|
|
199
|
-
this.visible = !1, this.panel.visible = !1, this.cursor.visible = !1;
|
|
200
|
-
for (const e of this.lasers) e.visible = !1;
|
|
201
|
-
this.revealCooldownUntil = P() + O;
|
|
202
|
-
}
|
|
203
|
-
/** World-lock the panel in front of the current head pose (the "recenter"). Uses
|
|
204
|
-
* yaw only (so head pitch/roll at summon doesn't skew it), drops it into the lower
|
|
205
|
-
* field of view, and tilts its face up toward the viewer. Re-run on every summon. */
|
|
206
|
-
place() {
|
|
207
|
-
const e = this.renderer.xr.getCamera(), t = e.getWorldPosition(new l.Vector3()), s = e.getWorldQuaternion(new l.Quaternion()), i = new l.Euler().setFromQuaternion(s, "YXZ").y, r = new l.Quaternion().setFromEuler(new l.Euler(0, i, 0, "YXZ")), n = new l.Vector3(0, 0, -1).applyQuaternion(r);
|
|
208
|
-
this.panel.position.copy(t).addScaledVector(n, 1.5), this.panel.position.y -= 0.55, this.panel.quaternion.copy(r), this.panel.rotateX(-0.3);
|
|
209
|
-
}
|
|
210
|
-
/** Trim text with a trailing ellipsis to fit maxWidth. Assumes ctx.font is set. */
|
|
211
|
-
ellipsize(e, t) {
|
|
212
|
-
const s = this.ctx;
|
|
213
|
-
if (s.measureText(e).width <= t) return e;
|
|
214
|
-
let i = e;
|
|
215
|
-
for (; i.length > 1 && s.measureText(i + "…").width > t; ) i = i.slice(0, -1);
|
|
216
|
-
return i.replace(/\s+$/, "") + "…";
|
|
217
|
-
}
|
|
218
|
-
roundRect(e, t, s, i, r) {
|
|
219
|
-
const n = this.ctx;
|
|
220
|
-
n.beginPath(), n.moveTo(e + r, t), n.arcTo(e + s, t, e + s, t + i, r), n.arcTo(e + s, t + i, e, t + i, r), n.arcTo(e, t + i, e, t, r), n.arcTo(e, t, e + s, t, r), n.closePath();
|
|
221
|
-
}
|
|
222
|
-
/** Repaint the panel only when something visible changed (state or hover). */
|
|
223
|
-
paint(e) {
|
|
224
|
-
const t = this.actions.currentTime(), s = this.actions.duration(), i = this.actions.volume(), r = this.actions.muted(), n = [this.actions.isPlaying(), Math.floor(t), Math.floor(s), i.toFixed(2), r, this.hover, this.actions.title()].join("|");
|
|
225
|
-
if (!e && n === this.paintKey) return;
|
|
226
|
-
this.paintKey = n;
|
|
227
|
-
const o = this.ctx, h = this.layout;
|
|
228
|
-
o.clearRect(0, 0, v, w), this.roundRect(0, 0, v, w, 28), o.fillStyle = "rgba(22,24,30,0.84)", o.fill(), o.lineWidth = 2, o.strokeStyle = "rgba(255,255,255,0.08)", o.stroke();
|
|
229
|
-
const u = this.actions.title();
|
|
230
|
-
if (u) {
|
|
231
|
-
o.font = '600 30px system-ui,"Segoe UI",Roboto,sans-serif', o.fillStyle = y, o.textAlign = "center", o.textBaseline = "middle";
|
|
232
|
-
const d = 2 * h.exit.x - 48 - v;
|
|
233
|
-
o.fillText(this.ellipsize(u, d), v / 2, h.title.y + h.title.h / 2);
|
|
234
|
-
}
|
|
235
|
-
this.drawPill(h.recenter, "Recenter", this.hover === "recenter"), this.drawPill(h.exit, "Exit VR", this.hover === "exit");
|
|
236
|
-
const c = { x: h.play.x + h.play.w / 2, y: h.play.y + h.play.h / 2 };
|
|
237
|
-
o.beginPath(), o.arc(c.x, c.y, 44, 0, Math.PI * 2), o.fillStyle = this.hover === "play" ? S : "rgba(255,255,255,0.14)", o.fill(), o.fillStyle = "#fff", this.actions.isPlaying() ? (o.fillRect(c.x - 13, c.y - 16, 9, 32), o.fillRect(c.x + 4, c.y - 16, 9, 32)) : (o.beginPath(), o.moveTo(c.x - 12, c.y - 17), o.lineTo(c.x - 12, c.y + 17), o.lineTo(c.x + 18, c.y), o.closePath(), o.fill()), this.drawSpeaker(h.volIcon, r);
|
|
238
|
-
const p = r ? 0 : Math.max(0, Math.min(1, i));
|
|
239
|
-
this.drawBar(h.volBar, p, this.hover === "volume");
|
|
240
|
-
const g = s > 0 ? Math.max(0, Math.min(1, t / s)) : 0;
|
|
241
|
-
this.drawBar(h.seekBar, g, this.hover === "seek", !0), o.fillStyle = E, o.font = "22px system-ui,sans-serif", o.textBaseline = "alphabetic", o.textAlign = "left", o.fillText(R(t), h.timeCur.x, h.timeCur.y), o.textAlign = "right", o.fillText(s > 0 ? R(s) : "--:--", h.timeDur.x, h.timeDur.y), this.texture.needsUpdate = !0;
|
|
242
|
-
}
|
|
243
|
-
drawPill(e, t, s) {
|
|
244
|
-
const i = this.ctx;
|
|
245
|
-
this.roundRect(e.x, e.y, e.w, e.h, e.h / 2), i.fillStyle = s ? S : "rgba(255,255,255,0.12)", i.fill(), i.fillStyle = s ? "#fff" : y, i.font = "600 24px system-ui,sans-serif", i.textAlign = "center", i.textBaseline = "middle", i.fillText(t, e.x + e.w / 2, e.y + e.h / 2);
|
|
246
|
-
}
|
|
247
|
-
drawBar(e, t, s, i = !1) {
|
|
248
|
-
const r = this.ctx, n = e.h / 2;
|
|
249
|
-
this.roundRect(e.x, e.y, e.w, e.h, n), r.fillStyle = "rgba(255,255,255,0.22)", r.fill(), t > 0 && (this.roundRect(e.x, e.y, e.w * t, e.h, n), r.fillStyle = S, r.fill()), (i || s) && (r.beginPath(), r.arc(e.x + e.w * t, e.y + e.h / 2, s ? 12 : 9, 0, Math.PI * 2), r.fillStyle = "#fff", r.fill());
|
|
250
|
-
}
|
|
251
|
-
drawSpeaker(e, t) {
|
|
252
|
-
const s = this.ctx, i = e.x, r = e.y + e.h / 2;
|
|
253
|
-
s.fillStyle = t ? E : y, s.beginPath(), s.moveTo(i, r - 7), s.lineTo(i + 10, r - 7), s.lineTo(i + 20, r - 16), s.lineTo(i + 20, r + 16), s.lineTo(i + 10, r + 7), s.lineTo(i, r + 7), s.closePath(), s.fill(), s.strokeStyle = t ? "#e06a6a" : y, s.lineWidth = 3, t ? (s.beginPath(), s.moveTo(i + 26, r - 9), s.lineTo(i + 40, r + 9), s.moveTo(i + 40, r - 9), s.lineTo(i + 26, r + 9), s.stroke()) : (s.beginPath(), s.arc(i + 24, r, 8, -Math.PI / 3, Math.PI / 3), s.stroke(), s.beginPath(), s.arc(i + 24, r, 15, -Math.PI / 3, Math.PI / 3), s.stroke());
|
|
254
|
-
}
|
|
255
|
-
dispose() {
|
|
256
|
-
for (const e of this.cleanups) e();
|
|
257
|
-
for (let e = 0; e < this.controllers.length; e++) {
|
|
258
|
-
const t = this.controllers[e];
|
|
259
|
-
t.remove(this.lasers[e]), this.lasers[e].geometry.dispose(), this.lasers[e].material.dispose(), this.scene.remove(t);
|
|
260
|
-
}
|
|
261
|
-
this.scene.remove(this.panel), this.panel.geometry.dispose(), this.panel.material.dispose(), this.scene.remove(this.cursor), this.cursor.geometry.dispose(), this.cursor.material.dispose(), this.texture.dispose();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function P() {
|
|
265
|
-
return typeof performance < "u" ? performance.now() : 0;
|
|
266
|
-
}
|
|
267
|
-
class j {
|
|
268
|
-
constructor(e) {
|
|
269
|
-
this.scene = new l.Scene(), this.meshes = [], this.frameCbs = [], this.animate = (h) => {
|
|
270
|
-
var u;
|
|
271
|
-
for (const c of this.frameCbs) c();
|
|
272
|
-
(u = this.vrControls) == null || u.update(h ?? 0), this.video.readyState >= this.video.HAVE_CURRENT_DATA && (this.texture.needsUpdate = !0), this.renderer.render(this.scene, this.camera);
|
|
273
|
-
}, this.vrSessionInit = { optionalFeatures: ["local-floor"] }, this.vrTitle = "", this.resize = () => {
|
|
274
|
-
if (this.renderer.xr.isPresenting) return;
|
|
275
|
-
const h = this.w(), u = this.h();
|
|
276
|
-
this.camera.aspect = h / u, this.camera.updateProjectionMatrix(), this.renderer.setSize(h, u, !1);
|
|
277
|
-
};
|
|
278
|
-
const { canvas: t, video: s, projection: i = "180-sbs", swapEyes: r = !1, fov: n = 70, supersampling: o = 1.5 } = e;
|
|
279
|
-
this.canvas = t, this.video = s, this.currentMode = m[i] ? i : "180-sbs", this.currentSwap = r, this.renderer = new l.WebGLRenderer({ canvas: t, antialias: !0 }), this.renderer.setPixelRatio(this.pixelRatioFor(o)), this.renderer.setSize(this.w(), this.h(), !1), this.renderer.xr.enabled = !0, this.camera = new l.PerspectiveCamera(n, this.w() / this.h(), 0.1, 1e3), this.camera.position.set(0, 0, 0), this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy(), this.texture = new l.VideoTexture(s), this.texture.colorSpace = l.SRGBColorSpace, this.texture.minFilter = l.LinearFilter, this.texture.magFilter = l.LinearFilter, this.texture.anisotropy = 1, this.applyProjection(this.currentMode, this.currentSwap), s.addEventListener("loadedmetadata", () => {
|
|
280
|
-
m[this.currentMode].flat && this.applyProjection(this.currentMode, this.currentSwap);
|
|
281
|
-
}), this.renderer.setAnimationLoop(this.animate), this.renderer.xr.addEventListener("sessionstart", () => this.buildVRControls()), this.renderer.xr.addEventListener("sessionend", () => {
|
|
282
|
-
var h;
|
|
283
|
-
(h = this.vrControls) == null || h.dispose(), this.vrControls = void 0, this.video.pause();
|
|
284
|
-
}), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(t);
|
|
285
|
-
}
|
|
286
|
-
buildVRControls() {
|
|
287
|
-
const e = this.video;
|
|
288
|
-
this.vrControls = new W({
|
|
289
|
-
renderer: this.renderer,
|
|
290
|
-
scene: this.scene,
|
|
291
|
-
actions: {
|
|
292
|
-
isPlaying: () => !e.paused,
|
|
293
|
-
currentTime: () => e.currentTime,
|
|
294
|
-
duration: () => e.duration || 0,
|
|
295
|
-
volume: () => e.volume,
|
|
296
|
-
muted: () => e.muted,
|
|
297
|
-
title: () => this.vrTitle,
|
|
298
|
-
togglePlay: () => {
|
|
299
|
-
e.paused ? e.play() : e.pause();
|
|
300
|
-
},
|
|
301
|
-
seekFraction: (t) => {
|
|
302
|
-
e.duration && (e.currentTime = t * e.duration);
|
|
303
|
-
},
|
|
304
|
-
setVolume: (t) => {
|
|
305
|
-
e.volume = t, t > 0 && (e.muted = !1);
|
|
306
|
-
},
|
|
307
|
-
toggleMute: () => {
|
|
308
|
-
e.muted = !e.muted;
|
|
309
|
-
},
|
|
310
|
-
exitVR: () => this.exitVR(),
|
|
311
|
-
recenter: () => this.recenter()
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
/** Recenter the view: make the viewer's current spot and facing the origin, so the
|
|
316
|
-
* video front is straight ahead again. Resets yaw and horizontal position; keeps
|
|
317
|
-
* head height and a level horizon (no pitch/roll). */
|
|
318
|
-
recenter() {
|
|
319
|
-
const e = this.renderer.xr;
|
|
320
|
-
if (!e.isPresenting) return;
|
|
321
|
-
const t = e.getReferenceSpace();
|
|
322
|
-
if (!t) return;
|
|
323
|
-
const s = e.getCamera(), i = s.getWorldPosition(new l.Vector3()), r = new l.Euler().setFromQuaternion(s.getWorldQuaternion(new l.Quaternion()), "YXZ").y, n = new l.Quaternion().setFromEuler(new l.Euler(0, r, 0, "YXZ")), o = new XRRigidTransform({ x: i.x, y: 0, z: i.z }, { x: n.x, y: n.y, z: n.z, w: n.w });
|
|
324
|
-
e.setReferenceSpace(t.getOffsetReferenceSpace(o));
|
|
325
|
-
}
|
|
326
|
-
/** Optional title shown on the in-VR control panel. */
|
|
327
|
-
setVRTitle(e) {
|
|
328
|
-
this.vrTitle = e;
|
|
329
|
-
}
|
|
330
|
-
w() {
|
|
331
|
-
return this.canvas.clientWidth || 1;
|
|
332
|
-
}
|
|
333
|
-
h() {
|
|
334
|
-
return this.canvas.clientHeight || 1;
|
|
335
|
-
}
|
|
336
|
-
pixelRatioFor(e) {
|
|
337
|
-
return Math.min(window.devicePixelRatio * e, 4);
|
|
338
|
-
}
|
|
339
|
-
planeAspect(e) {
|
|
340
|
-
const t = this.video.videoWidth, s = this.video.videoHeight;
|
|
341
|
-
return !t || !s ? 16 / 9 : m[e].aspect === "per-eye" ? t / 2 / s : t / s;
|
|
342
|
-
}
|
|
343
|
-
buildGeometry(e) {
|
|
344
|
-
const t = m[e].geom;
|
|
345
|
-
if (t === "sphere180") {
|
|
346
|
-
const n = new l.SphereGeometry(500, 64, 32, -Math.PI / 2, Math.PI, 0, Math.PI);
|
|
347
|
-
return n.scale(-1, 1, 1), n;
|
|
348
|
-
}
|
|
349
|
-
if (t === "sphere360") {
|
|
350
|
-
const n = new l.SphereGeometry(500, 64, 32);
|
|
351
|
-
return n.scale(-1, 1, 1), n;
|
|
352
|
-
}
|
|
353
|
-
const s = 2.4, i = s * this.planeAspect(e), r = new l.PlaneGeometry(i, s);
|
|
354
|
-
return r.translate(0, 0, -2), r;
|
|
355
|
-
}
|
|
356
|
-
/** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's
|
|
357
|
-
* onBeforeRender, so it's called once per eye per frame (the reference approach). */
|
|
358
|
-
updateStereoUV(e) {
|
|
359
|
-
const t = this.texture, s = m[this.currentMode].split;
|
|
360
|
-
let i = !0;
|
|
361
|
-
if (this.renderer.xr.isPresenting) {
|
|
362
|
-
const n = e, o = this.renderer.xr.getCamera().cameras;
|
|
363
|
-
o && o.length >= 2 ? n === o[0] ? i = !0 : n === o[1] ? i = !1 : i = Math.abs(n.matrixWorldInverse.elements[12] - o[0].matrixWorldInverse.elements[12]) <= Math.abs(n.matrixWorldInverse.elements[12] - o[1].matrixWorldInverse.elements[12]) : i = n.projectionMatrix.elements[8] <= 0;
|
|
364
|
-
}
|
|
365
|
-
const r = !i !== this.currentSwap;
|
|
366
|
-
s === "sbs" ? (t.repeat.set(0.5, 1), t.offset.set(r ? 0.5 : 0, 0)) : s === "tb" ? (t.repeat.set(1, 0.5), t.offset.set(0, r ? 0 : 0.5)) : (t.repeat.set(1, 1), t.offset.set(0, 0));
|
|
367
|
-
}
|
|
368
|
-
clearMeshes() {
|
|
369
|
-
for (const e of this.meshes)
|
|
370
|
-
this.scene.remove(e), e.geometry.dispose(), e.material.dispose();
|
|
371
|
-
this.meshes.length = 0;
|
|
372
|
-
}
|
|
373
|
-
applyProjection(e, t) {
|
|
374
|
-
m[e] || (e = "180-sbs"), this.clearMeshes(), this.currentMode = e, this.currentSwap = t;
|
|
375
|
-
const s = new l.Mesh(this.buildGeometry(e), new l.MeshBasicMaterial({ map: this.texture }));
|
|
376
|
-
s.layers.set(0), m[e].geom === "sphere180" && (s.rotation.y = Math.PI / 2), s.onBeforeRender = (i, r, n) => this.updateStereoUV(n), this.scene.add(s), this.meshes.push(s);
|
|
377
|
-
}
|
|
378
|
-
setProjection(e) {
|
|
379
|
-
this.applyProjection(e, this.currentSwap);
|
|
380
|
-
}
|
|
381
|
-
setSwapEyes(e) {
|
|
382
|
-
this.applyProjection(this.currentMode, e);
|
|
383
|
-
}
|
|
384
|
-
setFov(e) {
|
|
385
|
-
this.camera.fov = e, this.camera.updateProjectionMatrix();
|
|
386
|
-
}
|
|
387
|
-
setSupersampling(e) {
|
|
388
|
-
this.renderer.setPixelRatio(this.pixelRatioFor(e)), this.renderer.setSize(this.w(), this.h(), !1);
|
|
389
|
-
}
|
|
390
|
-
getProjection() {
|
|
391
|
-
return this.currentMode;
|
|
392
|
-
}
|
|
393
|
-
isFlat() {
|
|
394
|
-
return !!m[this.currentMode].flat;
|
|
395
|
-
}
|
|
396
|
-
onFrame(e) {
|
|
397
|
-
this.frameCbs.push(e);
|
|
398
|
-
}
|
|
399
|
-
pauseRendering() {
|
|
400
|
-
this.renderer.setAnimationLoop(null);
|
|
401
|
-
}
|
|
402
|
-
resumeRendering() {
|
|
403
|
-
this.renderer.setAnimationLoop(this.animate);
|
|
404
|
-
}
|
|
405
|
-
/** Enter immersive VR. Rejects with a readable message on failure so the caller can
|
|
406
|
-
* surface it — instead of the silent no-op three.js's VRButton produces. Called from
|
|
407
|
-
* a click handler so the request keeps the user's transient activation. */
|
|
408
|
-
async enterVR() {
|
|
409
|
-
if (!navigator.xr) throw new Error("WebXR is unavailable here — open the page over HTTPS or localhost.");
|
|
410
|
-
const e = await navigator.xr.requestSession("immersive-vr", this.vrSessionInit);
|
|
411
|
-
await this.renderer.xr.setSession(e);
|
|
412
|
-
}
|
|
413
|
-
exitVR() {
|
|
414
|
-
var e;
|
|
415
|
-
(e = this.renderer.xr.getSession()) == null || e.end();
|
|
416
|
-
}
|
|
417
|
-
/** Arm the browser/headset's own "Enter VR" affordance (e.g. the Quest system button). */
|
|
418
|
-
offerVR() {
|
|
419
|
-
var s;
|
|
420
|
-
const e = (s = navigator.xr) == null ? void 0 : s.offerSession;
|
|
421
|
-
if (!e) return;
|
|
422
|
-
const t = () => e.call(navigator.xr, "immersive-vr", this.vrSessionInit).then((i) => this.renderer.xr.setSession(i)).catch(() => {
|
|
423
|
-
});
|
|
424
|
-
this.renderer.xr.addEventListener("sessionend", () => void t()), t();
|
|
425
|
-
}
|
|
426
|
-
dispose() {
|
|
427
|
-
var e;
|
|
428
|
-
this.ro.disconnect(), this.renderer.setAnimationLoop(null), (e = this.vrControls) == null || e.dispose(), this.texture.dispose(), this.clearMeshes(), this.renderer.dispose();
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const L = Math.PI / 180;
|
|
432
|
-
function D(a, e) {
|
|
433
|
-
return {
|
|
434
|
-
lon: Math.max(-90, Math.min(90, a)),
|
|
435
|
-
lat: Math.max(-85, Math.min(85, e))
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
class $ {
|
|
439
|
-
constructor(e, t, s = {}) {
|
|
440
|
-
this.camera = e, this.dom = t, this.lon = 0, this.lat = 0, this.dragging = !1, this.px = 0, this.py = 0, this.enabled = !0, this.onDown = (i) => {
|
|
441
|
-
this.enabled && (this.dragging = !0, this.px = i.clientX, this.py = i.clientY, this.capture("setPointerCapture", i.pointerId));
|
|
442
|
-
}, this.onMove = (i) => {
|
|
443
|
-
if (!this.dragging) return;
|
|
444
|
-
const r = i.clientX - this.px, n = i.clientY - this.py;
|
|
445
|
-
this.px = i.clientX, this.py = i.clientY, { lon: this.lon, lat: this.lat } = D(this.lon - r * 0.15, this.lat + n * 0.15);
|
|
446
|
-
}, this.onUp = (i) => {
|
|
447
|
-
this.dragging = !1, this.capture("releasePointerCapture", i.pointerId);
|
|
448
|
-
}, this.isPresenting = s.isPresenting ?? (() => !1), this.dom.addEventListener("pointerdown", this.onDown), this.dom.addEventListener("pointermove", this.onMove), this.dom.addEventListener("pointerup", this.onUp), this.dom.addEventListener("pointercancel", this.onUp);
|
|
449
|
-
}
|
|
450
|
-
capture(e, t) {
|
|
451
|
-
var s, i;
|
|
452
|
-
try {
|
|
453
|
-
(i = (s = this.dom)[e]) == null || i.call(s, t);
|
|
454
|
-
} catch {
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
update() {
|
|
458
|
-
if (this.isPresenting() || !this.enabled) return;
|
|
459
|
-
const e = (90 - this.lat) * L, t = this.lon * L;
|
|
460
|
-
this.camera.lookAt(
|
|
461
|
-
Math.sin(e) * Math.sin(t),
|
|
462
|
-
Math.cos(e),
|
|
463
|
-
-Math.sin(e) * Math.cos(t)
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
reset() {
|
|
467
|
-
this.lon = 0, this.lat = 0;
|
|
468
|
-
}
|
|
469
|
-
setEnabled(e) {
|
|
470
|
-
this.enabled = e, e || (this.lon = 0, this.lat = 0, this.camera.lookAt(0, 0, -1));
|
|
471
|
-
}
|
|
472
|
-
getAngles() {
|
|
473
|
-
return { lon: this.lon, lat: this.lat };
|
|
474
|
-
}
|
|
475
|
-
dispose() {
|
|
476
|
-
this.dom.removeEventListener("pointerdown", this.onDown), this.dom.removeEventListener("pointermove", this.onMove), this.dom.removeEventListener("pointerup", this.onUp), this.dom.removeEventListener("pointercancel", this.onUp);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
class G {
|
|
480
|
-
constructor() {
|
|
481
|
-
this.hls = null;
|
|
482
|
-
}
|
|
483
|
-
async attach(e, t, s = {}) {
|
|
484
|
-
this.dispose();
|
|
485
|
-
const i = s.crossOrigin === void 0 ? "anonymous" : s.crossOrigin;
|
|
486
|
-
i === null ? e.removeAttribute("crossorigin") : e.crossOrigin = i, e.playsInline = !0;
|
|
487
|
-
const { url: r, format: n } = t, o = n === "hls" || n === "dash", h = e.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
488
|
-
return new Promise((u, c) => {
|
|
489
|
-
const p = () => {
|
|
490
|
-
e.removeEventListener("loadedmetadata", g), e.removeEventListener("error", d);
|
|
491
|
-
}, g = () => {
|
|
492
|
-
p(), u();
|
|
493
|
-
}, d = () => {
|
|
494
|
-
p();
|
|
495
|
-
const f = e.error ? e.error.code : "unknown";
|
|
496
|
-
c(new Error(`Video failed to load (media error ${f}). Is the source reachable and CORS-clean?`));
|
|
497
|
-
};
|
|
498
|
-
e.addEventListener("loadedmetadata", g), e.addEventListener("error", d), o && !h ? import("hls.js").then(({ default: f }) => {
|
|
499
|
-
if (!f.isSupported()) {
|
|
500
|
-
p(), c(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
this.hls = new f({ enableWorker: !0 }), this.hls.on(f.Events.ERROR, (Q, b) => {
|
|
504
|
-
b.fatal && (p(), this.dispose(), c(new Error(`HLS fatal error: ${b.type} / ${b.details}`)));
|
|
505
|
-
}), this.hls.loadSource(r), this.hls.attachMedia(e);
|
|
506
|
-
}).catch(() => {
|
|
507
|
-
p(), c(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install "hls.js".'));
|
|
508
|
-
}) : (e.src = r, e.load());
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
dispose() {
|
|
512
|
-
this.hls && (this.hls.destroy(), this.hls = null);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
export {
|
|
516
|
-
$ as L,
|
|
517
|
-
m as M,
|
|
518
|
-
z as P,
|
|
519
|
-
j as S,
|
|
520
|
-
G as V,
|
|
521
|
-
U as a,
|
|
522
|
-
_ as b,
|
|
523
|
-
D as c,
|
|
524
|
-
k as d,
|
|
525
|
-
R as f,
|
|
526
|
-
H as i
|
|
527
|
-
};
|
|
528
|
-
//# sourceMappingURL=VideoSource-Cx7XPpvZ.js.map
|