three-vr-player 0.6.0 → 0.8.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.
@@ -0,0 +1,528 @@
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