Let's build a web radio player from scratch ππ»
Pascal Thormeier
Posted on August 25, 2021
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
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:
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.
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>
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>
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>
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;
}
Which looks like this:
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;
}
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;
}
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 */
}
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;
}
Looks a lot more like the design:
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
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)
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')
}
}
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
}
})
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
})
Aaand we're done! Let's see the functionality in action:
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!
Posted on August 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.