three-vr-player 0.6.0 → 0.7.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
@@ -28,6 +28,8 @@ export interface PlayerOptions {
28
28
  shadowDom?: boolean;
29
29
  /** Show the Enter-VR button when a headset is available. Default `true`. */
30
30
  vrButton?: boolean;
31
+ /** Title shown on the in-VR control panel. Optional. */
32
+ title?: string;
31
33
  /**
32
34
  * If a cross-origin source can't be used as a WebGL texture (CORS-tainted) and
33
35
  * 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.6.0",
3
+ "version": "0.7.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",
@@ -1,263 +0,0 @@
1
- import * as a from "three";
2
- function S(n) {
3
- let e = n;
4
- try {
5
- e = new URL(n).pathname;
6
- } catch {
7
- }
8
- const t = e.toLowerCase();
9
- return t.endsWith(".m3u8") ? "hls" : t.endsWith(".mpd") ? "dash" : "progressive";
10
- }
11
- const w = {
12
- progressive: "/proxy/stream",
13
- hls: "/proxy/hls/manifest.m3u8",
14
- dash: "/proxy/mpd/manifest.m3u8"
15
- };
16
- function P(n, e) {
17
- const t = S(n);
18
- if (!e || !e.url) return { url: n, format: t };
19
- const s = e.url.replace(/\/+$/, ""), r = new URLSearchParams();
20
- r.set("d", n), e.apiPassword && r.set("api_password", e.apiPassword);
21
- for (const [o, i] of Object.entries(e.headers ?? {}))
22
- i != null && i !== "" && r.set(`h_${o}`, i);
23
- return { url: `${s}${w[t]}?${r.toString()}`, format: t };
24
- }
25
- const l = {
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
- }, y = [
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 E(n) {
45
- var e;
46
- return !!((e = l[n]) != null && e.flat);
47
- }
48
- function L(n) {
49
- const e = String(n).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
- class R {
53
- constructor(e) {
54
- this.scene = new a.Scene(), this.meshes = [], this.frameCbs = [], this.animate = () => {
55
- for (const c of this.frameCbs) c();
56
- this.video.readyState >= this.video.HAVE_CURRENT_DATA && (this.texture.needsUpdate = !0), this.renderer.render(this.scene, this.camera);
57
- }, this.vrSessionInit = { optionalFeatures: ["local-floor"] }, this.resize = () => {
58
- if (this.renderer.xr.isPresenting) return;
59
- const c = this.w(), u = this.h();
60
- this.camera.aspect = c / u, this.camera.updateProjectionMatrix(), this.renderer.setSize(c, u, !1);
61
- };
62
- const { canvas: t, video: s, projection: r = "180-sbs", swapEyes: o = !1, fov: i = 70, supersampling: h = 1.5 } = e;
63
- this.canvas = t, this.video = s, this.currentMode = l[r] ? r : "180-sbs", this.currentSwap = o, this.renderer = new a.WebGLRenderer({ canvas: t, antialias: !0 }), this.renderer.setPixelRatio(this.pixelRatioFor(h)), this.renderer.setSize(this.w(), this.h(), !1), this.renderer.xr.enabled = !0, this.camera = new a.PerspectiveCamera(i, this.w() / this.h(), 0.1, 1e3), this.camera.position.set(0, 0, 0), this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy(), this.texture = new a.VideoTexture(s), this.texture.colorSpace = a.SRGBColorSpace, this.texture.minFilter = a.LinearFilter, this.texture.magFilter = a.LinearFilter, this.texture.anisotropy = 1, this.applyProjection(this.currentMode, this.currentSwap), s.addEventListener("loadedmetadata", () => {
64
- l[this.currentMode].flat && this.applyProjection(this.currentMode, this.currentSwap);
65
- }), this.renderer.setAnimationLoop(this.animate), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(t);
66
- }
67
- w() {
68
- return this.canvas.clientWidth || 1;
69
- }
70
- h() {
71
- return this.canvas.clientHeight || 1;
72
- }
73
- pixelRatioFor(e) {
74
- return Math.min(window.devicePixelRatio * e, 4);
75
- }
76
- planeAspect(e) {
77
- const t = this.video.videoWidth, s = this.video.videoHeight;
78
- return !t || !s ? 16 / 9 : l[e].aspect === "per-eye" ? t / 2 / s : t / s;
79
- }
80
- buildGeometry(e) {
81
- const t = l[e].geom;
82
- if (t === "sphere180") {
83
- const i = new a.SphereGeometry(500, 64, 32, -Math.PI / 2, Math.PI, 0, Math.PI);
84
- return i.scale(-1, 1, 1), i;
85
- }
86
- if (t === "sphere360") {
87
- const i = new a.SphereGeometry(500, 64, 32);
88
- return i.scale(-1, 1, 1), i;
89
- }
90
- const s = 2.4, r = s * this.planeAspect(e), o = new a.PlaneGeometry(r, s);
91
- return o.translate(0, 0, -2), o;
92
- }
93
- /** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's
94
- * onBeforeRender, so it's called once per eye per frame (the reference approach). */
95
- updateStereoUV(e) {
96
- const t = this.texture, s = l[this.currentMode].split;
97
- let r = !0;
98
- if (this.renderer.xr.isPresenting) {
99
- const i = e, h = this.renderer.xr.getCamera().cameras;
100
- h && h.length >= 2 ? i === h[0] ? r = !0 : i === h[1] ? r = !1 : r = Math.abs(i.matrixWorldInverse.elements[12] - h[0].matrixWorldInverse.elements[12]) <= Math.abs(i.matrixWorldInverse.elements[12] - h[1].matrixWorldInverse.elements[12]) : r = i.projectionMatrix.elements[8] <= 0;
101
- }
102
- const o = !r !== this.currentSwap;
103
- s === "sbs" ? (t.repeat.set(0.5, 1), t.offset.set(o ? 0.5 : 0, 0)) : s === "tb" ? (t.repeat.set(1, 0.5), t.offset.set(0, o ? 0 : 0.5)) : (t.repeat.set(1, 1), t.offset.set(0, 0));
104
- }
105
- clearMeshes() {
106
- for (const e of this.meshes)
107
- this.scene.remove(e), e.geometry.dispose(), e.material.dispose();
108
- this.meshes.length = 0;
109
- }
110
- applyProjection(e, t) {
111
- l[e] || (e = "180-sbs"), this.clearMeshes(), this.currentMode = e, this.currentSwap = t;
112
- const s = new a.Mesh(this.buildGeometry(e), new a.MeshBasicMaterial({ map: this.texture }));
113
- s.layers.set(0), l[e].geom === "sphere180" && (s.rotation.y = Math.PI / 2), s.onBeforeRender = (r, o, i) => this.updateStereoUV(i), this.scene.add(s), this.meshes.push(s);
114
- }
115
- setProjection(e) {
116
- this.applyProjection(e, this.currentSwap);
117
- }
118
- setSwapEyes(e) {
119
- this.applyProjection(this.currentMode, e);
120
- }
121
- setFov(e) {
122
- this.camera.fov = e, this.camera.updateProjectionMatrix();
123
- }
124
- setSupersampling(e) {
125
- this.renderer.setPixelRatio(this.pixelRatioFor(e)), this.renderer.setSize(this.w(), this.h(), !1);
126
- }
127
- getProjection() {
128
- return this.currentMode;
129
- }
130
- isFlat() {
131
- return !!l[this.currentMode].flat;
132
- }
133
- onFrame(e) {
134
- this.frameCbs.push(e);
135
- }
136
- pauseRendering() {
137
- this.renderer.setAnimationLoop(null);
138
- }
139
- resumeRendering() {
140
- this.renderer.setAnimationLoop(this.animate);
141
- }
142
- /** Enter immersive VR. Rejects with a readable message on failure so the caller can
143
- * surface it — instead of the silent no-op three.js's VRButton produces. Called from
144
- * a click handler so the request keeps the user's transient activation. */
145
- async enterVR() {
146
- if (!navigator.xr) throw new Error("WebXR is unavailable here — open the page over HTTPS or localhost.");
147
- const e = await navigator.xr.requestSession("immersive-vr", this.vrSessionInit);
148
- await this.renderer.xr.setSession(e);
149
- }
150
- exitVR() {
151
- var e;
152
- (e = this.renderer.xr.getSession()) == null || e.end();
153
- }
154
- /** Arm the browser/headset's own "Enter VR" affordance (e.g. the Quest system button). */
155
- offerVR() {
156
- var s;
157
- const e = (s = navigator.xr) == null ? void 0 : s.offerSession;
158
- if (!e) return;
159
- const t = () => e.call(navigator.xr, "immersive-vr", this.vrSessionInit).then((r) => this.renderer.xr.setSession(r)).catch(() => {
160
- });
161
- this.renderer.xr.addEventListener("sessionend", () => void t()), t();
162
- }
163
- dispose() {
164
- this.ro.disconnect(), this.renderer.setAnimationLoop(null), this.texture.dispose(), this.clearMeshes(), this.renderer.dispose();
165
- }
166
- }
167
- const g = Math.PI / 180;
168
- function x(n, e) {
169
- return {
170
- lon: Math.max(-90, Math.min(90, n)),
171
- lat: Math.max(-85, Math.min(85, e))
172
- };
173
- }
174
- class I {
175
- constructor(e, t, s = {}) {
176
- 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 = (r) => {
177
- this.enabled && (this.dragging = !0, this.px = r.clientX, this.py = r.clientY, this.capture("setPointerCapture", r.pointerId));
178
- }, this.onMove = (r) => {
179
- if (!this.dragging) return;
180
- const o = r.clientX - this.px, i = r.clientY - this.py;
181
- this.px = r.clientX, this.py = r.clientY, { lon: this.lon, lat: this.lat } = x(this.lon - o * 0.15, this.lat + i * 0.15);
182
- }, this.onUp = (r) => {
183
- this.dragging = !1, this.capture("releasePointerCapture", r.pointerId);
184
- }, 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);
185
- }
186
- capture(e, t) {
187
- var s, r;
188
- try {
189
- (r = (s = this.dom)[e]) == null || r.call(s, t);
190
- } catch {
191
- }
192
- }
193
- update() {
194
- if (this.isPresenting() || !this.enabled) return;
195
- const e = (90 - this.lat) * g, t = this.lon * g;
196
- this.camera.lookAt(
197
- Math.sin(e) * Math.sin(t),
198
- Math.cos(e),
199
- -Math.sin(e) * Math.cos(t)
200
- );
201
- }
202
- reset() {
203
- this.lon = 0, this.lat = 0;
204
- }
205
- setEnabled(e) {
206
- this.enabled = e, e || (this.lon = 0, this.lat = 0, this.camera.lookAt(0, 0, -1));
207
- }
208
- getAngles() {
209
- return { lon: this.lon, lat: this.lat };
210
- }
211
- dispose() {
212
- this.dom.removeEventListener("pointerdown", this.onDown), this.dom.removeEventListener("pointermove", this.onMove), this.dom.removeEventListener("pointerup", this.onUp), this.dom.removeEventListener("pointercancel", this.onUp);
213
- }
214
- }
215
- class A {
216
- constructor() {
217
- this.hls = null;
218
- }
219
- async attach(e, t, s = {}) {
220
- this.dispose();
221
- const r = s.crossOrigin === void 0 ? "anonymous" : s.crossOrigin;
222
- r === null ? e.removeAttribute("crossorigin") : e.crossOrigin = r, e.playsInline = !0;
223
- const { url: o, format: i } = t, h = i === "hls" || i === "dash", c = e.canPlayType("application/vnd.apple.mpegurl") !== "";
224
- return new Promise((u, m) => {
225
- const d = () => {
226
- e.removeEventListener("loadedmetadata", v), e.removeEventListener("error", b);
227
- }, v = () => {
228
- d(), u();
229
- }, b = () => {
230
- d();
231
- const p = e.error ? e.error.code : "unknown";
232
- m(new Error(`Video failed to load (media error ${p}). Is the source reachable and CORS-clean?`));
233
- };
234
- e.addEventListener("loadedmetadata", v), e.addEventListener("error", b), h && !c ? import("hls.js").then(({ default: p }) => {
235
- if (!p.isSupported()) {
236
- d(), m(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
237
- return;
238
- }
239
- this.hls = new p({ enableWorker: !0 }), this.hls.on(p.Events.ERROR, (M, f) => {
240
- f.fatal && (d(), this.dispose(), m(new Error(`HLS fatal error: ${f.type} / ${f.details}`)));
241
- }), this.hls.loadSource(o), this.hls.attachMedia(e);
242
- }).catch(() => {
243
- d(), m(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install "hls.js".'));
244
- }) : (e.src = o, e.load());
245
- });
246
- }
247
- dispose() {
248
- this.hls && (this.hls.destroy(), this.hls = null);
249
- }
250
- }
251
- export {
252
- I as L,
253
- l as M,
254
- y as P,
255
- R as S,
256
- A as V,
257
- L as a,
258
- P as b,
259
- x as c,
260
- S as d,
261
- E as i
262
- };
263
- //# sourceMappingURL=VideoSource-CMM_vlRP.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"VideoSource-CMM_vlRP.js","sources":["../src/core/proxy.ts","../src/core/projections.ts","../src/core/StereoScene.ts","../src/core/LookControls.ts","../src/core/VideoSource.ts"],"sourcesContent":["export type StreamFormat = 'hls' | 'dash' | 'progressive';\r\n\r\nexport interface ProxyConfig {\r\n url: string;\r\n apiPassword?: string;\r\n headers?: Record<string, string>;\r\n}\r\n\r\n/** Detect streaming format from a URL by extension. */\r\nexport function detectFormat(rawUrl: string): StreamFormat {\r\n let pathname = rawUrl;\r\n try { pathname = new URL(rawUrl).pathname; } catch { /* relative/opaque: use raw */ }\r\n const lower = pathname.toLowerCase();\r\n if (lower.endsWith('.m3u8')) return 'hls';\r\n if (lower.endsWith('.mpd')) return 'dash';\r\n return 'progressive';\r\n}\r\n\r\nconst ENDPOINTS: Record<StreamFormat, string> = {\r\n progressive: '/proxy/stream',\r\n hls: '/proxy/hls/manifest.m3u8',\r\n dash: '/proxy/mpd/manifest.m3u8',\r\n};\r\n\r\n/**\r\n * Build the URL the `<video>`/hls.js should load. When `proxy` is omitted the raw\r\n * URL is returned (it must then be CORS-clean to be used as a WebGL texture).\r\n */\r\nexport function buildProxyUrl(rawUrl: string, proxy?: ProxyConfig): { url: string; format: StreamFormat } {\r\n const format = detectFormat(rawUrl);\r\n if (!proxy || !proxy.url) return { url: rawUrl, format };\r\n const base = proxy.url.replace(/\\/+$/, '');\r\n const params = new URLSearchParams();\r\n params.set('d', rawUrl);\r\n if (proxy.apiPassword) params.set('api_password', proxy.apiPassword);\r\n for (const [name, value] of Object.entries(proxy.headers ?? {})) {\r\n if (value != null && value !== '') params.set(`h_${name}`, value);\r\n }\r\n return { url: `${base}${ENDPOINTS[format]}?${params.toString()}`, format };\r\n}\r\n","import type { Projection } from '../types.js';\r\n\r\nexport type Split = 'mono' | 'sbs' | 'tb';\r\nexport type GeomKind = 'sphere180' | 'sphere360' | 'plane';\r\n\r\nexport interface ModeConfig {\r\n geom: GeomKind;\r\n split: Split;\r\n stereo: boolean;\r\n flat?: boolean;\r\n /** plane only: 'full' = displayW/H, 'per-eye' = (W/2)/H */\r\n aspect?: 'full' | 'per-eye';\r\n}\r\n\r\nexport const MODES: Record<Projection, ModeConfig> = {\r\n '180-sbs': { geom: 'sphere180', split: 'sbs', stereo: true },\r\n '180-mono': { geom: 'sphere180', split: 'mono', stereo: false },\r\n '360-mono': { geom: 'sphere360', split: 'mono', stereo: false },\r\n '360-sbs': { geom: 'sphere360', split: 'sbs', stereo: true },\r\n '360-tb': { geom: 'sphere360', split: 'tb', stereo: true },\r\n 'flat-2d': { geom: 'plane', split: 'mono', stereo: false, flat: true, aspect: 'full' },\r\n 'flat-sbs-full': { geom: 'plane', split: 'sbs', stereo: true, flat: true, aspect: 'per-eye' },\r\n 'flat-sbs-half': { geom: 'plane', split: 'sbs', stereo: true, flat: true, aspect: 'full' },\r\n};\r\n\r\nexport const PROJECTIONS: { value: Projection; label: string }[] = [\r\n { value: '180-sbs', label: '180° SBS (VR180)' },\r\n { value: '180-mono', label: '180° Mono' },\r\n { value: '360-mono', label: '360° Mono' },\r\n { value: '360-sbs', label: '360° SBS' },\r\n { value: '360-tb', label: '360° Top-Bottom' },\r\n { value: 'flat-2d', label: 'Flat 2D' },\r\n { value: 'flat-sbs-full', label: 'Flat 3D — Full SBS' },\r\n { value: 'flat-sbs-half', label: 'Flat 3D — Half SBS' },\r\n];\r\n\r\nexport function isFlatMode(p: Projection): boolean {\r\n return !!MODES[p]?.flat;\r\n}\r\n\r\n/** Guess a projection from a URL / filename; null when nothing recognizable. */\r\nexport function detectProjection(url: string): Projection | null {\r\n const s = String(url).toLowerCase();\r\n const tb = /(^|[^a-z])(tb|ou|top.?bottom|over.?under)([^a-z]|$)/.test(s);\r\n const sbs = /(sbs|side.?by.?side)/.test(s);\r\n if (s.includes('360')) {\r\n if (tb) return '360-tb';\r\n if (sbs) return '360-sbs';\r\n return '360-mono';\r\n }\r\n if (s.includes('180') || s.includes('vr180')) {\r\n return s.includes('mono') ? '180-mono' : '180-sbs';\r\n }\r\n if (sbs) return s.includes('half') ? 'flat-sbs-half' : 'flat-sbs-full';\r\n return null;\r\n}\r\n","import * as THREE from 'three';\r\nimport type { Projection } from '../types.js';\r\nimport { MODES } from './projections.js';\r\n\r\n/**\r\n * Maps a video onto the geometry for a chosen projection (inside-out 180/360\r\n * sphere or a flat screen plane). Stereo is done the way the reference VR180\r\n * players do it: a single mesh, with the shared video texture shifted per eye\r\n * in onBeforeRender (no per-eye meshes or camera layers). Sizes to its canvas's\r\n * container (not the window), for embedding.\r\n */\r\nexport class StereoScene {\r\n readonly renderer: THREE.WebGLRenderer;\r\n readonly scene = new THREE.Scene();\r\n readonly camera: THREE.PerspectiveCamera;\r\n readonly maxAnisotropy: number;\r\n\r\n private readonly canvas: HTMLCanvasElement;\r\n private readonly video: HTMLVideoElement;\r\n private readonly texture: THREE.VideoTexture;\r\n private readonly meshes: THREE.Mesh[] = [];\r\n private readonly frameCbs: (() => void)[] = [];\r\n private readonly animate = () => {\r\n for (const cb of this.frameCbs) cb();\r\n // Upload the current video frame synchronously, before the render, every frame. This\r\n // is load-bearing on the Quest: without it, VideoTexture's own async (video-frame-\r\n // callback) upload can land mid-render during an XR eye pass and corrupt the triangles\r\n // being drawn — the \"hard triangle edges popping to black\". Reference VR180 players do\r\n // the same. (Yes, it re-uploads at headset rate; that's the cost of a stable image.)\r\n if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) this.texture.needsUpdate = true;\r\n this.renderer.render(this.scene, this.camera);\r\n };\r\n private readonly ro: ResizeObserver;\r\n private currentMode: Projection;\r\n private currentSwap: boolean;\r\n // local-floor as OPTIONAL (widest device support — don't hard-fail headsets without it).\r\n // Deliberately NOT requesting 'layers': it flips three.js to an XRProjectionLayer depth\r\n // path that flickers on the Quest.\r\n private readonly vrSessionInit: XRSessionInit = { optionalFeatures: ['local-floor'] };\r\n\r\n constructor(opts: {\r\n canvas: HTMLCanvasElement; video: HTMLVideoElement;\r\n projection?: Projection; swapEyes?: boolean; fov?: number; supersampling?: number;\r\n }) {\r\n const { canvas, video, projection = '180-sbs', swapEyes = false, fov = 70, supersampling = 1.5 } = opts;\r\n this.canvas = canvas;\r\n this.video = video;\r\n this.currentMode = MODES[projection] ? projection : '180-sbs';\r\n this.currentSwap = swapEyes;\r\n\r\n this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });\r\n this.renderer.setPixelRatio(this.pixelRatioFor(supersampling));\r\n this.renderer.setSize(this.w(), this.h(), false);\r\n this.renderer.xr.enabled = true;\r\n\r\n this.camera = new THREE.PerspectiveCamera(fov, this.w() / this.h(), 0.1, 1000);\r\n this.camera.position.set(0, 0, 0);\r\n\r\n this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy();\r\n this.texture = new THREE.VideoTexture(video);\r\n this.texture.colorSpace = THREE.SRGBColorSpace;\r\n this.texture.minFilter = THREE.LinearFilter;\r\n this.texture.magFilter = THREE.LinearFilter;\r\n // Keep anisotropy at the default (1). Cranking it to max makes the equirect video\r\n // sphere shimmer/flicker at grazing angles (the upper part of the view) once the head\r\n // moves in VR — the reference VR180 players leave it at the default. maxAnisotropy is\r\n // still exposed on the instance for consumers who want to opt back in.\r\n this.texture.anisotropy = 1;\r\n\r\n this.applyProjection(this.currentMode, this.currentSwap);\r\n video.addEventListener('loadedmetadata', () => {\r\n if (MODES[this.currentMode].flat) this.applyProjection(this.currentMode, this.currentSwap);\r\n });\r\n\r\n this.renderer.setAnimationLoop(this.animate);\r\n\r\n this.ro = new ResizeObserver(() => this.resize());\r\n this.ro.observe(canvas);\r\n }\r\n\r\n private w() { return this.canvas.clientWidth || 1; }\r\n private h() { return this.canvas.clientHeight || 1; }\r\n private pixelRatioFor(ss: number) { return Math.min(window.devicePixelRatio * ss, 4); }\r\n\r\n private planeAspect(mode: Projection) {\r\n const vw = this.video.videoWidth, vh = this.video.videoHeight;\r\n if (!vw || !vh) return 16 / 9;\r\n return MODES[mode].aspect === 'per-eye' ? (vw / 2) / vh : vw / vh;\r\n }\r\n\r\n private buildGeometry(mode: Projection): THREE.BufferGeometry {\r\n // Match the reference VR180 geometry: 64×32 sphere, front gore at phiStart -π/2.\r\n const kind = MODES[mode].geom;\r\n if (kind === 'sphere180') { const g = new THREE.SphereGeometry(500, 64, 32, -Math.PI / 2, Math.PI, 0, Math.PI); g.scale(-1, 1, 1); return g; }\r\n if (kind === 'sphere360') { const g = new THREE.SphereGeometry(500, 64, 32); g.scale(-1, 1, 1); return g; }\r\n const h = 2.4, w = h * this.planeAspect(mode);\r\n const g = new THREE.PlaneGeometry(w, h); g.translate(0, 0, -2); return g;\r\n }\r\n\r\n /** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's\r\n * onBeforeRender, so it's called once per eye per frame (the reference approach). */\r\n private updateStereoUV(cam: THREE.Camera) {\r\n const map = this.texture;\r\n const split = MODES[this.currentMode].split;\r\n // Robust eye detection (the reference player's cascade): direct camera-ref match, then\r\n // view-matrix X, then projection asymmetry — so a frame never gets the wrong half.\r\n let isLeft = true;\r\n if (this.renderer.xr.isPresenting) {\r\n const c = cam as THREE.PerspectiveCamera;\r\n const cams = (this.renderer.xr.getCamera() as unknown as { cameras?: THREE.PerspectiveCamera[] }).cameras;\r\n if (cams && cams.length >= 2) {\r\n if (c === cams[0]) isLeft = true;\r\n else if (c === cams[1]) isLeft = false;\r\n else isLeft = Math.abs(c.matrixWorldInverse.elements[12] - cams[0].matrixWorldInverse.elements[12])\r\n <= Math.abs(c.matrixWorldInverse.elements[12] - cams[1].matrixWorldInverse.elements[12]);\r\n } else {\r\n isLeft = c.projectionMatrix.elements[8] <= 0;\r\n }\r\n }\r\n const second = !isLeft !== this.currentSwap; // this eye shows the 2nd half (right / bottom)\r\n if (split === 'sbs') { map.repeat.set(0.5, 1); map.offset.set(second ? 0.5 : 0, 0); }\r\n else if (split === 'tb') { map.repeat.set(1, 0.5); map.offset.set(0, second ? 0 : 0.5); }\r\n else { map.repeat.set(1, 1); map.offset.set(0, 0); }\r\n }\r\n\r\n private clearMeshes() {\r\n for (const m of this.meshes) { this.scene.remove(m); m.geometry.dispose(); (m.material as THREE.Material).dispose(); }\r\n this.meshes.length = 0;\r\n }\r\n\r\n private applyProjection(mode: Projection, swap: boolean) {\r\n if (!MODES[mode]) mode = '180-sbs';\r\n this.clearMeshes();\r\n this.currentMode = mode; this.currentSwap = swap;\r\n // One mesh (reference approach), plain FrontSide MeshBasicMaterial. Stereo is handled\r\n // per eye by updateStereoUV via onBeforeRender — not by per-eye meshes or camera layers.\r\n const mesh = new THREE.Mesh(this.buildGeometry(mode), new THREE.MeshBasicMaterial({ map: this.texture }));\r\n mesh.layers.set(0); // desktop + both XR eyes see the single mesh\r\n if (MODES[mode].geom === 'sphere180') mesh.rotation.y = Math.PI / 2; // orient the 180 gore forward\r\n mesh.onBeforeRender = (_r, _s, cam) => this.updateStereoUV(cam);\r\n this.scene.add(mesh); this.meshes.push(mesh);\r\n }\r\n\r\n setProjection(p: Projection) { this.applyProjection(p, this.currentSwap); }\r\n setSwapEyes(v: boolean) { this.applyProjection(this.currentMode, v); }\r\n setFov(deg: number) { this.camera.fov = deg; this.camera.updateProjectionMatrix(); }\r\n setSupersampling(ss: number) { this.renderer.setPixelRatio(this.pixelRatioFor(ss)); this.renderer.setSize(this.w(), this.h(), false); }\r\n getProjection() { return this.currentMode; }\r\n isFlat() { return !!MODES[this.currentMode].flat; }\r\n onFrame(cb: () => void) { this.frameCbs.push(cb); }\r\n pauseRendering() { this.renderer.setAnimationLoop(null); }\r\n resumeRendering() { this.renderer.setAnimationLoop(this.animate); }\r\n\r\n /** Enter immersive VR. Rejects with a readable message on failure so the caller can\r\n * surface it — instead of the silent no-op three.js's VRButton produces. Called from\r\n * a click handler so the request keeps the user's transient activation. */\r\n async enterVR(): Promise<void> {\r\n if (!navigator.xr) throw new Error('WebXR is unavailable here — open the page over HTTPS or localhost.');\r\n const session = await navigator.xr.requestSession('immersive-vr', this.vrSessionInit);\r\n await this.renderer.xr.setSession(session);\r\n }\r\n exitVR(): void { void this.renderer.xr.getSession()?.end(); }\r\n /** Arm the browser/headset's own \"Enter VR\" affordance (e.g. the Quest system button). */\r\n offerVR(): void {\r\n const offer = (navigator.xr as { offerSession?: (m: XRSessionMode, i: XRSessionInit) => Promise<XRSession> } | undefined)?.offerSession;\r\n if (!offer) return;\r\n const run = () => offer.call(navigator.xr, 'immersive-vr', this.vrSessionInit)\r\n .then((s) => this.renderer.xr.setSession(s))\r\n .catch(() => { /* the UA declined the offer or the user dismissed it */ });\r\n this.renderer.xr.addEventListener('sessionend', () => void run());\r\n void run();\r\n }\r\n\r\n resize = () => {\r\n if (this.renderer.xr.isPresenting) return; // never resize while the headset drives the size\r\n const w = this.w(), h = this.h();\r\n this.camera.aspect = w / h;\r\n this.camera.updateProjectionMatrix();\r\n this.renderer.setSize(w, h, false);\r\n };\r\n\r\n dispose() {\r\n this.ro.disconnect();\r\n this.renderer.setAnimationLoop(null);\r\n this.texture.dispose();\r\n this.clearMeshes();\r\n this.renderer.dispose();\r\n }\r\n}\r\n","import type { Camera } from 'three';\r\n\r\nconst DEG = Math.PI / 180;\r\n\r\n/** Clamp look angles (degrees) to keep the view inside the front hemisphere. */\r\nexport function clampAngles(lon: number, lat: number): { lon: number; lat: number } {\r\n return {\r\n lon: Math.max(-90, Math.min(90, lon)),\r\n lat: Math.max(-85, Math.min(85, lat)),\r\n };\r\n}\r\n\r\n/**\r\n * Pointer/touch drag → camera yaw/pitch, clamped to the front hemisphere.\r\n * Disabled while an XR session drives the head pose, and for flat-screen modes.\r\n */\r\nexport class LookControls {\r\n private lon = 0;\r\n private lat = 0;\r\n private dragging = false;\r\n private px = 0;\r\n private py = 0;\r\n private enabled = true;\r\n private readonly isPresenting: () => boolean;\r\n\r\n constructor(\r\n private readonly camera: Camera,\r\n private readonly dom: HTMLElement,\r\n opts: { isPresenting?: () => boolean } = {},\r\n ) {\r\n this.isPresenting = opts.isPresenting ?? (() => false);\r\n this.dom.addEventListener('pointerdown', this.onDown);\r\n this.dom.addEventListener('pointermove', this.onMove);\r\n this.dom.addEventListener('pointerup', this.onUp);\r\n this.dom.addEventListener('pointercancel', this.onUp);\r\n }\r\n\r\n private capture(fn: 'setPointerCapture' | 'releasePointerCapture', id: number) {\r\n try { this.dom[fn]?.(id); } catch { /* pointer not active */ }\r\n }\r\n\r\n private onDown = (e: PointerEvent) => {\r\n if (!this.enabled) return;\r\n this.dragging = true;\r\n this.px = e.clientX;\r\n this.py = e.clientY;\r\n this.capture('setPointerCapture', e.pointerId);\r\n };\r\n\r\n private onMove = (e: PointerEvent) => {\r\n if (!this.dragging) return;\r\n const dx = e.clientX - this.px;\r\n const dy = e.clientY - this.py;\r\n this.px = e.clientX;\r\n this.py = e.clientY;\r\n ({ lon: this.lon, lat: this.lat } = clampAngles(this.lon - dx * 0.15, this.lat + dy * 0.15));\r\n };\r\n\r\n private onUp = (e: PointerEvent) => {\r\n this.dragging = false;\r\n this.capture('releasePointerCapture', e.pointerId);\r\n };\r\n\r\n update() {\r\n if (this.isPresenting() || !this.enabled) return;\r\n const phi = (90 - this.lat) * DEG;\r\n const theta = this.lon * DEG;\r\n this.camera.lookAt(\r\n Math.sin(phi) * Math.sin(theta),\r\n Math.cos(phi),\r\n -Math.sin(phi) * Math.cos(theta),\r\n );\r\n }\r\n\r\n reset() { this.lon = 0; this.lat = 0; }\r\n\r\n setEnabled(v: boolean) {\r\n this.enabled = v;\r\n if (!v) { this.lon = 0; this.lat = 0; this.camera.lookAt(0, 0, -1); }\r\n }\r\n\r\n getAngles() { return { lon: this.lon, lat: this.lat }; }\r\n\r\n dispose() {\r\n this.dom.removeEventListener('pointerdown', this.onDown);\r\n this.dom.removeEventListener('pointermove', this.onMove);\r\n this.dom.removeEventListener('pointerup', this.onUp);\r\n this.dom.removeEventListener('pointercancel', this.onUp);\r\n }\r\n}\r\n","import type Hls from 'hls.js';\r\nimport type { StreamFormat } from './proxy.js';\r\n\r\n/**\r\n * Attaches a source to a `<video>`. Progressive files go straight on `video.src`;\r\n * HLS/DASH-as-HLS go through hls.js — which is imported dynamically, so consumers\r\n * who never play HLS don't pay for it (and it's an optional dependency).\r\n */\r\nexport class VideoSource {\r\n private hls: Hls | null = null;\r\n\r\n async attach(\r\n video: HTMLVideoElement,\r\n source: { url: string; format: StreamFormat },\r\n opts: { crossOrigin?: string | null } = {},\r\n ): Promise<void> {\r\n this.dispose();\r\n const co = opts.crossOrigin === undefined ? 'anonymous' : opts.crossOrigin;\r\n if (co === null) video.removeAttribute('crossorigin');\r\n else video.crossOrigin = co;\r\n video.playsInline = true;\r\n\r\n const { url, format } = source;\r\n const isHlsLike = format === 'hls' || format === 'dash';\r\n const nativeHls = video.canPlayType('application/vnd.apple.mpegurl') !== '';\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n const cleanup = () => {\r\n video.removeEventListener('loadedmetadata', onLoaded);\r\n video.removeEventListener('error', onError);\r\n };\r\n const onLoaded = () => { cleanup(); resolve(); };\r\n const onError = () => {\r\n cleanup();\r\n const code = video.error ? video.error.code : 'unknown';\r\n reject(new Error(`Video failed to load (media error ${code}). Is the source reachable and CORS-clean?`));\r\n };\r\n video.addEventListener('loadedmetadata', onLoaded);\r\n video.addEventListener('error', onError);\r\n\r\n if (isHlsLike && !nativeHls) {\r\n import('hls.js').then(({ default: HlsCtor }) => {\r\n if (!HlsCtor.isSupported()) {\r\n cleanup();\r\n reject(new Error('HLS/DASH stream needs hls.js, which is unsupported in this browser.'));\r\n return;\r\n }\r\n this.hls = new HlsCtor({ enableWorker: true });\r\n this.hls.on(HlsCtor.Events.ERROR, (_e, data) => {\r\n if (data.fatal) { cleanup(); this.dispose(); reject(new Error(`HLS fatal error: ${data.type} / ${data.details}`)); }\r\n });\r\n this.hls.loadSource(url);\r\n this.hls.attachMedia(video);\r\n }).catch(() => {\r\n cleanup();\r\n reject(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install \"hls.js\".'));\r\n });\r\n } else {\r\n video.src = url;\r\n video.load();\r\n }\r\n });\r\n }\r\n\r\n dispose() {\r\n if (this.hls) { this.hls.destroy(); this.hls = null; }\r\n }\r\n}\r\n"],"names":["detectFormat","rawUrl","pathname","lower","ENDPOINTS","buildProxyUrl","proxy","format","base","params","name","value","MODES","PROJECTIONS","isFlatMode","p","_a","detectProjection","url","s","tb","sbs","StereoScene","opts","THREE","cb","w","h","canvas","video","projection","swapEyes","fov","supersampling","ss","mode","vw","vh","kind","g","cam","map","split","isLeft","c","cams","second","m","swap","mesh","_r","_s","v","deg","session","offer","run","DEG","clampAngles","lon","lat","LookControls","camera","dom","e","dx","dy","fn","id","_b","phi","theta","VideoSource","source","co","isHlsLike","nativeHls","resolve","reject","cleanup","onLoaded","onError","code","HlsCtor","_e","data"],"mappings":"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"}