I'm creating a music player web app, and I've got almost everything worked out, but I've run into a major problem.
First, here's the part that works. When music begins playing, MediaSession
kicks in. This causes a notification to appear on the user's phone and let's them control playback with a Bluetooth audio device. The latter is especially important when driving, because if the user has paired their phone with their car, they can control playback with their car's audio controls instead of having to look down at their phone.
Now, here's where the problem comes in. In order to change songs, the src
attribute of HTMLAudioElement
gets changed, and it automatically begins loading the new song file from the server. If the new song starts playing right away, everything works fine, but if playback is paused or if an error is encountered while loading the song, then MediaSession
stops working. The notification disappears and the action handlers no longer react to input from the Bluetooth device.
Here's a simplified excerpt of the relevant code from my Pinia store in Vue:
export const useMusicStore = defineStore('music', {
state: () => ({
player: new Audio(),
queue: [],
currentQueueId: null,
isPaused: true,
}),
getters: {
currentSong() {
// ...
},
},
actions: {
initialize() {
this.addEventListeners()
this.setActionHandlers()
this.loadLastSession()
},
addEventListeners() {
this.player.addEventListener('play', () => this.updatePlaybackState('playing'))
this.player.addEventListener('pause', () => this.updatePlaybackState('paused'))
this.player.addEventListener('timeupdate', () => this.updatePositionState())
this.player.addEventListener('loadedmetadata', () => this.updatePositionState())
this.player.addEventListener('canplay', () => {
if (!this.isPaused) {
this.play()
}
})
this.player.addEventListener('ended', () => {
this.nextSong()
this.play()
})
this.player.addEventListener('error', (el, err) => {
console.error(err)
Notify.create("Error encounted while attempting to play song.")
})
},
setActionHandlers() {
const actionHandlers = {
play: () => this.play(),
pause: () => this.pause(),
stop: () => this.stop(),
seekto: (details) => this.seek(details.seekTime),
seekbackward: (details) => this.skipBack(details.seekOffset ?? 10),
seekforward: (details) => this.skipForward(details.seekOffset ?? 10),
previoustrack: () => this.skipBack(),
nexttrack: () => this.skipForward(),
}
if ('mediaSession' in navigator) {
for (const [action, handler] of Object.entries(actionHandlers)) {
try {
navigator.mediaSession.setActionHandler(action, handler)
} catch {
console.log(`Action '${action}' not supported.`)
}
}
}
},
loadCurrentSong() {
if (this.currentSong) {
this.player.src = this.getSongUrl(this.currentSong)
}
},
}
}
In the above code, you can see that when loadCurrentSong()
runs, it changes the src
attribute of the HTMLAudioElement
, which automatically begins loading the song file. When enough of the file has been loaded, HTMLAudioElement
fires a "canplay" event. This gets heard by the relevant event listener, which begins playing the song only if the user didn't have playback paused.
Is there a way of ensuring that MediaSession
stays "alive" when switching songs? I'm open to using a library, but only if lets me handle the user interface (in Vue). I tried Howler.js, but I still ran into the exact same problem.