1
0

more refactoer

This commit is contained in:
Aiden
2026-06-10 11:51:34 +10:00
parent cb332abd4f
commit f5c82d3b78
4 changed files with 678 additions and 382 deletions

View File

@@ -1,4 +1,3 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
import {
DEFAULT_PROJECTION,
PLANE_2D_DISTANCE,
@@ -17,6 +16,7 @@ import {
} from './projection.js';
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
import { FallbackCameraControls } from './fallback-camera-controls.js';
import { MediaController } from './media-controller.js';
import {
createVrController,
handleVrControllerSelect
@@ -30,7 +30,11 @@ import {
updateVrVolumeButtonIcon
} from './vr-control-panel.js';
import { VrPanelVisibility } from './vr-panel-visibility.js';
import { TwoDControlPanel } from './two-d-control-panel.js';
import { TwoDMode } from './two-d-mode.js';
import {
createPlayerRenderer,
resizePlayerRenderer
} from './renderer-lifecycle.js';
const _playerBase = new URL('.', import.meta.url).href;
@@ -43,10 +47,10 @@ let videoElement, playBtn;
let frameCounter = 0;
let isXrLoopActive = false;
let is2DMode = false;
let vrControlPanel;
let mediaController: MediaController | undefined;
let vrPanel: VrControlPanel | undefined;
let twoDControls: TwoDControlPanel | undefined;
let twoDMode: TwoDMode | undefined;
const vrPanelVisibility = new VrPanelVisibility();
// 2D Camera Controls
@@ -130,7 +134,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
function applySbsTextureWindow(renderingRenderer, activeCamera, material) {
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DMode);
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DModeActive());
}
function hideContentMeshes() {
@@ -151,6 +155,34 @@ function createVideoTexture() {
return videoTexture;
}
function is2DModeActive() {
return twoDMode?.isActive ?? false;
}
function closeActiveXrSessionAfterContextLoss() {
if (!xrSession) return;
const sessionToClose = xrSession;
xrSession = null;
sessionToClose.removeEventListener('end', onVRSessionEnd);
sessionToClose.end().catch(e => {
console.error("Error ending session on context lost:", e);
}).finally(() => {
onVRSessionEnd({ session: sessionToClose });
});
}
function restoreVideoTextureAfterContextRestored() {
if (video && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
videoTexture = createVideoTexture();
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
updateVRPlayPauseButtonIcon();
updateVRVolumeButtonIcon();
console.log("Re-initialized video texture after context restoration during VR.");
}
}
function getVideoTitle() {
return videoElement.getAttribute('title') ||
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
@@ -160,53 +192,22 @@ function getVideoTitle() {
function init() {
try {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 0.1);
scene.add(camera);
renderer = new THREE.WebGLRenderer({ antialias: true });
if (!renderer || !renderer.isWebGLRenderer) {
throw new Error("Failed to create WebGLRenderer or it's not a valid Three.js renderer type.");
}
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
renderer.outputColorSpace = THREE.SRGBColorSpace;
playerContainer.appendChild(renderer.domElement);
if (renderer.domElement) {
renderer.domElement.style.display = 'none';
}
const gl = renderer.getContext();
if (!gl) {
throw new Error("Failed to get WebGL context from renderer.");
}
gl.canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault();
console.error("CONTEXT_EVENT: WebGL Context Lost! xrSession active?", !!xrSession, event);
if (xrSession) {
const sessionToClose = xrSession;
xrSession = null; // Nullify global ref immediately
sessionToClose.removeEventListener('end', onVRSessionEnd); // Try to remove listener
sessionToClose.end().catch(e => { console.error("Error ending session on context lost:", e); }).finally(() => {
onVRSessionEnd({ session: sessionToClose });
});
}
}, false);
gl.canvas.addEventListener('webglcontextrestored', (event) => {
console.log("CONTEXT_EVENT: WebGL Context Restored.");
if (video && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
videoTexture = createVideoTexture();
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
updateVRPlayPauseButtonIcon();
updateVRVolumeButtonIcon();
console.log("Re-initialized video texture after context restoration during VR.");
}
}, false);
const playerRenderer = createPlayerRenderer(playerContainer, {
closeActiveXrSession: closeActiveXrSessionAfterContextLoss,
hasActiveXrSession: () => !!xrSession,
restoreAfterContextRestored: restoreVideoTextureAfterContextRestored
});
scene = playerRenderer.scene;
camera = playerRenderer.camera;
renderer = playerRenderer.renderer;
video = videoElement;
mediaController = new MediaController({
is2DModeActive,
on2DPlaybackResume: show2DControlPanel,
playButton: playBtn,
video
});
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
applySbsTextureWindow(renderer, activeCamera, material);
});
@@ -219,9 +220,32 @@ function init() {
camera2D = contentScene.fallbackCamera;
fallbackCameraControls = new FallbackCameraControls(camera2D, {
hideControls: hide2DControlPanel,
isEnabled: () => is2DMode,
isEnabled: is2DModeActive,
showControls: show2DControlPanel
});
twoDMode = new TwoDMode({
callbacks: {
createMediaTexture: createVideoTexture,
forward: () => mediaController?.forward(),
positionPlaneForPresentation,
rewind: () => mediaController?.rewind(),
seekToProgress: (progress) => mediaController?.seekToProgress(progress),
showActiveContentMesh,
toggleMute: () => mediaController?.toggleMute(),
togglePlayPause: () => mediaController?.togglePlayPause()
},
fullscreenTarget: playerContainer,
getActiveContentMesh: () => activeContentMesh,
getCamera: () => camera2D,
getCameraControls: () => fallbackCameraControls,
getMaterial: () => sphereMaterial,
getRenderer: () => renderer,
getScene: () => scene,
getVideo: () => video,
playerContainer,
projectionMode,
title: getVideoTitle()
});
} catch (e) {
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
renderer = null;
@@ -264,9 +288,6 @@ function init() {
video
});
}
// Initialize 2D control panel
init2DControlPanel();
} catch (e) {
console.error("INIT_ERROR (Phase 3 - Event Listeners):", e);
}
@@ -308,248 +329,81 @@ function hidePanel() {
function onWindowResize() {
if (!renderer) return;
if (renderer.xr && renderer.xr.isPresenting) return;
if (is2DMode) {
// In 2D mode, calculate canvas size based on container dimensions
const container = playerContainer;
if (container) {
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
// Calculate height based on 16:9 aspect ratio
const aspectRatio = 16 / 9;
const calculatedHeight = containerWidth / aspectRatio;
// Ensure minimum dimensions to prevent zero-sized canvas
const canvasWidth = Math.max(containerWidth, 320);
const canvasHeight = Math.max(calculatedHeight, 180);
renderer.setSize(canvasWidth, canvasHeight);
camera2D.aspect = canvasWidth / canvasHeight;
camera2D.updateProjectionMatrix();
// Update canvas styling to maintain proper positioning
const canvas = renderer.domElement;
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.aspectRatio = '16/9';
// Reposition control panel after resize
position2DControlPanel();
}
} else {
// Normal VR/window mode
if (camera && renderer.domElement.style.display !== 'none') {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
} else if (camera) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Update 2D camera aspect ratio for potential future use
if (camera2D) {
camera2D.aspect = window.innerWidth / window.innerHeight;
camera2D.updateProjectionMatrix();
}
}
}
// 2D Render Loop
function render2D() {
if (!is2DMode) return;
if (projectionMode === 'vr180') {
fallbackCameraControls?.updateCameraRotation();
} else if (camera2D) {
camera2D.rotation.set(0, 0, 0);
}
if (renderer && camera2D && scene) {
renderer.render(scene, camera2D);
}
requestAnimationFrame(render2D);
}
if (twoDMode?.resize()) return;
// 2D Control Panel Functions
function init2DControlPanel() {
twoDControls = new TwoDControlPanel({
callbacks: {
onForward: () => {
if (video && isFinite(video.duration)) {
video.currentTime = Math.min(video.duration, video.currentTime + 15);
}
},
onMute: () => {
if (video) {
video.muted = !video.muted;
}
},
onPlayPause: togglePlayPause,
onRewind: () => {
if (video) {
video.currentTime = Math.max(0, video.currentTime - 15);
}
},
onSeek: (progress) => {
if (video && isFinite(video.duration)) {
video.currentTime = progress * video.duration;
}
}
},
fullscreenTarget: playerContainer,
getIsActive: () => is2DMode,
resizePlayerRenderer({
camera,
camera2D,
is2DMode: false,
onFallbackResize: () => {},
playerContainer,
title: getVideoTitle()
renderer
});
}
function show2DControlPanel() {
twoDControls?.show();
twoDMode?.showControls();
}
function hide2DControlPanel() {
twoDControls?.hide();
twoDMode?.hideControls();
}
function update2DControlPanel() {
if (!is2DMode || !video) return;
twoDControls?.updateTime(video.currentTime, video.duration);
twoDMode?.updateTimeline();
}
function update2DPlayPauseButton() {
if (!is2DMode || !video) return;
twoDControls?.updatePlaybackButton(video.paused || video.ended);
twoDMode?.updatePlaybackButton();
}
function update2DMuteButton() {
if (!is2DMode || !video) return;
twoDControls?.updateMuteButton(video.muted);
twoDMode?.updateMuteButton();
}
function handle2DVideoEnd() {
if (!is2DMode || !video) return;
twoDControls?.showPersistent();
update2DPlayPauseButton();
}
function position2DControlPanel() {
if (!renderer) return;
twoDControls?.position(renderer.domElement);
}
function hidePlayButton() {
if (playBtn) {
playBtn.classList.add('hidden');
}
}
function enableNativeControls() {
if (video) {
video.controls = true;
}
}
function togglePlayPause() {
if (!video || !video.currentSrc) return;
if (video.paused || video.ended) {
// If video has ended in 2D mode, restart from beginning
if (video.ended && is2DMode) {
video.currentTime = 0;
}
if (video.readyState >= video.HAVE_ENOUGH_DATA || video.currentSrc) {
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
// Resume normal control panel auto-hide behavior after restart
if (is2DMode && video.ended === false) {
show2DControlPanel();
}
}).catch(err => console.error("Error during video.play():", err));
} else {
console.error("video.play() did not return a promise.");
}
}
} else {
video.pause();
}
twoDMode?.handleVideoEnd();
}
function resetToOriginalState() {
// Reset video to show poster frame
if (video) {
video.pause();
video.currentTime = 0;
video.controls = false; // Disable native controls
// Force video back to poster state by reloading
video.load();
}
// Show the play button in center position
if (playBtn) {
if (mediaController) {
mediaController.resetToOriginalState();
} else if (playBtn) {
playBtn.classList.remove('hidden');
playBtn.disabled = false;
}
// Reset 2D mode if it was active
if (is2DMode) {
is2DMode = false;
remove2DEventListeners();
// Hide 2D control panel
hide2DControlPanel();
// Reset camera rotation
fallbackCameraControls?.reset();
positionPlaneForPresentation(false);
// Hide WebGL canvas and show video element
if (renderer && renderer.domElement) {
renderer.domElement.style.display = 'none';
}
if (video) {
video.style.display = '';
}
// Reset renderer size
if (twoDMode?.isActive) {
twoDMode.stop();
onWindowResize();
}
}
function onVideoEnded() {
if (video && !video.paused) video.pause();
if (xrSession && renderer && renderer.xr.isPresenting) {
// VR mode - exit VR and reset to original state
actualSessionToggle().catch(err => {
console.error("Error during automatic VR exit on video end:", err);
// Fallback cleanup if actualSessionToggle fails or doesn't fully clean up
if(xrSession) { // Check if session still exists
const sessionToClean = xrSession;
xrSession = null; // Nullify global ref
sessionToClean.removeEventListener('end', onVRSessionEnd);
sessionToClean.end().catch(e => {}).finally(() => onVRSessionEnd({session: sessionToClean}));
} else {
onVRSessionEnd({session: null}); // Call with null session if already gone
}
});
} else if (is2DMode) {
// 2D mode - stay on last frame with controls visible
handle2DVideoEnd();
} else {
// Regular mode - reset to original state
if (!mediaController) {
resetToOriginalState();
return;
}
mediaController.handleEnded({
cleanupFailedVrExit,
exitVr: actualSessionToggle,
isIn2DMode: is2DModeActive,
isInVr: () => Boolean(xrSession && renderer && renderer.xr.isPresenting),
on2DEnded: handle2DVideoEnd,
resetToOriginalState
});
}
function cleanupFailedVrExit() {
if (xrSession) {
const sessionToClean = xrSession;
xrSession = null;
sessionToClean.removeEventListener('end', onVRSessionEnd);
sessionToClean.end().catch(e => {}).finally(() => onVRSessionEnd({session: sessionToClean}));
} else {
onVRSessionEnd({session: null});
}
}
@@ -559,31 +413,27 @@ function onSelectStartVR(event) {
if (xrSession) actualSessionToggle();
},
forward: () => {
if (video && isFinite(video.duration)) {
video.currentTime = Math.min(video.duration, video.currentTime + 15);
updateSeekBarAppearance();
}
mediaController?.forward();
updateSeekBarAppearance();
},
hidePanel,
isPanelVisible: () => vrPanelVisibility.isVisible,
raycaster,
rewind: () => {
if (video) {
video.currentTime = Math.max(0, video.currentTime - 15);
updateSeekBarAppearance();
}
mediaController?.rewind();
updateSeekBarAppearance();
},
seek: (progress) => {
if (video && isFinite(video.duration)) {
video.currentTime = progress * video.duration;
updateSeekBarAppearance();
}
mediaController?.seekToProgress(progress);
updateSeekBarAppearance();
},
showPanel,
toggleMute: () => {
if (video) video.muted = !video.muted;
mediaController?.toggleMute();
},
togglePlayPause: () => {
mediaController?.togglePlayPause();
},
togglePlayPause,
uiElements,
vrPanel
});
@@ -596,7 +446,7 @@ async function handleEnterVRButtonClick() {
}
// Hide the play button after click
hidePlayButton();
mediaController?.hidePlayButton();
// Check if VR is supported
if (playBtn.dataset.xrSupported === "true") {
@@ -604,110 +454,10 @@ async function handleEnterVRButtonClick() {
await actualSessionToggle();
} else {
// VR is not supported - start 2D rectilinear mode
start2DMode();
twoDMode?.start();
}
}
function start2DMode() {
if (!video || !renderer || !camera2D) {
console.error("Required components not available for 2D mode");
return;
}
// Set 2D mode flag
is2DMode = true;
// Calculate canvas size based on container dimensions (same logic as onWindowResize)
const container = playerContainer;
if (container) {
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
// Calculate height based on 16:9 aspect ratio
const aspectRatio = 16 / 9;
const calculatedHeight = containerWidth / aspectRatio;
// Ensure minimum dimensions to prevent zero-sized canvas
const canvasWidth = Math.max(containerWidth, 320);
const canvasHeight = Math.max(calculatedHeight, 180);
// Resize renderer with calculated dimensions
renderer.setSize(canvasWidth, canvasHeight);
// Update 2D camera aspect ratio
camera2D.aspect = canvasWidth / canvasHeight;
camera2D.updateProjectionMatrix();
}
// Position the canvas to match the video element
const canvas = renderer.domElement;
canvas.style.position = 'relative';
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.aspectRatio = '16/9';
// Hide HTML video element and show WebGL canvas
video.style.display = 'none';
canvas.style.display = '';
// Create video texture if not exists
videoTexture = createVideoTexture();
positionPlaneForPresentation(projectionMode === 'plane');
// Apply texture to the selected projection mesh and make it visible
if (sphereMaterial && activeContentMesh) {
sphereMaterial.map = videoTexture;
sphereMaterial.needsUpdate = true;
showActiveContentMesh();
}
// Start video playback
togglePlayPause();
// Add event listeners for 2D controls
add2DEventListeners();
// Add fullscreen event listeners to handle resize properly
document.addEventListener('fullscreenchange', onFullscreenChange);
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
document.addEventListener('mozfullscreenchange', onFullscreenChange);
document.addEventListener('MSFullscreenChange', onFullscreenChange);
// Show 2D control panel
show2DControlPanel();
// Position control panel relative to canvas
position2DControlPanel();
// Start 2D render loop
render2D();
}
function add2DEventListeners() {
fallbackCameraControls?.addEventListeners(renderer.domElement, projectionMode);
}
function remove2DEventListeners() {
if (!renderer || !renderer.domElement) return;
fallbackCameraControls?.removeEventListeners(renderer.domElement);
// Fullscreen events
document.removeEventListener('fullscreenchange', onFullscreenChange);
document.removeEventListener('webkitfullscreenchange', onFullscreenChange);
document.removeEventListener('mozfullscreenchange', onFullscreenChange);
document.removeEventListener('MSFullscreenChange', onFullscreenChange);
}
function onFullscreenChange() {
if (!is2DMode) return;
// Trigger resize handling when fullscreen state changes
setTimeout(() => {
onWindowResize();
}, 100); // Small delay to ensure fullscreen transition is complete
}
async function actualSessionToggle() {
if (!renderer || !renderer.isWebGLRenderer) {
console.error("CRITICAL_ERROR: actualSessionToggle: renderer is NOT a WebGLRenderer or is null!", renderer);
@@ -741,9 +491,9 @@ async function actualSessionToggle() {
video.style.display = 'none';
}
if (video && (video.paused || video.ended)) {
if (mediaController && video && (video.paused || video.ended)) {
try {
await video.play();
await mediaController.play();
} catch (playError) {
console.error("Failed to play video after obtaining XR session:", playError);
}
@@ -818,9 +568,7 @@ function onVRSessionEnd(event) {
video.style.display = '';
}
if (video && !video.paused) {
video.pause();
}
mediaController?.pauseIfPlaying();
if (sphereMaterial && sphereMaterial.map) {
sphereMaterial.map.dispose();