veza/apps/web/src/features/player/components/AudioPlayer.tsx

335 lines
11 KiB
TypeScript
Raw Normal View History

/**
* Composant AudioPlayer - Intégration complète
* Assemble tous les composants du player pour une expérience complète
*/
import { useRef, useEffect, useState } from 'react';
import { usePlayer } from '../hooks/usePlayer';
import { useStreamSync } from '../hooks/useStreamSync';
import { usePlayerStore } from '../store/playerStore';
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts';
import { TrackInfo } from './TrackInfo';
import { PlayPauseButton } from './PlayPauseButton';
import { NextPreviousButtons } from './NextPreviousButtons';
import { ProgressBar } from './ProgressBar';
import { TimeDisplay } from './TimeDisplay';
import { VolumeControl } from './VolumeControl';
import { RepeatShuffleButtons } from './RepeatShuffleButtons';
import { QualitySelector } from './QualitySelector';
import { PlaybackSpeedControl } from './PlaybackSpeedControl';
import { PlayerError } from './PlayerError';
import { PlayerLoading } from './PlayerLoading';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { Wifi, WifiOff } from 'lucide-react';
import type { AudioQuality } from './QualitySelector';
import type { PlaybackSpeed } from './PlaybackSpeedControl';
export interface AudioPlayerProps {
className?: string;
autoPlay?: boolean;
preload?: 'none' | 'metadata' | 'auto';
showQualitySelector?: boolean;
showSpeedControl?: boolean;
compact?: boolean;
}
export function AudioPlayer({
className,
autoPlay = false,
preload = 'metadata',
showQualitySelector = true,
showSpeedControl = true,
compact = false,
}: AudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null);
const player = usePlayer(audioRef);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [quality, setQuality] = useState<AudioQuality>('auto');
const [playbackSpeed, setPlaybackSpeed] = useState<PlaybackSpeed>(1);
const currentTrack = usePlayerStore(state => state.currentTrack);
// Intégration de la synchronisation (Session ID mocké pour l'instant ou via props)
// Dans le futur, ça viendra du contexte de "Room" ou "Session"
// On utilise l'ID de la track comme session ID temporaire pour le dev
const sessionId = currentTrack?.id ? `session_${currentTrack.id}` : null;
const { isSynced } = useStreamSync({
sessionId,
trackId: currentTrack?.id ?? null,
});
// Activer les raccourcis clavier
useKeyboardShortcuts(player, {
enabled: true,
seekStep: 5,
volumeStep: 5,
});
// Gérer le lifecycle de l'élément audio
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
// Configuration initiale
audio.preload = preload;
audio.autoplay = autoPlay;
// Gérer les erreurs
const handleError = () => {
const error = new Error('Erreur de lecture audio');
setError(error);
setIsLoading(false);
};
// Gérer le chargement
const handleLoadStart = () => {
setIsLoading(true);
setError(null);
};
const handleCanPlay = () => {
setIsLoading(false);
setError(null);
};
// Gérer la vitesse de lecture
const updatePlaybackRate = () => {
if (audio) {
audio.playbackRate = playbackSpeed;
}
};
audio.addEventListener('error', handleError);
audio.addEventListener('loadstart', handleLoadStart);
audio.addEventListener('canplay', handleCanPlay);
updatePlaybackRate();
return () => {
audio.removeEventListener('error', handleError);
audio.removeEventListener('loadstart', handleLoadStart);
audio.removeEventListener('canplay', handleCanPlay);
};
}, [preload, autoPlay, playbackSpeed]);
// Mettre à jour la vitesse de lecture quand elle change
useEffect(() => {
const audio = audioRef.current;
if (audio) {
audio.playbackRate = playbackSpeed;
}
}, [playbackSpeed]);
const handlePlayPause = () => {
if (player.isPlaying) {
player.pause();
} else {
player.resume();
}
};
const handleRetry = () => {
setError(null);
if (player.currentTrack) {
player.play(player.currentTrack);
}
};
const canGoNext = player.queue.length > 0 && player.currentIndex < player.queue.length - 1;
const canGoPrevious = player.queue.length > 0 && player.currentIndex > 0;
if (compact) {
return (
<div className={cn('flex items-center gap-4 p-4', className)} data-testid="audio-player">
<audio
ref={audioRef}
data-testid="audio-element"
style={{ display: 'none' }}
preload={preload}
autoPlay={autoPlay}
/>
{error && <PlayerError error={error} onRetry={handleRetry} />}
{isLoading && <PlayerLoading isLoading={isLoading} size="sm" />}
{player.currentTrack && !error && !isLoading && (
<>
<TrackInfo track={player.currentTrack} showCover={true} coverSize="sm" showMetadata={false} />
<PlayPauseButton isPlaying={player.isPlaying} onClick={handlePlayPause} size="sm" />
<NextPreviousButtons
onNext={player.next}
onPrevious={player.previous}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
size="sm"
/>
<VolumeControl
volume={player.volume}
muted={player.muted}
onVolumeChange={player.setVolume}
onMuteToggle={player.toggleMute}
showValue={false}
/>
</>
)}
{!player.currentTrack && !error && (
<div className="text-sm text-gray-500 dark:text-gray-400">Aucune piste sélectionnée</div>
)}
</div>
);
}
return (
<div className={cn('bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6', className)} data-testid="audio-player">
{/* Élément audio HTML - invisible mais nécessaire pour la lecture */}
<audio
ref={audioRef}
data-testid="audio-element"
style={{ display: 'none' }}
preload={preload}
autoPlay={autoPlay}
/>
{/* Gestion des erreurs */}
{error && (
<div className="mb-4">
<PlayerError error={error} onRetry={handleRetry} />
</div>
)}
{/* État de chargement */}
{isLoading && (
<div className="mb-4">
<PlayerLoading isLoading={isLoading} message="Chargement de la piste..." />
</div>
)}
{/* Contenu principal */}
{player.currentTrack && !error && !isLoading && (
<div className="space-y-6">
{/* Informations de la piste */}
<div>
<TrackInfo
track={player.currentTrack}
showCover={true}
coverSize="lg"
showMetadata={true}
/>
</div>
{/* Barre de progression */}
<div className="space-y-2">
<ProgressBar
currentTime={player.currentTime}
duration={player.duration}
onSeek={player.seek}
showTooltip={true}
/>
<div className="flex items-center justify-between">
<TimeDisplay currentTime={player.currentTime} duration={player.duration} />
</div>
</div>
{/* Contrôles principaux */}
<div className="flex items-center justify-center gap-4">
<RepeatShuffleButtons
repeat={player.repeat}
shuffle={player.shuffle}
onRepeatChange={player.setRepeat}
onShuffleToggle={player.toggleShuffle}
size="md"
/>
<NextPreviousButtons
onNext={player.next}
onPrevious={player.previous}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
size="md"
/>
<PlayPauseButton
isPlaying={player.isPlaying}
onClick={handlePlayPause}
size="lg"
variant="default"
/>
<NextPreviousButtons
onNext={player.next}
onPrevious={player.previous}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
size="md"
/>
<VolumeControl
volume={player.volume}
muted={player.muted}
onVolumeChange={player.setVolume}
onMuteToggle={player.toggleMute}
showValue={true}
showSlider={true}
/>
</div>
{/* Contrôles avancés */}
{(showQualitySelector || showSpeedControl) && (
<div className="flex items-center justify-between gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
{/* Sync Status Indicator */}
<div className="flex items-center gap-2">
<Badge variant={isSynced ? "success" : "default"} className={cn("gap-1", !isSynced && "text-gray-500")}>
{isSynced ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
<span className="text-xs">{isSynced ? 'Sync' : 'Local'}</span>
</Badge>
{sessionId && !isSynced && (
<span className="text-xs text-muted-foreground hidden lg:inline">Connecting to session...</span>
)}
</div>
<div className="flex items-center gap-4">
{showSpeedControl && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Vitesse:</span>
<PlaybackSpeedControl
currentSpeed={playbackSpeed}
onSpeedChange={setPlaybackSpeed}
/>
</div>
)}
{showQualitySelector && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Qualité:</span>
<QualitySelector
currentQuality={quality}
onQualityChange={setQuality}
/>
</div>
)}
</div>
</div>
)}
</div>
)}
{/* État vide */}
{!player.currentTrack && !error && !isLoading && (
<div className="text-center py-12">
<div className="text-gray-500 dark:text-gray-400 mb-2">
Aucune piste sélectionnée
</div>
<div className="text-sm text-gray-400 dark:text-gray-500">
Sélectionnez une piste pour commencer la lecture
</div>
</div>
)}
</div>
);
}
export default AudioPlayer;