Add VideoSprite with auto-pause and concurrent video limit
Implements inline video playback as a PixiJS Sprite subclass with: - MAX_CONCURRENT_VIDEOS (3) with oldest-eviction policy - Visibility-based auto play/pause for viewport culling - Play icon overlay, tap-to-toggle-mute interaction - Proper resource cleanup on destroy (video element + texture)
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
import { Sprite, Texture, Graphics } from 'pixi.js';
|
||||||
|
|
||||||
|
const MAX_CONCURRENT_VIDEOS = 3;
|
||||||
|
const activeVideos: Set<VideoSprite> = new Set();
|
||||||
|
|
||||||
|
export class VideoSprite extends Sprite {
|
||||||
|
readonly assetKey: string;
|
||||||
|
private videoEl: HTMLVideoElement;
|
||||||
|
private videoTexture: Texture | null = null;
|
||||||
|
private playIcon: Graphics;
|
||||||
|
private _isPlaying: boolean = false;
|
||||||
|
muted: boolean = true;
|
||||||
|
loop: boolean = true;
|
||||||
|
|
||||||
|
constructor(assetKey: string, w: number, h: number, url: string) {
|
||||||
|
super(Texture.EMPTY);
|
||||||
|
this.assetKey = assetKey;
|
||||||
|
this.width = w;
|
||||||
|
this.height = h;
|
||||||
|
|
||||||
|
// Create hidden video element
|
||||||
|
this.videoEl = document.createElement('video');
|
||||||
|
this.videoEl.src = url;
|
||||||
|
this.videoEl.crossOrigin = 'anonymous';
|
||||||
|
this.videoEl.muted = true;
|
||||||
|
this.videoEl.loop = true;
|
||||||
|
this.videoEl.playsInline = true;
|
||||||
|
this.videoEl.preload = 'metadata';
|
||||||
|
|
||||||
|
// Dark background
|
||||||
|
const bg = new Graphics();
|
||||||
|
bg.rect(0, 0, w, h).fill(0x1a1a1a);
|
||||||
|
this.addChildAt(bg, 0);
|
||||||
|
|
||||||
|
// Play icon — white triangle centered in the sprite
|
||||||
|
this.playIcon = new Graphics();
|
||||||
|
const triSize = Math.min(w, h) * 0.25;
|
||||||
|
const cx = w / 2;
|
||||||
|
const cy = h / 2;
|
||||||
|
this.playIcon
|
||||||
|
.poly([
|
||||||
|
cx - triSize * 0.4, cy - triSize * 0.5,
|
||||||
|
cx + triSize * 0.5, cy,
|
||||||
|
cx - triSize * 0.4, cy + triSize * 0.5,
|
||||||
|
])
|
||||||
|
.fill({ color: 0xffffff, alpha: 0.7 });
|
||||||
|
this.addChild(this.playIcon);
|
||||||
|
|
||||||
|
// Interaction
|
||||||
|
this.eventMode = 'static';
|
||||||
|
this.cursor = 'pointer';
|
||||||
|
this.on('pointertap', () => {
|
||||||
|
this.muted = !this.muted;
|
||||||
|
this.videoEl.muted = this.muted;
|
||||||
|
});
|
||||||
|
|
||||||
|
// When video metadata/frame is ready, create texture
|
||||||
|
this.videoEl.addEventListener(
|
||||||
|
'loadeddata',
|
||||||
|
() => {
|
||||||
|
this.videoTexture = Texture.from(this.videoEl);
|
||||||
|
this.texture = this.videoTexture;
|
||||||
|
this.width = w;
|
||||||
|
this.height = h;
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
play(): void {
|
||||||
|
if (this._isPlaying) return;
|
||||||
|
|
||||||
|
// Evict oldest if at capacity
|
||||||
|
if (activeVideos.size >= MAX_CONCURRENT_VIDEOS) {
|
||||||
|
const oldest = activeVideos.values().next().value as VideoSprite;
|
||||||
|
oldest.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videoEl.play();
|
||||||
|
this._isPlaying = true;
|
||||||
|
this.playIcon.visible = false;
|
||||||
|
activeVideos.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(): void {
|
||||||
|
if (!this._isPlaying) return;
|
||||||
|
|
||||||
|
this.videoEl.pause();
|
||||||
|
this._isPlaying = false;
|
||||||
|
this.playIcon.visible = true;
|
||||||
|
activeVideos.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isPlaying(): boolean {
|
||||||
|
return this._isPlaying;
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibilityChange(visible: boolean): void {
|
||||||
|
if (visible && !this._isPlaying) {
|
||||||
|
this.play();
|
||||||
|
} else if (!visible && this._isPlaying) {
|
||||||
|
this.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(options?: Parameters<Sprite['destroy']>[0]): void {
|
||||||
|
this.pause();
|
||||||
|
this.videoEl.src = '';
|
||||||
|
this.videoEl.load(); // release network resources
|
||||||
|
if (this.videoTexture) {
|
||||||
|
this.videoTexture.destroy(true);
|
||||||
|
this.videoTexture = null;
|
||||||
|
}
|
||||||
|
super.destroy(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user