1
0

Custom player
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
Aiden
2026-06-11 14:20:55 +10:00
parent 229c25947a
commit 69511e4549
5 changed files with 282 additions and 1 deletions

View File

@@ -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:

View File

@@ -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;

View File

@@ -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>

View 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);
}

View 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>