Crafting your perfect audio player using flexible RxJS controls
Everybody likes music. Whether you use Spotify, Deezer, YouTube, or something else to listen to your favorite jams, having control over your playlist with a sophisticated audio player is one of the essential conditions for providing an awesome user experience. In this recipe, we’ll create a lightweight RxJS audio player with reactive controls for playing and pausing songs, controlling volume, as well as skipping to the next song in the playlist.
How to do it…
The essential thing to understand in this recipe is the native HTMLAudioElement and, based on that, which events are the most important to react to.
Step 1 – Creating audio player events
In our audio-player.component.html
file, we must implement markup for the audio player:
<audio #audio></audio>
Concerning that audio HTML element, in the component audio-player.component.ts
file, we’ll define all the key events for that element:
@ViewChild('audio') audioElement!:
ElementRef<HTMLAudioElement>;
ngAfterViewInit(): void {
const audio = this.audioElement.nativeElement;
const duration$ = fromEvent(audio,
'loadedmetadata').pipe(map(() => (
{ duration: audio.duration }))
);
const playPauseClick$ = fromEvent(audio, 'play').pipe(
map(() => ({ isPlaying: true }))
);
const pauseClick$ = fromEvent(audio, 'pause').pipe(
map(() => ({ isPlaying: false }))
);
const volumeChange$ = fromEvent(audio,
'volumechange').pipe(
map(() => ({ volume: audio.volume })),
);
const time$ = fromEvent(audio, 'timeupdate').pipe(
map(() => ({ time: audio.currentTime }))
);
const error$ = fromEvent(audio, 'error');
}
Using the audio element, we can react to play
, pause
, volumechange
, and timeupdate
events, as well as metadata that holds information about the duration
value of a song. Also, in case network interruptions occur when we fetch the audio file or corrupted audio files, we can subscribe to the error
event from the audio element.
Now, we can combine all those events and hold the state of a song in a centralized place:
merge(
duration$,
playPauseClick$,
pauseClick$,
volumeChange$
).subscribe((state) =>
this.audioService.updateState(state));
Step 2 – Managing song state
In our audio.service.ts
file, we’ll store the state of the current song:
public audioState$ = new BehaviorSubject<AudioState>({
isPlaying: false,
volume: 0.5,
currentTrackIndex: 0,
duration: 0,
tracks: []
});
updateState(state: Partial<AudioState>): void {
this.audioState$.next({
...this.audioState$.value,
...state
});
}
Now, we can subscribe to all state changes in the component and have reactive audio player controls over user actions.
Step 3 – Playing/pausing a song
Back in our audio-player.component.ts
file, whenever play or pause events are being emitted, the state will update, at which point we can subscribe to the state change:
this.audioService.audioState$.subscribe(({ isPlaying }) =>
this.isPlaying = isPlaying;
);
Now, in the audio-player.component.html
file, we can present either a play or pause icon based on the following condition:
<button mat-fab class="play-pause-btn" (click)="playPause()">
@if (isPlaying) {
<mat-icon>pause</mat-icon>
} @else {
<mat-icon>play_arrow</mat-icon>
}
</button>
We can also control the audio when playing a song:
playPause(): void {
if (!this.isPlaying) {
this.audioElement.nativeElement.play();
} else {
this.audioElement.nativeElement.pause();
}
}
Step 4 – Controlling the song’s volume
By subscribing to the audio player state, we also have information about the volume based on the previously emitted volumechange
event:
this.audioService.audioState$.subscribe(({ volume }) => {
this.volume = volume;
});
We can represent this state in the UI like so:
<div class="volume">
@if (volume === 0) {
<mat-icon>volume_off</mat-icon>
} @else {
<mat-icon>volume_up</mat-icon>
}
<input
type="range"
[value]="volume"
min="0"
max="1"
step="0.01"
(input)="changeVolume($event)"
/>
</div>
Now, we can emit the same event by changing the volume of the audio player by invoking the changeVolume()
method:
changeVolume({ target: { value } }): void {
this.audioElement.nativeElement.volume = value;
}
This will automatically update the volume
state reactively on the audio player element.
Step 5 – Switching songs
Back in our audio.service.ts
file, we’ve implemented methods for changing the current song index in the list of tracks:
previousSong(): void {
let prevIndex =
this.audioState$.value.currentTrackIndex - 1;
const tracks = this.audioState$.value.tracks;
if (prevIndex < 0) {
prevIndex = tracks.length - 1; // Loop back to the
// end
}
this.updateState({
isPlaying: false,
currentTrackIndex: prevIndex
});
}
nextSong(): void {
let nextIndex =
this.audioState$.value.currentTrackIndex + 1;
const tracks = this.audioState$.value.tracks;
if (nextIndex >= tracks.length) {
nextIndex = 0; // Loop back to the beginning
}
this.updateState({
isPlaying: false,
currentTrackIndex: nextIndex
});
}
Also, when we come to the end of the list, we’ll loop to the beginning of the playlist.
Inside the audio-player.component.ts
component, we can subscribe to this state change and change the song using the audio element:
this.audioService.audioState$.subscribe(({
currentTrackIndex,
tracks
}) => {
if (
tracks[currentTrackIndex].title !==
this.currentTrack.title
) {
this.audioElement.nativeElement.src =
tracks[currentTrackIndex].song;
this.currentTrack = tracks[currentTrackIndex];
}
});
This means that we have all the information we need about the current song, which means we can display that data in our audio-player.component.html
template.
Step 6 – Skipping to the middle of a song
In our audio element, there’s a timeupdate
event that lets us track and update the current time of a song:
const time$ = fromEvent(audio, 'timeupdate').pipe(
map(() => ({ time: audio.currentTime }))
);
time$.subscribe(({ time }) => this.currentTime = time);
In the UI, we can combine this current time information with the previous song metadata, show it in a slider, and watch the song progress:
<p>{{ currentTime | time }}</p>
<audio #audio></audio>
<mat-slider [max]="duration" class="song">
<input matSliderThumb
[value]="currentTime"
(dragEnd)="skip($event)"
>
</mat-slider>
<p>{{ duration | time }}</p>
Finally, if we open our browser, we can inspect all these features and play our favorite jam:

Figure 2.10: Reactive audio player
See also
- The
BehaviorSubject
class: https://rxjs.dev/api/index/class/BehaviorSubject - The
fromEvent
function: https://rxjs.dev/api/index/function/fromEvent - The
map
operator: https://rxjs.dev/api/operators/map - The HTML audio tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio