forked from EXT/VR180-Web-Player
662 lines
18 KiB
TypeScript
662 lines
18 KiB
TypeScript
import {
|
|
DEFAULT_PROJECTION,
|
|
PLANE_2D_DISTANCE,
|
|
PLANE_DISTANCE,
|
|
PLAYER_SELECTOR,
|
|
type ProjectionMode,
|
|
VALID_PROJECTIONS
|
|
} from './config.js';
|
|
import { createContentScene } from './content-scene.js';
|
|
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom.js';
|
|
import {
|
|
applySbsTextureWindow as applySbsTextureWindowCore,
|
|
hideContentMeshes as hideContentMeshesCore,
|
|
positionPlaneForPresentation as positionPlaneForPresentationCore,
|
|
showActiveContentMesh as showActiveContentMeshCore
|
|
} 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
|
|
} from './vr-controller-interactions.js';
|
|
import { bindVideoEvents } from './video-events.js';
|
|
import {
|
|
createVrControlPanel,
|
|
type VrControlPanel,
|
|
updateVrPlayPauseButtonIcon,
|
|
updateVrSeekBarAppearance,
|
|
updateVrVolumeButtonIcon
|
|
} from './vr-control-panel.js';
|
|
import { VrPanelVisibility } from './vr-panel-visibility.js';
|
|
import { TwoDMode } from './two-d-mode.js';
|
|
import {
|
|
createPlayerRenderer,
|
|
resizePlayerRenderer
|
|
} from './renderer-lifecycle.js';
|
|
|
|
const _playerBase = new URL('.', import.meta.url).href;
|
|
|
|
let playerContainer, projectionMode: ProjectionMode = DEFAULT_PROJECTION;
|
|
let scene, camera, renderer, video, videoTexture, sphereMaterial;
|
|
let vr180Mesh, planeMesh, activeContentMesh;
|
|
let xrSession = null;
|
|
let raycaster, uiElements = [];
|
|
let videoElement, playBtn;
|
|
let frameCounter = 0;
|
|
|
|
let isXrLoopActive = false;
|
|
let vrControlPanel;
|
|
let mediaController: MediaController | undefined;
|
|
let vrPanel: VrControlPanel | undefined;
|
|
let twoDMode: TwoDMode | undefined;
|
|
const vrPanelVisibility = new VrPanelVisibility();
|
|
|
|
// 2D Camera Controls
|
|
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();
|
|
}
|
|
});
|
|
|
|
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 createVideoTexture() {
|
|
if (videoTexture) videoTexture.dispose();
|
|
videoTexture = createVideoTextureCore(video);
|
|
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, ' ') ||
|
|
"Video Title";
|
|
}
|
|
|
|
|
|
function init() {
|
|
try {
|
|
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);
|
|
});
|
|
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: 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;
|
|
return;
|
|
}
|
|
|
|
try { // Phase 2: VR Control Panel UI
|
|
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
|
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 (!video) {
|
|
console.error("Video element not found for VR button click.");
|
|
return;
|
|
}
|
|
|
|
// Hide the play button after click
|
|
mediaController?.hidePlayButton();
|
|
|
|
// 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);
|
|
|
|
|
|
// Hide the regular video element when entering VR
|
|
if (video) {
|
|
video.style.display = 'none';
|
|
}
|
|
|
|
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);
|
|
|
|
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 {
|
|
throw new Error("Video element not available for creating texture.");
|
|
}
|
|
|
|
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();
|
|
if (sphereMaterial) { sphereMaterial.map = null; sphereMaterial.needsUpdate = true; }
|
|
if (videoTexture) { videoTexture.dispose(); videoTexture = null; }
|
|
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 onVRSessionEnd(event) {
|
|
const endedSession = event.session;
|
|
|
|
isXrLoopActive = false;
|
|
if (renderer) {
|
|
if (renderer.getAnimationLoop && renderer.getAnimationLoop()) {
|
|
renderer.setAnimationLoop(null);
|
|
}
|
|
}
|
|
|
|
// Show the regular video element when exiting VR
|
|
if (video) {
|
|
video.style.display = '';
|
|
}
|
|
|
|
mediaController?.pauseIfPlaying();
|
|
|
|
if (sphereMaterial && sphereMaterial.map) {
|
|
sphereMaterial.map.dispose();
|
|
sphereMaterial.map = null;
|
|
sphereMaterial.needsUpdate = true;
|
|
}
|
|
if (videoTexture) {
|
|
videoTexture.dispose(); videoTexture = null;
|
|
}
|
|
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 {
|
|
// Sync video texture before render to ensure frame consistency
|
|
if (videoTexture && video && !video.paused && !video.ended) {
|
|
videoTexture.needsUpdate = true;
|
|
}
|
|
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 });
|
|
}
|
|
}
|
|
}
|