VUE.JS Audio Player Bug
Hi All, I'm creating a audio player, but it turns out that two Events are fighting each other. I have an onchange event for a range input which changes the track duration I also have another that updates the players time from the audio current time.
But when I update the track time using the range input it sometimes skips back to where it was.
I also have another bug where the Audio object isn't created before the playlist duration is calculated and displayed, so it appears that the playlist is 00:00 minutes long.
Please bare in mind that the data.audio is first the location of the file and then turned into an Audio object.
Any help would be much appreciated.
Thanks, Jamie
<body>
<div id="app" v-model="generateAudioObjects">
<i class="previous fa fa-fw fa-step-backward" v-on:click="previousTrack"></i>
<i class="play-pause" v-bind:class="playPauseIcon" v-on:click="ppState"></i>
<i class="previous fa fa-fw fa-step-forward" v-on:click="nextTrack"></i>
<span>{{ this.player.currentTime | time }}</span>
<input type="range" v-bind:value="this.player.currentTime" v-bind:max="currentTrackDuration" v-on:change="changeElapsed($event)" />
<span>{{ currentTrackDuration | time }}</span>
<h3>{{ tracks.length }} songs, {{ playlistDuration | time }}</h3>
<h1 v-for="track in tracksWithPlaying" class="track" v-on:click="gotoTrack(track.id)" v-html="track.title"></h1>
</div>
</body>
<script src="js/vue.min.js"></script>
<script>
let app = new Vue({
el: '#app',
data: {
tracks: [{
title: 'Just Be Good To Me (Lee Morrison Mashup)',
audio: 'sound/Just%20Be%20Good%20To%20Me%20(Lee%20Morrison%20Mashup).mp3'
},
{
title: 'Jess Glynne Vs Katy Perry',
audio: 'sound/Jess%20Glynne%20Vs%20Katy%20Perry.mp3'
},
{
title: 'Zara Larsson VS KNOXA - Lush Life (Ricardo\'s Weekend Booty)',
audio: 'sound/Zara%20Larsson%20VS%20KNOXA%20-%20Lush%20Life%20(Ricardo\'s%20Weekend%20Booty).mp3'
}
],
player: {
playPauseState: false,
currentTrackId: 0,
shuffle: 0,
repeat: 0,
volume: 100,
currentTime: 0,
}
},
filters: {
time: function(seconds) {
let h = Math.floor(seconds / 3600);
let m = Math.floor(seconds % 3600 / 60);
let s = Math.floor(seconds % 3600 % 60);
let hDisplay = h > 0 ? h + ":" : "";
let mDisplay = m > 0 ? ("0" + m).slice(-2) + ":" : "00:";
let sDisplay = s > 0 ? ("0" + s).slice(-2) : "00";
return hDisplay + mDisplay + sDisplay;
}
},
computed: {
generateAudioObjects: function() {
for (let i = 0; i < this.tracks.length; i++) {
if (!(this.tracks[i].audio instanceof Audio)) {
this.tracks[i].audio = new Audio(this.tracks[i].audio);
this.tracks[i].id = i;
}
}
},
playPauseIcon: function() {
if (!this.player.playPauseState) { // if the button text is play
return "fa fa-fw fa-play";
} else {
return "fa fa-fw fa-pause";
}
},
currentTrack: function() {
return this.tracks[this.player.currentTrackId];
},
currentTrackDuration: function() {
return this.tracks[this.player.currentTrackId].audio.duration;
},
tracksWithPlaying: function() {
let tempTracks = this.tracks.slice(0);
if (this.player.playPauseState) {
let id = this.player.currentTrackId;
tempTracks[id].title += ' <small>Playing</small>';
}
return tempTracks;
},
playlistDuration: {
cache: false,
get: function () {
let duration = 0;
for (let i = 0; i < this.tracks.length; i++) {
this.tracks[i].audio.readyState > 0 ? duration += this.tracks[i].audio.duration : 0;
}
return duration;
}
}
},
methods: {
ppState: function() {
if (!this.player.playPauseState) { // if the button text is play
this.play(); // play the current track
} else {
this.pause(); // pause the current track
}
},
play: function() {
this.player.playPauseState = true; // change the button text to pause
this.currentTrack.audio.play(); // play the audio track
this.currentTrack.audio.ontimeupdate = function () { // set the audio time listener
if (this.changeCurrentTime) {
this.currentTrack.audio.currentTime = this.player.currentTime;
this.changeCurrentTime = false;
} else {
this.player.currentTime = this.currentTrack.audio.currentTime; // update player time
}
if (this.currentTrack.audio.currentTime >= this.currentTrack.audio.duration) { // check if the track has ended
this.nextTrack(); // go to the next track
}
}.bind(this);
},
pause: function() {
this.player.playPauseState = false; // change the button text to play
this.currentTrack.audio.pause(); // pause the audio track
},
nextTrack: function() {
let id = this.player.currentTrackId; // save the track id to a temproary variable
if (++id >= this.tracks.length) { // check if the end of the tracklist
id = 0; // start at the beginning of the tracklist
}
this.gotoTrack(id); // goto the next track
},
previousTrack: function() {
let id = this.player.currentTrackId; // save the track id to a temproary variable
if (--id < 0) { // check if the begginning of the tracklist
id = this.tracks.length - 1; // start at the end of the tracklist
}
this.gotoTrack(id); // goto the previous track
},
changeElapsed: function(event) {
this.changeCurrentTime = true;
this.player.currentTime = event.target.value;
},
gotoTrack: function(id) {
let state = this.player.playPauseState; // get the player current play state
if (state) this.pause(); // if the player is playing the old track pause it
this.player.currentTime = this.currentTrack.audio.currentTime = 0; // clear the old track playtime
this.player.currentTrackId = id; // set the player to the new id
if (state) this.play(); // if the player was playing the old track play the new one
}
}
});
</script>
Hey!
I am just following up on some of the old unanswered questions on the site! I can see you're facing a couple of issues with your audio player. Let's dive into them and address them one by one!
Issue 1: Conflicting Events (Track Time Skipping)
The conflict is happening because you're updating the player's current time both in the changeElapsed
method and in the ontimeupdate
event listener. Here's a way to fix it:
- Use a flag to indicate when the current time is manually changed by the user.
- In the
changeElapsed
method, set the flag and update the current time. - In the
ontimeupdate
event listener, check the flag. If it's set, skip the automatic update, and reset the flag.
You've already started implementing this, but the implementation seems incorrect. You should define the changeCurrentTime
property inside data
and set it to false initially:
data: {
// ...
changeCurrentTime: false,
// ...
},
And in the changeElapsed
method:
changeElapsed: function(event) {
this.changeCurrentTime = true;
this.player.currentTime = event.target.value;
this.currentTrack.audio.currentTime = event.target.value;
},
In the ontimeupdate
listener, use this.changeCurrentTime
to check and reset the flag:
this.currentTrack.audio.ontimeupdate = function() {
if (this.changeCurrentTime) {
this.changeCurrentTime = false;
} else {
this.player.currentTime = this.currentTrack.audio.currentTime;
}
// ...
}.bind(this);
Issue 2: Audio Object Initialization and Playlist Duration Calculation
The audio objects are being generated in a computed property (generateAudioObjects
), which is not an ideal place for this type of operation. Since you want to initialize the audio objects as soon as your Vue instance is created, you should use the created
lifecycle hook.
- Move the audio object initialization code from
generateAudioObjects
to thecreated
lifecycle hook. - To handle the case where the audio metadata might not be loaded immediately, you can listen for the
loadedmetadata
event for each audio object and then update the duration.
Here's how you might do that:
created: function() {
this.tracks.forEach((track, i) => {
track.audio = new Audio(track.audio);
track.id = i;
track.audio.addEventListener('loadedmetadata', () => {
this.$forceUpdate(); // This will force Vue to update and recompute playlistDuration
});
});
},
In your playlistDuration
computed property, you might want to wait for the duration
property to be available for all audio objects:
playlistDuration: {
cache: false,
get: function() {
let duration = 0;
for (let i = 0; i < this.tracks.length; i++) {
if (this.tracks[i].audio.duration) {
duration += this.tracks[i].audio.duration;
}
}
return duration;
}
}
These changes should help in resolving the issues you've mentioned.