scottschiller / SoundManager2

A JavaScript Sound API supporting MP3, MPEG4 and HTML5 audio + RTMP, providing reliable cross-browser/platform audio control in as little as 12 KB. BSD licensed.
http://www.schillmania.com/projects/soundmanager2/
Other
5k stars 768 forks source link

onfinish not triggering on Safari iOS #158

Open dotpao opened 7 years ago

dotpao commented 7 years ago

Check this jsfiddle https://jsfiddle.net/dotpao/njjgpa5h/5/ from iOS Safari. I've noticed that when you try to load the same audio file twice and then pause the file before playing, the onfinish callback is never triggered. This happens only on iOS and it is related to the response received (readyState === 1)

/**
     * Pauses sound playback.
     *
     * @return {SMSound} The SMSound object
     */

    this.pause = function(_bCallFlash) {

      if (s.paused || (s.playState === 0 && s.readyState !== 1)) {
        return s;
      }

      sm2._wD(s.id + ': pause()');
      s.paused = true;

...

In the pause method, s.paused gets true on iOS while on other browsers gets false. The difference is that on iOS readyState == 1 while on other browsers readyState == 3

/**
       * Streams will pause when their buffer is full if they are being loaded.
       * In this case paused is true, but the song hasn't started playing yet.
       * If we just call resume() the onplay() callback will never be called.
       * So only call resume() if the position is > 0.
       * Another reason is because options like volume won't have been applied yet.
       * For normal sounds, just resume.
       */

      if (s.paused && s.position >= 0 && (!s._iO.serverURL || s.position > 0)) {

        // https://gist.github.com/37b17df75cc4d7a90bf6
        sm2._wD(fN + 'Resuming from paused state', 1);
        s.resume();

      } else {

        s._iO = mixin(oOptions, s._iO);

       ...

Later in the play method, instanceCount would be increased, but this doesn't happen because it comes from a paused state (s.paused true), so it runs s.resume().

So the onfinish callback never triggers because instanceCount is zero.

this._onfinish = function() {

      // store local copy before it gets trashed...
      var io_onfinish = s._iO.onfinish;

      s._onbufferchange(0);
      s._resetOnPosition(0);

      // reset some state items
      if (s.instanceCount) {

        s.instanceCount--;

        if (!s.instanceCount) {

          // remove onPosition listeners, if any
          detachOnPosition();

          // reset instance options
          s.playState = 0;
          s.paused = false;
          s.instanceCount = 0;
          s.instanceOptions = {};
          s._iO = {};
          stop_html5_timer();

          // reset position, too
          if (s.isHTML5) {
            s.position = 0;
          }

        }

        if (!s.instanceCount || s._iO.multiShotEvents) {
          // fire onfinish for last, or every instance
          if (io_onfinish) {
            sm2._wD(s.id + ': onfinish()');
            wrapCallback(s, function() {
              io_onfinish.apply(s);
            });
          }
        }

      }

    };
dotpao commented 7 years ago

It seems it is not related to the multiple audios. Just loading, pausing and playing a single audio causes an issue. This is the updated fiddle https://jsfiddle.net/dotpao/njjgpa5h/6/

scottschiller commented 7 years ago

Ah! Thanks for the update - sorry I missed this in my latest round of work, I pushed a batch of updates for a 2-year-anniversary release just this weekend. 😉 I was able to repro this and it's a good one to fix, so I'll look into that; thanks for the detail.

dotpao commented 7 years ago

Thanks to you.

And do you have an estimation when this issue will be solved?. Just to wait for it or apply a custom workaround at my side?

scottschiller commented 7 years ago

I haven't got a fix in for this one yet, sorry! I'll work this into the dev branch.

Perhaps the simplest fix is to not allow paused to become true when playing is false? That might be the safest route. I think it's a little odd to pause before playing, and so in that case (i.e., if the sound is not currently playing) it should just exit and not modify paused state.