forked from EXT/VR180-Web-Player
686 lines
21 KiB
TypeScript
686 lines
21 KiB
TypeScript
import {
|
|
PLANE_2D_DISTANCE,
|
|
PLANE_DISTANCE,
|
|
type HeadLockMode,
|
|
type ProjectionMode
|
|
} from './config.js';
|
|
import { bootstrapPlayer, type BootstrapContext } from './bootstrap.js';
|
|
import { createContentScene } from './rendering/content-scene.js';
|
|
import {
|
|
applyHeadPositionLock as applyHeadPositionLockCore,
|
|
applySbsTextureWindow as applySbsTextureWindowCore,
|
|
hideContentMeshes as hideContentMeshesCore,
|
|
positionPlaneForPresentation as positionPlaneForPresentationCore,
|
|
resetHeadPositionLockedContent as resetHeadPositionLockedContentCore,
|
|
shouldLockContentToHeadPosition,
|
|
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 { createVrInputRig } from './xr/input-rig.js';
|
|
import { handleVrControllerSelect } from './xr/vr-controller-interactions.js';
|
|
import { bindVideoEvents } from './media/video-events.js';
|
|
import {
|
|
createVrControlPanel,
|
|
type VrControlPanel,
|
|
updateVrLoopButtonIcon,
|
|
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';
|
|
|
|
export class PlayerSession {
|
|
private readonly headLockMode: HeadLockMode;
|
|
private readonly mediaAdapter: SupportedMediaAdapter;
|
|
private readonly playBtn: HTMLButtonElement;
|
|
private readonly playerContainer: HTMLElement;
|
|
private readonly projectionMode: ProjectionMode;
|
|
private readonly uiElements: any[] = [];
|
|
private readonly vrPanelVisibility = new VrPanelVisibility();
|
|
private readonly handleVrSessionEnd = (event: any) => this.onVRSessionEnd(event);
|
|
private readonly renderXrFrame = (timestamp: number, frame: any) => this.renderXR(timestamp, frame);
|
|
|
|
private activeContentMesh: any;
|
|
private camera: any;
|
|
private camera2D: any;
|
|
private fallbackCameraControls: FallbackCameraControls | undefined;
|
|
private frameCounter = 0;
|
|
private isXrLoopActive = false;
|
|
private mediaController: MediaController | undefined;
|
|
private planeMesh: any;
|
|
private raycaster: any;
|
|
private renderer: any;
|
|
private scene: any;
|
|
private sphereMaterial: any;
|
|
private textureManager: MediaTextureManager<HTMLImageElement | HTMLVideoElement> | undefined;
|
|
private twoDMode: TwoDMode | undefined;
|
|
private video: HTMLVideoElement | undefined;
|
|
private vr180Mesh: any;
|
|
private vrControlPanel: any;
|
|
private vrPanel: VrControlPanel | undefined;
|
|
private xrInputRig: any;
|
|
private xrSession: any = null;
|
|
|
|
constructor(context: BootstrapContext) {
|
|
this.playerContainer = context.playerContainer;
|
|
this.projectionMode = context.projectionMode;
|
|
this.headLockMode = context.headLockMode;
|
|
this.mediaAdapter = context.mediaAdapter;
|
|
this.video = context.mediaAdapter.kind === 'video' ? context.mediaAdapter.element : undefined;
|
|
this.playBtn = context.playButton;
|
|
}
|
|
|
|
init(): void {
|
|
try {
|
|
const playerRenderer = createPlayerRenderer(this.playerContainer, {
|
|
closeActiveXrSession: () => this.closeActiveXrSessionAfterContextLoss(),
|
|
hasActiveXrSession: () => !!this.xrSession,
|
|
restoreAfterContextRestored: () => this.restoreVideoTextureAfterContextRestored()
|
|
});
|
|
this.scene = playerRenderer.scene;
|
|
this.camera = playerRenderer.camera;
|
|
this.renderer = playerRenderer.renderer;
|
|
|
|
this.video = this.mediaAdapter.kind === 'video' ? this.mediaAdapter.element : undefined;
|
|
this.textureManager = new MediaTextureManager(
|
|
this.mediaAdapter.textureSource,
|
|
createMediaTextureCore,
|
|
() => this.mediaAdapter.shouldUpdateTexture()
|
|
);
|
|
this.mediaController = this.video
|
|
? new MediaController({
|
|
is2DModeActive: () => this.is2DModeActive(),
|
|
on2DPlaybackResume: () => this.show2DControlPanel(),
|
|
playButton: this.playBtn,
|
|
video: this.video
|
|
})
|
|
: undefined;
|
|
|
|
const contentScene = createContentScene(this.scene, this.projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
|
this.applySbsTextureWindow(renderer, activeCamera, material);
|
|
});
|
|
this.sphereMaterial = contentScene.material;
|
|
this.vr180Mesh = contentScene.vr180Mesh;
|
|
this.planeMesh = contentScene.planeMesh;
|
|
this.activeContentMesh = contentScene.activeContentMesh;
|
|
this.uiElements.push(this.activeContentMesh);
|
|
|
|
this.camera2D = contentScene.fallbackCamera;
|
|
this.fallbackCameraControls = new FallbackCameraControls(this.camera2D, {
|
|
hideControls: () => this.hide2DControlPanel(),
|
|
isEnabled: () => this.is2DModeActive(),
|
|
showControls: () => this.show2DControlPanel()
|
|
});
|
|
this.twoDMode = new TwoDMode({
|
|
callbacks: {
|
|
createMediaTexture: () => this.createMediaTexture(),
|
|
forward: () => this.navigateForward(),
|
|
getIsLooping: () => this.mediaController?.isLooping() ?? false,
|
|
positionPlaneForPresentation: (isFallback2D) => this.positionPlaneForPresentation(isFallback2D),
|
|
rewind: () => this.navigateBackward(),
|
|
seekToProgress: (progress) => this.mediaController?.seekToProgress(progress),
|
|
showActiveContentMesh: () => this.showActiveContentMesh(),
|
|
toggleLoop: () => this.toggleLoop(),
|
|
toggleMute: () => this.mediaController?.toggleMute(),
|
|
togglePlayPause: () => this.mediaController?.togglePlayPause()
|
|
},
|
|
fullscreenTarget: this.playerContainer,
|
|
mediaCapabilities: this.mediaAdapter.capabilities,
|
|
getActiveContentMesh: () => this.activeContentMesh,
|
|
getCamera: () => this.camera2D,
|
|
getCameraControls: () => this.fallbackCameraControls,
|
|
getMaterial: () => this.sphereMaterial,
|
|
getMediaElement: () => this.mediaAdapter.element,
|
|
getRenderer: () => this.renderer,
|
|
getScene: () => this.scene,
|
|
getVideo: () => this.video,
|
|
playerContainer: this.playerContainer,
|
|
projectionMode: this.projectionMode,
|
|
title: this.getMediaTitle()
|
|
});
|
|
} catch (e) {
|
|
console.error('INIT_ERROR (Phase 1 - Core Setup):', e);
|
|
this.renderer = null;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.vrPanel = createVrControlPanel(this.scene, this.getMediaTitle(), this.mediaAdapter.capabilities);
|
|
this.vrControlPanel = this.vrPanel.group;
|
|
this.vrPanelVisibility.setPanel(this.vrPanel);
|
|
this.uiElements.push(...this.vrPanel.interactables);
|
|
|
|
this.xrInputRig = createVrInputRig(this.scene, this.renderer, (event) => this.onSelectStartVR(event));
|
|
this.raycaster = this.xrInputRig.raycaster;
|
|
} catch (e) {
|
|
console.error('INIT_ERROR (Phase 2 - VR Controls Setup):', e);
|
|
}
|
|
|
|
try {
|
|
this.playBtn.addEventListener('click', () => {
|
|
void this.handleEnterVRButtonClick();
|
|
});
|
|
window.addEventListener('resize', () => this.onWindowResize());
|
|
|
|
if (this.video) {
|
|
bindVideoEvents({
|
|
onEnded: () => this.onVideoEnded(),
|
|
onPlaybackStateChange: () => {
|
|
this.updateVRPlayPauseButtonIcon();
|
|
this.update2DPlayPauseButton();
|
|
},
|
|
onTimelineChange: () => {
|
|
this.updateSeekBarAppearance();
|
|
this.update2DControlPanel();
|
|
},
|
|
onVolumeChange: () => {
|
|
this.updateVRVolumeButtonIcon();
|
|
this.update2DMuteButton();
|
|
},
|
|
playButton: this.playBtn,
|
|
video: this.video
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('INIT_ERROR (Phase 3 - Event Listeners):', e);
|
|
}
|
|
}
|
|
|
|
private applySbsTextureWindow(renderingRenderer: any, activeCamera: any, material: any): void {
|
|
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, this.is2DModeActive());
|
|
}
|
|
|
|
private hideContentMeshes(): void {
|
|
hideContentMeshesCore(this.vr180Mesh, this.planeMesh);
|
|
}
|
|
|
|
private showActiveContentMesh(): void {
|
|
showActiveContentMeshCore(this.vr180Mesh, this.planeMesh, this.activeContentMesh);
|
|
}
|
|
|
|
private positionPlaneForPresentation(isFallback2D = false): void {
|
|
positionPlaneForPresentationCore(this.planeMesh, this.camera2D, isFallback2D, PLANE_DISTANCE, PLANE_2D_DISTANCE);
|
|
}
|
|
|
|
private updateHeadPositionLock(): void {
|
|
if (!this.renderer?.xr?.isPresenting || !this.activeContentMesh) {
|
|
return;
|
|
}
|
|
|
|
const xrCamera = this.renderer.xr.getCamera?.(this.camera) || this.camera;
|
|
applyHeadPositionLockCore(
|
|
this.activeContentMesh,
|
|
xrCamera,
|
|
this.projectionMode,
|
|
shouldLockContentToHeadPosition(this.headLockMode, this.projectionMode),
|
|
PLANE_DISTANCE
|
|
);
|
|
}
|
|
|
|
private resetHeadPositionLock(): void {
|
|
resetHeadPositionLockedContentCore(this.vr180Mesh, this.planeMesh, PLANE_DISTANCE);
|
|
}
|
|
|
|
private createMediaTexture(): any {
|
|
if (!this.textureManager) {
|
|
throw new Error('Media texture manager is not initialized.');
|
|
}
|
|
return this.textureManager.create();
|
|
}
|
|
|
|
private refreshMediaTexture(): void {
|
|
if (!this.textureManager || !this.sphereMaterial) {
|
|
return;
|
|
}
|
|
|
|
this.textureManager.setSource(this.mediaAdapter.textureSource);
|
|
this.textureManager.assignToMaterial(this.sphereMaterial);
|
|
|
|
if (this.renderer?.xr?.isPresenting || this.twoDMode?.isActive) {
|
|
this.mediaAdapter.hideElement();
|
|
}
|
|
}
|
|
|
|
private navigateForward(): void {
|
|
if (this.mediaAdapter.next?.()) {
|
|
this.refreshMediaTexture();
|
|
return;
|
|
}
|
|
|
|
this.mediaController?.forward();
|
|
this.updateSeekBarAppearance();
|
|
}
|
|
|
|
private navigateBackward(): void {
|
|
if (this.mediaAdapter.previous?.()) {
|
|
this.refreshMediaTexture();
|
|
return;
|
|
}
|
|
|
|
this.mediaController?.rewind();
|
|
this.updateSeekBarAppearance();
|
|
}
|
|
|
|
private is2DModeActive(): boolean {
|
|
return this.twoDMode?.isActive ?? false;
|
|
}
|
|
|
|
private closeActiveXrSessionAfterContextLoss(): void {
|
|
if (!this.xrSession) return;
|
|
|
|
const sessionToClose = this.xrSession;
|
|
this.xrSession = null;
|
|
sessionToClose.removeEventListener('end', this.handleVrSessionEnd);
|
|
sessionToClose.end().catch((e) => {
|
|
console.error('Error ending session on context lost:', e);
|
|
}).finally(() => {
|
|
this.onVRSessionEnd({ session: sessionToClose });
|
|
});
|
|
}
|
|
|
|
private restoreVideoTextureAfterContextRestored(): void {
|
|
if (this.sphereMaterial && this.activeContentMesh && this.activeContentMesh.visible && this.renderer.xr.isPresenting && this.xrSession) {
|
|
this.textureManager?.assignToMaterial(this.sphereMaterial);
|
|
this.updateVRPlayPauseButtonIcon();
|
|
this.updateVRVolumeButtonIcon();
|
|
console.log('Re-initialized media texture after context restoration during VR.');
|
|
}
|
|
}
|
|
|
|
private getMediaTitle(): string {
|
|
return this.mediaAdapter.getTitle() || 'Media Title';
|
|
}
|
|
|
|
private updateVRPlayPauseButtonIcon(): void {
|
|
if (!this.video) {
|
|
return;
|
|
}
|
|
updateVrPlayPauseButtonIcon(this.vrPanel, this.video.paused || this.video.ended);
|
|
}
|
|
|
|
private updateVRLoopButtonIcon(): void {
|
|
updateVrLoopButtonIcon(this.vrPanel, this.mediaController?.isLooping() ?? false);
|
|
}
|
|
|
|
private updateVRVolumeButtonIcon(): void {
|
|
if (!this.video) {
|
|
return;
|
|
}
|
|
updateVrVolumeButtonIcon(this.vrPanel, this.video.muted || this.video.volume === 0);
|
|
}
|
|
|
|
private updateSeekBarAppearance(): void {
|
|
const progress = this.video && isFinite(this.video.duration) && this.video.duration > 0
|
|
? this.video.currentTime / this.video.duration
|
|
: null;
|
|
updateVrSeekBarAppearance(this.vrPanel, progress);
|
|
}
|
|
|
|
private animatePanelFade(timestamp: number): void {
|
|
this.vrPanelVisibility.updateFade(timestamp);
|
|
}
|
|
|
|
private showPanel(): void {
|
|
this.vrPanelVisibility.show();
|
|
}
|
|
|
|
private showPanelPersistent(): void {
|
|
this.vrPanelVisibility.showPersistent();
|
|
}
|
|
|
|
private hidePanel(): void {
|
|
this.vrPanelVisibility.hide();
|
|
}
|
|
|
|
private getVisibleVrPanelInteractables(): any[] {
|
|
return this.vrPanelVisibility.isVisible ? (this.vrPanel?.interactables ?? []) : [];
|
|
}
|
|
|
|
private onWindowResize(): void {
|
|
if (!this.renderer) return;
|
|
|
|
if (this.twoDMode?.resize()) return;
|
|
|
|
resizePlayerRenderer({
|
|
camera: this.camera,
|
|
camera2D: this.camera2D,
|
|
is2DMode: false,
|
|
onFallbackResize: () => {},
|
|
playerContainer: this.playerContainer,
|
|
renderer: this.renderer
|
|
});
|
|
}
|
|
|
|
private show2DControlPanel(): void {
|
|
this.twoDMode?.showControls();
|
|
}
|
|
|
|
private hide2DControlPanel(): void {
|
|
this.twoDMode?.hideControls();
|
|
}
|
|
|
|
private update2DControlPanel(): void {
|
|
this.twoDMode?.updateTimeline();
|
|
}
|
|
|
|
private update2DPlayPauseButton(): void {
|
|
this.twoDMode?.updatePlaybackButton();
|
|
}
|
|
|
|
private update2DMuteButton(): void {
|
|
this.twoDMode?.updateMuteButton();
|
|
}
|
|
|
|
private toggleLoop(): boolean {
|
|
const isLooping = this.mediaController?.toggleLoop() ?? false;
|
|
this.updateVRLoopButtonIcon();
|
|
return isLooping;
|
|
}
|
|
|
|
private handle2DVideoEnd(): void {
|
|
this.twoDMode?.handleVideoEnd();
|
|
}
|
|
|
|
private handleVrVideoEnd(): void {
|
|
this.updateVRPlayPauseButtonIcon();
|
|
this.updateSeekBarAppearance();
|
|
this.showPanelPersistent();
|
|
}
|
|
|
|
private resetToOriginalState(): void {
|
|
if (this.mediaController) {
|
|
this.mediaController.resetToOriginalState();
|
|
} else {
|
|
this.playBtn.classList.remove('hidden');
|
|
this.playBtn.disabled = false;
|
|
}
|
|
|
|
if (this.twoDMode?.isActive) {
|
|
this.twoDMode.stop();
|
|
this.onWindowResize();
|
|
}
|
|
}
|
|
|
|
private onVideoEnded(): void {
|
|
if (!this.mediaController) {
|
|
this.resetToOriginalState();
|
|
return;
|
|
}
|
|
|
|
this.mediaController.handleEnded({
|
|
isIn2DMode: () => this.is2DModeActive(),
|
|
isInVr: () => Boolean(this.xrSession && this.renderer && this.renderer.xr.isPresenting),
|
|
on2DEnded: () => this.handle2DVideoEnd(),
|
|
onVrEnded: () => this.handleVrVideoEnd(),
|
|
resetToOriginalState: () => this.resetToOriginalState()
|
|
});
|
|
}
|
|
|
|
private onSelectStartVR(event: any): void {
|
|
handleVrControllerSelect(event, {
|
|
beginSeekDrag: (controller) => {
|
|
this.xrInputRig?.beginSeekDrag(controller, this.vrPanel, (progress) => {
|
|
this.mediaController?.seekToProgress(progress);
|
|
this.updateSeekBarAppearance();
|
|
});
|
|
},
|
|
exitVr: () => {
|
|
if (this.xrSession) void this.actualSessionToggle();
|
|
},
|
|
forward: () => {
|
|
this.navigateForward();
|
|
},
|
|
hidePanel: () => this.hidePanel(),
|
|
isPanelVisible: () => this.vrPanelVisibility.isVisible,
|
|
raycaster: this.raycaster,
|
|
rewind: () => {
|
|
this.navigateBackward();
|
|
},
|
|
seek: (progress) => {
|
|
this.mediaController?.seekToProgress(progress);
|
|
this.updateSeekBarAppearance();
|
|
},
|
|
showPanel: () => this.showPanel(),
|
|
toggleMute: () => {
|
|
this.mediaController?.toggleMute();
|
|
},
|
|
toggleLoop: () => this.toggleLoop(),
|
|
togglePlayPause: () => {
|
|
this.mediaController?.togglePlayPause();
|
|
},
|
|
uiElements: this.uiElements,
|
|
vrPanel: this.vrPanel
|
|
});
|
|
}
|
|
|
|
private async handleEnterVRButtonClick(): Promise<void> {
|
|
if (!this.mediaAdapter) {
|
|
console.error('Media element not found for VR button click.');
|
|
return;
|
|
}
|
|
|
|
this.hideEnterButton();
|
|
|
|
if (this.playBtn.dataset.xrSupported === 'true') {
|
|
await this.actualSessionToggle();
|
|
return;
|
|
}
|
|
|
|
this.resetHeadPositionLock();
|
|
this.twoDMode?.start();
|
|
}
|
|
|
|
private async actualSessionToggle(): Promise<void> {
|
|
if (!this.renderer || !this.renderer.isWebGLRenderer) {
|
|
console.error('CRITICAL_ERROR: actualSessionToggle: renderer is NOT a WebGLRenderer or is null!', this.renderer);
|
|
return;
|
|
}
|
|
|
|
if (this.xrSession) {
|
|
const sessionToClose = this.xrSession;
|
|
this.xrSession = null;
|
|
|
|
if (this.vrControlPanel) {
|
|
this.vrPanelVisibility.hideImmediately();
|
|
}
|
|
sessionToClose.end().catch((err) => {
|
|
console.error('Error calling .end() on session:', err);
|
|
this.onVRSessionEnd({ session: sessionToClose });
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const session = await navigator.xr.requestSession('immersive-vr', {
|
|
requiredFeatures: ['local-floor'],
|
|
optionalFeatures: ['hand-tracking']
|
|
});
|
|
if (!session) { throw new Error('requestSession returned no session.'); }
|
|
|
|
this.xrSession = session;
|
|
this.xrSession.addEventListener('end', this.handleVrSessionEnd);
|
|
|
|
this.mediaAdapter.hideElement();
|
|
|
|
if (this.mediaController && this.video && (this.video.paused || this.video.ended)) {
|
|
try {
|
|
await this.mediaController.play();
|
|
} catch (playError) {
|
|
console.error('Failed to play video after obtaining XR session:', playError);
|
|
}
|
|
}
|
|
|
|
if (this.camera) this.camera.updateProjectionMatrix();
|
|
this.positionPlaneForPresentation(false);
|
|
|
|
this.textureManager?.dispose();
|
|
if (!this.activeContentMesh || !this.sphereMaterial) {
|
|
throw new Error('VR mesh components not ready for texture.');
|
|
}
|
|
if (!this.textureManager) {
|
|
throw new Error('Media texture manager is not initialized.');
|
|
}
|
|
this.textureManager.assignToMaterial(this.sphereMaterial);
|
|
this.showActiveContentMesh();
|
|
|
|
this.updateVRPlayPauseButtonIcon();
|
|
this.updateVRLoopButtonIcon();
|
|
this.updateVRVolumeButtonIcon();
|
|
if (this.vrControlPanel) {
|
|
this.vrPanelVisibility.hideImmediately();
|
|
}
|
|
|
|
await this.renderer.xr.setSession(this.xrSession);
|
|
this.xrInputRig?.showOverlays();
|
|
this.isXrLoopActive = true;
|
|
this.renderer.setAnimationLoop(this.renderXrFrame);
|
|
this.frameCounter = 0;
|
|
} catch (err) {
|
|
const sessionStartError = 'XR_ERROR: Failed to start VR session: ' + (err.message || String(err));
|
|
console.error(sessionStartError, err);
|
|
this.isXrLoopActive = false;
|
|
|
|
this.hideContentMeshes();
|
|
this.textureManager?.clearMaterial(this.sphereMaterial);
|
|
if (this.vrControlPanel) {
|
|
this.vrPanelVisibility.hideImmediately();
|
|
}
|
|
if (this.xrSession) {
|
|
this.xrSession.removeEventListener('end', this.handleVrSessionEnd);
|
|
const tempSession = this.xrSession;
|
|
this.xrSession = null;
|
|
tempSession.end().catch(() => {}).finally(() => {
|
|
this.onVRSessionEnd({ session: tempSession });
|
|
});
|
|
} else {
|
|
this.onVRSessionEnd({ session: null });
|
|
}
|
|
if (this.renderer && this.renderer.getAnimationLoop && this.renderer.getAnimationLoop()) {
|
|
this.renderer.setAnimationLoop(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private hideEnterButton(): void {
|
|
if (this.mediaController) {
|
|
this.mediaController.hidePlayButton();
|
|
return;
|
|
}
|
|
|
|
this.playBtn.classList.add('hidden');
|
|
}
|
|
|
|
private onVRSessionEnd(event: any): void {
|
|
const endedSession = event.session;
|
|
|
|
this.isXrLoopActive = false;
|
|
if (this.renderer) {
|
|
if (this.renderer.getAnimationLoop && this.renderer.getAnimationLoop()) {
|
|
this.renderer.setAnimationLoop(null);
|
|
}
|
|
}
|
|
|
|
this.mediaAdapter.showElement();
|
|
|
|
this.mediaController?.pauseIfPlaying();
|
|
|
|
this.textureManager?.clearMaterial(this.sphereMaterial);
|
|
this.hideContentMeshes();
|
|
this.resetHeadPositionLock();
|
|
if (this.vrControlPanel) {
|
|
this.vrPanelVisibility.hideImmediately();
|
|
}
|
|
this.xrInputRig?.hideOverlays();
|
|
|
|
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
|
endedSession.removeEventListener('end', this.handleVrSessionEnd);
|
|
}
|
|
|
|
if (this.xrSession === endedSession || this.xrSession === null) {
|
|
this.xrSession = null;
|
|
} else if (this.xrSession && endedSession) {
|
|
console.warn('onVRSessionEnd: Global xrSession was different from the endedSession. Global xrSession:', this.xrSession, 'Ended session:', endedSession);
|
|
this.xrSession = null;
|
|
}
|
|
|
|
this.resetToOriginalState();
|
|
this.onWindowResize();
|
|
}
|
|
|
|
private renderXR(timestamp: number, frame: any): void {
|
|
if (!this.isXrLoopActive) {
|
|
return;
|
|
}
|
|
this.frameCounter++;
|
|
|
|
if (!this.renderer || !this.renderer.xr || !this.renderer.xr.isPresenting) {
|
|
console.warn('renderXR called but not in a valid XR presenting state. Stopping loop.');
|
|
this.isXrLoopActive = false;
|
|
if (this.renderer && this.renderer.getAnimationLoop && this.renderer.getAnimationLoop()) {
|
|
this.renderer.setAnimationLoop(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.vrPanelVisibility.isFading) {
|
|
this.animatePanelFade(timestamp);
|
|
}
|
|
this.xrInputRig?.update(timestamp, this.getVisibleVrPanelInteractables());
|
|
|
|
if (!frame) {
|
|
console.warn('renderXR called without an XRFrame. Skipping render.');
|
|
return;
|
|
}
|
|
|
|
if (this.frameCounter > 0 && this.frameCounter % 3600 === 0) {
|
|
const gl = this.renderer.getContext();
|
|
const error = gl.getError();
|
|
if (error !== gl.NO_ERROR) {
|
|
console.error(`WEBGL_ERROR_IN_RENDER_LOOP (F${this.frameCounter}):`, error, gl.enumToString ? gl.enumToString(error) : error);
|
|
}
|
|
}
|
|
|
|
try {
|
|
this.updateHeadPositionLock();
|
|
this.textureManager?.updateIfNeeded();
|
|
this.renderer.render(this.scene, this.camera);
|
|
} catch (error) {
|
|
const renderErrorMsg = 'ERROR_IN_RENDERXR_LOOP (F' + this.frameCounter + '): ' + (error.message || String(error));
|
|
console.error(renderErrorMsg, error);
|
|
console.error('Render loop error. Attempting to exit VR.');
|
|
this.isXrLoopActive = false;
|
|
|
|
const sessionToCloseOnError = this.xrSession;
|
|
this.xrSession = null;
|
|
|
|
if (sessionToCloseOnError) {
|
|
sessionToCloseOnError.removeEventListener('end', this.handleVrSessionEnd);
|
|
sessionToCloseOnError.end().catch((e) => {
|
|
console.error('Error trying to end session after render loop crash:', e);
|
|
}).finally(() => {
|
|
this.onVRSessionEnd({ session: sessionToCloseOnError });
|
|
});
|
|
} else {
|
|
this.onVRSessionEnd({ session: null });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const playerBase = new URL('.', import.meta.url).href;
|
|
let activeSession: PlayerSession | undefined;
|
|
|
|
bootstrapPlayer(playerBase, (context) => {
|
|
activeSession = new PlayerSession(context);
|
|
activeSession.init();
|
|
});
|