forked from EXT/VR180-Web-Player
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d52a722ce7 | ||
|
|
40e3711925 | ||
|
|
c41ad12b32 | ||
|
|
dbe6e5b1d9 | ||
|
|
626b7f451b | ||
|
|
a56e36eaf6 | ||
|
|
bebcb3d355 | ||
|
|
f4fb9cf6bb | ||
|
|
38200c82f2 | ||
|
|
715e762fc9 | ||
|
|
928fafa290 | ||
|
|
065e8310e3 | ||
|
|
4183ae2530 | ||
|
|
d869f75e1e | ||
|
|
b559ea6ebf | ||
|
|
85baf3cd79 | ||
|
|
0a8cb8196c | ||
|
|
5087c3cbb2 | ||
|
|
0ef4ca56a5 | ||
|
|
ffb29bc4ec | ||
|
|
090ad5f315 | ||
|
|
957f1af8b0 |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
index.html export-ignore
|
||||
README.md export-ignore
|
||||
sbs-video.mp4 export-ignore
|
||||
poster.jpg export-ignore
|
||||
38
README.md
38
README.md
@@ -1,43 +1,37 @@
|
||||
# VR180 Web Player
|
||||
A web-based video player for 180 degree, 3D video.
|
||||
|
||||
Got an immersive video you want people to see with the Apple Vision Pro or Meta Quest headsets? You could build an app and deal with app stores. You could jump through some hoops to put it on YouTube but it will be limited to Meta headsets. Or you can use this web player and put it on your website.
|
||||
Got an immersive video you want people to see with the Apple Vision Pro or Meta Quest headsets? Now you can put it on your website just like any other video! People will see the immersive 3D video if they have a capable headset or they'll get a 2D version on other devices.
|
||||
|
||||
## How to use it
|
||||
Add the player script `<script type="module" src="vr180-player.js"></script>` before the closing body tag and use this HTML snippet:
|
||||
1. Link to the player CSS file `<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.css">`.
|
||||
2. Add the player script `<script type="module" src="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.js"></script>` before the closing body tag.
|
||||
3. And use this HTML snippet to embed your video:
|
||||
```
|
||||
<div id="vr-container">
|
||||
<video id="vr180" poster="poster.jpg" title="Demo Video" crossOrigin="anonymous" playsinline>
|
||||
<source src="sbs-video.mp4" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
<button id="playBtn">Play</button>
|
||||
```
|
||||
This creates a button on your page. When VR is available, the button will be active and clicking it will begin the immersive experience.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/05db6208-6d42-48fa-a0da-55de41f35e6d" width=50%>
|
||||
## How it works
|
||||
When the webpage loads, the video file is embeded normally, with a play button positioned over the poster frame. When the user clicks play, the player script checks if `navigator.xr` exists. If it does, a VR experience is initiated. If not, it builds a rectilinear, 2D view of your video and plays it that way.
|
||||
|
||||
*Example Button*
|
||||
**VR on Apple Vision Pro**
|
||||

|
||||
|
||||
Once the video is playing, you can bring up video controls. When the video is over, you'll automatically exit the experience.
|
||||
**2D in Safari on Mac**
|
||||
|
||||

