three-vr-player 0.1.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/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/Player.d.ts +42 -0
- package/dist/VideoSource-pc2RM7bl.js +240 -0
- package/dist/VideoSource-pc2RM7bl.js.map +1 -0
- package/dist/core/LookControls.d.ts +37 -0
- package/dist/core/StereoScene.d.ts +47 -0
- package/dist/core/VideoSource.d.ts +15 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/projections.d.ts +20 -0
- package/dist/core/proxy.d.ts +16 -0
- package/dist/core.js +14 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/three-vr-player.js +263 -0
- package/dist/three-vr-player.js.map +1 -0
- package/dist/three-vr-player.standalone.js +3726 -0
- package/dist/three-vr-player.standalone.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/ui/ControlsUI.d.ts +27 -0
- package/dist/ui/format.d.ts +2 -0
- package/dist/webcomponent.d.ts +17 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dartagnan309
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# three-vr-player
|
|
2
|
+
|
|
3
|
+
An embeddable web player for **3D and VR video** — VR **180°/360°** (mono or stereo)
|
|
4
|
+
and flat **side-by-side / top-bottom 3D movies** — rendered **undistorted** on desktop
|
|
5
|
+
(mouse drag to look around) and in **WebXR** (true per-eye stereo). Built on
|
|
6
|
+
[three.js](https://threejs.org).
|
|
7
|
+
|
|
8
|
+
Drop it into any page as a `<three-video>` element, or drive it from JavaScript with
|
|
9
|
+
the `Player` class. Optional [mediaflow-proxy](./proxy) companion handles CORS for
|
|
10
|
+
sources that don't send it.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install three-vr-player three
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`three` is a **peer dependency** (bring your own). `hls.js` is optional — install it
|
|
19
|
+
only if you need HLS: `npm install hls.js`.
|
|
20
|
+
|
|
21
|
+
## Quick start (JS API)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Player } from 'three-vr-player';
|
|
25
|
+
|
|
26
|
+
const player = new Player(document.getElementById('app')!, {
|
|
27
|
+
src: 'https://…/clip.mp4', // 180 SBS by default; auto-detected from the filename
|
|
28
|
+
controls: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
player.on('ready', () => console.log('loaded'));
|
|
32
|
+
player.setProjection('360-tb');
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Give the container a size (the player fills it): `#app { width: 100%; height: 480px }`.
|
|
36
|
+
|
|
37
|
+
## Web component (no build step)
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<script type="module" src="https://unpkg.com/three-vr-player/dist/three-vr-player.standalone.js"></script>
|
|
41
|
+
|
|
42
|
+
<three-video src="https://…/clip_360_sbs.mp4" projection="360-sbs" controls
|
|
43
|
+
style="width:100%;height:480px"></three-video>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `standalone` build bundles three.js and hls.js, so no import map or peer install
|
|
47
|
+
is needed. (With a bundler, `import 'three-vr-player'` also registers `<three-video>`.)
|
|
48
|
+
|
|
49
|
+
## Projections
|
|
50
|
+
|
|
51
|
+
Auto-detected from the filename on load (override any time). Pick a mode from the 🌐
|
|
52
|
+
menu in the controls, or set it programmatically:
|
|
53
|
+
|
|
54
|
+
| Mode | Geometry | Eyes |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `180-sbs` | 180° dome | left/right |
|
|
57
|
+
| `180-mono` | 180° dome | mono |
|
|
58
|
+
| `360-mono` | 360° sphere | mono |
|
|
59
|
+
| `360-sbs` | 360° sphere | left/right |
|
|
60
|
+
| `360-tb` | 360° sphere | top/bottom |
|
|
61
|
+
| `flat-2d` | flat screen | mono |
|
|
62
|
+
| `flat-sbs-full` | flat screen | left/right (full-width per eye) |
|
|
63
|
+
| `flat-sbs-half` | flat screen | left/right (half-width per eye) |
|
|
64
|
+
|
|
65
|
+
## Options
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
new Player(container, {
|
|
69
|
+
src?: string;
|
|
70
|
+
projection?: Projection; // default '180-sbs' (or auto-detected)
|
|
71
|
+
autoDetect?: boolean; // default true
|
|
72
|
+
controls?: boolean; // default true (built-in UI)
|
|
73
|
+
proxy?: { url: string; apiPassword?: string; headers?: Record<string,string> };
|
|
74
|
+
swapEyes?: boolean; // default false
|
|
75
|
+
fov?: number; // default 70
|
|
76
|
+
supersampling?: number; // default 1.5 (× devicePixelRatio, capped at 4)
|
|
77
|
+
crossOrigin?: 'anonymous' | 'use-credentials' | null; // default 'anonymous'
|
|
78
|
+
persistSettings?: boolean; // default false (localStorage)
|
|
79
|
+
shadowDom?: boolean; // default true (style isolation)
|
|
80
|
+
vrButton?: boolean; // default true (shown only when a headset is present)
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Methods & events
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
player.load(src, { projection? }); player.play(); player.pause();
|
|
88
|
+
player.setProjection(p); player.setSwapEyes(b); player.setFov(deg); player.setSupersampling(x);
|
|
89
|
+
player.enterVR(); player.dispose();
|
|
90
|
+
player.video; // the underlying <video>
|
|
91
|
+
player.three; // { renderer, scene, camera }
|
|
92
|
+
|
|
93
|
+
player.on('ready'|'play'|'pause'|'ended'|'error'|'timeupdate'|'projectionchange'|'enterxr'|'exitxr', cb);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Headless core
|
|
97
|
+
|
|
98
|
+
Want your own controls? Import the engine directly:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { StereoScene, VideoSource, LookControls, buildProxyUrl } from 'three-vr-player/core';
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CORS / proxy
|
|
105
|
+
|
|
106
|
+
To use a cross-origin video as a WebGL texture it must be CORS-clean. If your host
|
|
107
|
+
doesn't send `Access-Control-Allow-Origin`, run the optional [proxy companion](./proxy)
|
|
108
|
+
and pass `proxy: { url, apiPassword }`. CORS-clean sources need no proxy.
|
|
109
|
+
|
|
110
|
+
## Codecs
|
|
111
|
+
|
|
112
|
+
The browser must be able to decode the file: MP4/WebM with H.264/HEVC/VP9 + AAC/Opus is
|
|
113
|
+
safest. `.mkv` and Dolby AC-3/DTS audio are browser limitations, not the player.
|
|
114
|
+
|
|
115
|
+
## Develop
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install
|
|
119
|
+
npm run dev # demo at http://localhost:8080 (paste a video URL; start ./proxy for non-CORS sources)
|
|
120
|
+
npm test # unit tests (vitest)
|
|
121
|
+
npm run typecheck
|
|
122
|
+
npm run build # dist/: ESM + types + standalone
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Roadmap & license
|
|
126
|
+
|
|
127
|
+
See [ROADMAP.md](./ROADMAP.md) — VR controllers + hand-tracking + in-VR panel, WebRTC
|
|
128
|
+
live, fisheye lens-correction, framework wrappers, and more. Licensed **MIT**.
|
package/dist/Player.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PlayerOptions, PlayerEvent, Projection } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A drop-in 3D/VR video player. Mounts a canvas + controls (into a Shadow DOM by
|
|
5
|
+
* default) inside `container`, composing the headless core.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Player {
|
|
8
|
+
readonly video: HTMLVideoElement;
|
|
9
|
+
private readonly opts;
|
|
10
|
+
private readonly wrap;
|
|
11
|
+
private readonly canvas;
|
|
12
|
+
private readonly scene;
|
|
13
|
+
private readonly look;
|
|
14
|
+
private readonly source;
|
|
15
|
+
private readonly ui?;
|
|
16
|
+
private readonly listeners;
|
|
17
|
+
private view;
|
|
18
|
+
private readyEmitted;
|
|
19
|
+
constructor(container: HTMLElement, options?: PlayerOptions);
|
|
20
|
+
private vrSupported;
|
|
21
|
+
load(src: string, o?: {
|
|
22
|
+
projection?: Projection;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
play(): Promise<void>;
|
|
25
|
+
pause(): void;
|
|
26
|
+
setProjection(p: Projection): void;
|
|
27
|
+
setSwapEyes(v: boolean): void;
|
|
28
|
+
setFov(deg: number): void;
|
|
29
|
+
setSupersampling(x: number): void;
|
|
30
|
+
enterVR(): Promise<void>;
|
|
31
|
+
get three(): {
|
|
32
|
+
renderer: import('three').WebGLRenderer;
|
|
33
|
+
scene: import('three').Scene;
|
|
34
|
+
camera: import('three').PerspectiveCamera;
|
|
35
|
+
};
|
|
36
|
+
on(e: PlayerEvent, cb: (p?: unknown) => void): this;
|
|
37
|
+
off(e: PlayerEvent, cb: (p?: unknown) => void): this;
|
|
38
|
+
private emit;
|
|
39
|
+
private persist;
|
|
40
|
+
private loadSettings;
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as o from "three";
|
|
2
|
+
import { VRButton as g } from "three/examples/jsm/webxr/VRButton.js";
|
|
3
|
+
function v(a) {
|
|
4
|
+
let e = a;
|
|
5
|
+
try {
|
|
6
|
+
e = new URL(a).pathname;
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
const s = e.toLowerCase();
|
|
10
|
+
return s.endsWith(".m3u8") ? "hls" : s.endsWith(".mpd") ? "dash" : "progressive";
|
|
11
|
+
}
|
|
12
|
+
const w = {
|
|
13
|
+
progressive: "/proxy/stream",
|
|
14
|
+
hls: "/proxy/hls/manifest.m3u8",
|
|
15
|
+
dash: "/proxy/mpd/manifest.m3u8"
|
|
16
|
+
};
|
|
17
|
+
function P(a, e) {
|
|
18
|
+
const s = v(a);
|
|
19
|
+
if (!e || !e.url) return { url: a, format: s };
|
|
20
|
+
const r = e.url.replace(/\/+$/, ""), t = new URLSearchParams();
|
|
21
|
+
t.set("d", a), e.apiPassword && t.set("api_password", e.apiPassword);
|
|
22
|
+
for (const [i, n] of Object.entries(e.headers ?? {}))
|
|
23
|
+
n != null && n !== "" && t.set(`h_${i}`, n);
|
|
24
|
+
return { url: `${r}${w[s]}?${t.toString()}`, format: s };
|
|
25
|
+
}
|
|
26
|
+
const c = {
|
|
27
|
+
"180-sbs": { geom: "sphere180", split: "sbs", stereo: !0 },
|
|
28
|
+
"180-mono": { geom: "sphere180", split: "mono", stereo: !1 },
|
|
29
|
+
"360-mono": { geom: "sphere360", split: "mono", stereo: !1 },
|
|
30
|
+
"360-sbs": { geom: "sphere360", split: "sbs", stereo: !0 },
|
|
31
|
+
"360-tb": { geom: "sphere360", split: "tb", stereo: !0 },
|
|
32
|
+
"flat-2d": { geom: "plane", split: "mono", stereo: !1, flat: !0, aspect: "full" },
|
|
33
|
+
"flat-sbs-full": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "per-eye" },
|
|
34
|
+
"flat-sbs-half": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "full" }
|
|
35
|
+
}, L = [
|
|
36
|
+
{ value: "180-sbs", label: "180° SBS (VR180)" },
|
|
37
|
+
{ value: "180-mono", label: "180° Mono" },
|
|
38
|
+
{ value: "360-mono", label: "360° Mono" },
|
|
39
|
+
{ value: "360-sbs", label: "360° SBS" },
|
|
40
|
+
{ value: "360-tb", label: "360° Top-Bottom" },
|
|
41
|
+
{ value: "flat-2d", label: "Flat 2D (regular movie)" },
|
|
42
|
+
{ value: "flat-sbs-full", label: "Flat 3D — Full SBS" },
|
|
43
|
+
{ value: "flat-sbs-half", label: "Flat 3D — Half SBS" }
|
|
44
|
+
];
|
|
45
|
+
function x(a) {
|
|
46
|
+
var e;
|
|
47
|
+
return !!((e = c[a]) != null && e.flat);
|
|
48
|
+
}
|
|
49
|
+
function E(a) {
|
|
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
|
+
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
|
+
}
|
|
53
|
+
class R {
|
|
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();
|
|
64
|
+
this.renderer.render(this.scene, this.camera);
|
|
65
|
+
}), this.vrButton = g.createButton(this.renderer), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(s);
|
|
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 s = this.video.videoWidth, r = this.video.videoHeight;
|
|
78
|
+
return !s || !r ? 16 / 9 : c[e].aspect === "per-eye" ? s / 2 / r : s / r;
|
|
79
|
+
}
|
|
80
|
+
buildGeometry(e) {
|
|
81
|
+
const s = c[e].geom;
|
|
82
|
+
if (s === "sphere180") {
|
|
83
|
+
const n = new o.SphereGeometry(500, 60, 40, -Math.PI, Math.PI, 0, Math.PI);
|
|
84
|
+
return n.scale(-1, 1, 1), n;
|
|
85
|
+
}
|
|
86
|
+
if (s === "sphere360") {
|
|
87
|
+
const n = new o.SphereGeometry(500, 60, 40);
|
|
88
|
+
return n.scale(-1, 1, 1), n;
|
|
89
|
+
}
|
|
90
|
+
const r = 2.4, t = r * this.planeAspect(e), i = new o.PlaneGeometry(t, r);
|
|
91
|
+
return i.translate(0, 0, -2), i;
|
|
92
|
+
}
|
|
93
|
+
splitUV(e, s, r) {
|
|
94
|
+
if (s === "mono") return;
|
|
95
|
+
const t = e.attributes.uv.array;
|
|
96
|
+
for (let i = 0; i < t.length; i += 2)
|
|
97
|
+
s === "sbs" ? t[i] = t[i] * 0.5 + (r === "right" ? 0.5 : 0) : s === "tb" && (t[i + 1] = t[i + 1] * 0.5 + (r === "left" ? 0.5 : 0));
|
|
98
|
+
e.attributes.uv.needsUpdate = !0;
|
|
99
|
+
}
|
|
100
|
+
clearMeshes() {
|
|
101
|
+
for (const e of this.meshes)
|
|
102
|
+
this.scene.remove(e), e.geometry.dispose(), e.material.dispose();
|
|
103
|
+
this.meshes.length = 0;
|
|
104
|
+
}
|
|
105
|
+
applyProjection(e, s) {
|
|
106
|
+
c[e] || (e = "180-sbs"), this.clearMeshes();
|
|
107
|
+
const r = c[e];
|
|
108
|
+
if (r.stereo) {
|
|
109
|
+
const t = this.buildGeometry(e);
|
|
110
|
+
this.splitUV(t, r.split, "left");
|
|
111
|
+
const i = this.buildGeometry(e);
|
|
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);
|
|
115
|
+
} else {
|
|
116
|
+
const t = new o.Mesh(this.buildGeometry(e), new o.MeshBasicMaterial({ map: this.texture }));
|
|
117
|
+
t.layers.set(0), this.scene.add(t), this.meshes.push(t);
|
|
118
|
+
}
|
|
119
|
+
this.currentMode = e, this.currentSwap = s;
|
|
120
|
+
}
|
|
121
|
+
setProjection(e) {
|
|
122
|
+
this.applyProjection(e, this.currentSwap);
|
|
123
|
+
}
|
|
124
|
+
setSwapEyes(e) {
|
|
125
|
+
this.applyProjection(this.currentMode, e);
|
|
126
|
+
}
|
|
127
|
+
setFov(e) {
|
|
128
|
+
this.camera.fov = e, this.camera.updateProjectionMatrix();
|
|
129
|
+
}
|
|
130
|
+
setSupersampling(e) {
|
|
131
|
+
this.renderer.setPixelRatio(this.pixelRatioFor(e)), this.renderer.setSize(this.w(), this.h(), !1);
|
|
132
|
+
}
|
|
133
|
+
getProjection() {
|
|
134
|
+
return this.currentMode;
|
|
135
|
+
}
|
|
136
|
+
isFlat() {
|
|
137
|
+
return !!c[this.currentMode].flat;
|
|
138
|
+
}
|
|
139
|
+
onFrame(e) {
|
|
140
|
+
this.frameCbs.push(e);
|
|
141
|
+
}
|
|
142
|
+
dispose() {
|
|
143
|
+
this.ro.disconnect(), this.renderer.setAnimationLoop(null), this.texture.dispose(), this.clearMeshes(), this.renderer.dispose();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const b = Math.PI / 180;
|
|
147
|
+
function M(a, e) {
|
|
148
|
+
return {
|
|
149
|
+
lon: Math.max(-90, Math.min(90, a)),
|
|
150
|
+
lat: Math.max(-85, Math.min(85, e))
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
class F {
|
|
154
|
+
constructor(e, s, r = {}) {
|
|
155
|
+
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
|
+
this.enabled && (this.dragging = !0, this.px = t.clientX, this.py = t.clientY, this.capture("setPointerCapture", t.pointerId));
|
|
157
|
+
}, this.onMove = (t) => {
|
|
158
|
+
if (!this.dragging) return;
|
|
159
|
+
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);
|
|
161
|
+
}, this.onUp = (t) => {
|
|
162
|
+
this.dragging = !1, this.capture("releasePointerCapture", t.pointerId);
|
|
163
|
+
}, 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);
|
|
164
|
+
}
|
|
165
|
+
capture(e, s) {
|
|
166
|
+
var r, t;
|
|
167
|
+
try {
|
|
168
|
+
(t = (r = this.dom)[e]) == null || t.call(r, s);
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
update() {
|
|
173
|
+
if (this.isPresenting() || !this.enabled) return;
|
|
174
|
+
const e = (90 - this.lat) * b, s = this.lon * b;
|
|
175
|
+
this.camera.lookAt(
|
|
176
|
+
Math.sin(e) * Math.sin(s),
|
|
177
|
+
Math.cos(e),
|
|
178
|
+
-Math.sin(e) * Math.cos(s)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
reset() {
|
|
182
|
+
this.lon = 0, this.lat = 0;
|
|
183
|
+
}
|
|
184
|
+
setEnabled(e) {
|
|
185
|
+
this.enabled = e, e || (this.lon = 0, this.lat = 0, this.camera.lookAt(0, 0, -1));
|
|
186
|
+
}
|
|
187
|
+
getAngles() {
|
|
188
|
+
return { lon: this.lon, lat: this.lat };
|
|
189
|
+
}
|
|
190
|
+
dispose() {
|
|
191
|
+
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
|
+
}
|
|
193
|
+
}
|
|
194
|
+
class A {
|
|
195
|
+
constructor() {
|
|
196
|
+
this.hls = null;
|
|
197
|
+
}
|
|
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();
|
|
208
|
+
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?`));
|
|
210
|
+
};
|
|
211
|
+
e.addEventListener("loadedmetadata", m), e.addEventListener("error", f), i && !n ? import("hls.js").then(({ default: p }) => {
|
|
212
|
+
if (!p.isSupported()) {
|
|
213
|
+
l(), h(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
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);
|
|
219
|
+
}).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());
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
dispose() {
|
|
225
|
+
this.hls && (this.hls.destroy(), this.hls = null);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
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
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=VideoSource-pc2RM7bl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoSource-pc2RM7bl.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 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(() => {\r\n for (const cb of this.frameCbs) cb();\r\n this.renderer.render(this.scene, this.camera);\r\n });\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\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(video: HTMLVideoElement, source: { url: string; format: StreamFormat }): Promise<void> {\r\n this.dispose();\r\n video.crossOrigin = video.crossOrigin ?? 'anonymous';\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","w","h","canvas","video","projection","swapEyes","fov","supersampling","cb","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","isHlsLike","nativeHls","resolve","reject","cleanup","onLoaded","onError","code","HlsCtor","_e","data"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Camera } from 'three';
|
|
2
|
+
|
|
3
|
+
/** Clamp look angles (degrees) to keep the view inside the front hemisphere. */
|
|
4
|
+
export declare function clampAngles(lon: number, lat: number): {
|
|
5
|
+
lon: number;
|
|
6
|
+
lat: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Pointer/touch drag → camera yaw/pitch, clamped to the front hemisphere.
|
|
10
|
+
* Disabled while an XR session drives the head pose, and for flat-screen modes.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LookControls {
|
|
13
|
+
private readonly camera;
|
|
14
|
+
private readonly dom;
|
|
15
|
+
private lon;
|
|
16
|
+
private lat;
|
|
17
|
+
private dragging;
|
|
18
|
+
private px;
|
|
19
|
+
private py;
|
|
20
|
+
private enabled;
|
|
21
|
+
private readonly isPresenting;
|
|
22
|
+
constructor(camera: Camera, dom: HTMLElement, opts?: {
|
|
23
|
+
isPresenting?: () => boolean;
|
|
24
|
+
});
|
|
25
|
+
private capture;
|
|
26
|
+
private onDown;
|
|
27
|
+
private onMove;
|
|
28
|
+
private onUp;
|
|
29
|
+
update(): void;
|
|
30
|
+
reset(): void;
|
|
31
|
+
setEnabled(v: boolean): void;
|
|
32
|
+
getAngles(): {
|
|
33
|
+
lon: number;
|
|
34
|
+
lat: number;
|
|
35
|
+
};
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Projection } from '../types.js';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
/**
|
|
4
|
+
* Maps a video onto the geometry for a chosen projection (inside-out 180/360
|
|
5
|
+
* sphere or a flat screen plane) and packs stereo eyes via UV split + WebXR
|
|
6
|
+
* layers. Sizes to its canvas's container (not the window), for embedding.
|
|
7
|
+
*/
|
|
8
|
+
export declare class StereoScene {
|
|
9
|
+
readonly renderer: THREE.WebGLRenderer;
|
|
10
|
+
readonly scene: THREE.Scene;
|
|
11
|
+
readonly camera: THREE.PerspectiveCamera;
|
|
12
|
+
readonly vrButton: HTMLElement;
|
|
13
|
+
readonly maxAnisotropy: number;
|
|
14
|
+
private readonly canvas;
|
|
15
|
+
private readonly video;
|
|
16
|
+
private readonly texture;
|
|
17
|
+
private readonly meshes;
|
|
18
|
+
private readonly frameCbs;
|
|
19
|
+
private readonly ro;
|
|
20
|
+
private currentMode;
|
|
21
|
+
private currentSwap;
|
|
22
|
+
constructor(opts: {
|
|
23
|
+
canvas: HTMLCanvasElement;
|
|
24
|
+
video: HTMLVideoElement;
|
|
25
|
+
projection?: Projection;
|
|
26
|
+
swapEyes?: boolean;
|
|
27
|
+
fov?: number;
|
|
28
|
+
supersampling?: number;
|
|
29
|
+
});
|
|
30
|
+
private w;
|
|
31
|
+
private h;
|
|
32
|
+
private pixelRatioFor;
|
|
33
|
+
private planeAspect;
|
|
34
|
+
private buildGeometry;
|
|
35
|
+
private splitUV;
|
|
36
|
+
private clearMeshes;
|
|
37
|
+
private applyProjection;
|
|
38
|
+
setProjection(p: Projection): void;
|
|
39
|
+
setSwapEyes(v: boolean): void;
|
|
40
|
+
setFov(deg: number): void;
|
|
41
|
+
setSupersampling(ss: number): void;
|
|
42
|
+
getProjection(): Projection;
|
|
43
|
+
isFlat(): boolean;
|
|
44
|
+
onFrame(cb: () => void): void;
|
|
45
|
+
resize: () => void;
|
|
46
|
+
dispose(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StreamFormat } from './proxy.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attaches a source to a `<video>`. Progressive files go straight on `video.src`;
|
|
5
|
+
* HLS/DASH-as-HLS go through hls.js — which is imported dynamically, so consumers
|
|
6
|
+
* who never play HLS don't pay for it (and it's an optional dependency).
|
|
7
|
+
*/
|
|
8
|
+
export declare class VideoSource {
|
|
9
|
+
private hls;
|
|
10
|
+
attach(video: HTMLVideoElement, source: {
|
|
11
|
+
url: string;
|
|
12
|
+
format: StreamFormat;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { buildProxyUrl, detectFormat } from './proxy.js';
|
|
2
|
+
export type { StreamFormat, ProxyConfig } from './proxy.js';
|
|
3
|
+
export { MODES, PROJECTIONS, isFlatMode, detectProjection } from './projections.js';
|
|
4
|
+
export type { ModeConfig, Split, GeomKind } from './projections.js';
|
|
5
|
+
export { StereoScene } from './StereoScene.js';
|
|
6
|
+
export { LookControls, clampAngles } from './LookControls.js';
|
|
7
|
+
export { VideoSource } from './VideoSource.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Projection } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export type Split = 'mono' | 'sbs' | 'tb';
|
|
4
|
+
export type GeomKind = 'sphere180' | 'sphere360' | 'plane';
|
|
5
|
+
export interface ModeConfig {
|
|
6
|
+
geom: GeomKind;
|
|
7
|
+
split: Split;
|
|
8
|
+
stereo: boolean;
|
|
9
|
+
flat?: boolean;
|
|
10
|
+
/** plane only: 'full' = displayW/H, 'per-eye' = (W/2)/H */
|
|
11
|
+
aspect?: 'full' | 'per-eye';
|
|
12
|
+
}
|
|
13
|
+
export declare const MODES: Record<Projection, ModeConfig>;
|
|
14
|
+
export declare const PROJECTIONS: {
|
|
15
|
+
value: Projection;
|
|
16
|
+
label: string;
|
|
17
|
+
}[];
|
|
18
|
+
export declare function isFlatMode(p: Projection): boolean;
|
|
19
|
+
/** Guess a projection from a URL / filename; null when nothing recognizable. */
|
|
20
|
+
export declare function detectProjection(url: string): Projection | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type StreamFormat = 'hls' | 'dash' | 'progressive';
|
|
2
|
+
export interface ProxyConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
apiPassword?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** Detect streaming format from a URL by extension. */
|
|
8
|
+
export declare function detectFormat(rawUrl: string): StreamFormat;
|
|
9
|
+
/**
|
|
10
|
+
* Build the URL the `<video>`/hls.js should load. When `proxy` is omitted the raw
|
|
11
|
+
* URL is returned (it must then be CORS-clean to be used as a WebGL texture).
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildProxyUrl(rawUrl: string, proxy?: ProxyConfig): {
|
|
14
|
+
url: string;
|
|
15
|
+
format: StreamFormat;
|
|
16
|
+
};
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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";
|
|
2
|
+
export {
|
|
3
|
+
o as LookControls,
|
|
4
|
+
s as MODES,
|
|
5
|
+
t as PROJECTIONS,
|
|
6
|
+
r as StereoScene,
|
|
7
|
+
c as VideoSource,
|
|
8
|
+
d as buildProxyUrl,
|
|
9
|
+
l as clampAngles,
|
|
10
|
+
S as detectFormat,
|
|
11
|
+
i as detectProjection,
|
|
12
|
+
n as isFlatMode
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|