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:

  1. trackIndex– Index of tracks being played.
  2. trackProgress– Current progress of the track.
  3. 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.

  1. audioRef– Audio elements created through the Audio constructor.
  2. intervalRef– A reference to the setInterval timer.
  3. 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.