VUE.JS Audio Player Bug

jamie-bonnett

Sep 6th, 2017 01:25 AM

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>
bobbyiliev

Aug 20th, 2023 11:24 PM

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:

  1. Use a flag to indicate when the current time is manually changed by the user.
  2. In the changeElapsed method, set the flag and update the current time.
  3. 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.

  1. Move the audio object initialization code from generateAudioObjects to the created lifecycle hook.
  2. 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.