354 lines
8.3 KiB
TypeScript
354 lines
8.3 KiB
TypeScript
/**
|
|
* Service pour gérer les opérations liées au player
|
|
*/
|
|
|
|
import type { Track } from '../types';
|
|
|
|
/**
|
|
* Formate le temps en format MM:SS
|
|
*/
|
|
export function formatTime(seconds: number): string {
|
|
if (isNaN(seconds) || !isFinite(seconds) || seconds < 0) {
|
|
return '0:00';
|
|
}
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.floor(seconds % 60);
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
/**
|
|
* Calcule le pourcentage de progression
|
|
*/
|
|
export function calculateProgress(
|
|
currentTime: number,
|
|
duration: number,
|
|
): number {
|
|
if (!duration || duration === 0) {
|
|
return 0;
|
|
}
|
|
return (currentTime / duration) * 100;
|
|
}
|
|
|
|
/**
|
|
* Valide qu'une piste est valide
|
|
*/
|
|
export function isValidTrack(track: Track | null): boolean {
|
|
if (!track) {
|
|
return false;
|
|
}
|
|
return !!(track.id && track.title && track.url);
|
|
}
|
|
|
|
/**
|
|
* Trouve l'index d'une piste dans la queue
|
|
*/
|
|
export function findTrackIndex(queue: Track[], trackId: string): number {
|
|
return queue.findIndex((track) => track.id === trackId);
|
|
}
|
|
|
|
/**
|
|
* Mélange un tableau de pistes
|
|
*/
|
|
export function shuffleTracks(tracks: Track[]): Track[] {
|
|
const shuffled = [...tracks];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
}
|
|
|
|
/**
|
|
* Types pour les callbacks d'événements audio
|
|
*/
|
|
export type AudioEventCallback = () => void;
|
|
export type TimeUpdateCallback = (currentTime: number) => void;
|
|
export type DurationChangeCallback = (duration: number) => void;
|
|
export type ErrorCallback = (error: Error) => void;
|
|
|
|
/**
|
|
* Classe pour gérer l'élément audio HTML
|
|
*/
|
|
export class AudioPlayerService {
|
|
private audioElement: HTMLAudioElement | null = null;
|
|
private timeUpdateCallback: TimeUpdateCallback | null = null;
|
|
private durationChangeCallback: DurationChangeCallback | null = null;
|
|
private endedCallback: AudioEventCallback | null = null;
|
|
private errorCallback: ErrorCallback | null = null;
|
|
private playCallback: AudioEventCallback | null = null;
|
|
private pauseCallback: AudioEventCallback | null = null;
|
|
|
|
/**
|
|
* Initialise le service avec un élément audio
|
|
*/
|
|
initialize(audioElement: HTMLAudioElement): void {
|
|
this.audioElement = audioElement;
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Configure les écouteurs d'événements
|
|
*/
|
|
private setupEventListeners(): void {
|
|
if (!this.audioElement) return;
|
|
|
|
this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);
|
|
this.audioElement.addEventListener(
|
|
'loadedmetadata',
|
|
this.handleLoadedMetadata,
|
|
);
|
|
this.audioElement.addEventListener(
|
|
'durationchange',
|
|
this.handleDurationChange,
|
|
);
|
|
this.audioElement.addEventListener('ended', this.handleEnded);
|
|
this.audioElement.addEventListener('error', this.handleError);
|
|
this.audioElement.addEventListener('play', this.handlePlay);
|
|
this.audioElement.addEventListener('pause', this.handlePause);
|
|
}
|
|
|
|
/**
|
|
* Nettoie les écouteurs d'événements
|
|
*/
|
|
cleanup(): void {
|
|
if (!this.audioElement) return;
|
|
|
|
this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);
|
|
this.audioElement.removeEventListener(
|
|
'loadedmetadata',
|
|
this.handleLoadedMetadata,
|
|
);
|
|
this.audioElement.removeEventListener(
|
|
'durationchange',
|
|
this.handleDurationChange,
|
|
);
|
|
this.audioElement.removeEventListener('ended', this.handleEnded);
|
|
this.audioElement.removeEventListener('error', this.handleError);
|
|
this.audioElement.removeEventListener('play', this.handlePlay);
|
|
this.audioElement.removeEventListener('pause', this.handlePause);
|
|
|
|
this.audioElement = null;
|
|
}
|
|
|
|
/**
|
|
* Charge une track dans l'élément audio
|
|
*/
|
|
async loadTrack(track: Track | null): Promise<void> {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
if (!track) {
|
|
this.audioElement.src = '';
|
|
return;
|
|
}
|
|
|
|
if (!isValidTrack(track)) {
|
|
throw new Error('Invalid track');
|
|
}
|
|
|
|
this.audioElement.src = track.url;
|
|
this.audioElement.load();
|
|
}
|
|
|
|
/**
|
|
* Joue l'audio
|
|
*/
|
|
async play(): Promise<void> {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
try {
|
|
await this.audioElement.play();
|
|
} catch (error) {
|
|
throw new Error(`Failed to play audio: ${error}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met en pause l'audio
|
|
*/
|
|
pause(): void {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
this.audioElement.pause();
|
|
}
|
|
|
|
/**
|
|
* Arrête l'audio et remet à zéro
|
|
*/
|
|
stop(): void {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
this.audioElement.pause();
|
|
this.audioElement.currentTime = 0;
|
|
}
|
|
|
|
/**
|
|
* Va à une position spécifique
|
|
*/
|
|
seek(time: number): void {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
const duration = this.audioElement.duration || 0;
|
|
const clampedTime = Math.max(0, Math.min(time, duration));
|
|
this.audioElement.currentTime = clampedTime;
|
|
}
|
|
|
|
/**
|
|
* Définit le volume (0-1)
|
|
*/
|
|
setVolume(volume: number): void {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
this.audioElement.volume = clampedVolume;
|
|
}
|
|
|
|
/**
|
|
* Active ou désactive le mute
|
|
*/
|
|
setMuted(muted: boolean): void {
|
|
if (!this.audioElement) {
|
|
throw new Error('Audio element not initialized');
|
|
}
|
|
|
|
this.audioElement.muted = muted;
|
|
}
|
|
|
|
/**
|
|
* Obtient le temps actuel
|
|
*/
|
|
getCurrentTime(): number {
|
|
if (!this.audioElement) {
|
|
return 0;
|
|
}
|
|
return this.audioElement.currentTime || 0;
|
|
}
|
|
|
|
/**
|
|
* Obtient la durée
|
|
*/
|
|
getDuration(): number {
|
|
if (!this.audioElement) {
|
|
return 0;
|
|
}
|
|
return this.audioElement.duration || 0;
|
|
}
|
|
|
|
/**
|
|
* Obtient le volume
|
|
*/
|
|
getVolume(): number {
|
|
if (!this.audioElement) {
|
|
return 1;
|
|
}
|
|
return this.audioElement.volume;
|
|
}
|
|
|
|
/**
|
|
* Vérifie si l'audio est en cours de lecture
|
|
*/
|
|
isPlaying(): boolean {
|
|
if (!this.audioElement) {
|
|
return false;
|
|
}
|
|
return !this.audioElement.paused && !this.audioElement.ended;
|
|
}
|
|
|
|
/**
|
|
* Vérifie si l'audio est en mute
|
|
*/
|
|
isMuted(): boolean {
|
|
if (!this.audioElement) {
|
|
return false;
|
|
}
|
|
return this.audioElement.muted;
|
|
}
|
|
|
|
// Handlers d'événements
|
|
private handleTimeUpdate = (): void => {
|
|
if (this.timeUpdateCallback && this.audioElement) {
|
|
this.timeUpdateCallback(this.audioElement.currentTime);
|
|
}
|
|
};
|
|
|
|
private handleLoadedMetadata = (): void => {
|
|
if (this.durationChangeCallback && this.audioElement) {
|
|
this.durationChangeCallback(this.audioElement.duration);
|
|
}
|
|
};
|
|
|
|
private handleDurationChange = (): void => {
|
|
if (this.durationChangeCallback && this.audioElement) {
|
|
this.durationChangeCallback(this.audioElement.duration);
|
|
}
|
|
};
|
|
|
|
private handleEnded = (): void => {
|
|
if (this.endedCallback) {
|
|
this.endedCallback();
|
|
}
|
|
};
|
|
|
|
private handleError = (): void => {
|
|
if (this.errorCallback && this.audioElement) {
|
|
const error = new Error(
|
|
this.audioElement.error?.message || 'Unknown audio error',
|
|
);
|
|
this.errorCallback(error);
|
|
}
|
|
};
|
|
|
|
private handlePlay = (): void => {
|
|
if (this.playCallback) {
|
|
this.playCallback();
|
|
}
|
|
};
|
|
|
|
private handlePause = (): void => {
|
|
if (this.pauseCallback) {
|
|
this.pauseCallback();
|
|
}
|
|
};
|
|
|
|
// Méthodes pour enregistrer les callbacks
|
|
onTimeUpdate(callback: TimeUpdateCallback | null): void {
|
|
this.timeUpdateCallback = callback;
|
|
}
|
|
|
|
onDurationChange(callback: DurationChangeCallback | null): void {
|
|
this.durationChangeCallback = callback;
|
|
}
|
|
|
|
onEnded(callback: AudioEventCallback | null): void {
|
|
this.endedCallback = callback;
|
|
}
|
|
|
|
onError(callback: ErrorCallback | null): void {
|
|
this.errorCallback = callback;
|
|
}
|
|
|
|
onPlay(callback: AudioEventCallback | null): void {
|
|
this.playCallback = callback;
|
|
}
|
|
|
|
onPause(callback: AudioEventCallback | null): void {
|
|
this.pauseCallback = callback;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instance singleton du service audio
|
|
*/
|
|
export const audioPlayerService = new AudioPlayerService();
|