forked from EXT/VR180-Web-Player
@@ -94,7 +94,7 @@ When the page loads, the script binds every `[data-vr-web-launcher]` on the page
|
|||||||
- Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required.
|
- Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
Run `npm run build`, then open `test-pages/index.html` through a local web server. The test hub is a gallery with launchers for flat 3D image, VR180 3D image, image carousels, flat 3D video, and VR180 3D video.
|
Run `npm run build`, then open `test-pages/index.html` through a local web server. The test hub is a gallery with launchers for flat 3D image, VR180 3D image, image carousels, flat 3D video, VR180 3D video, and a local file picker for browser-selected image/video media.
|
||||||
|
|
||||||
For local experimentation, run:
|
For local experimentation, run:
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,88 @@ a {
|
|||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-local-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
width: min(100%, 760px);
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid #d5d5cf;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-field input,
|
||||||
|
.demo-field select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
border: 1px solid #c7c7c0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-field input {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-field select {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-name {
|
||||||
|
margin: 0;
|
||||||
|
color: #606058;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-preview {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #111;
|
||||||
|
color: #d8d8d8;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-preview img,
|
||||||
|
.demo-local-preview video {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-preview p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-launch {
|
||||||
|
min-height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #151515;
|
||||||
|
color: #fff;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 750;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-local-launch:disabled {
|
||||||
|
background: #9a9a92;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.demo-page {
|
.demo-page {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
<a href="./test-vr180-3d-image-carousel.html">VR180 image carousel</a>
|
<a href="./test-vr180-3d-image-carousel.html">VR180 image carousel</a>
|
||||||
<a href="./test-3d-video.html">3D video</a>
|
<a href="./test-3d-video.html">3D video</a>
|
||||||
<a href="./test-vr180-3d-video.html">VR180 video</a>
|
<a href="./test-vr180-3d-video.html">VR180 video</a>
|
||||||
|
<a href="./test-local-media.html">Local media</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p class="demo-note">Image tests use files in <code>../media/</code>. Video tests expect <code>../media/sbs-video.mp4</code>.</p>
|
<p class="demo-note">Image tests use files in <code>../media/</code>. Video tests expect <code>../media/sbs-video.mp4</code>.</p>
|
||||||
|
|||||||
140
test-pages/local-media-picker.js
Normal file
140
test-pages/local-media-picker.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
const fileInput = document.querySelector('[data-local-media-file]');
|
||||||
|
const launchButton = document.querySelector('[data-local-media-launch]');
|
||||||
|
const projectionSelect = document.querySelector('[data-local-media-projection]');
|
||||||
|
const preview = document.querySelector('[data-local-media-preview]');
|
||||||
|
const fileName = document.querySelector('[data-local-media-name]');
|
||||||
|
|
||||||
|
let activeObjectUrl = '';
|
||||||
|
|
||||||
|
fileInput?.addEventListener('change', () => {
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
clearLocalMedia();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaType = getMediaType(file);
|
||||||
|
if (!mediaType) {
|
||||||
|
clearLocalMedia();
|
||||||
|
setPreviewMessage('Choose an image or video file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeObjectUrl) {
|
||||||
|
URL.revokeObjectURL(activeObjectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeObjectUrl = URL.createObjectURL(file);
|
||||||
|
updateLauncher(file, mediaType, activeObjectUrl);
|
||||||
|
renderPreview(file, mediaType, activeObjectUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
projectionSelect?.addEventListener('change', () => {
|
||||||
|
if (launchButton && projectionSelect) {
|
||||||
|
launchButton.dataset.projection = projectionSelect.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('pagehide', () => {
|
||||||
|
if (activeObjectUrl) {
|
||||||
|
URL.revokeObjectURL(activeObjectUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearLocalMedia() {
|
||||||
|
if (activeObjectUrl) {
|
||||||
|
URL.revokeObjectURL(activeObjectUrl);
|
||||||
|
activeObjectUrl = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchButton) {
|
||||||
|
launchButton.disabled = true;
|
||||||
|
delete launchButton.dataset.src;
|
||||||
|
delete launchButton.dataset.mediaType;
|
||||||
|
delete launchButton.dataset.type;
|
||||||
|
delete launchButton.dataset.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
fileName.textContent = 'No file selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewMessage('Preview will appear here.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMediaType(file) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type.startsWith('video/')) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (['avif', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(extension)) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['m4v', 'mov', 'mp4', 'ogv', 'webm'].includes(extension)) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLauncher(file, mediaType, objectUrl) {
|
||||||
|
if (!launchButton || !projectionSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchButton.disabled = false;
|
||||||
|
launchButton.dataset.src = objectUrl;
|
||||||
|
launchButton.dataset.mediaType = mediaType;
|
||||||
|
launchButton.dataset.projection = projectionSelect.value;
|
||||||
|
launchButton.dataset.title = file.name;
|
||||||
|
|
||||||
|
if (file.type) {
|
||||||
|
launchButton.dataset.type = file.type;
|
||||||
|
} else {
|
||||||
|
delete launchButton.dataset.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
fileName.textContent = `${file.name} (${mediaType})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview(file, mediaType, objectUrl) {
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.replaceChildren();
|
||||||
|
|
||||||
|
if (mediaType === 'image') {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.src = objectUrl;
|
||||||
|
image.alt = file.name;
|
||||||
|
preview.appendChild(image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = objectUrl;
|
||||||
|
video.controls = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.preload = 'metadata';
|
||||||
|
preview.appendChild(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreviewMessage(message) {
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholder = document.createElement('p');
|
||||||
|
placeholder.textContent = message;
|
||||||
|
preview.replaceChildren(placeholder);
|
||||||
|
}
|
||||||
58
test-pages/test-local-media.html
Normal file
58
test-pages/test-local-media.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<title>Local Media Test</title>
|
||||||
|
<link rel="stylesheet" href="./demo.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="demo-page">
|
||||||
|
<div class="demo-shell demo-player">
|
||||||
|
<header class="demo-topbar">
|
||||||
|
<div>
|
||||||
|
<h1 class="demo-brand">Local Media</h1>
|
||||||
|
<p class="demo-kicker">Select a local SBS image or video, choose the projection, then launch it.</p>
|
||||||
|
</div>
|
||||||
|
<a class="demo-back" href="./index.html">Back</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
||||||
|
|
||||||
|
<section class="demo-local-panel" aria-label="Local media launcher">
|
||||||
|
<label class="demo-field">
|
||||||
|
<span>Media file</span>
|
||||||
|
<input data-local-media-file type="file" accept="image/*,video/*">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="demo-field">
|
||||||
|
<span>Projection</span>
|
||||||
|
<select data-local-media-projection>
|
||||||
|
<option value="plane">3D plane</option>
|
||||||
|
<option value="vr180">VR180 3D</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="demo-local-name" data-local-media-name>No file selected</p>
|
||||||
|
|
||||||
|
<div class="demo-local-preview" data-local-media-preview>
|
||||||
|
<p>Preview will appear here.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="demo-local-launch"
|
||||||
|
data-vr-web-launcher
|
||||||
|
data-local-media-launch
|
||||||
|
data-projection="plane"
|
||||||
|
disabled>
|
||||||
|
Launch selected media
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./demo-xr-status.js"></script>
|
||||||
|
<script type="module" src="./local-media-picker.js"></script>
|
||||||
|
<script type="module" src="../vr180player/vr180-player.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user