Let's build a web radio player from scratch πŸŒπŸ“»

thormeier

Pascal Thormeier

Posted on August 25, 2021

Let's build a web radio player from scratch πŸŒπŸ“»

Video killed the radio star, internet killed the video star, and with this tutorial I'm going to show you how to come full circle!

You might know that I like to combine my love for music with coding, so instead of building full-blown instruments, today I'll focus on a way to transport music: radio!

Wait, radio is still a thing?

Indeed! Streaming services detach the listeners from the moderators, editors and the artists. In a radio stream, moderators can actually engage with listeners: think, interviews with artists with questions from the crowd or quiz shows!

Radio stations have more advantages over your average streaming service:

  • Editorial content
  • Shows on various topics
  • Local news
  • Ability to randomly discover a new catchy song every now and then
  • Not having to care about what to listen to next

A lot of people still listen to radio stations today, but they often don't use those clunky old extra-made machines anymore. Like for most tasks, listeners today use a computer and, more specifically, a browser.

While this post does not cover how to set up your own stream (that one's for another time), I will show you how to present a stream to your listeners in an accessible and visually appealing way!

No stream, ok - but how do we test the interface, then?

Excellent question. There's a lot of radio stations out there that can be used to test the player.

So step 1 is to find a stream and ideally an API endpoint that gives us the currently playing song. A popular search engineβ„’ will yield a ton of different stations to test with, so I select one that I personally like.

With the stream ready, let's talk about the design next.

What will this thing look like?

There's a myriad of options. It could run in a popup, sit in a navigation, a side bar or a top bar that scrolls with the content. Let's look at a few examples of radio players on the web.

Rock Antenne Hamburg

Player of

The first example, the player of "Rock Antenne Hamburg", is a good example for how visual clues (the album covers, the text "Jetzt lΓ€uft", translating to "Now playing") can greatly enhance the user experience of a radio player. The focus seems to be on the music, which is exactly what I want.

Wacken Radio

The next example I want to look at, is Wacken Radio, the dedicated radio station for the Wacken Open Air festival:

Wacken Radio player. It covers the entire screen and has a large play button in the center of the screen. A grey bar at the bottom contains controls, such as volume, mute and another play/pause button.

The first impression is that the player is covering the entire screen, whereas in reality, the player itself is only the grey bar at the bottom. There's actually more content on the page (news, upcoming songs, etc.) that is revealed when scrolling. The grey bar is sticky and stays at the bottom of the view port. That's a similar pattern to other websites that have their player sticking to the top of the screen.

Similar to Rock Antenne Hamburg, there's a label for the currently playing song and an album cover. Since the stream I'm using doesn't offer album covers, that's not really an option, though.

A possible design

I will probably go with something simple. There's no website I could really put this example into, so I'll make it more or less standalone.

A draft player design. A play button takes up about one fourth of the player on the left, the right consists of a label, the title of the current song, a mute/unmute button and a slider. The currently playing song reads

The slider on the bottom right will be used to control the volume. The mute/unmute button will have an icon roughly indicating the current volume. A click on it will toggle the volume to 0 and back to the last setting again.

The color scheme will be one that's apparently (at least from what I can tell) popular with radio stations that play jazz a lot: Yellow, black and white. If someone knows why they tend to use yellow a lot, please leave a comment!

The HTML part

First, I need to set things up a bit. I create an empty CSS file, an empty JS file and an HTML file called player.html. I'm planning to use Fontawesome for the icons, so I include a CDN version of that as well.



<!-- player.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf8">
  <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans" />
  <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
  <link rel="stylesheet" href="player.css">
</head>
<body>
  <div class="radio-player">
  <!-- Player goes here -->
  </div>
  <script src="player.js"></script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

Next, I add a div for the player and an audio element for stream.



<div class="radio-player">
  <audio src="..." class="visually-hidden" id="stream">
  <!-- More stuff here -->
</audio>


Enter fullscreen mode Exit fullscreen mode

I now add the controls right underneath the audio element. I also add some containers to later add the layout with flexbox.



<div class="player-controls">
  <button name="play-pause" class="button play-pause-button" aria-label="Play/pause">
    <i class="fas fa-play" aria-hidden></i>
  </button>

  <div class="volume-and-title">
    <div class="currently-playing" aria-label="Currently playing">
      <span class="currently-playing-label">Now playing on Some Radio Station</span>
      <span class="currently-playing-title">Listen to Some Radio Station</span>
    </div>

    <div class="volume-controls">
      <button name="mute" class="button mute-button" aria-label="Mute/unmute">
        <i class="fas fa-volume-down" aria-hidden></i>
      </button>

      <input type="range" name="volume" class="volume" min="0" max="1" step="0.05" value="0.2" aria-label="Volume">
    </div>
  </div>
</div>


Enter fullscreen mode Exit fullscreen mode

So far so good! Now for the styling.

Making it look nice

As a first step, I want to make the buttons look decent. I also give the entire player some margin so it's not stuck to the corner of the viewport.



.radio-player {
  margin: 30px;
}
.button {
  vertical-align: middle;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border: none;
  background-color: #F59E0B;
  color: #fff;
  border-radius: 100%;
}
.play-pause-button {
  width: 70px;
  height: 70px;
  font-size: 25px;
  margin-right: 24px;
}
.mute-button {
  width: 30px;
  height: 30px;
  margin-right: 12px;
}


Enter fullscreen mode Exit fullscreen mode

Which looks like this:

Player elements, the buttons are styled, the rest is not.

Next, I align the elements with flexbox to give the entire thing the structure I want.



.player-controls {
  display: flex;
  align-items: center;
}
.currently-playing {
  display: flex;
  flex-direction: column;
  margin-bottom: 12px;
}
.volume-controls {
  display: flex;
  align-items: center;
}


Enter fullscreen mode Exit fullscreen mode

Structured player controls

Getting somewhere! Then I play around with font size and font weight a little to give the title more visual weight:



.currently-playing-label {
    font-size: 12px;
    font-weight: 300;
}
.currently-playing-title {
    font-size: 22px;
}


Enter fullscreen mode Exit fullscreen mode

Better visual weight for the song title and the label

Next comes the fun part: Styling the <input type="range"> for the volume.

I reset some of the styles using appearance and start styling it according to the rough design:



.volume {
  -webkit-appearance: none;
  appearance: none;
  border: 1px solid #000;
  border-radius: 50px;
  overflow: hidden; /* This will help with styling the thumb */
}


Enter fullscreen mode Exit fullscreen mode

Range input, halfway styled. The thumb still looks like it's standard

There's a problem when styling the thumb, though: I need to use non-standard features. This means vendor prefixes. I'll use a box shadow to color the left part of the thumb differently than the right.



input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;

  height: 15px;
  width: 15px;

  cursor: ew-resize;
  background: #F59E0B;
  box-shadow: -400px 0 0 390px #FDE68A;
  border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
  /* same as above */
}
input[type="range"]::-ms-thumb {
  /* same as above */
}
input[type="range"]:focus {
  border-radius: 50px;
  box-shadow: 0 0 15px -4px #F59E0B;
}


