three-vr-player 0.1.0 → 0.3.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/README.md CHANGED
@@ -78,6 +78,7 @@ new Player(container, {
78
78
  persistSettings?: boolean; // default false (localStorage)
79
79
  shadowDom?: boolean; // default true (style isolation)
80
80
  vrButton?: boolean; // default true (shown only when a headset is present)
81
+ nativeFallback?: boolean; // default true — plain 2D <video> fallback on CORS-taint
81
82
  });
82
83
  ```
83
84
 
@@ -86,11 +87,12 @@ new Player(container, {
86
87
  ```ts
87
88
  player.load(src, { projection? }); player.play(); player.pause();
88
89
  player.setProjection(p); player.setSwapEyes(b); player.setFov(deg); player.setSupersampling(x);
90
+ player.setProxy({ url, apiPassword?, enabled }); // also exposed as a toggle in the ⚙ settings
89
91
  player.enterVR(); player.dispose();
90
92
  player.video; // the underlying <video>
91
93
  player.three; // { renderer, scene, camera }
92
94
 
93
- player.on('ready'|'play'|'pause'|'ended'|'error'|'timeupdate'|'projectionchange'|'enterxr'|'exitxr', cb);
95
+ player.on('ready'|'play'|'pause'|'ended'|'error'|'timeupdate'|'projectionchange'|'enterxr'|'exitxr'|'fallback', cb);
94
96
  ```
95
97
 
96
98
  ## Headless core
@@ -107,6 +109,10 @@ To use a cross-origin video as a WebGL texture it must be CORS-clean. If your ho
107
109
  doesn't send `Access-Control-Allow-Origin`, run the optional [proxy companion](./proxy)
108
110
  and pass `proxy: { url, apiPassword }`. CORS-clean sources need no proxy.
109
111
 
112
+ If a source is neither CORS-clean nor proxied, the player automatically falls back to
113
+ plain **2D `<video>` playback** (no reprojection) and emits a `fallback` event, instead of
114
+ failing on a black screen. Disable with `nativeFallback: false`.
115
+
110
116
  ## Codecs
111
117
 
112
118
  The browser must be able to decode the file: MP4/WebM with H.264/HEVC/VP9 + AAC/Opus is
package/dist/Player.d.ts CHANGED
@@ -16,17 +16,30 @@ export declare class Player {
16
16
  private readonly listeners;
17
17
  private view;
18
18
  private readyEmitted;
19
+ private native;
20
+ private loading;
21
+ private proxyConfig?;
22
+ private useProxy;
23
+ private currentSrc?;
19
24
  constructor(container: HTMLElement, options?: PlayerOptions);
20
25
  private vrSupported;
21
26
  load(src: string, o?: {
22
27
  projection?: Projection;
23
28
  }): Promise<void>;
29
+ /** Swap between WebGL (3D) rendering and plain 2D `<video>` playback. */
30
+ private setNativeFallback;
24
31
  play(): Promise<void>;
25
32
  pause(): void;
26
33
  setProjection(p: Projection): void;
27
34
  setSwapEyes(v: boolean): void;
28
35
  setFov(deg: number): void;
29
36
  setSupersampling(x: number): void;
37
+ /** Update the CORS proxy config / toggle it, and reload the current source if any. */
38
+ setProxy(p: {
39
+ url: string;
40
+ apiPassword?: string;
41
+ enabled: boolean;
42
+ }): void;
30
43
  enterVR(): Promise<void>;
31
44
  get three(): {
32
45
  renderer: import('three').WebGLRenderer;
@@ -1,6 +1,6 @@
1
1
  import * as o from "three";
2
- import { VRButton as g } from "three/examples/jsm/webxr/VRButton.js";
3
- function v(a) {
2
+ import { VRButton as w } from "three/examples/jsm/webxr/VRButton.js";
3
+ function M(a) {
4
4
  let e = a;
5
5
  try {
6
6
  e = new URL(a).pathname;
@@ -9,21 +9,21 @@ function v(a) {
9
9
  const s = e.toLowerCase();
10
10
  return s.endsWith(".m3u8") ? "hls" : s.endsWith(".mpd") ? "dash" : "progressive";
11
11
  }
12
- const w = {
12
+ const S = {
13
13
  progressive: "/proxy/stream",
14
14
  hls: "/proxy/hls/manifest.m3u8",
15
15
  dash: "/proxy/mpd/manifest.m3u8"
16
16
  };
17
- function P(a, e) {
18
- const s = v(a);
17
+ function x(a, e) {
18
+ const s = M(a);
19
19
  if (!e || !e.url) return { url: a, format: s };
20
20
  const r = e.url.replace(/\/+$/, ""), t = new URLSearchParams();
21
21
  t.set("d", a), e.apiPassword && t.set("api_password", e.apiPassword);
22
22
  for (const [i, n] of Object.entries(e.headers ?? {}))
23
23
  n != null && n !== "" && t.set(`h_${i}`, n);
24
- return { url: `${r}${w[s]}?${t.toString()}`, format: s };
24
+ return { url: `${r}${S[s]}?${t.toString()}`, format: s };
25
25
  }
26
- const c = {
26
+ const h = {
27
27
  "180-sbs": { geom: "sphere180", split: "sbs", stereo: !0 },
28
28
  "180-mono": { geom: "sphere180", split: "mono", stereo: !1 },
29
29
  "360-mono": { geom: "sphere360", split: "mono", stereo: !1 },
@@ -32,7 +32,7 @@ const c = {
32
32
  "flat-2d": { geom: "plane", split: "mono", stereo: !1, flat: !0, aspect: "full" },
33
33
  "flat-sbs-full": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "per-eye" },
34
34
  "flat-sbs-half": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "full" }
35
- }, L = [
35
+ }, E = [
36
36
  { value: "180-sbs", label: "180° SBS (VR180)" },
37
37
  { value: "180-mono", label: "180° Mono" },
38
38
  { value: "360-mono", label: "360° Mono" },
@@ -42,27 +42,27 @@ const c = {
42
42
  { value: "flat-sbs-full", label: "Flat 3D — Full SBS" },
43
43
  { value: "flat-sbs-half", label: "Flat 3D — Half SBS" }
44
44
  ];
45
- function x(a) {
45
+ function R(a) {
46
46
  var e;
47
- return !!((e = c[a]) != null && e.flat);
47
+ return !!((e = h[a]) != null && e.flat);
48
48
  }
49
- function E(a) {
49
+ function A(a) {
50
50
  const e = String(a).toLowerCase(), s = /(^|[^a-z])(tb|ou|top.?bottom|over.?under)([^a-z]|$)/.test(e), r = /(sbs|side.?by.?side)/.test(e);
51
51
  return e.includes("360") ? s ? "360-tb" : r ? "360-sbs" : "360-mono" : e.includes("180") || e.includes("vr180") ? e.includes("mono") ? "180-mono" : "180-sbs" : r ? e.includes("half") ? "flat-sbs-half" : "flat-sbs-full" : null;
52
52
  }
53
- class R {
53
+ class F {
54
54
  constructor(e) {
55
- this.scene = new o.Scene(), this.meshes = [], this.frameCbs = [], this.resize = () => {
56
- const h = this.w(), l = this.h();
57
- this.camera.aspect = h / l, this.camera.updateProjectionMatrix(), this.renderer.setSize(h, l, !1);
58
- };
59
- const { canvas: s, video: r, projection: t = "180-sbs", swapEyes: i = !1, fov: n = 70, supersampling: u = 1.5 } = e;
60
- this.canvas = s, this.video = r, this.currentMode = c[t] ? t : "180-sbs", this.currentSwap = i, this.renderer = new o.WebGLRenderer({ canvas: s, antialias: !0 }), this.renderer.setPixelRatio(this.pixelRatioFor(u)), this.renderer.setSize(this.w(), this.h(), !1), this.renderer.xr.enabled = !0, this.camera = new o.PerspectiveCamera(n, this.w() / this.h(), 0.1, 1e3), this.camera.position.set(0, 0, 0), this.camera.layers.enable(1), this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy(), this.texture = new o.VideoTexture(r), this.texture.colorSpace = o.SRGBColorSpace, this.texture.minFilter = o.LinearFilter, this.texture.magFilter = o.LinearFilter, this.texture.anisotropy = this.maxAnisotropy, this.applyProjection(this.currentMode, this.currentSwap), r.addEventListener("loadedmetadata", () => {
61
- c[this.currentMode].flat && this.applyProjection(this.currentMode, this.currentSwap);
62
- }), this.renderer.setAnimationLoop(() => {
63
- for (const h of this.frameCbs) h();
55
+ this.scene = new o.Scene(), this.meshes = [], this.frameCbs = [], this.animate = () => {
56
+ for (const c of this.frameCbs) c();
64
57
  this.renderer.render(this.scene, this.camera);
65
- }), this.vrButton = g.createButton(this.renderer), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(s);
58
+ }, this.resize = () => {
59
+ const c = this.w(), d = this.h();
60
+ this.camera.aspect = c / d, this.camera.updateProjectionMatrix(), this.renderer.setSize(c, d, !1);
61
+ };
62
+ const { canvas: s, video: r, projection: t = "180-sbs", swapEyes: i = !1, fov: n = 70, supersampling: l = 1.5 } = e;
63
+ this.canvas = s, this.video = r, this.currentMode = h[t] ? t : "180-sbs", this.currentSwap = i, this.renderer = new o.WebGLRenderer({ canvas: s, antialias: !0 }), this.renderer.setPixelRatio(this.pixelRatioFor(l)), this.renderer.setSize(this.w(), this.h(), !1), this.renderer.xr.enabled = !0, this.camera = new o.PerspectiveCamera(n, this.w() / this.h(), 0.1, 1e3), this.camera.position.set(0, 0, 0), this.camera.layers.enable(1), this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy(), this.texture = new o.VideoTexture(r), this.texture.colorSpace = o.SRGBColorSpace, this.texture.minFilter = o.LinearFilter, this.texture.magFilter = o.LinearFilter, this.texture.anisotropy = this.maxAnisotropy, this.applyProjection(this.currentMode, this.currentSwap), r.addEventListener("loadedmetadata", () => {
64
+ h[this.currentMode].flat && this.applyProjection(this.currentMode, this.currentSwap);
65
+ }), this.renderer.setAnimationLoop(this.animate), this.vrButton = w.createButton(this.renderer), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(s);
66
66
  }
67
67
  w() {
68
68
  return this.canvas.clientWidth || 1;
@@ -75,10 +75,10 @@ class R {
75
75
  }
76
76
  planeAspect(e) {
77
77
  const s = this.video.videoWidth, r = this.video.videoHeight;
78
- return !s || !r ? 16 / 9 : c[e].aspect === "per-eye" ? s / 2 / r : s / r;
78
+ return !s || !r ? 16 / 9 : h[e].aspect === "per-eye" ? s / 2 / r : s / r;
79
79
  }
80
80
  buildGeometry(e) {
81
- const s = c[e].geom;
81
+ const s = h[e].geom;
82
82
  if (s === "sphere180") {
83
83
  const n = new o.SphereGeometry(500, 60, 40, -Math.PI, Math.PI, 0, Math.PI);
84
84
  return n.scale(-1, 1, 1), n;
@@ -103,15 +103,15 @@ class R {
103
103
  this.meshes.length = 0;
104
104
  }
105
105
  applyProjection(e, s) {
106
- c[e] || (e = "180-sbs"), this.clearMeshes();
107
- const r = c[e];
106
+ h[e] || (e = "180-sbs"), this.clearMeshes();
107
+ const r = h[e];
108
108
  if (r.stereo) {
109
109
  const t = this.buildGeometry(e);
110
110
  this.splitUV(t, r.split, "left");
111
111
  const i = this.buildGeometry(e);
112
112
  this.splitUV(i, r.split, "right");
113
- const n = new o.Mesh(t, new o.MeshBasicMaterial({ map: this.texture })), u = new o.Mesh(i, new o.MeshBasicMaterial({ map: this.texture }));
114
- n.layers.set(s ? 2 : 1), u.layers.set(s ? 1 : 2), this.scene.add(n, u), this.meshes.push(n, u);
113
+ const n = new o.Mesh(t, new o.MeshBasicMaterial({ map: this.texture })), l = new o.Mesh(i, new o.MeshBasicMaterial({ map: this.texture }));
114
+ n.layers.set(s ? 2 : 1), l.layers.set(s ? 1 : 2), this.scene.add(n, l), this.meshes.push(n, l);
115
115
  } else {
116
116
  const t = new o.Mesh(this.buildGeometry(e), new o.MeshBasicMaterial({ map: this.texture }));
117
117
  t.layers.set(0), this.scene.add(t), this.meshes.push(t);
@@ -134,30 +134,36 @@ class R {
134
134
  return this.currentMode;
135
135
  }
136
136
  isFlat() {
137
- return !!c[this.currentMode].flat;
137
+ return !!h[this.currentMode].flat;
138
138
  }
139
139
  onFrame(e) {
140
140
  this.frameCbs.push(e);
141
141
  }
142
+ pauseRendering() {
143
+ this.renderer.setAnimationLoop(null);
144
+ }
145
+ resumeRendering() {
146
+ this.renderer.setAnimationLoop(this.animate);
147
+ }
142
148
  dispose() {
143
149
  this.ro.disconnect(), this.renderer.setAnimationLoop(null), this.texture.dispose(), this.clearMeshes(), this.renderer.dispose();
144
150
  }
145
151
  }
146
- const b = Math.PI / 180;
147
- function M(a, e) {
152
+ const v = Math.PI / 180;
153
+ function y(a, e) {
148
154
  return {
149
155
  lon: Math.max(-90, Math.min(90, a)),
150
156
  lat: Math.max(-85, Math.min(85, e))
151
157
  };
152
158
  }
153
- class F {
159
+ class j {
154
160
  constructor(e, s, r = {}) {
155
161
  this.camera = e, this.dom = s, this.lon = 0, this.lat = 0, this.dragging = !1, this.px = 0, this.py = 0, this.enabled = !0, this.onDown = (t) => {
156
162
  this.enabled && (this.dragging = !0, this.px = t.clientX, this.py = t.clientY, this.capture("setPointerCapture", t.pointerId));
157
163
  }, this.onMove = (t) => {
158
164
  if (!this.dragging) return;
159
165
  const i = t.clientX - this.px, n = t.clientY - this.py;
160
- this.px = t.clientX, this.py = t.clientY, { lon: this.lon, lat: this.lat } = M(this.lon - i * 0.15, this.lat + n * 0.15);
166
+ this.px = t.clientX, this.py = t.clientY, { lon: this.lon, lat: this.lat } = y(this.lon - i * 0.15, this.lat + n * 0.15);
161
167
  }, this.onUp = (t) => {
162
168
  this.dragging = !1, this.capture("releasePointerCapture", t.pointerId);
163
169
  }, this.isPresenting = r.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);
@@ -171,7 +177,7 @@ class F {
171
177
  }
172
178
  update() {
173
179
  if (this.isPresenting() || !this.enabled) return;
174
- const e = (90 - this.lat) * b, s = this.lon * b;
180
+ const e = (90 - this.lat) * v, s = this.lon * v;
175
181
  this.camera.lookAt(
176
182
  Math.sin(e) * Math.sin(s),
177
183
  Math.cos(e),
@@ -191,34 +197,36 @@ class F {
191
197
  this.dom.removeEventListener("pointerdown", this.onDown), this.dom.removeEventListener("pointermove", this.onMove), this.dom.removeEventListener("pointerup", this.onUp), this.dom.removeEventListener("pointercancel", this.onUp);
192
198
  }
193
199
  }
194
- class A {
200
+ class B {
195
201
  constructor() {
196
202
  this.hls = null;
197
203
  }
198
- async attach(e, s) {
199
- this.dispose(), e.crossOrigin = e.crossOrigin ?? "anonymous", e.playsInline = !0;
200
- const { url: r, format: t } = s, i = t === "hls" || t === "dash", n = e.canPlayType("application/vnd.apple.mpegurl") !== "";
201
- return new Promise((u, h) => {
202
- const l = () => {
203
- e.removeEventListener("loadedmetadata", m), e.removeEventListener("error", f);
204
- }, m = () => {
205
- l(), u();
206
- }, f = () => {
207
- l();
204
+ async attach(e, s, r = {}) {
205
+ this.dispose();
206
+ const t = r.crossOrigin === void 0 ? "anonymous" : r.crossOrigin;
207
+ t === null ? e.removeAttribute("crossorigin") : e.crossOrigin = t, e.playsInline = !0;
208
+ const { url: i, format: n } = s, l = n === "hls" || n === "dash", c = e.canPlayType("application/vnd.apple.mpegurl") !== "";
209
+ return new Promise((d, m) => {
210
+ const u = () => {
211
+ e.removeEventListener("loadedmetadata", b), e.removeEventListener("error", g);
212
+ }, b = () => {
213
+ u(), d();
214
+ }, g = () => {
215
+ u();
208
216
  const p = e.error ? e.error.code : "unknown";
209
- h(new Error(`Video failed to load (media error ${p}). Is the source reachable and CORS-clean?`));
217
+ m(new Error(`Video failed to load (media error ${p}). Is the source reachable and CORS-clean?`));
210
218
  };
211
- e.addEventListener("loadedmetadata", m), e.addEventListener("error", f), i && !n ? import("hls.js").then(({ default: p }) => {
219
+ e.addEventListener("loadedmetadata", b), e.addEventListener("error", g), l && !c ? import("hls.js").then(({ default: p }) => {
212
220
  if (!p.isSupported()) {
213
- l(), h(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
221
+ u(), m(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
214
222
  return;
215
223
  }
216
- this.hls = new p({ enableWorker: !0 }), this.hls.on(p.Events.ERROR, (S, d) => {
217
- d.fatal && (l(), this.dispose(), h(new Error(`HLS fatal error: ${d.type} / ${d.details}`)));
218
- }), this.hls.loadSource(r), this.hls.attachMedia(e);
224
+ this.hls = new p({ enableWorker: !0 }), this.hls.on(p.Events.ERROR, (L, f) => {
225
+ f.fatal && (u(), this.dispose(), m(new Error(`HLS fatal error: ${f.type} / ${f.details}`)));
226
+ }), this.hls.loadSource(i), this.hls.attachMedia(e);
219
227
  }).catch(() => {
220
- l(), h(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install "hls.js".'));
221
- }) : (e.src = r, e.load());
228
+ u(), m(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install "hls.js".'));
229
+ }) : (e.src = i, e.load());
222
230
  });
223
231
  }
224
232
  dispose() {
@@ -226,15 +234,15 @@ class A {
226
234
  }
227
235
  }
228
236
  export {
229
- F as L,
230
- c as M,
231
- L as P,
232
- R as S,
233
- A as V,
234
- E as a,
235
- P as b,
236
- M as c,
237
- v as d,
238
- x as i
237
+ j as L,
238
+ h as M,
239
+ E as P,
240
+ F as S,
241
+ B as V,
242
+ A as a,
243
+ x as b,
244
+ y as c,
245
+ M as d,
246
+ R as i
239
247
  };
240
- //# sourceMappingURL=VideoSource-pc2RM7bl.js.map
248
+ //# sourceMappingURL=VideoSource-CJGIX8gL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoSource-CJGIX8gL.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 (regular movie)' },\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 { VRButton } from 'three/examples/jsm/webxr/VRButton.js';\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) and packs stereo eyes via UV split + WebXR\r\n * layers. Sizes to its canvas's 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 vrButton: HTMLElement;\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 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\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 this.camera.layers.enable(1); // desktop shows Layer 1 (left eye) for stereo modes\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 this.texture.anisotropy = this.maxAnisotropy;\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 this.vrButton = VRButton.createButton(this.renderer);\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 const kind = MODES[mode].geom;\r\n if (kind === 'sphere180') { const g = new THREE.SphereGeometry(500, 60, 40, -Math.PI, Math.PI, 0, Math.PI); g.scale(-1, 1, 1); return g; }\r\n if (kind === 'sphere360') { const g = new THREE.SphereGeometry(500, 60, 40); 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 private splitUV(geo: THREE.BufferGeometry, split: string, eye: 'left' | 'right') {\r\n if (split === 'mono') return;\r\n const uv = geo.attributes.uv.array as Float32Array;\r\n for (let i = 0; i < uv.length; i += 2) {\r\n if (split === 'sbs') uv[i] = uv[i] * 0.5 + (eye === 'right' ? 0.5 : 0);\r\n else if (split === 'tb') uv[i + 1] = uv[i + 1] * 0.5 + (eye === 'left' ? 0.5 : 0); // top = left\r\n }\r\n geo.attributes.uv.needsUpdate = true;\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 const cfg = MODES[mode];\r\n if (!cfg.stereo) {\r\n const m = new THREE.Mesh(this.buildGeometry(mode), new THREE.MeshBasicMaterial({ map: this.texture }));\r\n m.layers.set(0); // mono: desktop + both XR eyes\r\n this.scene.add(m); this.meshes.push(m);\r\n } else {\r\n const gL = this.buildGeometry(mode); this.splitUV(gL, cfg.split, 'left');\r\n const gR = this.buildGeometry(mode); this.splitUV(gR, cfg.split, 'right');\r\n const mL = new THREE.Mesh(gL, new THREE.MeshBasicMaterial({ map: this.texture }));\r\n const mR = new THREE.Mesh(gR, new THREE.MeshBasicMaterial({ map: this.texture }));\r\n mL.layers.set(swap ? 2 : 1); mR.layers.set(swap ? 1 : 2);\r\n this.scene.add(mL, mR); this.meshes.push(mL, mR);\r\n }\r\n this.currentMode = mode; this.currentSwap = swap;\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 resize = () => {\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","VRButton","ss","mode","vw","vh","kind","g","geo","split","eye","uv","m","swap","cfg","gL","gR","mL","mR","v","deg","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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"}
@@ -16,6 +16,7 @@ export declare class StereoScene {
16
16
  private readonly texture;
17
17
  private readonly meshes;
18
18
  private readonly frameCbs;
19
+ private readonly animate;
19
20
  private readonly ro;
20
21
  private currentMode;
21
22
  private currentSwap;
@@ -42,6 +43,8 @@ export declare class StereoScene {
42
43
  getProjection(): Projection;
43
44
  isFlat(): boolean;
44
45
  onFrame(cb: () => void): void;
46
+ pauseRendering(): void;
47
+ resumeRendering(): void;
45
48
  resize: () => void;
46
49
  dispose(): void;
47
50
  }
@@ -10,6 +10,8 @@ export declare class VideoSource {
10
10
  attach(video: HTMLVideoElement, source: {
11
11
  url: string;
12
12
  format: StreamFormat;
13
+ }, opts?: {
14
+ crossOrigin?: string | null;
13
15
  }): Promise<void>;
14
16
  dispose(): void;
15
17
  }
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- import { L as o, M as s, P as t, S as r, V as c, b as d, c as l, d as S, a as i, i as n } from "./VideoSource-pc2RM7bl.js";
1
+ import { L as o, M as s, P as t, S as r, V as c, b as d, c as l, d as S, a as i, i as n } from "./VideoSource-CJGIX8gL.js";
2
2
  export {
3
3
  o as LookControls,
4
4
  s as MODES,