three-vr-player 0.8.0 → 0.10.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/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.8.0",
3
+ "version": "0.10.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