Composant React pour le Contrôle de Lecture Audio

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 onerror de l'élément audio est 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 setTimeout pour le changement de currentTime via le slider vise à améliorer la réactivité. Si l'implémentation du Slider d'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 preload est configuré sur 'auto' par défaut.
  • Personnalisation : Ce composant offre une flexibilité accrue par rappport à l'utilisation directe de la balise &lt;audio&gt;, permettant une intégration plus poussée dans l'interface utilisateur.

Étiquettes: React JavaScript HTML5 Audio composant UI Gestion d'état

Publié le 15 juillet à 02h50