user2338256
user2338256

Reputation: 33

Using JQueryUI as an audio Seek control, and rebinding a new slider to the same <audio> control when a song is changed

Thank you for looking at my question. Fiddle is HERE.

I am trying to implement an audio player for many songs on a page. This is using JQueryUI Slider and HTML5 Audio, with one Audio element and multiple sliders.

The problems right now are:

  1. The slider does not animate with the audio.
  2. The slider does not seek to the audio.
  3. previously, when both of the above WERE working, once you chose a spot in the song, the slider would no longer animate.

I have created function to rebindSlider() when a new song is clicked. Inside this function, two things happen: a) the new slider is created, with slide and stop listeners defined, and b) the new slider is bound to the timeupdate event on the audio element for the new song. I feel like this should be all that I need, but the slider does not bind, and an undefined error can be seen when you try to drag the slider.

Using a single slider, and a single audio element, I have gotten this 90% the way there; as soon as I introduced multiple divs for the sliders though, problems started occurring.

Here is the code for rebindSlider:

function rebindSlider(sliderDiv) {
  var createSeek = function() {
    sliderDiv.slider({
      value: 0,
      step: 1,
      orientation: "horizontal",
      range: "min",
      max: audioPlayer.duration,
      animate: true,

      slide: function() {
        manualSeek = true;
      },

      stop: function(e, ui) {
        manualSeek = false;
        audioPlayer.currentTime = ui.value;
      }
    });
  };

  createSeek();

  $(audioPlayer).bind('timeupdate', function() {
    if (!manualSeek) {
      sliderDiv.slider('value', audioPlayer.currentTime);
    }
  });
} 

Further description is below.

There is a list of songs included on the page. Each song has a containing div, within which is an <a> that contains meta data (absent in the fiddle), as well as a div designated for the audio seek slider. There is a single audio element on the page, which has its source re-loaded as you click through the songs.

When a song is clicked, I would like to destroy the sliders bound to the playing audio (if necessary), and bind the new slider for the clicked song to the audio player.

The closest I have gotten was to have the slider 1) begin animating when the song plays 2) dragging of the slider moved to a different position in the song. Once the slider had moved though, it no longer animated. After a refactor, the slider no longer works, and though I could go to a previous commit to get the working code, the refactor was so drastic that I would prefer to present the current, non-working code, as it better represents what I would like to end up with.

Reasoning, and additional information.

I am making a web app, for which I have a concept for an audio player that I would rather build myself than modify anything that I have come across. That being said, if you know of something that I can implement, I would love suggestions.

The idea sounds simple enough, and most of it is done, but there is a very crucial part of it that I am having trouble with, which is setting up the slider to animate with, and seek to the desired spot in the audio track, and to be able to rebind a new slider to the audio when a song is clicked.

Upvotes: 2

Views: 765

Answers (1)

Michael Gaskill
Michael Gaskill

Reputation: 8042

There are a few changes needed to get the slider working correctly. I'll cover them each, in order that they apply.

Invalid Slider Max Value

The first issue is that the slider's max value is set to the audioPlayer.duration, which would normally be all well and good, except that the HTML5 audio player loads the audio assets asynchronously. What this means is that even though you might have loaded the audio prior to setting the slider, the audio asset may not be loaded yet, and the audioPlayer.duration may be invalid (likely NaN).

Simply remove the max key (and value) from the slider initialization, and this will work. Et viola! The slider moves!

One caveat: the slider defaults to a max value of 100 units, and the duration of the audio is a little over 25 seconds, so the song finishes playing when the slider is 1/4th of the way along. We can set the max key to 25 (seconds), but that's a little inelegant, and wont work if we change the audio to use a different source.

Set the Slider Max Value

Capturing the audioPlayer.duration must be done be handling an event, once the asynchronous load has completed. Javascript provides such an event: onloadedmetadata. Let's install an event handler, and make this work:

audioPlayer.onloadedmetadata = function() {
    $(".slider").slider("option", { max: Math.floor(audioPlayer.duration) });
};

Now, what this does is set the slider's max to the audioPlayer.duration once the audio asset has loaded. Actually, this is currently setting the max value for ALL of the sliders, but that shouldn't be a problem, since they're all hidden. If you have a very large number of songs, there might be a bit of a delay, and you may want to find the specific slider to update.

Smooth Sliding

Now, you may notice after these changes that the slider is somewhat jumpy. It pauses for a second, then jumps, then pauses, etc. This can be fixed very easily, by changing the step key to 0.1 in the slider initialization, like so:

sliderDiv.slider({
  value: 0,
  step: 0.1,
  orientation: "horizontal",

The time updates occur every 50 to 250 milliseconds, but the slider could only move in 1-second increments. Now, with 1/10th of a second increments, the slider will move more smoothly. You can decrease that slightly if you like, but don't make the number too small; 0.01 is the practical lower bound.

Update the End-of-Play State

When the song has finished, the "Play" button is left in play state, even though there's no longer anything playing. There's an event for that, as well: onended. We'll use that event to update the UI, so that the user doesn't get confused:

audioPlayer.onended = function() {
  playButton.removeClass('fa-pause-circle-o');
  playButton.addClass('fa-play-circle-o');
};

Now, when the song finishes, the Play button will go back to the "play" state, and the user will know that clicking on it will play the song.

Refactor Play Button State

Since there are now 3 places that update the state of the Play button, and this involves duplication, we can refactor the state handling. This function will definitively set the state of the Play button:

function setUIState() {
  if (audioPlayer.paused || audioPlayer.ended) {
    playButton.removeClass('fa-pause-circle-o');
    playButton.addClass('fa-play-circle-o');
  } else {
    playButton.removeClass('fa-play-circle-o');
    playButton.addClass('fa-pause-circle-o');
  }
}

Now, calling it from the other functions, you end up with the onended event handler:

audioPlayer.onended = function() {
  setUIState();
};

The changeUI function:

function changeUI(selectedSong) {
  setUIState();

  var sliderDiv = selectedSong.parent().find('.slider');
  rebindSlider(sliderDiv);//Bind the desired slider
  $('.slider').hide(); //Hide all sliders
  sliderDiv.show();//Show only the desired slider
} 

And, the Play button click handler:

$(".playButton").click(function() {    
  if (audioLoaded === true) {
    if (!audioPlayer.paused) {
      audioPlayer.pause();
    } else {
      audioPlayer.play();
    }
    setUIState();
  } else {
    alert("Please click a song");
  }
  return false;
});

This makes the UI state management for the button simple to deal with, and keeps the state from creeping out through the app. You can already see how the refactored code is noticeable clearer and will be easier to maintain. Plus, as a bonus, we got to use the cool audioPlayer.ended attribute, which you don't see used much.

But Wait, There's More!

With the changes above, you're well on your way to having a very functional audio player. But that's certainly not the end of the line for feature functionality. There's always room to grow!

I've create a jsFiddle that includes all of these changes, as well as a few other mods to the original code. You can find the most recent version at: https://jsfiddle.net/mgaskill/mp087adp/. Additional features are likely to keep popping up, but the jsFiddle already includes these additional features:

  • Volume control (slider)
  • Time display
  • Dynamically generated HTML for audio controls (keeps the HTML simpler)

Upvotes: 1

Related Questions