Building an Audio Player With React Hooks
Originally written by Ryan Finni
Translator: Giriawsh
Personal translation, if there are mistakes, welcome to correct
preface
Today we will build a basic React audio player component using the HTMLAudioElement interface. The player has playlists that you can pause, swipe, jump to the previous or the next track. And each song has a different animated background color.
React-audio-player-demo – CodeSandbox
The audio player design was provided by Dribbble Shot, and the demo music file was provided by Pixabay.
HTMLAudioElement overview
There are several different ways to handle Web audio under existing technologies. The most common is through HTML < Audio > tags, or using the Web Audio API for more low-level control. The approach used in this tutorial is the middle of the HTMLAudioElement interface.
The HTMLAudioElement interface provides access to the attributes of the
It’s very simple to use:
const audioElement = new Audio(audio source);
Copy the code
The Audio() constructor above returns an Audio Audio element that contains some methods and data about the source.
audioElement.play();
audioElement.pause();
audioElement.currentTime;
audioElement.ended;
audioElement.duration;
Copy the code
We’ll use these in a minute, but first, we should define the audio component props.
Define the Props
The only prop our component needs is the tracklist it can play. We’ll give it a set of objects, each containing title, Artist, audioSrc, Image, and color
const tracks = [
{
title: string,
artist: string,
audioSrc: string | import.image: string,
color: string, }, ... . ] ;Copy the code
Build the audio player component
Start by creating a new file called AudioPlayer.jsx and importing useState, useEffect, and useRef hooks.
We should maintain three state values:
trackIndex
– Index of tracks being played.trackProgress
– Current progress of the track.isPlaying
– Whether the track is playing.
import React, { useState, useEffect, useRef } from 'react';
const AudioPlayer = ({ tracks }) = > {
// State
const [trackIndex, setTrackIndex] = useState(0);
const [trackProgress, setTrackProgress] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
return ( ... );
}
export default AudioPlayer;
Copy the code
In addition to the state, three refs are required.
audioRef
– Audio elements created through the Audio constructor.intervalRef
– A reference to the setInterval timer.isReady
– A Boolean value that determines when certain operations are ready to run.
const AudioPlayer = () = > {
// State.// Destructure for conciseness
const { title, artist, color, image, audioSrc } = tracks[trackIndex];
// Refs
const audioRef = useRef(new Audio(audioSrc));
const intervalRef = useRef();
const isReady = useRef(false);
// Destructure for conciseness
const { duration } = audioRef.current;
return ( ... );
}
Copy the code
Next we add two function placeholders. We will follow up and complete them in later sections.
One function, toPrevTrack, handles the previous track button click, and another, toNextTrack, handles the next button click.
const AudioPlayer = () = > {
// State.// Refs.const toPrevTrack = () = > {
console.log('TODO go to prev');
}
const toNextTrack = () = > {
console.log('TODO go to next');
}
return ( ... );
}
Copy the code
Finally, complete the main parts of the player. It displays the track image, title, and artist, defaulting to the first track in the list.
const AudioPlayer = () = >{...return (
<div className="audio-player">
<div className="track-info">
<img
className="artwork"
src={image}
alt={`track artwork forThe ${title} byThe ${artist} `} / >
<h2 className="title">{title}</h2>
<h3 className="artist">{artist}</h3>
</div>
</div>
);
}
Copy the code
It looks nothing special, and so far so good. We don’t have styles yet, so let’s add some.
Player style
We’ll use some CSS variables in our styles, but they won’t be too complicated.
Note the –active-color variable. You will use it later to set the active track color to the background color.
:root {
--white: #fff;
--active-color: #00aeb0; {} *box-sizing: border-box;
}
html {
font-family: Arial, Helvetica, sans-serif;
height: 100%;
background: var(--active-color);
transition: background 0.4 s ease;
}
button {
background: none;
border: none;
cursor: pointer;
}
Copy the code
Next, add some specific styles to the audio player.
.audio-player {
max-width: 350px;
border-radius: 20px;
padding: 24px;
box-shadow: 0 28px 28px rgba(0.0.0.0.2);
margin: auto;
color: var(--white);
}
.artwork {
border-radius: 120px;
display: block;
margin: auto;
height: 200px;
width: 200px;
}
.track-info {
text-align: center;
z-index: 1;
position: relative;
}
.title {
font-weight: 700;
margin-bottom: 4px;
}
.artist {
font-weight: 300;
margin-top: 0;
}
Copy the code
Now we need the player control.
Control components
The audio control component stores play, pause, previous, and next track buttons. We’ll split it into its own components and move some functionality out of the main AudioPlayer component.
Start by creating a new file, AudioControls.jsx. We need some props: We need to know if the audio is playing so we can show the play or pause button. This is done by passing the isPlaying state value as props. We also need some click handlers for play, pause, previous and next action. They are onPlayPauseClick, onPrevClick, and onNextClick.
const AudioControls = ({ isPlaying, onPlayPauseClick, onPrevClick, onNextClick, }) = >(...).export default AudioControls;
Copy the code
How you use SVG will depend on your environment Settings. For libraries like the Create React App, importing and using them should work fine by default, but in other cases you may need some Webpack tools to make it work.
import React from 'react';
import { ReactComponent as Play } from './assets/play.svg';
import { ReactComponent as Pause } from './assets/pause.svg';
import { ReactComponent as Next } from './assets/next.svg';
import { ReactComponent as Prev } from './assets/prev.svg';
const AudioControls = ({... }) = > (
<div className="audio-controls">
<button
type="button"
className="prev"
aria-label="Previous"
onClick={onPrevClick}
>
<Prev />
</button>
{isPlaying ? (
<button
type="button"
className="pause"
onClick={()= > onPlayPauseClick(false)}
aria-label="Pause"
>
<Pause />
</button>
) : (
<button
type="button"
className="play"
onClick={()= > onPlayPauseClick(true)}
aria-label="Play"
>
<Play />
</button>
)}
<button
type="button"
className="next"
aria-label="Next"
onClick={onNextClick}
>
<Next />
</button>
</div>
);
Copy the code
Finally, we add styles for the control buttons and spacing.
.audio-controls {
display: flex;
justify-content: space-between;
width: 75%;
margin: 0 auto 15px;
}
.audio-controls .prev svg,
.audio-controls .next svg {
width: 35px;
height: 35px;
}
.audio-controls .play svg,
.audio-controls .pause svg {
height: 40px;
width: 40px;
}
.audio-controls path {
fill: var(--white);
}
Copy the code
The AudioControls component is done. Add it to the AudioPlayer main component, passing it the props mentioned above.
import AudioControls from './AudioControls';
const AudioPlayer = () = >{... .return (
<div className="audio-player">
<div className="track-info">
<img
className="artwork"
src={image}
alt={`track artwork forThe ${title} byThe ${artist} `} / >
<h2>{title}</h2>
<h3>{artist}</h3>
<AudioControls
isPlaying={isPlaying}
onPrevClick={toPrevTrack}
onNextClick={toNextTrack}
onPlayPauseClick={setIsPlaying}
/>
</div>
</div>
);
}
Copy the code
Having written the AudioControls component, let’s get the player working next!
Player operation function
Back to the AudioPlayer component, we need to complete the toPrevTrack and toNextTrack functions that we added earlier.
Clicking the Next button should go to the next track in the list, or return to the first track. If the previous button is clicked, the opposite is true.
const toPrevTrack = () = > {
if (trackIndex - 1 < 0) {
setTrackIndex(tracks.length - 1);
} else {
setTrackIndex(trackIndex - 1); }}const toNextTrack = () = > {
if (trackIndex < tracks.length - 1) {
setTrackIndex(trackIndex + 1);
} else {
setTrackIndex(0); }}Copy the code
With these changes, you can now switch between tracks in the playlist.
The next step is to add some useEffect hooks.
The first is used to start or pause the audio when the Play or pause button is clicked.
useEffect(() = > {
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying]);
Copy the code
Whenever isPlaying state changes, we call the Play () or pause() methods on audioRef based on their value.
The next useEffect hook will do some cleaning when the component is unloaded. When uninstalling, we make sure the music pauses and clear any setInterval timers that might be running. The next section covers timers in more detail!
useEffect(() = > {
// Pause and clean up on unmount
return () = > {
audioRef.current.pause();
clearInterval(intervalRef.current); }} []);Copy the code
The last useEffect hook runs when the trackIndex state changes. It allows us to pause the currently playing track, update the audioRef value to a new source, reset the progress state, and set the new track to play.
// Handle setup when changing tracks
useEffect(() = > {
audioRef.current.pause();
audioRef.current = new Audio(audioSrc);
setTrackProgress(audioRef.current.currentTime);
if (isReady.current) {
audioRef.current.play();
setIsPlaying(true);
startTimer();
} else {
// Set the isReady ref as true for the next pass
isReady.current = true;
}
}, [trackIndex]);
Copy the code
We also set the value of isReady Ref here on the first pass (initial installation). This is to prevent the audio from playing automatically when the useEffect hook is run for the first time, which we don’t want. It is on the second and subsequent time runs (when trackIndex changes) that we expect the playback logic to occur.
Note that you must pause before recycling.
“If all Audio elements created using the Audio() constructor are deleted, according to JavaScript garbage collection, if playback is ongoing, the Audio element in memory is not removed. Instead, the Audio continues to play and its objects remain in memory. Until playback ends or the object is paused (for example by calling pause()), at which point the object becomes a target for garbage collection.” -MDN doc
If you test the audio player controls, they should now work.
Play progress and selection
Next, we need to display the track progress and add the ability to select different parts of the audio.
Start by defining a new function called startTimer in the AudioPlayer component. This function is responsible for starting a new setInterval timer when the track starts playing.
const AudioPlayer = () = >{... .const startTimer = () = > {
// Clear any timers already running
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() = > {
if (audioRef.current.ended) {
toNextTrack();
} else{ setTrackProgress(audioRef.current.currentTime); }},1000]); }}Copy the code
Every second, we check to see if the audio is over/finished. If so, move on to the next track, otherwise update the trackProgress status. The timer ID is stored in the intervalRef so that we can clean it up in the rest of the component.
The startTimer function is called as part of the useEffect hook we added in the previous section. First, add isPlaying when the state changes and is true.
useEffect(() = > {
if (isPlaying) {
audioRef.current.play();
startTimer();
} else {
clearInterval(intervalRef.current);
audioRef.current.pause();
}
}, [isPlaying]);
Copy the code
The startTimer function also needs to run when the trackIndex value changes.
useEffect(() = > {
audioRef.current.pause();
audioRef.current = new Audio(audioSrc);
setTrackProgress(audioRef.current.currentTime);
if (isReady.current) {
audioRef.current.play();
setIsPlaying(true);
startTimer();// I don't think this is necessary. Because isPlaying changes must trigger useEffect hooks above
}
}, [trackIndex]);
Copy the code
Now we can write the progress indicator.
To make our UI easy to access, we’ll use native HTML Range Input as our playback progress indicator. This gives us the freedom to use the mouse or keyboard for track manipulation, and a large number of events for processing.
return (
<div className="audio-player">
<div className="track-info">.<AudioControls . />
<input
type="range"
value={trackProgress}
step="1"
min="0"
max={duration ? duration :` ${duration} `}className="progress"
onChange={(e)= > onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
/>
</div>
</div>
);
Copy the code
The duration value is initially NaN. React will warn: “NaN received for Max property. If this is what you expect, convert the value to a string. We are following the advice of the warning and converting it to a string until the track starts playing and the actual duration value replaces it.
We have two more functions to add: onScrubEnd and onScrub. These functions run on these interactions: onKeyUp, onChange, and onMouseUp.
const onScrub = (value) = > {
// Clear any timers already running
clearInterval(intervalRef.current);
audioRef.current.currentTime = value;
setTrackProgress(audioRef.current.currentTime);
}
const onScrubEnd = () = > {
// If not already playing, start
if(! isPlaying) { setIsPlaying(true);
}
startTimer();
}
Copy the code
Now style the playback progress indicator itself.
There may be other ways, but there doesn’t seem to be a good cross-browser standard for setting the background style of range Input. The solution below uses the WebKit-gradient solution and works with Chrome, Firefox, and Safari, but has not been tested with other browsers.
Creates a constant that holds the current percentage of tracks played. We use this percentage value in WebKit-gradient to update the background style of the Range input.
const currentPercentage = duration ? `${(trackProgress / duration) * 100}% ` : '0%';
const trackStyling = `
-webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, #fff), color-stop(${currentPercentage}`, # 777));
return ( ... );
Copy the code
This will create a white background over the range input to visually show track progress.
TrackStyling constants are then applied to the input as style attributes.
return(... . <input type="range"
value={trackProgress}
step="1"
min="0"
max={duration ? duration : `${duration}`}
className="progress"
onChange={(e) = > onScrub(e.target.value)}
onMouseUp={onScrubEnd}
onKeyUp={onScrubEnd}
style={{ background: trackStyling }}
/>
);
Copy the code
Now write CSS for the playback progress
input[type=range] {
height: 5px;
-webkit-appearance: none;
width: 100%;
margin-bottom: 10px
border-radius: 8px;
background: #3b7677;
transition: background 0.2 s ease;
cursor: pointer;
}
Copy the code
One thing to watch out for. I found that if you try to hide the range Input slider in CSS, use the left and right arrow keys to select it back and forth in Safari. Therefore, I chose to leave the Range Input slider as the browser default
Change the background color
The last thing to do is dynamically change the page background color. Since each song has an associated color value, all we need to do is update the active-color CSS variable. We set the HTML background to use this variable earlier, and by updating it, we will see the color change as we loop through the song.
Start by creating a new component called Predicable.jsx. In the useEffect hook, the setProperty method updates the CSS variable value when the trackIndex changes.
import React, { useEffect } from 'react';
const Backdrop = ({ activeColor, trackIndex, isPlaying, }) = > {
useEffect(() = > {
document.documentElement.style.setProperty('--active-color', activeColor);
}, [trackIndex]);
return (
<div className={`color-backdropThe ${isPlaying ? 'playing' : 'idle'} `} / >
);
};
export default Backdrop;
Copy the code
Return to the AudioPlayer component and add adjacent background components.
Now let’s add some animation. Use the same –active-color variable to add a linear gradient background and position it to the full height and width of the screen.
.color-backdrop {
background: linear-gradient(45deg.var(--active-color) 20%, transparent 100%) no-repeat;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.color-backdrop.playing {
animation: colorChange 20s alternate infinite;
}
Copy the code
Display this animation when the audio is playing and the background has a Playing class. It is done using the hue rotation hue-Rotate filter function.
Hue -rotate() The CSS function rotates the hue of an element and its content. – MDN Docs
Hue-rotate Receives an Angle as a parameter. Within the animation keyframe, all we did was set it to rotate 360 degrees at most.
@keyframes colorChange {
from {
filter: hue-rotate(0deg);
}
to {
filter: hue-rotate(360deg); }}Copy the code
Note that Hue-rotate is not supported in Internet Explorer.
If you are going to use this type of animation in a production scenario, you should consider wrapping the animation style in a preferred medium query called ANa-reduced-motion. That way, users who want to avoid seeing it don’t have to. See my post on this topic for more information.
conclusion
Thank you for seeing this: we’ve covered a lot so far! By now, you should have a pretty good idea of how to handle audio, which I hope inspires you to build your own cool audio projects.
Since we’ve only covered the basics of the audio player, you can certainly add more. Adding volume controls, adjusting playback speed, and saving playback progress using localStorage are just a few viable options that you can explore. You can also connect to this component to use the Spotify API or some other audio source.