react-pro-audio-player
Version:
A React component for playing collection of audio files with controls for forward, backward, play, pause, loop, sound control, and playback speed.
297 lines (265 loc) • 9.71 kB
JSX
import React, { useEffect, useRef, useState } from "react";
import {
FaBackward,
FaForward,
FaVolumeUp,
FaVolumeMute,
FaRedo,
FaPauseCircle,
FaPlayCircle,
} from "react-icons/fa";
import { ImLoop2 } from "react-icons/im";
import { MdCancel } from "react-icons/md";
const CustomAudioPlayer = ({
// Songs
initialSongs = [],
songs: controlledSongs, // optional external songs array
onSongsChange, // callback when songs array changes
// Playback state
isPlaying: controlledIsPlaying,
onPlayPauseChange, // callback when play/pause changes
// Song index state
currentSongIndex: controlledSongIndex,
onSongChange, // callback when the song index changes
// Dynamic keys
songUrlKey = "src",
songNameKey = "name",
songThumbnailKey = "thumbnail",
songSingerKey = "singer",
}) => {
// Use controlled values if provided; otherwise, use internal state.
const [internalSongs, setInternalSongs] = useState(initialSongs);
const songs = controlledSongs !== undefined ? controlledSongs : internalSongs;
const [internalIsPlaying, setInternalIsPlaying] = useState(false);
const isPlaying =
controlledIsPlaying !== undefined ? controlledIsPlaying : internalIsPlaying;
const [internalSongIndex, setInternalSongIndex] = useState(0);
const currentSongIndex =
controlledSongIndex !== undefined ? controlledSongIndex : internalSongIndex;
// Other internal states.
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [isLooping, setIsLooping] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const audioRef = useRef(new Audio());
// Update audio source when current song or songs list changes.
useEffect(() => {
if (songs.length === 0) return;
const audio = audioRef.current;
audio.src = songs[currentSongIndex]?.[songUrlKey];
audio.load();
const updateTime = () => setCurrentTime(audio.currentTime);
const setMetadata = () => setDuration(audio.duration);
const handleSongEnd = () => handleForward();
audio.addEventListener("loadedmetadata", setMetadata);
audio.addEventListener("timeupdate", updateTime);
audio.addEventListener("ended", handleSongEnd);
if (isPlaying) {
audio.play().catch(() => console.error("Playback failed."));
}
return () => {
audio.removeEventListener("loadedmetadata", setMetadata);
audio.removeEventListener("timeupdate", updateTime);
audio.removeEventListener("ended", handleSongEnd);
};
}, [songs, currentSongIndex]);
// Effect to handle play/pause changes.
useEffect(() => {
const audio = audioRef.current;
if (isPlaying) {
audio.play().catch(() => console.error("Playback failed."));
} else {
audio.pause();
}
}, [isPlaying]);
// Volume, mute, loop, and playback rate effects.
useEffect(() => {
audioRef.current.volume = isMuted ? 0 : volume;
}, [volume, isMuted]);
useEffect(() => {
audioRef.current.loop = isLooping;
}, [isLooping]);
useEffect(() => {
audioRef.current.playbackRate = playbackRate;
}, [playbackRate]);
const handleSeekChange = e => {
const newTime = parseFloat(e.target.value);
audioRef.current.currentTime = newTime;
setCurrentTime(newTime);
};
// If the user interacts with the volume slider while muted, unmute automatically.
const handleVolumeChange = e => {
const newVolume = parseFloat(e.target.value);
if (isMuted) {
setIsMuted(false);
}
setVolume(newVolume);
};
// Helper to update song index both internally and externally.
const updateSongIndex = newIndex => {
if (controlledSongIndex === undefined) {
setInternalSongIndex(newIndex);
}
onSongChange && onSongChange(newIndex);
};
// Helper to update isPlaying state.
const updateIsPlaying = newState => {
if (controlledIsPlaying === undefined) {
setInternalIsPlaying(newState);
}
onPlayPauseChange && onPlayPauseChange(newState);
};
const handleForward = () => {
const nextIndex = (currentSongIndex + 1) % songs.length;
updateSongIndex(nextIndex);
updateIsPlaying(true);
};
const handleBackward = () => {
const prevIndex =
currentSongIndex === 0 ? songs.length - 1 : currentSongIndex - 1;
updateSongIndex(prevIndex);
updateIsPlaying(true);
};
const toggleMute = () => setIsMuted(prev => !prev);
const toggleLoop = () => setIsLooping(prev => !prev);
const togglePlayPause = () => updateIsPlaying(!isPlaying);
// Updated handleCancel: removes current song, resets audioRef and related states.
const handleCancel = () => {
// Reset the audio reference.
audioRef.current.pause();
setCurrentTime(0);
audioRef.current.currentTime = 0;
updateIsPlaying(false);
if (controlledSongIndex !== undefined) {
onSongChange && onSongChange(null);
}
};
if (songs.length === 0) return <p>No songs available.</p>;
// Calculate progress percentage for the seek bar.
const progressPercent =
duration && currentTime ? (currentTime / duration) * 100 : 0;
const progressBarStyle = {
background: `linear-gradient(to right, #e11d48 ${progressPercent}%, #d1d5db ${progressPercent}%)`,
};
// Calculate volume percentage for the volume slider.
const volumePercent = isMuted ? 0 : volume * 100;
const volumeSliderStyle = {
background: `linear-gradient(to right, #e11d48 ${volumePercent}%, #d1d5db ${volumePercent}%)`,
};
return (
<section className="custom-audio-player">
<div className="audio-player">
<div className="controls-wrapper">
<div className="left-controls">
<div className="song-details">
<span className="song-thumbnail">
{songs[currentSongIndex]?.[songThumbnailKey] && (
<img src={songs[currentSongIndex]?.[songThumbnailKey]} />
)}
</span>
<div className="song-detail">
<span className="song-title">
{songs[currentSongIndex]?.[songNameKey]}
</span>
<span className="song-singer">
{songs[currentSongIndex]?.[songSingerKey]}
</span>
</div>
</div>
</div>
<div className="center-controls">
<button
onClick={handleBackward}
aria-label="Previous track"
className="btn playback-btn"
>
<FaBackward />
</button>
<button
onClick={togglePlayPause}
aria-label={isPlaying ? "Pause" : "Play"}
className="btn playback-btn play-pause-btn"
>
{isPlaying ? <FaPauseCircle /> : <FaPlayCircle />}
</button>
<button
onClick={handleForward}
aria-label="Next track"
className="btn playback-btn"
>
<FaForward />
</button>
</div>
<div className="right-controls">
<button
onClick={toggleLoop}
aria-label="Toggle loop"
className="btn loop-btn"
>
{isLooping ? <ImLoop2 /> : <FaRedo />}
</button>
<button
onClick={toggleMute}
aria-label={isMuted ? "Unmute" : "Mute"}
className="btn mute-btn"
>
{isMuted ? <FaVolumeMute /> : <FaVolumeUp />}
</button>
<input
type="range"
className="volume-slider"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
aria-label="Volume slider"
style={volumeSliderStyle}
/>
<select
className="playback-speed"
value={playbackRate}
onChange={e => setPlaybackRate(parseFloat(e.target.value))}
aria-label="Playback speed"
>
<option value="0.5">0.5x</option>
<option value="1">1x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<button
onClick={handleCancel}
aria-label="Cancel playback"
className="btn cancel-btn"
>
<MdCancel />
</button>
</div>
</div>
<div className="progress-wrapper">
<span className="time">
{Math.floor(currentTime / 60)}:
{String(Math.floor(currentTime % 60)).padStart(2, "0")}
</span>
<input
type="range"
className="progress-bar"
min="0"
max={duration || 1}
value={currentTime}
onChange={handleSeekChange}
aria-label="Seek slider"
style={progressBarStyle}
/>
<span className="time">
{Math.floor(duration / 60)}:
{String(Math.floor(duration % 60)).padStart(2, "0")}
</span>
</div>
</div>
</section>
);
};
export default CustomAudioPlayer;