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.
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
@@ -204,6 +204,88 @@ a {
|
||||
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) {
|
||||
.demo-page {
|
||||
padding: 20px;
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
<a href="./test-vr180-3d-image-carousel.html">VR180 image carousel</a>
|
||||
<a href="./test-3d-video.html">3D video</a>
|
||||
<a href="./test-vr180-3d-video.html">VR180 video</a>
|
||||
<a href="./test-local-media.html">Local media</a>
|
||||
</nav>
|
||||
|
||||
<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