import { PLANE_2D_DISTANCE, PLANE_DISTANCE, type ProjectionMode } from './config.js'; import { bootstrapPlayer } from './bootstrap.js'; import { createContentScene } from './rendering/content-scene.js'; import { applySbsTextureWindow as applySbsTextureWindowCore, hideContentMeshes as hideContentMeshesCore, positionPlaneForPresentation as positionPlaneForPresentationCore, showActiveContentMesh as showActiveContentMeshCore } from './rendering/projection.js'; import { createMediaTexture as createMediaTextureCore } from './rendering/three-utils.js'; import { FallbackCameraControls } from './modes/fallback-camera-controls.js'; import { MediaController } from './media/media-controller.js'; import { createVrController, handleVrControllerSelect } from './xr/vr-controller-interactions.js'; import { bindVideoEvents } from './media/video-events.js'; import { createVrControlPanel, type VrControlPanel, updateVrPlayPauseButtonIcon, updateVrSeekBarAppearance, updateVrVolumeButtonIcon } from './xr/vr-control-panel.js'; import { VrPanelVisibility } from './xr/vr-panel-visibility.js'; import { TwoDMode } from './modes/two-d-mode.js'; import { createPlayerRenderer, resizePlayerRenderer } from './rendering/renderer-lifecycle.js'; import { MediaTextureManager } from './rendering/texture-manager.js'; import type { SupportedMediaAdapter } from './media/media-adapter.js'; const _playerBase = new URL('.', import.meta.url).href; let playerContainer, projectionMode: ProjectionMode; let scene, camera, renderer, video, sphereMaterial; let vr180Mesh, planeMesh, activeContentMesh; let xrSession = null; let raycaster, uiElements = []; let mediaAdapter: SupportedMediaAdapter | undefined; let playBtn; let frameCounter = 0; let isXrLoopActive = false; let vrControlPanel; let mediaController: MediaController | undefined; let textureManager: MediaTextureManager | undefined; let vrPanel: VrControlPanel | undefined; let twoDMode: TwoDMode | undefined; const vrPanelVisibility = new VrPanelVisibility(); // 2D Camera Controls let camera2D; let fallbackCameraControls: FallbackCameraControls | undefined; bootstrapPlayer(_playerBase, (context) => { playerContainer = context.playerContainer; projectionMode = context.projectionMode; mediaAdapter = context.mediaAdapter; video = mediaAdapter.kind === 'video' ? mediaAdapter.element : undefined; playBtn = context.playButton; init(); }); function applySbsTextureWindow(renderingRenderer, activeCamera, material) { applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DModeActive()); } function hideContentMeshes() { hideContentMeshesCore(vr180Mesh, planeMesh); } function showActiveContentMesh() { showActiveContentMeshCore(vr180Mesh, planeMesh, activeContentMesh); } function positionPlaneForPresentation(isFallback2D = false) { positionPlaneForPresentationCore(planeMesh, camera2D, isFallback2D, PLANE_DISTANCE, PLANE_2D_DISTANCE); } function createMediaTexture() { if (!textureManager) { throw new Error('Media texture manager is not initialized.'); } return textureManager.create(); } 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 (mediaAdapter && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) { textureManager?.assignToMaterial(sphereMaterial); updateVRPlayPauseButtonIcon(); updateVRVolumeButtonIcon(); console.log("Re-initialized media texture after context restoration during VR."); } } function getMediaTitle() { return mediaAdapter?.getTitle() || 'Media Title'; } function init() { try { const playerRenderer = createPlayerRenderer(playerContainer, { closeActiveXrSession: closeActiveXrSessionAfterContextLoss, hasActiveXrSession: () => !!xrSession, restoreAfterContextRestored: restoreVideoTextureAfterContextRestored }); scene = playerRenderer.scene; camera = playerRenderer.camera; renderer = playerRenderer.renderer; if (!mediaAdapter) { throw new Error('Media adapter is not initialized.'); } video = mediaAdapter.kind === 'video' ? mediaAdapter.element : undefined; textureManager = new MediaTextureManager( mediaAdapter.textureSource, createMediaTextureCore, () => mediaAdapter?.shouldUpdateTexture() ?? false ); mediaController = video ? new MediaController({ is2DModeActive, on2DPlaybackResume: show2DControlPanel, playButton: playBtn, video }) : undefined; const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => { applySbsTextureWindow(renderer, activeCamera, material); }); sphereMaterial = contentScene.material; vr180Mesh = contentScene.vr180Mesh; planeMesh = contentScene.planeMesh; activeContentMesh = contentScene.activeContentMesh; uiElements.push(activeContentMesh); camera2D = contentScene.fallbackCamera; fallbackCameraControls = new FallbackCameraControls(camera2D, { hideControls: hide2DControlPanel, isEnabled: is2DModeActive, showControls: show2DControlPanel }); twoDMode = new TwoDMode({ callbacks: { createMediaTexture, forward: () => mediaController?.forward(), positionPlaneForPresentation, rewind: () => mediaController?.rewind(), seekToProgress: (progress) => mediaController?.seekToProgress(progress), showActiveContentMesh, toggleMute: () => mediaController?.toggleMute(), togglePlayPause: () => mediaController?.togglePlayPause() }, fullscreenTarget: playerContainer, mediaCapabilities: mediaAdapter.capabilities, getActiveContentMesh: () => activeContentMesh, getCamera: () => camera2D, getCameraControls: () => fallbackCameraControls, getMaterial: () => sphereMaterial, getMediaElement: () => mediaAdapter?.element, getRenderer: () => renderer, getScene: () => scene, getVideo: () => video, playerContainer, projectionMode, title: getMediaTitle() }); } catch (e) { console.error("INIT_ERROR (Phase 1 - Core Setup):", e); renderer = null; return; } try { // Phase 2: VR Control Panel UI vrPanel = createVrControlPanel(scene, getMediaTitle(), mediaAdapter?.capabilities); vrControlPanel = vrPanel.group; vrPanelVisibility.setPanel(vrPanel); uiElements.push(...vrPanel.interactables); raycaster = createVrController(scene, renderer, onSelectStartVR).raycaster; } catch (e) { console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e); } try { // Phase 3: Event Listeners if (playBtn) { playBtn.addEventListener('click', handleEnterVRButtonClick); } window.addEventListener('resize', onWindowResize); if (video) { bindVideoEvents({ onEnded: onVideoEnded, onPlaybackStateChange: () => { updateVRPlayPauseButtonIcon(); update2DPlayPauseButton(); }, onTimelineChange: () => { updateSeekBarAppearance(); update2DControlPanel(); }, onVolumeChange: () => { updateVRVolumeButtonIcon(); update2DMuteButton(); }, playButton: playBtn, video }); } } catch (e) { console.error("INIT_ERROR (Phase 3 - Event Listeners):", e); } } function updateVRPlayPauseButtonIcon() { if (!video) { return; } updateVrPlayPauseButtonIcon(vrPanel, video.paused || video.ended); } function updateVRVolumeButtonIcon() { if (!video) { return; } updateVrVolumeButtonIcon(vrPanel, video.muted || video.volume === 0); } function updateSeekBarAppearance() { const progress = video && isFinite(video.duration) && video.duration > 0 ? video.currentTime / video.duration : null; updateVrSeekBarAppearance(vrPanel, progress); } function animatePanelFade(timestamp) { vrPanelVisibility.updateFade(timestamp); } function showPanel() { vrPanelVisibility.show(); } function hidePanel() { vrPanelVisibility.hide(); } function onWindowResize() { if (!renderer) return; if (twoDMode?.resize()) return; resizePlayerRenderer({ camera, camera2D, is2DMode: false, onFallbackResize: () => {}, playerContainer, renderer }); } function show2DControlPanel() { twoDMode?.showControls(); } function hide2DControlPanel() { twoDMode?.hideControls(); } function update2DControlPanel() { twoDMode?.updateTimeline(); } function update2DPlayPauseButton() { twoDMode?.updatePlaybackButton(); } function update2DMuteButton() { twoDMode?.updateMuteButton(); } function handle2DVideoEnd() { twoDMode?.handleVideoEnd(); } function resetToOriginalState() { if (mediaController) { mediaController.resetToOriginalState(); } else if (playBtn) { playBtn.classList.remove('hidden'); playBtn.disabled = false; } if (twoDMode?.isActive) { twoDMode.stop(); onWindowResize(); } } function onVideoEnded() { 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}); } } function onSelectStartVR(event) { handleVrControllerSelect(event, { exitVr: () => { if (xrSession) actualSessionToggle(); }, forward: () => { mediaController?.forward(); updateSeekBarAppearance(); }, hidePanel, isPanelVisible: () => vrPanelVisibility.isVisible, raycaster, rewind: () => { mediaController?.rewind(); updateSeekBarAppearance(); }, seek: (progress) => { mediaController?.seekToProgress(progress); updateSeekBarAppearance(); }, showPanel, toggleMute: () => { mediaController?.toggleMute(); }, togglePlayPause: () => { mediaController?.togglePlayPause(); }, uiElements, vrPanel }); } async function handleEnterVRButtonClick() { if (!mediaAdapter) { console.error("Media element not found for VR button click."); return; } hideEnterButton(); // Check if VR is supported if (playBtn.dataset.xrSupported === "true") { // VR is supported - use VR functionality await actualSessionToggle(); } else { // VR is not supported - start 2D rectilinear mode twoDMode?.start(); } } async function actualSessionToggle() { if (!renderer || !renderer.isWebGLRenderer) { console.error("CRITICAL_ERROR: actualSessionToggle: renderer is NOT a WebGLRenderer or is null!", renderer); return; } if (xrSession) { // --- EXITING VR --- const sessionToClose = xrSession; xrSession = null; if (vrControlPanel) { vrPanelVisibility.hideImmediately(); } sessionToClose.end().catch(err => { console.error("Error calling .end() on session:", err); onVRSessionEnd({ session: sessionToClose }); }); } else { // --- ENTERING VR --- try { const session = await navigator.xr.requestSession('immersive-vr', { requiredFeatures: ['local-floor'], }); if (!session) { throw new Error("requestSession returned no session."); } xrSession = session; xrSession.addEventListener('end', onVRSessionEnd); mediaAdapter?.hideElement(); if (mediaController && video && (video.paused || video.ended)) { try { await mediaController.play(); } catch (playError) { console.error("Failed to play video after obtaining XR session:", playError); } } if (camera) camera.updateProjectionMatrix(); positionPlaneForPresentation(false); textureManager?.dispose(); if (!mediaAdapter) { throw new Error("Media adapter not available for creating texture."); } if (!activeContentMesh || !sphereMaterial) { throw new Error("VR mesh components not ready for texture."); } if (!textureManager) { throw new Error("Media texture manager is not initialized."); } textureManager.assignToMaterial(sphereMaterial); showActiveContentMesh(); updateVRPlayPauseButtonIcon(); updateVRVolumeButtonIcon(); if (vrControlPanel) { vrPanelVisibility.hideImmediately(); } await renderer.xr.setSession(xrSession); isXrLoopActive = true; renderer.setAnimationLoop(renderXR); frameCounter = 0; } catch (err) { const sessionStartError = "XR_ERROR: Failed to start VR session: " + (err.message || String(err)); console.error(sessionStartError, err); isXrLoopActive = false; hideContentMeshes(); textureManager?.clearMaterial(sphereMaterial); if (vrControlPanel) { vrPanelVisibility.hideImmediately(); } if (xrSession) { xrSession.removeEventListener('end', onVRSessionEnd); const tempSession = xrSession; xrSession = null; tempSession.end().catch(e => {}).finally(() => { onVRSessionEnd({session: tempSession}); }); } else { onVRSessionEnd({session: null}); } if (renderer && renderer.getAnimationLoop && renderer.getAnimationLoop()) { renderer.setAnimationLoop(null); } } } } function hideEnterButton() { if (mediaController) { mediaController.hidePlayButton(); return; } playBtn?.classList.add('hidden'); } function onVRSessionEnd(event) { const endedSession = event.session; isXrLoopActive = false; if (renderer) { if (renderer.getAnimationLoop && renderer.getAnimationLoop()) { renderer.setAnimationLoop(null); } } mediaAdapter?.showElement(); mediaController?.pauseIfPlaying(); textureManager?.clearMaterial(sphereMaterial); hideContentMeshes(); if (vrControlPanel) { vrPanelVisibility.hideImmediately(); } if (endedSession && typeof endedSession.removeEventListener === 'function') { endedSession.removeEventListener('end', onVRSessionEnd); } if (xrSession === endedSession || xrSession === null) { xrSession = null; } else if (xrSession && endedSession) { console.warn("onVRSessionEnd: Global xrSession was different from the endedSession. Global xrSession:", xrSession, "Ended session:", endedSession); xrSession = null; } // Reset to original state when exiting VR resetToOriginalState(); onWindowResize(); } function renderXR(timestamp, frame) { if (!isXrLoopActive) { return; } frameCounter++; if (!renderer || !renderer.xr || !renderer.xr.isPresenting) { console.warn("renderXR called but not in a valid XR presenting state. Stopping loop."); isXrLoopActive = false; if (renderer && renderer.getAnimationLoop && renderer.getAnimationLoop()) { renderer.setAnimationLoop(null); } return; } if (vrPanelVisibility.isFading) { animatePanelFade(timestamp); } if (!frame) { console.warn("renderXR called without an XRFrame. Skipping render."); return; } if (frameCounter > 0 && frameCounter % 3600 === 0) { const gl = renderer.getContext(); const error = gl.getError(); if (error !== gl.NO_ERROR) { console.error(`WEBGL_ERROR_IN_RENDER_LOOP (F${frameCounter}):`, error, gl.enumToString ? gl.enumToString(error) : error); } } try { textureManager?.updateIfNeeded(); renderer.render(scene, camera); } catch (error) { const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error)); console.error(renderErrorMsg, error); console.error("Render loop error. Attempting to exit VR."); isXrLoopActive = false; const sessionToCloseOnError = xrSession; xrSession = null; if (sessionToCloseOnError) { sessionToCloseOnError.removeEventListener('end', onVRSessionEnd); sessionToCloseOnError.end().catch(e => { console.error("Error trying to end session after render loop crash:", e); }).finally(() => { onVRSessionEnd({ session: sessionToCloseOnError }); }); } else { onVRSessionEnd({ session: null }); } } }