forked from EXT/VR180-Web-Player
This commit is contained in:
99
src/vr180player/bootstrap.ts
Normal file
99
src/vr180player/bootstrap.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
DEFAULT_PROJECTION,
|
||||
PLAYER_SELECTOR,
|
||||
type ProjectionMode,
|
||||
VALID_PROJECTIONS
|
||||
} from './config.js';
|
||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom/dom.js';
|
||||
|
||||
export type BootstrapContext = {
|
||||
playButton: HTMLButtonElement;
|
||||
playerContainer: HTMLElement;
|
||||
projectionMode: ProjectionMode;
|
||||
videoElement: HTMLVideoElement;
|
||||
};
|
||||
|
||||
export function bootstrapPlayer(playerBase: string, onReady: (context: BootstrapContext) => void): void {
|
||||
injectPlayerStyles(playerBase);
|
||||
|
||||
onDocumentReady(() => {
|
||||
const containers = document.querySelectorAll<HTMLElement>(PLAYER_SELECTOR);
|
||||
|
||||
if (containers.length === 0) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: Expected exactly one ${PLAYER_SELECTOR} container, found none.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containers.length > 1) {
|
||||
console.warn(`VR_WEB_PLAYER_DOM: This version supports exactly one ${PLAYER_SELECTOR} container per page. Found ${containers.length}; no player was initialized.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const playerContainer = containers[0];
|
||||
playerContainer.classList.add('vrwp');
|
||||
|
||||
const configuredProjection = (playerContainer.dataset.projection || DEFAULT_PROJECTION).trim().toLowerCase();
|
||||
if (!VALID_PROJECTIONS.has(configuredProjection as ProjectionMode)) {
|
||||
console.error(`VR_WEB_PLAYER_CONFIG: Unsupported data-projection="${configuredProjection}". Use "vr180" or "plane".`);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElement = playerContainer.querySelector('video');
|
||||
if (!videoElement) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a video element.`);
|
||||
return;
|
||||
}
|
||||
videoElement.classList.add('vrwp-video');
|
||||
|
||||
const playButton = createPlayButton();
|
||||
playerContainer.appendChild(playButton);
|
||||
playerContainer.appendChild(create2DControlPanel());
|
||||
playButton.disabled = true;
|
||||
videoElement.load();
|
||||
|
||||
completeXrSupportCheck(playButton, () => {
|
||||
onReady({
|
||||
playButton,
|
||||
playerContainer,
|
||||
projectionMode: configuredProjection as ProjectionMode,
|
||||
videoElement
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onDocumentReady(callback: () => void): void {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function completeXrSupportCheck(playButton: HTMLButtonElement, onComplete: () => void): void {
|
||||
if (!navigator.xr) {
|
||||
markXrUnsupported(playButton);
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
|
||||
if (supported) {
|
||||
playButton.dataset.xrSupported = 'true';
|
||||
} else {
|
||||
markXrUnsupported(playButton);
|
||||
}
|
||||
|
||||
onComplete();
|
||||
}).catch((err) => {
|
||||
console.error('XR Support Check Error:', err);
|
||||
markXrUnsupported(playButton);
|
||||
onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function markXrUnsupported(playButton: HTMLButtonElement): void {
|
||||
playButton.dataset.xrSupported = 'false';
|
||||
playButton.disabled = false;
|
||||
}
|
||||
67
src/vr180player/rendering/texture-manager.ts
Normal file
67
src/vr180player/rendering/texture-manager.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export type ManagedTexture = {
|
||||
needsUpdate?: boolean;
|
||||
dispose(): void;
|
||||
};
|
||||
|
||||
export type ManagedMaterial<TTexture extends ManagedTexture = ManagedTexture> = {
|
||||
map: TTexture | null;
|
||||
needsUpdate: boolean;
|
||||
};
|
||||
|
||||
export type TextureFactory<TTexture extends ManagedTexture = ManagedTexture> = (video: HTMLVideoElement) => TTexture;
|
||||
|
||||
export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTexture> {
|
||||
private texture: TTexture | null = null;
|
||||
private readonly createTexture: TextureFactory<TTexture>;
|
||||
private readonly video: HTMLVideoElement;
|
||||
|
||||
constructor(video: HTMLVideoElement, createTexture: TextureFactory<TTexture>) {
|
||||
this.createTexture = createTexture;
|
||||
this.video = video;
|
||||
}
|
||||
|
||||
get current(): TTexture | null {
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
assignToMaterial(material: ManagedMaterial<TTexture>): TTexture {
|
||||
const texture = this.create();
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
clearMaterial(material?: ManagedMaterial<TTexture> | null): void {
|
||||
if (material?.map) {
|
||||
const materialTexture = material.map;
|
||||
material.map = null;
|
||||
material.needsUpdate = true;
|
||||
materialTexture.dispose();
|
||||
|
||||
if (materialTexture === this.texture) {
|
||||
this.texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
create(): TTexture {
|
||||
this.dispose();
|
||||
this.texture = this.createTexture(this.video);
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.texture) {
|
||||
this.texture.dispose();
|
||||
this.texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateIfPlaying(): void {
|
||||
if (this.texture && !this.video.paused && !this.video.ended) {
|
||||
this.texture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
DEFAULT_PROJECTION,
|
||||
PLANE_2D_DISTANCE,
|
||||
PLANE_DISTANCE,
|
||||
PLAYER_SELECTOR,
|
||||
type ProjectionMode,
|
||||
VALID_PROJECTIONS
|
||||
type ProjectionMode
|
||||
} from './config.js';
|
||||
import { bootstrapPlayer } from './bootstrap.js';
|
||||
import { createContentScene } from './rendering/content-scene.js';
|
||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom/dom.js';
|
||||
import {
|
||||
applySbsTextureWindow as applySbsTextureWindowCore,
|
||||
hideContentMeshes as hideContentMeshesCore,
|
||||
@@ -35,11 +32,12 @@ import {
|
||||
createPlayerRenderer,
|
||||
resizePlayerRenderer
|
||||
} from './rendering/renderer-lifecycle.js';
|
||||
import { VideoTextureManager } from './rendering/texture-manager.js';
|
||||
|
||||
const _playerBase = new URL('.', import.meta.url).href;
|
||||
|
||||
let playerContainer, projectionMode: ProjectionMode = DEFAULT_PROJECTION;
|
||||
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
||||
let playerContainer, projectionMode: ProjectionMode;
|
||||
let scene, camera, renderer, video, sphereMaterial;
|
||||
let vr180Mesh, planeMesh, activeContentMesh;
|
||||
let xrSession = null;
|
||||
let raycaster, uiElements = [];
|
||||
@@ -49,6 +47,7 @@ let frameCounter = 0;
|
||||
let isXrLoopActive = false;
|
||||
let vrControlPanel;
|
||||
let mediaController: MediaController | undefined;
|
||||
let textureManager: VideoTextureManager | undefined;
|
||||
let vrPanel: VrControlPanel | undefined;
|
||||
let twoDMode: TwoDMode | undefined;
|
||||
const vrPanelVisibility = new VrPanelVisibility();
|
||||
@@ -57,82 +56,15 @@ const vrPanelVisibility = new VrPanelVisibility();
|
||||
let camera2D;
|
||||
let fallbackCameraControls: FallbackCameraControls | undefined;
|
||||
|
||||
injectPlayerStyles(_playerBase);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const containers = document.querySelectorAll(PLAYER_SELECTOR);
|
||||
|
||||
if (containers.length === 0) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: Expected exactly one ${PLAYER_SELECTOR} container, found none.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containers.length > 1) {
|
||||
console.warn(`VR_WEB_PLAYER_DOM: This version supports exactly one ${PLAYER_SELECTOR} container per page. Found ${containers.length}; no player was initialized.`);
|
||||
return;
|
||||
}
|
||||
|
||||
playerContainer = containers[0];
|
||||
playerContainer.classList.add('vrwp');
|
||||
|
||||
const configuredProjection = (playerContainer.dataset.projection || DEFAULT_PROJECTION).trim().toLowerCase();
|
||||
if (!VALID_PROJECTIONS.has(configuredProjection as ProjectionMode)) {
|
||||
console.error(`VR_WEB_PLAYER_CONFIG: Unsupported data-projection="${configuredProjection}". Use "vr180" or "plane".`);
|
||||
return;
|
||||
}
|
||||
projectionMode = configuredProjection as ProjectionMode;
|
||||
|
||||
videoElement = playerContainer.querySelector('video');
|
||||
if (!videoElement) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a video element.`);
|
||||
return;
|
||||
}
|
||||
videoElement.classList.add('vrwp-video');
|
||||
|
||||
// Create and insert play button
|
||||
playBtn = createPlayButton();
|
||||
playerContainer.appendChild(playBtn);
|
||||
|
||||
// Create and insert 2D control panel
|
||||
const controlPanel = create2DControlPanel();
|
||||
playerContainer.appendChild(controlPanel);
|
||||
|
||||
playBtn.disabled = true;
|
||||
|
||||
if (videoElement) {
|
||||
videoElement.load();
|
||||
}
|
||||
|
||||
if (navigator.xr) {
|
||||
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
|
||||
if (supported) {
|
||||
playBtn.dataset.xrSupported = "true";
|
||||
} else {
|
||||
playBtn.dataset.xrSupported = "false";
|
||||
// Enable button for regular video playback when VR is not supported
|
||||
playBtn.disabled = false;
|
||||
}
|
||||
// Always call init() regardless of VR support
|
||||
init();
|
||||
}).catch(err => {
|
||||
console.error("XR Support Check Error:", err);
|
||||
playBtn.dataset.xrSupported = "false";
|
||||
// Enable button for regular video playback when VR check fails
|
||||
playBtn.disabled = false;
|
||||
// Call init() even when VR check fails
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
playBtn.dataset.xrSupported = "false";
|
||||
// If navigator.xr itself is not available, enable button for regular video playback
|
||||
if (playBtn) {
|
||||
playBtn.disabled = false;
|
||||
}
|
||||
// Call init() even when XR is not available
|
||||
init();
|
||||
}
|
||||
bootstrapPlayer(_playerBase, (context) => {
|
||||
playerContainer = context.playerContainer;
|
||||
projectionMode = context.projectionMode;
|
||||
videoElement = context.videoElement;
|
||||
playBtn = context.playButton;
|
||||
init();
|
||||
});
|
||||
|
||||
|
||||
function applySbsTextureWindow(renderingRenderer, activeCamera, material) {
|
||||
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DModeActive());
|
||||
}
|
||||
@@ -150,9 +82,10 @@ function positionPlaneForPresentation(isFallback2D = false) {
|
||||
}
|
||||
|
||||
function createVideoTexture() {
|
||||
if (videoTexture) videoTexture.dispose();
|
||||
videoTexture = createVideoTextureCore(video);
|
||||
return videoTexture;
|
||||
if (!textureManager) {
|
||||
throw new Error('Video texture manager is not initialized.');
|
||||
}
|
||||
return textureManager.create();
|
||||
}
|
||||
|
||||
function is2DModeActive() {
|
||||
@@ -174,9 +107,7 @@ function closeActiveXrSessionAfterContextLoss() {
|
||||
|
||||
function restoreVideoTextureAfterContextRestored() {
|
||||
if (video && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
|
||||
videoTexture = createVideoTexture();
|
||||
sphereMaterial.map = videoTexture;
|
||||
sphereMaterial.needsUpdate = true;
|
||||
textureManager?.assignToMaterial(sphereMaterial);
|
||||
updateVRPlayPauseButtonIcon();
|
||||
updateVRVolumeButtonIcon();
|
||||
console.log("Re-initialized video texture after context restoration during VR.");
|
||||
@@ -202,6 +133,7 @@ function init() {
|
||||
renderer = playerRenderer.renderer;
|
||||
|
||||
video = videoElement;
|
||||
textureManager = new VideoTextureManager(video, createVideoTextureCore);
|
||||
mediaController = new MediaController({
|
||||
is2DModeActive,
|
||||
on2DPlaybackResume: show2DControlPanel,
|
||||
@@ -502,17 +434,18 @@ async function actualSessionToggle() {
|
||||
if (camera) camera.updateProjectionMatrix();
|
||||
positionPlaneForPresentation(false);
|
||||
|
||||
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
||||
if (video) {
|
||||
videoTexture = createVideoTexture();
|
||||
if (activeContentMesh && sphereMaterial) {
|
||||
sphereMaterial.map = videoTexture;
|
||||
sphereMaterial.needsUpdate = true;
|
||||
showActiveContentMesh();
|
||||
} else { throw new Error("VR mesh components not ready for texture."); }
|
||||
} else {
|
||||
textureManager?.dispose();
|
||||
if (!video) {
|
||||
throw new Error("Video element not available for creating texture.");
|
||||
}
|
||||
if (!activeContentMesh || !sphereMaterial) {
|
||||
throw new Error("VR mesh components not ready for texture.");
|
||||
}
|
||||
if (!textureManager) {
|
||||
throw new Error("Video texture manager is not initialized.");
|
||||
}
|
||||
textureManager.assignToMaterial(sphereMaterial);
|
||||
showActiveContentMesh();
|
||||
|
||||
updateVRPlayPauseButtonIcon();
|
||||
updateVRVolumeButtonIcon();
|
||||
@@ -531,8 +464,7 @@ async function actualSessionToggle() {
|
||||
isXrLoopActive = false;
|
||||
|
||||
hideContentMeshes();
|
||||
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
|
||||
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
||||
textureManager?.clearMaterial(sphereMaterial);
|
||||
if (vrControlPanel) {
|
||||
vrPanelVisibility.hideImmediately();
|
||||
}
|
||||
@@ -570,14 +502,7 @@ function onVRSessionEnd(event) {
|
||||
|
||||
mediaController?.pauseIfPlaying();
|
||||
|
||||
if (sphereMaterial && sphereMaterial.map) {
|
||||
sphereMaterial.map.dispose();
|
||||
sphereMaterial.map = null;
|
||||
sphereMaterial.needsUpdate = true;
|
||||
}
|
||||
if (videoTexture) {
|
||||
videoTexture.dispose(); videoTexture = null;
|
||||
}
|
||||
textureManager?.clearMaterial(sphereMaterial);
|
||||
hideContentMeshes();
|
||||
if (vrControlPanel) {
|
||||
vrPanelVisibility.hideImmediately();
|
||||
@@ -633,10 +558,7 @@ function renderXR(timestamp, frame) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Sync video texture before render to ensure frame consistency
|
||||
if (videoTexture && video && !video.paused && !video.ended) {
|
||||
videoTexture.needsUpdate = true;
|
||||
}
|
||||
textureManager?.updateIfPlaying();
|
||||
renderer.render(scene, camera);
|
||||
} catch (error) {
|
||||
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
||||
|
||||
Reference in New Issue
Block a user