forked from EXT/VR180-Web-Player
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
||||
VALID_PROJECTIONS
|
||||
} from './config.js';
|
||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom/dom.js';
|
||||
import { createMediaAdapter, type SupportedMediaAdapter } from './media/media-adapter.js';
|
||||
|
||||
export type BootstrapContext = {
|
||||
mediaAdapter: SupportedMediaAdapter;
|
||||
playButton: HTMLButtonElement;
|
||||
playerContainer: HTMLElement;
|
||||
projectionMode: ProjectionMode;
|
||||
videoElement: HTMLVideoElement;
|
||||
};
|
||||
|
||||
export function bootstrapPlayer(playerBase: string, onReady: (context: BootstrapContext) => void): void {
|
||||
@@ -38,25 +39,24 @@ export function bootstrapPlayer(playerBase: string, onReady: (context: Bootstrap
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElement = playerContainer.querySelector('video');
|
||||
if (!videoElement) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a video element.`);
|
||||
const mediaAdapter = createMediaAdapter(playerContainer);
|
||||
if (!mediaAdapter) {
|
||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a supported media element (video).`);
|
||||
return;
|
||||
}
|
||||
videoElement.classList.add('vrwp-video');
|
||||
|
||||
const playButton = createPlayButton();
|
||||
playerContainer.appendChild(playButton);
|
||||
playerContainer.appendChild(create2DControlPanel());
|
||||
playButton.disabled = true;
|
||||
videoElement.load();
|
||||
mediaAdapter.load();
|
||||
|
||||
completeXrSupportCheck(playButton, () => {
|
||||
onReady({
|
||||
mediaAdapter,
|
||||
playButton,
|
||||
playerContainer,
|
||||
projectionMode: configuredProjection as ProjectionMode,
|
||||
videoElement
|
||||
projectionMode: configuredProjection as ProjectionMode
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
70
src/vr180player/media/media-adapter.ts
Normal file
70
src/vr180player/media/media-adapter.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export type MediaCapabilities = {
|
||||
audio: boolean;
|
||||
dynamicTexture: boolean;
|
||||
playback: boolean;
|
||||
timeline: boolean;
|
||||
};
|
||||
|
||||
export interface MediaAdapter<TElement extends HTMLElement = HTMLElement, TTextureSource = TElement> {
|
||||
readonly capabilities: MediaCapabilities;
|
||||
readonly element: TElement;
|
||||
readonly kind: string;
|
||||
readonly textureSource: TTextureSource;
|
||||
getTitle(): string;
|
||||
hideElement(): void;
|
||||
load(): void;
|
||||
shouldUpdateTexture(): boolean;
|
||||
showElement(): void;
|
||||
}
|
||||
|
||||
export type SupportedMediaAdapter = VideoMediaAdapter;
|
||||
|
||||
const VIDEO_CAPABILITIES: MediaCapabilities = {
|
||||
audio: true,
|
||||
dynamicTexture: true,
|
||||
playback: true,
|
||||
timeline: true
|
||||
};
|
||||
|
||||
export class VideoMediaAdapter implements MediaAdapter<HTMLVideoElement, HTMLVideoElement> {
|
||||
readonly capabilities = VIDEO_CAPABILITIES;
|
||||
readonly kind = 'video';
|
||||
|
||||
constructor(readonly element: HTMLVideoElement) {}
|
||||
|
||||
get textureSource(): HTMLVideoElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return this.element.getAttribute('title') ||
|
||||
this.element.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
||||
'Video Title';
|
||||
}
|
||||
|
||||
hideElement(): void {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.element.load();
|
||||
}
|
||||
|
||||
shouldUpdateTexture(): boolean {
|
||||
return !this.element.paused && !this.element.ended;
|
||||
}
|
||||
|
||||
showElement(): void {
|
||||
this.element.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
export function createMediaAdapter(playerContainer: HTMLElement): SupportedMediaAdapter | null {
|
||||
const videoElement = playerContainer.querySelector<HTMLVideoElement>('video');
|
||||
if (!videoElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
videoElement.classList.add('vrwp-video');
|
||||
return new VideoMediaAdapter(videoElement);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type TwoDModeOptions = {
|
||||
getCamera: () => any;
|
||||
getCameraControls: () => FallbackCameraControls | undefined;
|
||||
getMaterial: () => any;
|
||||
getMediaElement: () => HTMLElement | undefined;
|
||||
getRenderer: () => any;
|
||||
getScene: () => any;
|
||||
getVideo: () => HTMLVideoElement | undefined;
|
||||
@@ -43,6 +44,7 @@ export class TwoDMode {
|
||||
private readonly getCamera: () => any;
|
||||
private readonly getCameraControls: () => FallbackCameraControls | undefined;
|
||||
private readonly getMaterial: () => any;
|
||||
private readonly getMediaElement: () => HTMLElement | undefined;
|
||||
private readonly getRenderer: () => any;
|
||||
private readonly getScene: () => any;
|
||||
private readonly getVideo: () => HTMLVideoElement | undefined;
|
||||
@@ -57,6 +59,7 @@ export class TwoDMode {
|
||||
this.getCamera = options.getCamera;
|
||||
this.getCameraControls = options.getCameraControls;
|
||||
this.getMaterial = options.getMaterial;
|
||||
this.getMediaElement = options.getMediaElement;
|
||||
this.getRenderer = options.getRenderer;
|
||||
this.getScene = options.getScene;
|
||||
this.getVideo = options.getVideo;
|
||||
@@ -91,11 +94,11 @@ export class TwoDMode {
|
||||
}
|
||||
|
||||
start(): void {
|
||||
const video = this.getVideo();
|
||||
const mediaElement = this.getMediaElement();
|
||||
const renderer = this.getRenderer();
|
||||
const camera = this.getCamera();
|
||||
|
||||
if (!video || !renderer || !camera) {
|
||||
if (!mediaElement || !renderer || !camera) {
|
||||
console.error("Required components not available for 2D mode");
|
||||
return;
|
||||
}
|
||||
@@ -104,7 +107,7 @@ export class TwoDMode {
|
||||
this.resizeCanvasFor2D(renderer, camera);
|
||||
|
||||
const canvas = showFallbackCanvas(renderer);
|
||||
video.style.display = 'none';
|
||||
mediaElement.style.display = 'none';
|
||||
|
||||
const mediaTexture = this.callbacks.createMediaTexture();
|
||||
this.callbacks.positionPlaneForPresentation(this.projectionMode === 'plane');
|
||||
@@ -139,9 +142,9 @@ export class TwoDMode {
|
||||
this.getCameraControls()?.reset();
|
||||
this.callbacks.positionPlaneForPresentation(false);
|
||||
|
||||
const video = this.getVideo();
|
||||
if (video) {
|
||||
video.style.display = '';
|
||||
const mediaElement = this.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,18 @@ export type ManagedMaterial<TTexture extends ManagedTexture = ManagedTexture> =
|
||||
needsUpdate: boolean;
|
||||
};
|
||||
|
||||
export type TextureFactory<TTexture extends ManagedTexture = ManagedTexture> = (video: HTMLVideoElement) => TTexture;
|
||||
export type TextureFactory<TSource, TTexture extends ManagedTexture = ManagedTexture> = (source: TSource) => TTexture;
|
||||
|
||||
export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTexture> {
|
||||
export class MediaTextureManager<TSource, TTexture extends ManagedTexture = ManagedTexture> {
|
||||
private texture: TTexture | null = null;
|
||||
private readonly createTexture: TextureFactory<TTexture>;
|
||||
private readonly video: HTMLVideoElement;
|
||||
private readonly createTexture: TextureFactory<TSource, TTexture>;
|
||||
private readonly shouldUpdateTexture: () => boolean;
|
||||
private readonly source: TSource;
|
||||
|
||||
constructor(video: HTMLVideoElement, createTexture: TextureFactory<TTexture>) {
|
||||
constructor(source: TSource, createTexture: TextureFactory<TSource, TTexture>, shouldUpdateTexture: () => boolean) {
|
||||
this.createTexture = createTexture;
|
||||
this.video = video;
|
||||
this.shouldUpdateTexture = shouldUpdateTexture;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
get current(): TTexture | null {
|
||||
@@ -48,7 +50,7 @@ export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTextur
|
||||
|
||||
create(): TTexture {
|
||||
this.dispose();
|
||||
this.texture = this.createTexture(this.video);
|
||||
this.texture = this.createTexture(this.source);
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
@@ -59,8 +61,8 @@ export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTextur
|
||||
}
|
||||
}
|
||||
|
||||
updateIfPlaying(): void {
|
||||
if (this.texture && !this.video.paused && !this.video.ended) {
|
||||
updateIfNeeded(): void {
|
||||
if (this.texture && this.shouldUpdateTexture()) {
|
||||
this.texture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import {
|
||||
createPlayerRenderer,
|
||||
resizePlayerRenderer
|
||||
} from './rendering/renderer-lifecycle.js';
|
||||
import { VideoTextureManager } from './rendering/texture-manager.js';
|
||||
import { MediaTextureManager } from './rendering/texture-manager.js';
|
||||
import type { SupportedMediaAdapter } from './media/media-adapter.js';
|
||||
|
||||
const _playerBase = new URL('.', import.meta.url).href;
|
||||
|
||||
@@ -41,13 +42,14 @@ let scene, camera, renderer, video, sphereMaterial;
|
||||
let vr180Mesh, planeMesh, activeContentMesh;
|
||||
let xrSession = null;
|
||||
let raycaster, uiElements = [];
|
||||
let videoElement, playBtn;
|
||||
let mediaAdapter: SupportedMediaAdapter | undefined;
|
||||
let playBtn;
|
||||
let frameCounter = 0;
|
||||
|
||||
let isXrLoopActive = false;
|
||||
let vrControlPanel;
|
||||
let mediaController: MediaController | undefined;
|
||||
let textureManager: VideoTextureManager | undefined;
|
||||
let textureManager: MediaTextureManager<HTMLVideoElement> | undefined;
|
||||
let vrPanel: VrControlPanel | undefined;
|
||||
let twoDMode: TwoDMode | undefined;
|
||||
const vrPanelVisibility = new VrPanelVisibility();
|
||||
@@ -59,7 +61,8 @@ let fallbackCameraControls: FallbackCameraControls | undefined;
|
||||
bootstrapPlayer(_playerBase, (context) => {
|
||||
playerContainer = context.playerContainer;
|
||||
projectionMode = context.projectionMode;
|
||||
videoElement = context.videoElement;
|
||||
mediaAdapter = context.mediaAdapter;
|
||||
video = mediaAdapter.element;
|
||||
playBtn = context.playButton;
|
||||
init();
|
||||
});
|
||||
@@ -106,18 +109,16 @@ function closeActiveXrSessionAfterContextLoss() {
|
||||
}
|
||||
|
||||
function restoreVideoTextureAfterContextRestored() {
|
||||
if (video && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
|
||||
if (mediaAdapter && sphereMaterial && activeContentMesh && activeContentMesh.visible && renderer.xr.isPresenting && xrSession) {
|
||||
textureManager?.assignToMaterial(sphereMaterial);
|
||||
updateVRPlayPauseButtonIcon();
|
||||
updateVRVolumeButtonIcon();
|
||||
console.log("Re-initialized video texture after context restoration during VR.");
|
||||
console.log("Re-initialized media 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 getMediaTitle() {
|
||||
return mediaAdapter?.getTitle() || 'Video Title';
|
||||
}
|
||||
|
||||
|
||||
@@ -131,9 +132,16 @@ function init() {
|
||||
scene = playerRenderer.scene;
|
||||
camera = playerRenderer.camera;
|
||||
renderer = playerRenderer.renderer;
|
||||
if (!mediaAdapter) {
|
||||
throw new Error('Media adapter is not initialized.');
|
||||
}
|
||||
|
||||
video = videoElement;
|
||||
textureManager = new VideoTextureManager(video, createVideoTextureCore);
|
||||
video = mediaAdapter.element;
|
||||
textureManager = new MediaTextureManager(
|
||||
mediaAdapter.textureSource,
|
||||
createVideoTextureCore,
|
||||
() => mediaAdapter?.shouldUpdateTexture() ?? false
|
||||
);
|
||||
mediaController = new MediaController({
|
||||
is2DModeActive,
|
||||
on2DPlaybackResume: show2DControlPanel,
|
||||
@@ -171,12 +179,13 @@ function init() {
|
||||
getCamera: () => camera2D,
|
||||
getCameraControls: () => fallbackCameraControls,
|
||||
getMaterial: () => sphereMaterial,
|
||||
getMediaElement: () => mediaAdapter?.element,
|
||||
getRenderer: () => renderer,
|
||||
getScene: () => scene,
|
||||
getVideo: () => video,
|
||||
playerContainer,
|
||||
projectionMode,
|
||||
title: getVideoTitle()
|
||||
title: getMediaTitle()
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
||||
@@ -185,7 +194,7 @@ function init() {
|
||||
}
|
||||
|
||||
try { // Phase 2: VR Control Panel UI
|
||||
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
||||
vrPanel = createVrControlPanel(scene, getMediaTitle());
|
||||
vrControlPanel = vrPanel.group;
|
||||
vrPanelVisibility.setPanel(vrPanel);
|
||||
uiElements.push(...vrPanel.interactables);
|
||||
@@ -372,8 +381,8 @@ function onSelectStartVR(event) {
|
||||
}
|
||||
|
||||
async function handleEnterVRButtonClick() {
|
||||
if (!video) {
|
||||
console.error("Video element not found for VR button click.");
|
||||
if (!mediaAdapter) {
|
||||
console.error("Media element not found for VR button click.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -418,10 +427,7 @@ async function actualSessionToggle() {
|
||||
xrSession.addEventListener('end', onVRSessionEnd);
|
||||
|
||||
|
||||
// Hide the regular video element when entering VR
|
||||
if (video) {
|
||||
video.style.display = 'none';
|
||||
}
|
||||
mediaAdapter?.hideElement();
|
||||
|
||||
if (mediaController && video && (video.paused || video.ended)) {
|
||||
try {
|
||||
@@ -435,8 +441,8 @@ async function actualSessionToggle() {
|
||||
positionPlaneForPresentation(false);
|
||||
|
||||
textureManager?.dispose();
|
||||
if (!video) {
|
||||
throw new Error("Video element not available for creating texture.");
|
||||
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.");
|
||||
@@ -495,10 +501,7 @@ function onVRSessionEnd(event) {
|
||||
}
|
||||
}
|
||||
|
||||
// Show the regular video element when exiting VR
|
||||
if (video) {
|
||||
video.style.display = '';
|
||||
}
|
||||
mediaAdapter?.showElement();
|
||||
|
||||
mediaController?.pauseIfPlaying();
|
||||
|
||||
@@ -558,7 +561,7 @@ function renderXR(timestamp, frame) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
textureManager?.updateIfPlaying();
|
||||
textureManager?.updateIfNeeded();
|
||||
renderer.render(scene, camera);
|
||||
} catch (error) {
|
||||
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
||||
|
||||
Reference in New Issue
Block a user