forked from EXT/VR180-Web-Player
Compare commits
2 Commits
481ca9fc47
...
24a166046e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a166046e | ||
|
|
d9a5ec9018 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules/
|
||||
# Generated by `npm run build`.
|
||||
vr180player/*.js
|
||||
vr180player/**/*.js
|
||||
/media
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run build && vite --host 127.0.0.1",
|
||||
"dev": "npm run build && vite --host 0.0.0.0",
|
||||
"build": "tsc",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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));
|
||||
|
||||
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 assert from 'node:assert/strict';
|
||||
import { VideoTextureManager } from '../vr180player/rendering/texture-manager.js';
|
||||
import { MediaTextureManager } from '../vr180player/rendering/texture-manager.js';
|
||||
|
||||
function createTexture(name) {
|
||||
return {
|
||||
@@ -17,13 +17,13 @@ function createVideo({ paused = false, ended = false } = {}) {
|
||||
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 manager = new VideoTextureManager(createVideo(), () => {
|
||||
const manager = new MediaTextureManager(createVideo(), () => {
|
||||
const texture = createTexture(`texture-${created.length}`);
|
||||
created.push(texture);
|
||||
return texture;
|
||||
});
|
||||
}, () => true);
|
||||
|
||||
const first = 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);
|
||||
});
|
||||
|
||||
test('VideoTextureManager assigns and clears material maps', () => {
|
||||
test('MediaTextureManager assigns and clears material maps', () => {
|
||||
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);
|
||||
|
||||
@@ -53,21 +53,25 @@ test('VideoTextureManager assigns and clears material maps', () => {
|
||||
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 manager = new VideoTextureManager(video, () => createTexture('playing'));
|
||||
const manager = new MediaTextureManager(
|
||||
video,
|
||||
() => createTexture('playing'),
|
||||
() => !video.paused && !video.ended
|
||||
);
|
||||
const texture = manager.create();
|
||||
|
||||
manager.updateIfPlaying();
|
||||
manager.updateIfNeeded();
|
||||
assert.equal(texture.needsUpdate, true);
|
||||
|
||||
texture.needsUpdate = false;
|
||||
video.paused = true;
|
||||
manager.updateIfPlaying();
|
||||
manager.updateIfNeeded();
|
||||
assert.equal(texture.needsUpdate, false);
|
||||
|
||||
video.paused = false;
|
||||
video.ended = true;
|
||||
manager.updateIfPlaying();
|
||||
manager.updateIfNeeded();
|
||||
assert.equal(texture.needsUpdate, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user