Enter fullscreen mode Exit fullscreen mode

Looks a lot more like the design:

Fully styled player

Adding the functionality

Now I can wire up the buttons with the stream. I start by collecting all the DOM elements I need and initialize a few variables:



const audio = document.querySelector('#stream')
const playPauseButton = document.querySelector('[name="play-pause"]')
const playPauseButtonIcon = playPauseButton.querySelector('i.fas')
const volumeControl = document.querySelector('[name="volume"]')
const currentlyPlaying = document.querySelector('.currently-playing-title')
const volumeButton = document.querySelector('[name="mute"]')
const volumeButtonIcon = volumeButton.querySelector('i.fas')

let isPlaying = false
let fetchInterval = null
let currentVolume = 0.2

audio.volume = currentVolume


Enter fullscreen mode Exit fullscreen mode

The function to fetch and apply the currently playing song depends a lot on how the endpoint used structures the info. In my example, I assume a simple JSON object with a single key in the form of { currentSong: "..." }. I use fetch to get the info.



/**
 * Fetches the currently playing
 * @returns {Promise<any>}
 */
const fetchCurrentlyPlaying = () => fetch('...')
  .then(response => response.json())
  .then(data => currentlyPlaying.innerText = data.currentSong)


Enter fullscreen mode Exit fullscreen mode

