- 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.
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
|