forked from EXT/VR180-Web-Player
more refactoer
This commit is contained in:
133
src/vr180player/media-controller.ts
Normal file
133
src/vr180player/media-controller.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
type MediaControllerOptions = {
|
||||||
|
is2DModeActive: () => boolean;
|
||||||
|
on2DPlaybackResume: () => void;
|
||||||
|
playButton?: HTMLButtonElement;
|
||||||
|
video: HTMLVideoElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HandleMediaEndedOptions = {
|
||||||
|
cleanupFailedVrExit: () => void;
|
||||||
|
exitVr: () => Promise<void>;
|
||||||
|
isIn2DMode: () => boolean;
|
||||||
|
isInVr: () => boolean;
|
||||||
|
on2DEnded: () => void;
|
||||||
|
resetToOriginalState: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SKIP_SECONDS = 15;
|
||||||
|
|
||||||
|
export class MediaController {
|
||||||
|
private readonly is2DModeActive: () => boolean;
|
||||||
|
private readonly on2DPlaybackResume: () => void;
|
||||||
|
private readonly playButton?: HTMLButtonElement;
|
||||||
|
private readonly video: HTMLVideoElement;
|
||||||
|
|
||||||
|
constructor({ is2DModeActive, on2DPlaybackResume, playButton, video }: MediaControllerOptions) {
|
||||||
|
this.is2DModeActive = is2DModeActive;
|
||||||
|
this.on2DPlaybackResume = on2DPlaybackResume;
|
||||||
|
this.playButton = playButton;
|
||||||
|
this.video = video;
|
||||||
|
}
|
||||||
|
|
||||||
|
enableNativeControls(): void {
|
||||||
|
this.video.controls = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
forward(seconds = DEFAULT_SKIP_SECONDS): void {
|
||||||
|
if (!isFinite(this.video.duration)) return;
|
||||||
|
|
||||||
|
this.video.currentTime = Math.min(this.video.duration, this.video.currentTime + seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEnded({
|
||||||
|
cleanupFailedVrExit,
|
||||||
|
exitVr,
|
||||||
|
isIn2DMode,
|
||||||
|
isInVr,
|
||||||
|
on2DEnded,
|
||||||
|
resetToOriginalState
|
||||||
|
}: HandleMediaEndedOptions): void {
|
||||||
|
this.pauseIfPlaying();
|
||||||
|
|
||||||
|
if (isInVr()) {
|
||||||
|
exitVr().catch((err) => {
|
||||||
|
console.error('Error during automatic VR exit on video end:', err);
|
||||||
|
cleanupFailedVrExit();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIn2DMode()) {
|
||||||
|
on2DEnded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToOriginalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePlayButton(): void {
|
||||||
|
this.playButton?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseIfPlaying(): void {
|
||||||
|
if (!this.video.paused) {
|
||||||
|
this.video.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
play(): Promise<void> {
|
||||||
|
return this.video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToOriginalState(): void {
|
||||||
|
this.video.pause();
|
||||||
|
this.video.currentTime = 0;
|
||||||
|
this.video.controls = false;
|
||||||
|
this.video.load();
|
||||||
|
|
||||||
|
this.playButton?.classList.remove('hidden');
|
||||||
|
if (this.playButton) {
|
||||||
|
this.playButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rewind(seconds = DEFAULT_SKIP_SECONDS): void {
|
||||||
|
this.video.currentTime = Math.max(0, this.video.currentTime - seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
seekToProgress(progress: number): void {
|
||||||
|
if (!isFinite(this.video.duration)) return;
|
||||||
|
|
||||||
|
this.video.currentTime = progress * this.video.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMute(): void {
|
||||||
|
this.video.muted = !this.video.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlayPause(): void {
|
||||||
|
if (!this.video.currentSrc) return;
|
||||||
|
|
||||||
|
if (this.video.paused || this.video.ended) {
|
||||||
|
if (this.video.ended && this.is2DModeActive()) {
|
||||||
|
this.video.currentTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.video.readyState >= this.video.HAVE_ENOUGH_DATA || this.video.currentSrc) {
|
||||||
|
const playPromise = this.video.play() as Promise<void> | undefined;
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise.then(() => {
|
||||||
|
if (this.is2DModeActive() && this.video.ended === false) {
|
||||||
|
this.on2DPlaybackResume();
|
||||||
|
}
|
||||||
|
}).catch((err) => console.error('Error during video.play():', err));
|
||||||
|
} else {
|
||||||
|
console.error('video.play() did not return a promise.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.video.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/vr180player/renderer-lifecycle.ts
Normal file
150
src/vr180player/renderer-lifecycle.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
||||||
|
|
||||||
|
const FALLBACK_ASPECT_RATIO = 16 / 9;
|
||||||
|
const MIN_FALLBACK_CANVAS_WIDTH = 320;
|
||||||
|
const MIN_FALLBACK_CANVAS_HEIGHT = 180;
|
||||||
|
|
||||||
|
type RendererContextHandlers = {
|
||||||
|
closeActiveXrSession: () => void;
|
||||||
|
hasActiveXrSession: () => boolean;
|
||||||
|
restoreAfterContextRestored: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlayerRenderer = {
|
||||||
|
camera: any;
|
||||||
|
renderer: any;
|
||||||
|
scene: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResizePlayerRendererOptions = {
|
||||||
|
camera: any;
|
||||||
|
camera2D: any;
|
||||||
|
is2DMode: boolean;
|
||||||
|
onFallbackResize: () => void;
|
||||||
|
playerContainer: HTMLElement;
|
||||||
|
renderer: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResizeFallbackRendererOptions = {
|
||||||
|
camera2D: any;
|
||||||
|
playerContainer: HTMLElement;
|
||||||
|
renderer: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createPlayerRenderer(
|
||||||
|
playerContainer: HTMLElement,
|
||||||
|
contextHandlers: RendererContextHandlers
|
||||||
|
): PlayerRenderer {
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
|
camera.position.set(0, 1.6, 0.1);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
hideRendererCanvas(renderer);
|
||||||
|
bindWebGlContextEvents(renderer, contextHandlers);
|
||||||
|
|
||||||
|
return { camera, renderer, scene };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideRendererCanvas(renderer: any): void {
|
||||||
|
if (renderer?.domElement) {
|
||||||
|
renderer.domElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resizeFallbackRenderer({
|
||||||
|
camera2D,
|
||||||
|
playerContainer,
|
||||||
|
renderer
|
||||||
|
}: ResizeFallbackRendererOptions): void {
|
||||||
|
const { height, width } = getFallbackCanvasSize(playerContainer);
|
||||||
|
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
camera2D.aspect = width / height;
|
||||||
|
camera2D.updateProjectionMatrix();
|
||||||
|
styleFallbackCanvas(renderer.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resizePlayerRenderer({
|
||||||
|
camera,
|
||||||
|
camera2D,
|
||||||
|
is2DMode,
|
||||||
|
onFallbackResize,
|
||||||
|
playerContainer,
|
||||||
|
renderer
|
||||||
|
}: ResizePlayerRendererOptions): void {
|
||||||
|
if (!renderer) return;
|
||||||
|
if (renderer.xr && renderer.xr.isPresenting) return;
|
||||||
|
|
||||||
|
if (is2DMode) {
|
||||||
|
if (!playerContainer || !camera2D) return;
|
||||||
|
resizeFallbackRenderer({ camera2D, playerContainer, renderer });
|
||||||
|
onFallbackResize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera) {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera2D) {
|
||||||
|
camera2D.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera2D.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showFallbackCanvas(renderer: any): HTMLElement {
|
||||||
|
const canvas = renderer.domElement;
|
||||||
|
canvas.style.position = 'relative';
|
||||||
|
styleFallbackCanvas(canvas);
|
||||||
|
canvas.style.display = '';
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindWebGlContextEvents(renderer: any, handlers: RendererContextHandlers): void {
|
||||||
|
const gl = renderer.getContext();
|
||||||
|
if (!gl) {
|
||||||
|
throw new Error('Failed to get WebGL context from renderer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.canvas.addEventListener('webglcontextlost', (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.error('CONTEXT_EVENT: WebGL Context Lost! xrSession active?', handlers.hasActiveXrSession(), event);
|
||||||
|
if (handlers.hasActiveXrSession()) {
|
||||||
|
handlers.closeActiveXrSession();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
gl.canvas.addEventListener('webglcontextrestored', () => {
|
||||||
|
console.log('CONTEXT_EVENT: WebGL Context Restored.');
|
||||||
|
handlers.restoreAfterContextRestored();
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackCanvasSize(playerContainer: HTMLElement): { height: number; width: number } {
|
||||||
|
const containerRect = playerContainer.getBoundingClientRect();
|
||||||
|
const containerWidth = containerRect.width;
|
||||||
|
const calculatedHeight = containerWidth / FALLBACK_ASPECT_RATIO;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: Math.max(calculatedHeight, MIN_FALLBACK_CANVAS_HEIGHT),
|
||||||
|
width: Math.max(containerWidth, MIN_FALLBACK_CANVAS_WIDTH)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleFallbackCanvas(canvas: HTMLElement): void {
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = 'auto';
|
||||||
|
canvas.style.aspectRatio = '16/9';
|
||||||
|
}
|
||||||
265
src/vr180player/two-d-mode.ts
Normal file
265
src/vr180player/two-d-mode.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import type { ProjectionMode } from './config.js';
|
||||||
|
import type { FallbackCameraControls } from './fallback-camera-controls.js';
|
||||||
|
import {
|
||||||
|
hideRendererCanvas,
|
||||||
|
resizeFallbackRenderer,
|
||||||
|
showFallbackCanvas
|
||||||
|
} from './renderer-lifecycle.js';
|
||||||
|
import { TwoDControlPanel } from './two-d-control-panel.js';
|
||||||
|
|
||||||
|
type TwoDModeCallbacks = {
|
||||||
|
createMediaTexture: () => any;
|
||||||
|
forward: () => void;
|
||||||
|
positionPlaneForPresentation: (isFallback2D?: boolean) => void;
|
||||||
|
rewind: () => void;
|
||||||
|
seekToProgress: (progress: number) => void;
|
||||||
|
showActiveContentMesh: () => void;
|
||||||
|
toggleMute: () => void;
|
||||||
|
togglePlayPause: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoDModeOptions = {
|
||||||
|
callbacks: TwoDModeCallbacks;
|
||||||
|
fullscreenTarget: HTMLElement;
|
||||||
|
getActiveContentMesh: () => any;
|
||||||
|
getCamera: () => any;
|
||||||
|
getCameraControls: () => FallbackCameraControls | undefined;
|
||||||
|
getMaterial: () => any;
|
||||||
|
getRenderer: () => any;
|
||||||
|
getScene: () => any;
|
||||||
|
getVideo: () => HTMLVideoElement | undefined;
|
||||||
|
playerContainer: HTMLElement;
|
||||||
|
projectionMode: ProjectionMode;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FULLSCREEN_RESIZE_DELAY = 100;
|
||||||
|
|
||||||
|
export class TwoDMode {
|
||||||
|
private readonly callbacks: TwoDModeCallbacks;
|
||||||
|
private readonly controls: TwoDControlPanel;
|
||||||
|
private readonly fullscreenTarget: HTMLElement;
|
||||||
|
private readonly getActiveContentMesh: () => any;
|
||||||
|
private readonly getCamera: () => any;
|
||||||
|
private readonly getCameraControls: () => FallbackCameraControls | undefined;
|
||||||
|
private readonly getMaterial: () => any;
|
||||||
|
private readonly getRenderer: () => any;
|
||||||
|
private readonly getScene: () => any;
|
||||||
|
private readonly getVideo: () => HTMLVideoElement | undefined;
|
||||||
|
private readonly playerContainer: HTMLElement;
|
||||||
|
private readonly projectionMode: ProjectionMode;
|
||||||
|
private active = false;
|
||||||
|
|
||||||
|
constructor(options: TwoDModeOptions) {
|
||||||
|
this.callbacks = options.callbacks;
|
||||||
|
this.fullscreenTarget = options.fullscreenTarget;
|
||||||
|
this.getActiveContentMesh = options.getActiveContentMesh;
|
||||||
|
this.getCamera = options.getCamera;
|
||||||
|
this.getCameraControls = options.getCameraControls;
|
||||||
|
this.getMaterial = options.getMaterial;
|
||||||
|
this.getRenderer = options.getRenderer;
|
||||||
|
this.getScene = options.getScene;
|
||||||
|
this.getVideo = options.getVideo;
|
||||||
|
this.playerContainer = options.playerContainer;
|
||||||
|
this.projectionMode = options.projectionMode;
|
||||||
|
|
||||||
|
this.controls = new TwoDControlPanel({
|
||||||
|
callbacks: {
|
||||||
|
onForward: () => {
|
||||||
|
this.callbacks.forward();
|
||||||
|
},
|
||||||
|
onMute: () => {
|
||||||
|
this.callbacks.toggleMute();
|
||||||
|
},
|
||||||
|
onPlayPause: this.callbacks.togglePlayPause,
|
||||||
|
onRewind: () => {
|
||||||
|
this.callbacks.rewind();
|
||||||
|
},
|
||||||
|
onSeek: (progress) => {
|
||||||
|
this.callbacks.seekToProgress(progress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fullscreenTarget: this.fullscreenTarget,
|
||||||
|
getIsActive: () => this.active,
|
||||||
|
playerContainer: this.playerContainer,
|
||||||
|
title: options.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get isActive(): boolean {
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
const video = this.getVideo();
|
||||||
|
const renderer = this.getRenderer();
|
||||||
|
const camera = this.getCamera();
|
||||||
|
|
||||||
|
if (!video || !renderer || !camera) {
|
||||||
|
console.error("Required components not available for 2D mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.active = true;
|
||||||
|
this.resizeCanvasFor2D(renderer, camera);
|
||||||
|
|
||||||
|
const canvas = showFallbackCanvas(renderer);
|
||||||
|
video.style.display = 'none';
|
||||||
|
|
||||||
|
const mediaTexture = this.callbacks.createMediaTexture();
|
||||||
|
this.callbacks.positionPlaneForPresentation(this.projectionMode === 'plane');
|
||||||
|
|
||||||
|
const material = this.getMaterial();
|
||||||
|
const activeContentMesh = this.getActiveContentMesh();
|
||||||
|
if (material && activeContentMesh) {
|
||||||
|
material.map = mediaTexture;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
this.callbacks.showActiveContentMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks.togglePlayPause();
|
||||||
|
this.addEventListeners(canvas);
|
||||||
|
this.controls.show();
|
||||||
|
this.positionControls();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
|
const renderer = this.getRenderer();
|
||||||
|
if (renderer?.domElement) {
|
||||||
|
this.removeEventListeners(renderer.domElement);
|
||||||
|
hideRendererCanvas(renderer);
|
||||||
|
} else {
|
||||||
|
this.removeFullscreenEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.controls.hide();
|
||||||
|
this.getCameraControls()?.reset();
|
||||||
|
this.callbacks.positionPlaneForPresentation(false);
|
||||||
|
|
||||||
|
const video = this.getVideo();
|
||||||
|
if (video) {
|
||||||
|
video.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(): boolean {
|
||||||
|
if (!this.active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = this.getRenderer();
|
||||||
|
const camera = this.getCamera();
|
||||||
|
if (renderer && camera) {
|
||||||
|
this.resizeCanvasFor2D(renderer, camera);
|
||||||
|
this.positionControls();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showControls(): void {
|
||||||
|
this.controls.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideControls(): void {
|
||||||
|
this.controls.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeline(): void {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const video = this.getVideo();
|
||||||
|
if (video) {
|
||||||
|
this.controls.updateTime(video.currentTime, video.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlaybackButton(): void {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const video = this.getVideo();
|
||||||
|
if (video) {
|
||||||
|
this.controls.updatePlaybackButton(video.paused || video.ended);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMuteButton(): void {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const video = this.getVideo();
|
||||||
|
if (video) {
|
||||||
|
this.controls.updateMuteButton(video.muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVideoEnd(): void {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
this.controls.showPersistent();
|
||||||
|
this.updatePlaybackButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEventListeners(canvas: HTMLElement): void {
|
||||||
|
this.getCameraControls()?.addEventListeners(canvas, this.projectionMode);
|
||||||
|
document.addEventListener('fullscreenchange', this.onFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', this.onFullscreenChange);
|
||||||
|
document.addEventListener('mozfullscreenchange', this.onFullscreenChange);
|
||||||
|
document.addEventListener('MSFullscreenChange', this.onFullscreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeEventListeners(canvas: HTMLElement): void {
|
||||||
|
this.getCameraControls()?.removeEventListeners(canvas);
|
||||||
|
this.removeFullscreenEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeFullscreenEventListeners(): void {
|
||||||
|
document.removeEventListener('fullscreenchange', this.onFullscreenChange);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange);
|
||||||
|
document.removeEventListener('mozfullscreenchange', this.onFullscreenChange);
|
||||||
|
document.removeEventListener('MSFullscreenChange', this.onFullscreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly onFullscreenChange = (): void => {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.resize();
|
||||||
|
}, FULLSCREEN_RESIZE_DELAY);
|
||||||
|
};
|
||||||
|
|
||||||
|
positionControls(): void {
|
||||||
|
const renderer = this.getRenderer();
|
||||||
|
if (renderer?.domElement) {
|
||||||
|
this.controls.position(renderer.domElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly render = (): void => {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const camera = this.getCamera();
|
||||||
|
if (this.projectionMode === 'vr180') {
|
||||||
|
this.getCameraControls()?.updateCameraRotation();
|
||||||
|
} else if (camera) {
|
||||||
|
camera.rotation.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = this.getRenderer();
|
||||||
|
const scene = this.getScene();
|
||||||
|
if (renderer && camera && scene) {
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(this.render);
|
||||||
|
};
|
||||||
|
|
||||||
|
private resizeCanvasFor2D(renderer: any, camera: any): void {
|
||||||
|
resizeFallbackRenderer({
|
||||||
|
camera2D: camera,
|
||||||
|
playerContainer: this.playerContainer,
|
||||||
|
renderer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_PROJECTION,
|
DEFAULT_PROJECTION,
|
||||||
PLANE_2D_DISTANCE,
|
PLANE_2D_DISTANCE,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
} from './projection.js';
|
} from './projection.js';
|
||||||
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
|
import { createVideoTexture as createVideoTextureCore } from './three-utils.js';
|
||||||
import { FallbackCameraControls } from './fallback-camera-controls.js';
|
import { FallbackCameraControls } from './fallback-camera-controls.js';
|
||||||
|
import { MediaController } from './media-controller.js';
|
||||||
import {
|
import {
|
||||||
createVrController,
|
createVrController,
|
||||||
handleVrControllerSelect
|
handleVrControllerSelect
|
||||||
@@ -30,7 +30,11 @@ import {
|
|||||||
updateVrVolumeButtonIcon
|
updateVrVolumeButtonIcon
|
||||||
} from './vr-control-panel.js';
|
} from './vr-control-panel.js';
|
||||||
import { VrPanelVisibility } from './vr-panel-visibility.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;
|
const _playerBase = new URL('.', import.meta.url).href;
|
||||||
|
|
||||||
@@ -43,10 +47,10 @@ let videoElement, playBtn;
|
|||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
|
|
||||||
let isXrLoopActive = false;
|
let isXrLoopActive = false;
|
||||||
let is2DMode = false;
|
|
||||||
let vrControlPanel;
|
let vrControlPanel;
|
||||||
|
let mediaController: MediaController | undefined;
|
||||||
let vrPanel: VrControlPanel | undefined;
|
let vrPanel: VrControlPanel | undefined;
|
||||||
let twoDControls: TwoDControlPanel | undefined;
|
let twoDMode: TwoDMode | undefined;
|
||||||
const vrPanelVisibility = new VrPanelVisibility();
|
const vrPanelVisibility = new VrPanelVisibility();
|
||||||
|
|
||||||
// 2D Camera Controls
|
// 2D Camera Controls
|
||||||
@@ -130,7 +134,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function applySbsTextureWindow(renderingRenderer, activeCamera, material) {
|
function applySbsTextureWindow(renderingRenderer, activeCamera, material) {
|
||||||
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DMode);
|
applySbsTextureWindowCore(renderingRenderer, activeCamera, material, is2DModeActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideContentMeshes() {
|
function hideContentMeshes() {
|
||||||
@@ -151,6 +155,34 @@ function createVideoTexture() {
|
|||||||
return videoTexture;
|
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() {
|
function getVideoTitle() {
|
||||||
return videoElement.getAttribute('title') ||
|
return videoElement.getAttribute('title') ||
|
||||||
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
||||||
@@ -160,53 +192,22 @@ function getVideoTitle() {
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
try {
|
try {
|
||||||
scene = new THREE.Scene();
|
const playerRenderer = createPlayerRenderer(playerContainer, {
|
||||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
closeActiveXrSession: closeActiveXrSessionAfterContextLoss,
|
||||||
camera.position.set(0, 1.6, 0.1);
|
hasActiveXrSession: () => !!xrSession,
|
||||||
scene.add(camera);
|
restoreAfterContextRestored: restoreVideoTextureAfterContextRestored
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
});
|
||||||
}
|
scene = playerRenderer.scene;
|
||||||
}, false);
|
camera = playerRenderer.camera;
|
||||||
gl.canvas.addEventListener('webglcontextrestored', (event) => {
|
renderer = playerRenderer.renderer;
|
||||||
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);
|
|
||||||
|
|
||||||
video = videoElement;
|
video = videoElement;
|
||||||
|
mediaController = new MediaController({
|
||||||
|
is2DModeActive,
|
||||||
|
on2DPlaybackResume: show2DControlPanel,
|
||||||
|
playButton: playBtn,
|
||||||
|
video
|
||||||
|
});
|
||||||
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
const contentScene = createContentScene(scene, projectionMode, (renderer, scene, activeCamera, geometry, material, group) => {
|
||||||
applySbsTextureWindow(renderer, activeCamera, material);
|
applySbsTextureWindow(renderer, activeCamera, material);
|
||||||
});
|
});
|
||||||
@@ -219,9 +220,32 @@ function init() {
|
|||||||
camera2D = contentScene.fallbackCamera;
|
camera2D = contentScene.fallbackCamera;
|
||||||
fallbackCameraControls = new FallbackCameraControls(camera2D, {
|
fallbackCameraControls = new FallbackCameraControls(camera2D, {
|
||||||
hideControls: hide2DControlPanel,
|
hideControls: hide2DControlPanel,
|
||||||
isEnabled: () => is2DMode,
|
isEnabled: is2DModeActive,
|
||||||
showControls: show2DControlPanel
|
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) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
||||||
renderer = null;
|
renderer = null;
|
||||||
@@ -264,9 +288,6 @@ function init() {
|
|||||||
video
|
video
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 2D control panel
|
|
||||||
init2DControlPanel();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 3 - Event Listeners):", e);
|
console.error("INIT_ERROR (Phase 3 - Event Listeners):", e);
|
||||||
}
|
}
|
||||||
@@ -308,248 +329,81 @@ function hidePanel() {
|
|||||||
|
|
||||||
function onWindowResize() {
|
function onWindowResize() {
|
||||||
if (!renderer) return;
|
if (!renderer) return;
|
||||||
if (renderer.xr && renderer.xr.isPresenting) return;
|
|
||||||
|
|
||||||
if (is2DMode) {
|
if (twoDMode?.resize()) return;
|
||||||
// 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
|
resizePlayerRenderer({
|
||||||
const aspectRatio = 16 / 9;
|
camera,
|
||||||
const calculatedHeight = containerWidth / aspectRatio;
|
camera2D,
|
||||||
|
is2DMode: false,
|
||||||
// Ensure minimum dimensions to prevent zero-sized canvas
|
onFallbackResize: () => {},
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
playerContainer,
|
playerContainer,
|
||||||
title: getVideoTitle()
|
renderer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function show2DControlPanel() {
|
function show2DControlPanel() {
|
||||||
twoDControls?.show();
|
twoDMode?.showControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide2DControlPanel() {
|
function hide2DControlPanel() {
|
||||||
twoDControls?.hide();
|
twoDMode?.hideControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DControlPanel() {
|
function update2DControlPanel() {
|
||||||
if (!is2DMode || !video) return;
|
twoDMode?.updateTimeline();
|
||||||
|
|
||||||
twoDControls?.updateTime(video.currentTime, video.duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DPlayPauseButton() {
|
function update2DPlayPauseButton() {
|
||||||
if (!is2DMode || !video) return;
|
twoDMode?.updatePlaybackButton();
|
||||||
|
|
||||||
twoDControls?.updatePlaybackButton(video.paused || video.ended);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update2DMuteButton() {
|
function update2DMuteButton() {
|
||||||
if (!is2DMode || !video) return;
|
twoDMode?.updateMuteButton();
|
||||||
|
|
||||||
twoDControls?.updateMuteButton(video.muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle2DVideoEnd() {
|
function handle2DVideoEnd() {
|
||||||
if (!is2DMode || !video) return;
|
twoDMode?.handleVideoEnd();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetToOriginalState() {
|
function resetToOriginalState() {
|
||||||
// Reset video to show poster frame
|
if (mediaController) {
|
||||||
if (video) {
|
mediaController.resetToOriginalState();
|
||||||
video.pause();
|
} else if (playBtn) {
|
||||||
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) {
|
|
||||||
playBtn.classList.remove('hidden');
|
playBtn.classList.remove('hidden');
|
||||||
playBtn.disabled = false;
|
playBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset 2D mode if it was active
|
if (twoDMode?.isActive) {
|
||||||
if (is2DMode) {
|
twoDMode.stop();
|
||||||
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
|
|
||||||
onWindowResize();
|
onWindowResize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onVideoEnded() {
|
function onVideoEnded() {
|
||||||
if (video && !video.paused) video.pause();
|
if (!mediaController) {
|
||||||
|
resetToOriginalState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (xrSession && renderer && renderer.xr.isPresenting) {
|
mediaController.handleEnded({
|
||||||
// VR mode - exit VR and reset to original state
|
cleanupFailedVrExit,
|
||||||
actualSessionToggle().catch(err => {
|
exitVr: actualSessionToggle,
|
||||||
console.error("Error during automatic VR exit on video end:", err);
|
isIn2DMode: is2DModeActive,
|
||||||
// Fallback cleanup if actualSessionToggle fails or doesn't fully clean up
|
isInVr: () => Boolean(xrSession && renderer && renderer.xr.isPresenting),
|
||||||
if(xrSession) { // Check if session still exists
|
on2DEnded: handle2DVideoEnd,
|
||||||
|
resetToOriginalState
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupFailedVrExit() {
|
||||||
|
if (xrSession) {
|
||||||
const sessionToClean = xrSession;
|
const sessionToClean = xrSession;
|
||||||
xrSession = null; // Nullify global ref
|
xrSession = null;
|
||||||
sessionToClean.removeEventListener('end', onVRSessionEnd);
|
sessionToClean.removeEventListener('end', onVRSessionEnd);
|
||||||
sessionToClean.end().catch(e => {}).finally(() => onVRSessionEnd({session: sessionToClean}));
|
sessionToClean.end().catch(e => {}).finally(() => onVRSessionEnd({session: sessionToClean}));
|
||||||
} else {
|
} else {
|
||||||
onVRSessionEnd({session: null}); // Call with null session if already gone
|
onVRSessionEnd({session: null});
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (is2DMode) {
|
|
||||||
// 2D mode - stay on last frame with controls visible
|
|
||||||
handle2DVideoEnd();
|
|
||||||
} else {
|
|
||||||
// Regular mode - reset to original state
|
|
||||||
resetToOriginalState();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,31 +413,27 @@ function onSelectStartVR(event) {
|
|||||||
if (xrSession) actualSessionToggle();
|
if (xrSession) actualSessionToggle();
|
||||||
},
|
},
|
||||||
forward: () => {
|
forward: () => {
|
||||||
if (video && isFinite(video.duration)) {
|
mediaController?.forward();
|
||||||
video.currentTime = Math.min(video.duration, video.currentTime + 15);
|
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hidePanel,
|
hidePanel,
|
||||||
isPanelVisible: () => vrPanelVisibility.isVisible,
|
isPanelVisible: () => vrPanelVisibility.isVisible,
|
||||||
raycaster,
|
raycaster,
|
||||||
rewind: () => {
|
rewind: () => {
|
||||||
if (video) {
|
mediaController?.rewind();
|
||||||
video.currentTime = Math.max(0, video.currentTime - 15);
|
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
seek: (progress) => {
|
seek: (progress) => {
|
||||||
if (video && isFinite(video.duration)) {
|
mediaController?.seekToProgress(progress);
|
||||||
video.currentTime = progress * video.duration;
|
|
||||||
updateSeekBarAppearance();
|
updateSeekBarAppearance();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
showPanel,
|
showPanel,
|
||||||
toggleMute: () => {
|
toggleMute: () => {
|
||||||
if (video) video.muted = !video.muted;
|
mediaController?.toggleMute();
|
||||||
|
},
|
||||||
|
togglePlayPause: () => {
|
||||||
|
mediaController?.togglePlayPause();
|
||||||
},
|
},
|
||||||
togglePlayPause,
|
|
||||||
uiElements,
|
uiElements,
|
||||||
vrPanel
|
vrPanel
|
||||||
});
|
});
|
||||||
@@ -596,7 +446,7 @@ async function handleEnterVRButtonClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide the play button after click
|
// Hide the play button after click
|
||||||
hidePlayButton();
|
mediaController?.hidePlayButton();
|
||||||
|
|
||||||
// Check if VR is supported
|
// Check if VR is supported
|
||||||
if (playBtn.dataset.xrSupported === "true") {
|
if (playBtn.dataset.xrSupported === "true") {
|
||||||
@@ -604,110 +454,10 @@ async function handleEnterVRButtonClick() {
|
|||||||
await actualSessionToggle();
|
await actualSessionToggle();
|
||||||
} else {
|
} else {
|
||||||
// VR is not supported - start 2D rectilinear mode
|
// 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() {
|
async function actualSessionToggle() {
|
||||||
if (!renderer || !renderer.isWebGLRenderer) {
|
if (!renderer || !renderer.isWebGLRenderer) {
|
||||||
console.error("CRITICAL_ERROR: actualSessionToggle: renderer is NOT a WebGLRenderer or is null!", renderer);
|
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';
|
video.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video && (video.paused || video.ended)) {
|
if (mediaController && video && (video.paused || video.ended)) {
|
||||||
try {
|
try {
|
||||||
await video.play();
|
await mediaController.play();
|
||||||
} catch (playError) {
|
} catch (playError) {
|
||||||
console.error("Failed to play video after obtaining XR session:", playError);
|
console.error("Failed to play video after obtaining XR session:", playError);
|
||||||
}
|
}
|
||||||
@@ -818,9 +568,7 @@ function onVRSessionEnd(event) {
|
|||||||
video.style.display = '';
|
video.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video && !video.paused) {
|
mediaController?.pauseIfPlaying();
|
||||||
video.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sphereMaterial && sphereMaterial.map) {
|
if (sphereMaterial && sphereMaterial.map) {
|
||||||
sphereMaterial.map.dispose();
|
sphereMaterial.map.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user