forked from EXT/VR180-Web-Player
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules/
|
|||||||
# Generated by `npm run build`.
|
# Generated by `npm run build`.
|
||||||
vr180player/*.js
|
vr180player/*.js
|
||||||
vr180player/**/*.js
|
vr180player/**/*.js
|
||||||
|
/media
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"dev": "npm run build && vite --host 0.0.0.0",
|
"dev": "npm run build && vite --host 0.0.0.0",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"test": "npm run build && node --test tests/time.test.mjs tests/projection.test.mjs tests/media-controller.test.mjs tests/texture-manager.test.mjs",
|
"test": "npm run build && node --test tests/time.test.mjs tests/projection.test.mjs tests/media-controller.test.mjs tests/texture-manager.test.mjs tests/media-adapter.test.mjs",
|
||||||
"preview": "npm run build && vite preview --host 127.0.0.1"
|
"preview": "npm run build && vite preview --host 127.0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import {
|
|||||||
VALID_PROJECTIONS
|
VALID_PROJECTIONS
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom/dom.js';
|
import { create2DControlPanel, createPlayButton, injectPlayerStyles } from './dom/dom.js';
|
||||||
|
import { createMediaAdapter, type SupportedMediaAdapter } from './media/media-adapter.js';
|
||||||
|
|
||||||
export type BootstrapContext = {
|
export type BootstrapContext = {
|
||||||
|
mediaAdapter: SupportedMediaAdapter;
|
||||||
playButton: HTMLButtonElement;
|
playButton: HTMLButtonElement;
|
||||||
playerContainer: HTMLElement;
|
playerContainer: HTMLElement;
|
||||||
projectionMode: ProjectionMode;
|
projectionMode: ProjectionMode;
|
||||||
videoElement: HTMLVideoElement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function bootstrapPlayer(playerBase: string, onReady: (context: BootstrapContext) => void): void {
|
export function bootstrapPlayer(playerBase: string, onReady: (context: BootstrapContext) => void): void {
|
||||||
@@ -38,25 +39,24 @@ export function bootstrapPlayer(playerBase: string, onReady: (context: Bootstrap
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoElement = playerContainer.querySelector('video');
|
const mediaAdapter = createMediaAdapter(playerContainer);
|
||||||
if (!videoElement) {
|
if (!mediaAdapter) {
|
||||||
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a video element.`);
|
console.error(`VR_WEB_PLAYER_DOM: ${PLAYER_SELECTOR} must contain a supported media element (video).`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
videoElement.classList.add('vrwp-video');
|
|
||||||
|
|
||||||
const playButton = createPlayButton();
|
const playButton = createPlayButton();
|
||||||
playerContainer.appendChild(playButton);
|
playerContainer.appendChild(playButton);
|
||||||
playerContainer.appendChild(create2DControlPanel());
|
playerContainer.appendChild(create2DControlPanel());
|
||||||
playButton.disabled = true;
|
playButton.disabled = true;
|
||||||
videoElement.load();
|
mediaAdapter.load();
|
||||||
|
|
||||||
completeXrSupportCheck(playButton, () => {
|
completeXrSupportCheck(playButton, () => {
|
||||||
onReady({
|
onReady({
|
||||||
|
mediaAdapter,
|
||||||
playButton,
|
playButton,
|
||||||
playerContainer,
|
playerContainer,
|
||||||
projectionMode: configuredProjection as ProjectionMode,
|
projectionMode: configuredProjection as ProjectionMode
|
||||||
videoElement
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
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;
|
getCamera: () => any;
|
||||||
getCameraControls: () => FallbackCameraControls | undefined;
|
getCameraControls: () => FallbackCameraControls | undefined;
|
||||||
getMaterial: () => any;
|
getMaterial: () => any;
|
||||||
|
getMediaElement: () => HTMLElement | undefined;
|
||||||
getRenderer: () => any;
|
getRenderer: () => any;
|
||||||
getScene: () => any;
|
getScene: () => any;
|
||||||
getVideo: () => HTMLVideoElement | undefined;
|
getVideo: () => HTMLVideoElement | undefined;
|
||||||
@@ -43,6 +44,7 @@ export class TwoDMode {
|
|||||||
private readonly getCamera: () => any;
|
private readonly getCamera: () => any;
|
||||||
private readonly getCameraControls: () => FallbackCameraControls | undefined;
|
private readonly getCameraControls: () => FallbackCameraControls | undefined;
|
||||||
private readonly getMaterial: () => any;
|
private readonly getMaterial: () => any;
|
||||||
|
private readonly getMediaElement: () => HTMLElement | undefined;
|
||||||
private readonly getRenderer: () => any;
|
private readonly getRenderer: () => any;
|
||||||
private readonly getScene: () => any;
|
private readonly getScene: () => any;
|
||||||
private readonly getVideo: () => HTMLVideoElement | undefined;
|
private readonly getVideo: () => HTMLVideoElement | undefined;
|
||||||
@@ -57,6 +59,7 @@ export class TwoDMode {
|
|||||||
this.getCamera = options.getCamera;
|
this.getCamera = options.getCamera;
|
||||||
this.getCameraControls = options.getCameraControls;
|
this.getCameraControls = options.getCameraControls;
|
||||||
this.getMaterial = options.getMaterial;
|
this.getMaterial = options.getMaterial;
|
||||||
|
this.getMediaElement = options.getMediaElement;
|
||||||
this.getRenderer = options.getRenderer;
|
this.getRenderer = options.getRenderer;
|
||||||
this.getScene = options.getScene;
|
this.getScene = options.getScene;
|
||||||
this.getVideo = options.getVideo;
|
this.getVideo = options.getVideo;
|
||||||
@@ -91,11 +94,11 @@ export class TwoDMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
const video = this.getVideo();
|
const mediaElement = this.getMediaElement();
|
||||||
const renderer = this.getRenderer();
|
const renderer = this.getRenderer();
|
||||||
const camera = this.getCamera();
|
const camera = this.getCamera();
|
||||||
|
|
||||||
if (!video || !renderer || !camera) {
|
if (!mediaElement || !renderer || !camera) {
|
||||||
console.error("Required components not available for 2D mode");
|
console.error("Required components not available for 2D mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,7 +107,7 @@ export class TwoDMode {
|
|||||||
this.resizeCanvasFor2D(renderer, camera);
|
this.resizeCanvasFor2D(renderer, camera);
|
||||||
|
|
||||||
const canvas = showFallbackCanvas(renderer);
|
const canvas = showFallbackCanvas(renderer);
|
||||||
video.style.display = 'none';
|
mediaElement.style.display = 'none';
|
||||||
|
|
||||||
const mediaTexture = this.callbacks.createMediaTexture();
|
const mediaTexture = this.callbacks.createMediaTexture();
|
||||||
this.callbacks.positionPlaneForPresentation(this.projectionMode === 'plane');
|
this.callbacks.positionPlaneForPresentation(this.projectionMode === 'plane');
|
||||||
@@ -139,9 +142,9 @@ export class TwoDMode {
|
|||||||
this.getCameraControls()?.reset();
|
this.getCameraControls()?.reset();
|
||||||
this.callbacks.positionPlaneForPresentation(false);
|
this.callbacks.positionPlaneForPresentation(false);
|
||||||
|
|
||||||
const video = this.getVideo();
|
const mediaElement = this.getMediaElement();
|
||||||
if (video) {
|
if (mediaElement) {
|
||||||
video.style.display = '';
|
mediaElement.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ export type ManagedMaterial<TTexture extends ManagedTexture = ManagedTexture> =
|
|||||||
needsUpdate: boolean;
|
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 texture: TTexture | null = null;
|
||||||
private readonly createTexture: TextureFactory<TTexture>;
|
private readonly createTexture: TextureFactory<TSource, TTexture>;
|
||||||
private readonly video: HTMLVideoElement;
|
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.createTexture = createTexture;
|
||||||
this.video = video;
|
this.shouldUpdateTexture = shouldUpdateTexture;
|
||||||
|
this.source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
get current(): TTexture | null {
|
get current(): TTexture | null {
|
||||||
@@ -48,7 +50,7 @@ export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTextur
|
|||||||
|
|
||||||
create(): TTexture {
|
create(): TTexture {
|
||||||
this.dispose();
|
this.dispose();
|
||||||
this.texture = this.createTexture(this.video);
|
this.texture = this.createTexture(this.source);
|
||||||
return this.texture;
|
return this.texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +61,8 @@ export class VideoTextureManager<TTexture extends ManagedTexture = ManagedTextur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateIfPlaying(): void {
|
updateIfNeeded(): void {
|
||||||
if (this.texture && !this.video.paused && !this.video.ended) {
|
if (this.texture && this.shouldUpdateTexture()) {
|
||||||
this.texture.needsUpdate = true;
|
this.texture.needsUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import {
|
|||||||
createPlayerRenderer,
|
createPlayerRenderer,
|
||||||
resizePlayerRenderer
|
resizePlayerRenderer
|
||||||
} from './rendering/renderer-lifecycle.js';
|
} 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;
|
const _playerBase = new URL('.', import.meta.url).href;
|
||||||
|
|
||||||
@@ -41,13 +42,14 @@ let scene, camera, renderer, video, sphereMaterial;
|
|||||||
let vr180Mesh, planeMesh, activeContentMesh;
|
let vr180Mesh, planeMesh, activeContentMesh;
|
||||||
let xrSession = null;
|
let xrSession = null;
|
||||||
let raycaster, uiElements = [];
|
let raycaster, uiElements = [];
|
||||||
let videoElement, playBtn;
|
let mediaAdapter: SupportedMediaAdapter | undefined;
|
||||||
|
let playBtn;
|
||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
|
|
||||||
let isXrLoopActive = false;
|
let isXrLoopActive = false;
|
||||||
let vrControlPanel;
|
let vrControlPanel;
|
||||||
let mediaController: MediaController | undefined;
|
let mediaController: MediaController | undefined;
|
||||||
let textureManager: VideoTextureManager | undefined;
|
let textureManager: MediaTextureManager<HTMLVideoElement> | undefined;
|
||||||
let vrPanel: VrControlPanel | undefined;
|
let vrPanel: VrControlPanel | undefined;
|
||||||
let twoDMode: TwoDMode | undefined;
|
let twoDMode: TwoDMode | undefined;
|
||||||
const vrPanelVisibility = new VrPanelVisibility();
|
const vrPanelVisibility = new VrPanelVisibility();
|
||||||
@@ -59,7 +61,8 @@ let fallbackCameraControls: FallbackCameraControls | undefined;
|
|||||||
bootstrapPlayer(_playerBase, (context) => {
|
bootstrapPlayer(_playerBase, (context) => {
|
||||||
playerContainer = context.playerContainer;
|
playerContainer = context.playerContainer;
|
||||||
projectionMode = context.projectionMode;
|
projectionMode = context.projectionMode;
|
||||||
videoElement = context.videoElement;
|
mediaAdapter = context.mediaAdapter;
|
||||||
|
video = mediaAdapter.element;
|
||||||
playBtn = context.playButton;
|
playBtn = context.playButton;
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
@@ -106,18 +109,16 @@ function closeActiveXrSessionAfterContextLoss() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restoreVideoTextureAfterContextRestored() {
|
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);
|
textureManager?.assignToMaterial(sphereMaterial);
|
||||||
updateVRPlayPauseButtonIcon();
|
updateVRPlayPauseButtonIcon();
|
||||||
updateVRVolumeButtonIcon();
|
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() {
|
function getMediaTitle() {
|
||||||
return videoElement.getAttribute('title') ||
|
return mediaAdapter?.getTitle() || 'Video Title';
|
||||||
videoElement.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
|
|
||||||
"Video Title";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -131,9 +132,16 @@ function init() {
|
|||||||
scene = playerRenderer.scene;
|
scene = playerRenderer.scene;
|
||||||
camera = playerRenderer.camera;
|
camera = playerRenderer.camera;
|
||||||
renderer = playerRenderer.renderer;
|
renderer = playerRenderer.renderer;
|
||||||
|
if (!mediaAdapter) {
|
||||||
|
throw new Error('Media adapter is not initialized.');
|
||||||
|
}
|
||||||
|
|
||||||
video = videoElement;
|
video = mediaAdapter.element;
|
||||||
textureManager = new VideoTextureManager(video, createVideoTextureCore);
|
textureManager = new MediaTextureManager(
|
||||||
|
mediaAdapter.textureSource,
|
||||||
|
createVideoTextureCore,
|
||||||
|
() => mediaAdapter?.shouldUpdateTexture() ?? false
|
||||||
|
);
|
||||||
mediaController = new MediaController({
|
mediaController = new MediaController({
|
||||||
is2DModeActive,
|
is2DModeActive,
|
||||||
on2DPlaybackResume: show2DControlPanel,
|
on2DPlaybackResume: show2DControlPanel,
|
||||||
@@ -171,12 +179,13 @@ function init() {
|
|||||||
getCamera: () => camera2D,
|
getCamera: () => camera2D,
|
||||||
getCameraControls: () => fallbackCameraControls,
|
getCameraControls: () => fallbackCameraControls,
|
||||||
getMaterial: () => sphereMaterial,
|
getMaterial: () => sphereMaterial,
|
||||||
|
getMediaElement: () => mediaAdapter?.element,
|
||||||
getRenderer: () => renderer,
|
getRenderer: () => renderer,
|
||||||
getScene: () => scene,
|
getScene: () => scene,
|
||||||
getVideo: () => video,
|
getVideo: () => video,
|
||||||
playerContainer,
|
playerContainer,
|
||||||
projectionMode,
|
projectionMode,
|
||||||
title: getVideoTitle()
|
title: getMediaTitle()
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
console.error("INIT_ERROR (Phase 1 - Core Setup):", e);
|
||||||
@@ -185,7 +194,7 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try { // Phase 2: VR Control Panel UI
|
try { // Phase 2: VR Control Panel UI
|
||||||
vrPanel = createVrControlPanel(scene, getVideoTitle());
|
vrPanel = createVrControlPanel(scene, getMediaTitle());
|
||||||
vrControlPanel = vrPanel.group;
|
vrControlPanel = vrPanel.group;
|
||||||
vrPanelVisibility.setPanel(vrPanel);
|
vrPanelVisibility.setPanel(vrPanel);
|
||||||
uiElements.push(...vrPanel.interactables);
|
uiElements.push(...vrPanel.interactables);
|
||||||
@@ -372,8 +381,8 @@ function onSelectStartVR(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnterVRButtonClick() {
|
async function handleEnterVRButtonClick() {
|
||||||
if (!video) {
|
if (!mediaAdapter) {
|
||||||
console.error("Video element not found for VR button click.");
|
console.error("Media element not found for VR button click.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,10 +427,7 @@ async function actualSessionToggle() {
|
|||||||
xrSession.addEventListener('end', onVRSessionEnd);
|
xrSession.addEventListener('end', onVRSessionEnd);
|
||||||
|
|
||||||
|
|
||||||
// Hide the regular video element when entering VR
|
mediaAdapter?.hideElement();
|
||||||
if (video) {
|
|
||||||
video.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaController && video && (video.paused || video.ended)) {
|
if (mediaController && video && (video.paused || video.ended)) {
|
||||||
try {
|
try {
|
||||||
@@ -435,8 +441,8 @@ async function actualSessionToggle() {
|
|||||||
positionPlaneForPresentation(false);
|
positionPlaneForPresentation(false);
|
||||||
|
|
||||||
textureManager?.dispose();
|
textureManager?.dispose();
|
||||||
if (!video) {
|
if (!mediaAdapter) {
|
||||||
throw new Error("Video element not available for creating texture.");
|
throw new Error("Media adapter not available for creating texture.");
|
||||||
}
|
}
|
||||||
if (!activeContentMesh || !sphereMaterial) {
|
if (!activeContentMesh || !sphereMaterial) {
|
||||||
throw new Error("VR mesh components not ready for texture.");
|
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
|
mediaAdapter?.showElement();
|
||||||
if (video) {
|
|
||||||
video.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaController?.pauseIfPlaying();
|
mediaController?.pauseIfPlaying();
|
||||||
|
|
||||||
@@ -558,7 +561,7 @@ function renderXR(timestamp, frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
textureManager?.updateIfPlaying();
|
textureManager?.updateIfNeeded();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
const renderErrorMsg = "ERROR_IN_RENDERXR_LOOP (F" + frameCounter + "): " + (error.message || String(error));
|
||||||
|
|||||||
93
tests/media-adapter.test.mjs
Normal file
93
tests/media-adapter.test.mjs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createMediaAdapter,
|
||||||
|
VideoMediaAdapter
|
||||||
|
} from '../vr180player/media/media-adapter.js';
|
||||||
|
|
||||||
|
function createVideo({
|
||||||
|
ended = false,
|
||||||
|
paused = false,
|
||||||
|
source = 'https://cdn.example.com/videos/demo-video.mp4',
|
||||||
|
title = ''
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
classList: {
|
||||||
|
values: [],
|
||||||
|
add(value) {
|
||||||
|
this.values.push(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ended,
|
||||||
|
loadCount: 0,
|
||||||
|
paused,
|
||||||
|
style: { display: '' },
|
||||||
|
getAttribute(name) {
|
||||||
|
return name === 'title' ? title : '';
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
this.loadCount += 1;
|
||||||
|
},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector !== 'source' || !source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { src: source };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('VideoMediaAdapter exposes video capabilities and lifecycle helpers', () => {
|
||||||
|
const video = createVideo({ title: 'Demo Title' });
|
||||||
|
const adapter = new VideoMediaAdapter(video);
|
||||||
|
|
||||||
|
assert.deepEqual(adapter.capabilities, {
|
||||||
|
audio: true,
|
||||||
|
dynamicTexture: true,
|
||||||
|
playback: true,
|
||||||
|
timeline: true
|
||||||
|
});
|
||||||
|
assert.equal(adapter.element, video);
|
||||||
|
assert.equal(adapter.textureSource, video);
|
||||||
|
assert.equal(adapter.getTitle(), 'Demo Title');
|
||||||
|
|
||||||
|
adapter.hideElement();
|
||||||
|
assert.equal(video.style.display, 'none');
|
||||||
|
|
||||||
|
adapter.showElement();
|
||||||
|
assert.equal(video.style.display, '');
|
||||||
|
|
||||||
|
adapter.load();
|
||||||
|
assert.equal(video.loadCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VideoMediaAdapter falls back to source filename and tracks texture update state', () => {
|
||||||
|
const video = createVideo({ source: 'https://cdn.example.com/media/flat-sbs-demo.mp4' });
|
||||||
|
const adapter = new VideoMediaAdapter(video);
|
||||||
|
|
||||||
|
assert.equal(adapter.getTitle(), 'flat sbs demo');
|
||||||
|
assert.equal(adapter.shouldUpdateTexture(), true);
|
||||||
|
|
||||||
|
video.paused = true;
|
||||||
|
assert.equal(adapter.shouldUpdateTexture(), false);
|
||||||
|
|
||||||
|
video.paused = false;
|
||||||
|
video.ended = true;
|
||||||
|
assert.equal(adapter.shouldUpdateTexture(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createMediaAdapter finds and marks the supported video element', () => {
|
||||||
|
const video = createVideo();
|
||||||
|
const playerContainer = {
|
||||||
|
querySelector(selector) {
|
||||||
|
return selector === 'video' ? video : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const adapter = createMediaAdapter(playerContainer);
|
||||||
|
|
||||||
|
assert.ok(adapter instanceof VideoMediaAdapter);
|
||||||
|
assert.equal(adapter.element, video);
|
||||||
|
assert.deepEqual(video.classList.values, ['vrwp-video']);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { VideoTextureManager } from '../vr180player/rendering/texture-manager.js';
|
import { MediaTextureManager } from '../vr180player/rendering/texture-manager.js';
|
||||||
|
|
||||||
function createTexture(name) {
|
function createTexture(name) {
|
||||||
return {
|
return {
|
||||||
@@ -17,13 +17,13 @@ function createVideo({ paused = false, ended = false } = {}) {
|
|||||||
return { ended, paused };
|
return { ended, paused };
|
||||||
}
|
}
|
||||||
|
|
||||||
test('VideoTextureManager replaces the previous texture when creating a new one', () => {
|
test('MediaTextureManager replaces the previous texture when creating a new one', () => {
|
||||||
const created = [];
|
const created = [];
|
||||||
const manager = new VideoTextureManager(createVideo(), () => {
|
const manager = new MediaTextureManager(createVideo(), () => {
|
||||||
const texture = createTexture(`texture-${created.length}`);
|
const texture = createTexture(`texture-${created.length}`);
|
||||||
created.push(texture);
|
created.push(texture);
|
||||||
return texture;
|
return texture;
|
||||||
});
|
}, () => true);
|
||||||
|
|
||||||
const first = manager.create();
|
const first = manager.create();
|
||||||
const second = manager.create();
|
const second = manager.create();
|
||||||
@@ -34,9 +34,9 @@ test('VideoTextureManager replaces the previous texture when creating a new one'
|
|||||||
assert.equal(created.length, 2);
|
assert.equal(created.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('VideoTextureManager assigns and clears material maps', () => {
|
test('MediaTextureManager assigns and clears material maps', () => {
|
||||||
const material = { map: null, needsUpdate: false };
|
const material = { map: null, needsUpdate: false };
|
||||||
const manager = new VideoTextureManager(createVideo(), () => createTexture('assigned'));
|
const manager = new MediaTextureManager(createVideo(), () => createTexture('assigned'), () => true);
|
||||||
|
|
||||||
const texture = manager.assignToMaterial(material);
|
const texture = manager.assignToMaterial(material);
|
||||||
|
|
||||||
@@ -53,21 +53,25 @@ test('VideoTextureManager assigns and clears material maps', () => {
|
|||||||
assert.equal(manager.current, null);
|
assert.equal(manager.current, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('VideoTextureManager only marks textures dirty while playback is active', () => {
|
test('MediaTextureManager only marks textures dirty when the update predicate allows it', () => {
|
||||||
const video = createVideo();
|
const video = createVideo();
|
||||||
const manager = new VideoTextureManager(video, () => createTexture('playing'));
|
const manager = new MediaTextureManager(
|
||||||
|
video,
|
||||||
|
() => createTexture('playing'),
|
||||||
|
() => !video.paused && !video.ended
|
||||||
|
);
|
||||||
const texture = manager.create();
|
const texture = manager.create();
|
||||||
|
|
||||||
manager.updateIfPlaying();
|
manager.updateIfNeeded();
|
||||||
assert.equal(texture.needsUpdate, true);
|
assert.equal(texture.needsUpdate, true);
|
||||||
|
|
||||||
texture.needsUpdate = false;
|
texture.needsUpdate = false;
|
||||||
video.paused = true;
|
video.paused = true;
|
||||||
manager.updateIfPlaying();
|
manager.updateIfNeeded();
|
||||||
assert.equal(texture.needsUpdate, false);
|
assert.equal(texture.needsUpdate, false);
|
||||||
|
|
||||||
video.paused = false;
|
video.paused = false;
|
||||||
video.ended = true;
|
video.ended = true;
|
||||||
manager.updateIfPlaying();
|
manager.updateIfNeeded();
|
||||||
assert.equal(texture.needsUpdate, false);
|
assert.equal(texture.needsUpdate, false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user