The next function I add is to adjust the icon of the mute button to reflect the current volume. If the volume drops to 0, it should show a muted icon, the higher the volume, the more "sound waves the speaker emits". At least figuratively.



/**
 * Adjusts the icon of the "mute" button based on the given volume.
 * @param volume
 */
const adjustVolumeIcon = volume => {
  volumeButtonIcon.classList.remove('fa-volume-off')
  volumeButtonIcon.classList.remove('fa-volume-down')
  volumeButtonIcon.classList.remove('fa-volume-up')
  volumeButtonIcon.classList.remove('fa-volume-mute')

  if (volume >= 0.75) {
    volumeButtonIcon.classList.add('fa-volume-up')
  }

  if (volume < 0.75 && volume >= 0.2) {
    volumeButtonIcon.classList.add('fa-volume-down')
  }

  if (volume < 0.2 && volume > 0) {
    volumeButtonIcon.classList.add('fa-volume-off')
  }

  if (volume === 0) {
    volumeButtonIcon.classList.add('fa-volume-mute')
  }
}


Enter fullscreen mode Exit fullscreen mode

Now for the functionality of the mute button and the volume control. I want it to remember where the volume was last when muting and unmuting. That way, the user can quickly mute and later unmute the stream without having to adjust the volume again. I hook this up with the volume control and the <audio>s volume:



volumeControl.addEventListener('input', () => {
  const volume = parseFloat(volumeControl.value)

  audio.volume = currentVolume = volume
  currentVolume = volume

  adjustVolumeIcon(volume)
})

volumeButton.addEventListener('click', () => {
  if (audio.volume > 0) {
    adjustVolumeIcon(0)
    audio.volume = 0
    volumeControl.value = 0
  } else {
    adjustVolumeIcon(currentVolume)
    audio.volume = currentVolume
    volumeControl.value = currentVolume
  }
})


Enter fullscreen mode Exit fullscreen mode

The last step is the play/pause button. When starting the stream, I set an interval to fetch the currently playing song every 3 seconds. Enough time to be almost real time, but not too much, so it doesn't cause too many unnecessary requests. I also switch out the icon.



playPauseButton.addEventListener('click', () => {
  if (isPlaying) {
    audio.pause()

    playPauseButtonIcon.classList.remove('fa-pause')
    playPauseButtonIcon.classList.add('fa-play')

    clearInterval(fetchInterval)
    currentlyPlaying.innerText = 'Listen to Some Radio Station'
  } else {
    audio.play()

    playPauseButtonIcon.classList.remove('fa-play')
    playPauseButtonIcon.classList.add('fa-pause')

    fetchCurrentlyPlaying()
    fetchInterval = setInterval(fetchCurrentlyPlaying, 3000)
  }

  isPlaying = !isPlaying
})


Enter fullscreen mode Exit fullscreen mode

Aaand we're done! Let's see the functionality in action:

Radio player in cation, showing all the controls and how they work.


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❀️ or a πŸ¦„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, please consider buying me a coffee β˜• or following me on Twitter 🐦! You can also support me and my writing directly via Paypal!

Buy me a coffee button

πŸ’– πŸ’ͺ πŸ™… 🚩
thormeier
Pascal Thormeier

Posted on August 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related