Implémentation d'un Lecteur Audio Personnalisé avec React
Ce composant React permet de gérer la lecture audio avec une interface utilisateur personnalisée, incluant une barre de progression et des contrôles de lecture/pause.
Code JavaScript (AudioPlayer.js)
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Slider, message } from 'antd-mobile'; // Assurez-vous d'avoir installé antd-mobile
import iconPlay from '@/assets/icons/play.png'; // Remplacez par vos chemins d'icônes
import iconPause from '@/assets/icons/pause.png'; // Remplacez par vos chemins d'icônes
import './AudioPlayer.less'; // Assurez-vous d'avoir votre fichier CSS/Less
const AudioPlayer = ({ src }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [isReady, setIsReady] = useState(false); // Pour indiquer si l'audio est prêt à jouer
const audioRef = useRef(new Audio(src));
const intervalRef = useRef(null);
const sliderTimeoutRef = useRef(null);
// Formate les secondes en chaîne HH:MM:SS ou MM:SS
const formatTime = (seconds) => {
if (isNaN(seconds) || seconds <= 0 || seconds === Infinity) {
return '00:00';
}
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
};
// Gère la lecture
const handlePlay = () => {
if (!isReady) {
message.info('Audio non prêt.');
return;
}
audioRef.current.play();
setIsPlaying(true);
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
const current = Math.floor(audioRef.current.currentTime);
if (current < duration) {
setCurrentTime(current);
} else {
// Fin de la lecture
clearInterval(intervalRef.current);
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
setCurrentTime(0);
message.info('Lecture terminée.');
}
}, 1000);
};
// Gère la pause
const handlePause = () => {
audioRef.current.pause();
setIsPlaying(false);
if (intervalRef.current) clearInterval(intervalRef.current);
};
// Bascule entre lecture et pause
const togglePlayPause = () => {
if (isPlaying) {
handlePause();
} else {
handlePlay();
}
};
// Gère le changement de la barre de progression
const handleSliderChange = (value) => {
if (sliderTimeoutRef.current) clearTimeout(sliderTimeoutRef.current);
// Débrayage pour éviter les mises à jour trop fréquentes
sliderTimeoutRef.current = setTimeout(() => {
audioRef.current.pause(); // Met en pause pour changer le currentTime
const newTime = Math.floor(value);
audioRef.current.currentTime = newTime;
setCurrentTime(newTime);
// Si le lecteur était en mode lecture, relance la lecture depuis le nouveau temps
if (isPlaying) {
audioRef.current.play();
}
}, 200);
};
// Initialise l'audio et ses événements
const initializeAudio = (source) => {
if (!source) return;
audioRef.current.src = source;
audioRef.current.preload = 'auto'; // 'auto', 'metadata', 'none'
audioRef.current.load(); // Charge les métadonnées audio
audioRef.current.onloadedmetadata = () => {
const audioDuration = Math.floor(audioRef.current.duration);
setDuration(audioDuration);
setIsReady(true);
};
audioRef.current.onerror = () => {
console.error("Erreur lors du chargement de l'audio.");
message.error("Impossible de lire l'audio.");
setIsReady(false);
};
};
// Effet pour charger l'audio au montage et lors du changement de src
useEffect(() => {
initializeAudio(src);
// Nettoyage à la désinstallation du composant
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (sliderTimeoutRef.current) clearTimeout(sliderTimeoutRef.current);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.src = ''; // Libère la ressource
}
};
}, [src]); // Réinitialise si la source change
const playPauseIcon = isPlaying ? iconPause : iconPlay;
const formattedDuration = formatTime(duration);
const formattedCurrentTime = formatTime(currentTime);
return (
<div className="custom-audio-player">
<div className="player-controls">
<button onClick={togglePlayPause} className="play-pause-button" disabled={!isReady}>
<img src={playPauseIcon} alt={isPlaying ? "Pause" : "Play"} />
</button>
<div className="progress-container">
<div className="time-display">
<span>{formattedCurrentTime}</span>
<span>/</span>
<span>{formattedDuration}</span>
</div>
<Slider
value={currentTime}
min={0}
max={duration}
onChange={handleSliderChange}
style={{ '--fill-color': '#10C0DC', '--track-color': '#DEEAEC' }} // Styles personnalisés pour antd-mobile
className="progress-slider"
/>
</div>
</div>
</div>
);
};
AudioPlayer.propTypes = {
src: PropTypes.string.isRequired,
};
export default AudioPlayer;
Styles CSS/Less (AudioPlayer.less)
.custom-audio-player {
display: flex;
justify-content: center;
padding: 1rem 0;
.player-controls {
display: flex;
align-items: center;
width: 80%;
max-width: 400px;
height: 60px;
background-color: #f0f2f5;
border-radius: 30px;
padding: 0 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
.play-pause-button {
width: 40px;
height: 40px;
background: none;
border: none;
cursor: pointer;
margin-right: 15px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.progress-container {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
.time-display {
font-size: 0.8rem;
color: #555;
margin-right: 10px;
min-width: 80px; // Assure une largeur suffisante pour les temps
text-align: right;
span {
margin: 0 2px;
}
}
.progress-slider {
flex-grow: 1;
// antd-mobile utilise des variables CSS pour le style
// Assurez-vous que les variables --fill-color et --track-color sont bien définies
// ou utilisez des sélecteurs plus spécifiques si nécessaire.
}
}
}
}
Points d'Attention et Améliorations Potentielles
- Gestion des Erreurs : L'attribut
onerrorde l'élémentaudioest utilisé pour signaler les problèmes de chargement. Une gestion plus fine pourrait être nécessaire selon les cas. - Chargeemnt des Durées : Pour certains navigateurs ou formats audio (particulièrement sur iOS), la durée peut ne pas être immédiatement disponible. Si la durée est critique avant la lecture, une apporche où la durée est fournie par le backend via une API serait plus fiable.
- Expérience Utilisateur avec Slider : L'utilisation de
setTimeoutpour le changement decurrentTimevia le slider vise à améliorer la réactivité. Si l'implémentation duSliderd'antd-mobile ne convient pas, un composant personnalisé pourrait être envisagé. - Préchargement : Pour les fichiers audio volumineux, une stratégie de préchargement (par exemple, charger uniquement les métadonnées au début, puis le contenu si l'utilisateur interagit) peut améliorer l'expérience. L'attribut
preloadest configuré sur'auto'par défaut. - Personnalisation : Ce composant offre une flexibilité accrue par rappport à l'utilisation directe de la balise
<audio>, permettant une intégration plus poussée dans l'interface utilisateur.