music-video-gen/tv-player/src/core/video-player.js
2025-11-16 13:25:42 +01:00

115 lines
4.2 KiB
JavaScript

import * as THREE from 'three';
import { state } from '../state.js';
// --- Play video by index ---
export function playVideoByIndex(index) {
state.currentVideoIndex = index;
const url = state.videoUrls[index];
// Dispose of previous texture to free resources
if (state.videoTexture) {
state.videoTexture.dispose();
state.videoTexture = null;
}
if (index < 0 || index >= state.videoUrls.length) {
console.info('End of playlist reached. Reload tapes to start again.');
state.screenLight.intensity = 0.0;
state.tvScreen.material.dispose();
state.tvScreen.material = new THREE.MeshPhongMaterial({
color: 0x0a0a0a, // Deep black
shininess: 5,
specular: 0x111111
});
state.tvScreen.material.needsUpdate = true;
state.isVideoLoaded = false;
state.lastUpdateTime = -1; // force VCR to redraw
return;
}
state.videoElement.src = url;
state.videoElement.muted = true;
state.videoElement.load();
// Set loop property: only loop if it's the only video loaded
state.videoElement.loop = false; //state.videoUrls.length === 1;
state.videoElement.onloadeddata = () => {
// 1. Create the Three.js texture
state.videoTexture = new THREE.VideoTexture(state.videoElement);
state.videoTexture.minFilter = THREE.LinearFilter;
state.videoTexture.magFilter = THREE.LinearFilter;
state.videoTexture.format = THREE.RGBAFormat;
state.videoTexture.needsUpdate = true;
// 2. Apply the video texture to the screen mesh
state.tvScreen.material.dispose();
state.tvScreen.material = new THREE.MeshBasicMaterial({ map: state.videoTexture });
state.tvScreen.material.needsUpdate = true;
// 3. Start playback
state.videoElement.play().then(() => {
state.isVideoLoaded = true;
// Use the defined base intensity for screen glow
state.screenLight.intensity = state.originalScreenIntensity;
// Initial status message with tape count
console.info(`Playing tape ${state.currentVideoIndex + 1} of ${state.videoUrls.length}.`);
}).catch(error => {
state.screenLight.intensity = state.originalScreenIntensity * 0.5; // Dim the light if playback fails
console.error(`Playback blocked for tape ${state.currentVideoIndex + 1}. Click Next Tape to try again.`);
console.error('Playback Error: Could not start video playback.', error);
});
};
state.videoElement.onerror = (e) => {
state.screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
console.error(`Error loading tape ${state.currentVideoIndex + 1}.`);
console.error('Video Load Error:', e);
};
}
// --- Cycle to the next video ---
export function playNextVideo() {
// Determine the next index, cycling back to 0 if we reach the end
let nextIndex = state.currentVideoIndex + 1;
if (nextIndex < state.videoUrls.length) {
state.baseTime += state.videoElement.duration;
}
playVideoByIndex(nextIndex);
}
// --- Video Loading Logic (handles multiple files) ---
export function loadVideoFile(event) {
const files = event.target.files;
if (files.length === 0) {
console.info('File selection cancelled.');
return;
}
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks
state.videoUrls.forEach(url => URL.revokeObjectURL(url));
state.videoUrls = [];
// 2. Populate the new videoUrls array
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('video/')) {
state.videoUrls.push(URL.createObjectURL(file));
}
}
if (state.videoUrls.length === 0) {
console.info('No valid video files selected.');
return;
}
// 3. Start playback of the first video
console.info(`Loaded ${state.videoUrls.length} tapes. Starting playback...`);
state.loadTapeButton.classList.add("hidden");
const startDelay = 5;
console.info(`Video will start in ${startDelay} seconds.`);
setTimeout(() => { playVideoByIndex(0); }, startDelay * 1000);
}