|
||||
You can drag the 2D video around to see things outside the frame.
|
||||
<img width="1000" height="793" alt="2d" src="https://github.com/user-attachments/assets/094d30b7-7175-44ba-a700-d333196f8bb3" />
|
||||
|
||||
## Video Format
|
||||
**The player only supports 2:1, side-by-side video using either H.264 or HEVC in an mp4 file.** It does not support over-under, MV-HEVC, or .aivu.
|
||||
**The player only supports 2:1, side-by-side video using either H.264 or HEVC in an mp4 file.** It does not support over-under, MV-HEVC, APMP, or .aivu.
|
||||
|
||||
## Features
|
||||
Tapping anywhere will bring up the controls. Without interaction they will go away in 10 seconds. Tapping outside of the controls will close them right away.
|
||||
- Play/Pause
|
||||
- Rewind 15 seconds
|
||||
- Skip 15 seconds
|
||||
- Mute/Unmute
|
||||
- Seek
|
||||
- Exit VR
|
||||
|
||||
## Future
|
||||
I'm not a developer. I used AI to help create this and give me the ability to post immersive videos on my website. I'm unlikely to be able to help you if you run into problems, want to customize this, or add new features. I'm releasing this with the [unlicense](https://unlicense.org/) so you're free to do anything at all with it. That said, if you have ideas or want to contribute code, I'd love to [hear](mailto:hello@michaelverdi.com) from you.
|
||||
## Support
|
||||
I'm not a developer and I might not be able to help you if you run into problems, want to customize this, or add new features (not that I won't try). I'm releasing this with the [unlicense](https://unlicense.org/) so you're free to do anything at all with it. That said, if you have ideas or want to contribute code, I'd love to [hear](mailto:hello@michaelverdi.com) from you.
|
||||
|
||||
## Demo
|
||||
**Test it out in a headset!**
|
||||
Open [https://verdi.github.io/VR180-Web-Player/](https://verdi.github.io/VR180-Web-Player/) in a browser on your headset and then click the Enter VR button.
|
||||
**Test it out!**
|
||||
Open [https://verdi.github.io/VR180-Web-Player/](https://verdi.github.io/VR180-Web-Player/) in a browser on your headset (or another device). Or check out this short film I made -> [Blandscape](https://michaelverdi.com/blandscape)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>VR180 Web Player</title>
|
||||
<link rel="stylesheet" href="vr180player/vr180-player.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
@@ -15,11 +15,6 @@
|
||||
max-width: 750px;
|
||||
margin: auto;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -33,6 +28,6 @@
|
||||
<!-- UI elements will be dynamically inserted here by JavaScript -->
|
||||
</div>
|
||||
</main>
|
||||
<script type="module" src="vr180player/vr180-player.js"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/gh/Verdi/VR180-Web-Player@v1.0.1/vr180player/vr180-player.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
sbs-video.mp4
BIN
sbs-video.mp4
Binary file not shown.
@@ -4,6 +4,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
#playBtn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.module.js';
|
||||
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||
const _playerBase = new URL('.', import.meta.url).href;
|
||||
|
||||
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
||||
let vr180Mesh;
|
||||
@@ -111,7 +112,7 @@ function createPlayButton() {
|
||||
playButton.setAttribute('aria-label', 'Play video');
|
||||
|
||||
const playImg = document.createElement('img');
|
||||
playImg.src = 'vr180player/images/play.png';
|
||||
playImg.src = _playerBase + 'images/play.png';
|
||||
playImg.alt = 'Play';
|
||||
|
||||
playButton.appendChild(playImg);
|
||||
@@ -429,24 +430,41 @@ function init() {
|
||||
}
|
||||
|
||||
const xrCamera = renderer.xr.getCamera();
|
||||
let isLeftEye = true; // Default to left eye
|
||||
|
||||
if (xrCamera && xrCamera.cameras && xrCamera.cameras.length >= 2) {
|
||||
// Method 1: Direct camera reference comparison
|
||||
if (activeCamera === xrCamera.cameras[0]) {
|
||||
material.map.offset.x = 0;
|
||||
isLeftEye = true;
|
||||
} else if (activeCamera === xrCamera.cameras[1]) {
|
||||
material.map.offset.x = 0.5;
|
||||
isLeftEye = false;
|
||||
} else {
|
||||
material.map.offset.x = 0;
|
||||
}
|
||||
material.map.repeat.x = 0.5;
|
||||
// Method 2: View matrix position (matrixWorldInverse.elements[12] is the X translation)
|
||||
const viewMatrixX = activeCamera.matrixWorldInverse.elements[12];
|
||||
const leftCamX = xrCamera.cameras[0].matrixWorldInverse.elements[12];
|
||||
const rightCamX = xrCamera.cameras[1].matrixWorldInverse.elements[12];
|
||||
|
||||
const diffToLeft = Math.abs(viewMatrixX - leftCamX);
|
||||
const diffToRight = Math.abs(viewMatrixX - rightCamX);
|
||||
|
||||
if (diffToLeft < 0.001 || diffToLeft < diffToRight) {
|
||||
isLeftEye = true;
|
||||
} else if (diffToRight < 0.001) {
|
||||
isLeftEye = false;
|
||||
} else {
|
||||
// Method 3: Projection matrix asymmetry (elements[8] indicates eye offset)
|
||||
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
|
||||
if (projMatrixEl8 < -0.0001) {
|
||||
material.map.offset.x = 0; material.map.repeat.x = 0.5;
|
||||
} else if (projMatrixEl8 > 0.0001) {
|
||||
material.map.offset.x = 0.5; material.map.repeat.x = 0.5;
|
||||
isLeftEye = projMatrixEl8 <= 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback when xrCamera.cameras is not available
|
||||
const projMatrixEl8 = activeCamera.projectionMatrix.elements[8];
|
||||
isLeftEye = projMatrixEl8 <= 0;
|
||||
}
|
||||
|
||||
material.map.offset.x = isLeftEye ? 0 : 0.5;
|
||||
material.map.repeat.x = 0.5;
|
||||
};
|
||||
|
||||
// Initialize 2D camera
|
||||
@@ -1436,9 +1454,7 @@ function start2DMode() {
|
||||
|
||||
// Position the canvas to match the video element
|
||||
const canvas = renderer.domElement;
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.left = '0';
|
||||
canvas.style.position = 'relative';
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = 'auto';
|
||||
canvas.style.aspectRatio = '16/9';
|
||||
@@ -1733,6 +1749,10 @@ function renderXR(timestamp, frame) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Sync video texture before render to ensure frame consistency
|
||||
if (videoTexture && video && !video.paused && !video.ended) {
|
||||
videoTexture.needsUpdate = true;
|
||||
}
|
||||
handleControllerInteractions();
|
||||
renderer.render(scene, camera);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user