goldfire / howler.js

Javascript audio library for the modern web.
https://howlerjs.com
MIT License
23.88k stars 2.23k forks source link

resume/seek/play issue (and how I workaround it) #1156

Closed inear closed 3 years ago

inear commented 5 years ago

I recently had a problem with resuming and pausing. Sometimes the sound started to play from the beginning each time even if I seek to a stored time. If I bruteforce-clicked my UI to pause-play-seek rapidly I could recreate the issue directly. I use a single sound per instance, so now sprite-list. What solved the issue for me was to send the id in the play()-method. But I had to pick the internal id like this: myInstance.play(myInstance._sounds[0]._id). It's too much going on in the play-method for me to see why this happens, but if someone with more knowledge can understand why a sound starts from 0 and stops update the current-time/seek-position, it's worth looking into. I think many issues about play/resume/seek can be because of this, whether it's a race condition, internal queue-problem or a id/sprite reference issue.

crazywire commented 5 years ago

If you don't pass an ID or sprite of the sound to play method, Howler uses the default sprite. This is rather odd and should be examined. It assumes you want to play from start to finish if you don't pass an ID.

darren-dev commented 5 years ago

You lost me in the description of your issue - it was more of a statement than a question. But, I will try add some advice.

If you are using a new Howl instance per sound (I don't think that's best practice?), then it should be rather easy to seek where you need to be.

In this case, you will instantiate the sound const howl = new Howl({...}), then play the sound and store it's ID somewhere const soundId = howl.play(/* Nothing here because it's default*/). Then, when you seek, you should use the soundId in the seek parameter howl.seek(0, soundId)

That SHOULD work

goldfire commented 5 years ago

I'm having a hard time following the description of the issue. Could you provide sample code so that I can reproduce the issue and try to find a fix?

inear commented 5 years ago

The part that I missed was that you need to provide with the ID in the play/pause. In my app I had one single master audio track for a long time. Then I had to add subtracks for individual instruments and problems with stop/start/seek-position started. Each instance is created with "audioInstance = new howler.Howl(howlerOptions)". And I control the seek like this:

audioInstance.pause() audioInstance.seek(this.currentTime) audioInstance.play()

That sometimes restart the sound at time=0 event if this.currentTime was not 0. Not always though so suspect it collide with the state of the master track. Because if I do like this instead:

audioInstance.pause() audioInstance.seek.(this.currentTime) audioInstance.play(ch.audioInstance._sounds[0]._id)

Then it works. What was strange to me was that the problem just happened in some rare cases. And I don't have to use ID:s since it's not multiple sprites in the same file, so I don't want to use the internal _id. If calling play on a sound instance, I want it to control that only. Not defaulting to the sprite "default". I think @crazywire is on something there. To me it seems like the wrong internal instance is controlled even if I run the play() method on my instance. Getting the "default" sprite instead of my sound instance (that is stored at _sounds[0]._id) But I might be completely wrong here...

My app works right now so I don't have any issues and don't have time to look into it further now that it works, but wanted to bring it up here since I did not understand why it happened and it could be helpful for others with similar problems. So feel free to close this at any time.

smakinson commented 5 years ago

@inear I've been trying to determine the best way to autoplay after a seek ( but not from load ) and I made a pen that may also help show what you are seeing. The trouble I am running into is overlapping sounds sometimes and also starting from the start. This is not quite what I have in my project, but it is close and it allows showing rapid calls to seek & play for the defaults:

https://codepen.io/smakinson/pen/ROvabG?editors=1010

Some things I've noticed but I'm not sure are correct:

Should it just work by calling seek no matter if its playing or not and also even if no id is passed in ( since I never play more than one mp3 )? Should there be an autoplay option for the seek() method? Am I just dumb? :)

Thoughts?

st-h commented 5 years ago

@smakinson do you know if this might only happen using iOS and safari on macOS?

inear commented 5 years ago

For me this issues occur on both Mac Chrome and Firefox, and Windows Chrome and Edge, so it's a framework "issue" with how the internal state is handled, if not a general WebAudio limitation.

st-h commented 5 years ago

@inear I was asking because I have lately seen a few quite strange things on webkit with apple devices when html5 audio is used as in @smakinson example. One of them was being unable to seek and play under certain conditions. However, this most likely are separate issues then.

smakinson commented 5 years ago

