Add initial project structure with Docker setup, SOLID principles, and video player implementation
- 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.
This commit is contained in:
159
src/classes/VolumeMeter.ts
Normal file
159
src/classes/VolumeMeter.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { IVolumeMeter } from '../interfaces/IVolumeMeter';
|
||||
|
||||
/**
|
||||
* VolumeMeter class
|
||||
* Follows Single Responsibility Principle (SRP) - only responsible for audio analysis
|
||||
*/
|
||||
export class VolumeMeter implements IVolumeMeter {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private analyser: AnalyserNode | null = null;
|
||||
private source: MediaElementAudioSourceNode | null = null;
|
||||
private videoElement: HTMLVideoElement;
|
||||
private isRunning: boolean = false;
|
||||
private animationFrameId: number | null = null;
|
||||
private volumeCallback?: (volume: number) => void;
|
||||
|
||||
constructor(videoElement: HTMLVideoElement) {
|
||||
this.videoElement = videoElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback function to be called with volume updates
|
||||
*/
|
||||
setVolumeCallback(callback: (volume: number) => void): void {
|
||||
this.volumeCallback = callback;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create audio context
|
||||
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
|
||||
// Resume audio context if suspended (required by browser autoplay policies)
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume();
|
||||
console.log('[VolumeMeter] Audio context resumed');
|
||||
}
|
||||
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 512; // Increased for better frequency resolution
|
||||
this.analyser.smoothingTimeConstant = 0.3; // Lower for more reactive visualization
|
||||
|
||||
// Create source from video element
|
||||
this.source = this.audioContext.createMediaElementSource(this.videoElement);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
|
||||
this.isRunning = true;
|
||||
console.log('[VolumeMeter] Started successfully, frequencyBinCount:', this.analyser.frequencyBinCount);
|
||||
|
||||
// Check for CORS issues by testing if we get any audio data
|
||||
if (this.analyser) {
|
||||
setTimeout(() => {
|
||||
if (this.analyser) {
|
||||
const testArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.analyser.getByteFrequencyData(testArray);
|
||||
const hasData = testArray.some(value => value > 0);
|
||||
if (!hasData) {
|
||||
console.warn('[VolumeMeter] No audio data detected - likely CORS restriction. Volume meters may not react.');
|
||||
console.warn('[VolumeMeter] Solution: Add CORS headers to the HLS stream server or use same-origin content.');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
this.startVolumeMonitoring();
|
||||
} catch (error) {
|
||||
console.error('[VolumeMeter] Error starting audio analysis:', error);
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
if (this.source) {
|
||||
this.source.disconnect();
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
if (this.analyser) {
|
||||
this.analyser.disconnect();
|
||||
this.analyser = null;
|
||||
}
|
||||
|
||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
getVolumeLevel(): number {
|
||||
if (!this.analyser) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
// Calculate average volume
|
||||
let sum = 0;
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
sum += dataArray[i];
|
||||
}
|
||||
const average = sum / dataArray.length;
|
||||
|
||||
// Normalize to 0-1 range
|
||||
return average / 255;
|
||||
}
|
||||
|
||||
getFrequencyData(array: Uint8Array): void {
|
||||
if (this.analyser) {
|
||||
this.analyser.getByteFrequencyData(array as Uint8Array<ArrayBuffer>);
|
||||
}
|
||||
}
|
||||
|
||||
getTimeDomainData(array: Uint8Array): void {
|
||||
if (this.analyser) {
|
||||
this.analyser.getByteTimeDomainData(array as Uint8Array<ArrayBuffer>);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuously monitors volume and calls the callback
|
||||
*/
|
||||
private startVolumeMonitoring(): void {
|
||||
const update = () => {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = this.getVolumeLevel();
|
||||
if (this.volumeCallback) {
|
||||
this.volumeCallback(volume);
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user