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
changeElapsedmethod, set the flag and update the current time. - In the
ontimeupdateevent 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
generateAudioObjectsto thecreatedlifecycle hook. - To handle the case where the audio metadata might not be loaded immediately, you can listen for the
loadedmetadataevent 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.