I am using chrome while working most of the time, this one is chrome/macOS. The biggest thing I am trying to avoid is the overlapping when trying to play if a seek is done from a pause using a slider.

st-h commented 5 years ago

@smakinson I just tried the example from your codepen in current safari and chrome. I did not notice any issues when seeking within the file. Also, I think within the updateAudioPosition you should not need to call pause and start. You should just be able to seek to the position:

methods: {
    updateAudioPosition: function () {
      // this.player.pause()
      this.player.seek(this.position);
      // this.player.play()
    }
}
smakinson commented 5 years ago

@st-h I don't see any issue either until I try to make it play right after the seek. Then if I click around on the slider it seems to do things like play from the beginning and sometimes play the track more than once over itself. I think I am able to work around that with the pause and then use a delay before calling play again, but I don't really like that workflow.

inear commented 5 years ago

@smakinson I have the same problem in the Pen as I stated in this issue. But if I change to this.player.play(this.player._sounds[0]._id) the sound seeks as it should.

smakinson commented 5 years ago

@inear @st-h So the issue I suppose must be in how these differ:

_soundById: function(id) {
      var self = this;

      // Loop through all sounds and find the one with this ID.
      for (var i=0; i<self._sounds.length; i++) {
        if (id === self._sounds[i]._id) {
          return self._sounds[i];
        }
      }

      return null;
    }

https://github.com/goldfire/howler.js/blob/master/src/howler.core.js#L1992

_inactiveSound: function() {
      var self = this;

      self._drain();

      // Find the first inactive node to recycle.
      for (var i=0; i<self._sounds.length; i++) {
        if (self._sounds[i]._ended) {
          return self._sounds[i].reset();
        }
      }

      // If no inactive node was found, create a new one.
      return new Sound(self);
    }

https://github.com/goldfire/howler.js/blob/master/src/howler.core.js#L2009

since play() does: var sound = id ? self._soundById(id) : self._inactiveSound();

Also when the id is not given, it resets the sound that is returned from _inactiveSound() and that sets its _seek = 0, might that be why it seems the audio starts at the beginning when its incorrect and/or doubled up?

For reference here is reset() & _drain():

reset: function() {
      var self = this;
      var parent = self._parent;

      // Reset all of the parameters of this sound.
      self._muted = parent._muted;
      self._loop = parent._loop;
      self._volume = parent._volume;
      self._rate = parent._rate;
      self._seek = 0;
      self._rateSeek = 0;
      self._paused = true;
      self._ended = true;
      self._sprite = '__default';

      // Generate a new ID so that it isn't confused with the previous sound.
      self._id = ++Howler._counter;

      return self;
    }

https://github.com/goldfire/howler.js/blob/master/src/howler.core.js#L2233

_drain: function() {
      var self = this;
      var limit = self._pool;
      var cnt = 0;
      var i = 0;

      // If there are less sounds than the max pool size, we are done.
      if (self._sounds.length < limit) {
        return;
      }

      // Count the number of inactive sounds.
      for (i=0; i<self._sounds.length; i++) {
        if (self._sounds[i]._ended) {
          cnt++;
        }
      }

      // Remove excess inactive sounds, going in reverse order.
      for (i=self._sounds.length - 1; i>=0; i--) {
        if (cnt <= limit) {
          return;
        }

        if (self._sounds[i]._ended) {
          // Disconnect the audio source when using Web Audio.
          if (self._webAudio && self._sounds[i]._node) {
            self._sounds[i]._node.disconnect(0);
          }

          // Remove sounds until we have the pool size.
          self._sounds.splice(i, 1);
          cnt--;
        }
      }
    }

https://github.com/goldfire/howler.js/blob/master/src/howler.core.js#L2028

smakinson commented 5 years ago

Another way I am trying seeking with play locally is checking if the instance playing() and if not saving the seek position to be set in the onplay event and then calling play(), if playing() is true then just to the seek() right away. This seems to work, but the oddity I see with is on my slider, sometimes it will slide back over to 0 for a moment, then move back to the position I seeked to or even back to where it was for a moment then to where I seeked to.

abrehamgezahegn commented 4 years ago

This seems to happen when the audio file is not fully loaded.

goldfire commented 3 years ago

This should be fixed in 2.2.1 (which I'll push to npm later today I suspect), but if not I'll reopen.