- Created .gitignore to exclude logs and build artifacts. - Added Dockerfile and docker-compose.yml for containerized deployment. - Implemented a video player following SOLID principles with classes for video source, audio control, and volume meter. - Introduced interfaces for audio, video source, and volume meter to ensure adherence to Interface Segregation Principle. - Developed main entry point and HTML structure for the video player application. - Included TypeScript configuration and package.json for project dependencies.
118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import type { IVolumeMeter } from '../interfaces/IVolumeMeter';
|
|
|
|
/**
|
|
* VolumeMeterVisualizer class
|
|
* Follows Single Responsibility Principle (SRP) - only responsible for visualizing volume data
|
|
*/
|
|
export class VolumeMeterVisualizer {
|
|
private container: HTMLElement;
|
|
private volumeMeter: IVolumeMeter;
|
|
private bars: HTMLElement[] = [];
|
|
private barCount: number;
|
|
private animationFrameId: number | null = null;
|
|
|
|
constructor(container: HTMLElement, volumeMeter: IVolumeMeter, barCount: number = 20) {
|
|
this.container = container;
|
|
this.volumeMeter = volumeMeter;
|
|
this.barCount = barCount;
|
|
this.createBars();
|
|
}
|
|
|
|
/**
|
|
* Creates the visual bar elements
|
|
*/
|
|
private createBars(): void {
|
|
this.container.innerHTML = '';
|
|
this.bars = [];
|
|
|
|
for (let i = 0; i < this.barCount; i++) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'volume-bar';
|
|
this.container.appendChild(bar);
|
|
this.bars.push(bar);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the visualization animation
|
|
*/
|
|
start(): void {
|
|
if (this.animationFrameId !== null) {
|
|
return;
|
|
}
|
|
|
|
const update = () => {
|
|
if (!this.volumeMeter.isActive()) {
|
|
// Keep running even if not active, but show minimal activity so bars are visible
|
|
this.bars.forEach(bar => {
|
|
bar.style.height = '10%';
|
|
bar.style.opacity = '0.4';
|
|
});
|
|
this.animationFrameId = requestAnimationFrame(update);
|
|
return;
|
|
}
|
|
|
|
// Get volume level
|
|
const volume = this.volumeMeter.getVolumeLevel();
|
|
|
|
// Update each bar based on volume and frequency
|
|
// Use the correct size based on analyser's frequencyBinCount (fftSize / 2)
|
|
const frequencyArray = new Uint8Array(256);
|
|
this.volumeMeter.getFrequencyData(frequencyArray);
|
|
|
|
// Check if we're getting any actual audio data (CORS check)
|
|
const hasAudioData = frequencyArray.some(value => value > 0) || volume > 0.001;
|
|
|
|
if (!hasAudioData) {
|
|
// If no audio data (likely CORS issue), show a subtle pulsing animation
|
|
const time = Date.now() / 1000;
|
|
this.bars.forEach((bar, index) => {
|
|
const pulse = Math.sin(time * 2 + index * 0.3) * 0.3 + 0.5;
|
|
bar.style.height = `${10 + pulse * 15}%`;
|
|
bar.style.opacity = `${0.3 + pulse * 0.2}`;
|
|
});
|
|
this.animationFrameId = requestAnimationFrame(update);
|
|
return;
|
|
}
|
|
|
|
this.bars.forEach((bar, index) => {
|
|
// Map bar index to frequency bin with some overlap for smoother visualization
|
|
const frequencyIndex = Math.floor((index / this.barCount) * frequencyArray.length);
|
|
const frequencyValue = frequencyArray[frequencyIndex] || 0;
|
|
|
|
// Calculate height with better scaling - ensure minimum visibility
|
|
const normalizedFreq = frequencyValue / 255;
|
|
// Scale more aggressively for better visibility
|
|
const height = Math.max(normalizedFreq * 100, volume * 30, 8); // At least 8% when active
|
|
|
|
bar.style.height = `${Math.min(height, 100)}%`;
|
|
|
|
// Add color and opacity based on volume level - more dynamic
|
|
const intensity = Math.max(normalizedFreq, volume);
|
|
bar.style.opacity = `${0.5 + intensity * 0.5}`;
|
|
});
|
|
|
|
this.animationFrameId = requestAnimationFrame(update);
|
|
};
|
|
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* Stops the visualization animation
|
|
*/
|
|
stop(): void {
|
|
if (this.animationFrameId !== null) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
|
|
// Reset all bars
|
|
this.bars.forEach(bar => {
|
|
bar.style.height = '0%';
|
|
bar.style.opacity = '0.3';
|
|
});
|
|
}
|
|
}
|
|
|