sampotts / plyr

A simple HTML5, YouTube and Vimeo player
https://plyr.io
MIT License
26.09k stars 2.91k forks source link

Can't set initial player speed if you set size (quality) on sources #2123

Open bowczarek opened 3 years ago

bowczarek commented 3 years ago

First and foremost @sampotts thanks for your awesome contribution πŸ₯‡ πŸ‘ πŸ™ !

Expected behaviour

If you pass selected speed option when initializing Plyr instance and then set sources programatically with size (quality) specified then the selected speed should be set on player, for example it should be set to 2 initially in the following code snippet:

const player = new Plyr("#videoplayer", {
  storage: { enabled: false },
  speed: { selected: 2, options: [0.5, 1, 1.5, 2] }
});

player.source = {
  type: "video",
  sources: [
    {
      src:
        "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
      type: "video/mp4",
      size: 1080
    }
  ]
};

Actual behaviour

Actual behavior is that it is set back to 1 after Plyr instance initialization. By debugging the sources, I believe I found the reason or at least some initial place in code that you can refer to and might help to fix the bug. It will be quite long, took me some time to investigate but please stay a while and listen ;)

There are two actions here:

  1. Plyr initialization with default options
  2. Setting sources on Plyr instance

I'll start from the 2nd instruction (backwards). Namely, if you specify the size (quality) on sources then the whole quality setter on plyr object (plyr.js) is invoked that finally invokes quality setter on media object which in our case is html5 so its implementation is in html5.js (I'll skip irrelevant lines):

set quality(input) {
    const config = this.config.quality;
    const options = this.options.quality;

    if (!options.length) {
      return;
    }

     ....

    // Set quality
    this.media.quality = quality;
}

If you don't specify size (quality) on source then it returns immediately and quality setter on media object is not invoked as you can see above if (!options.length) { return; }.

Otherwise, if we specify the size (which is our case) then here is first most important thing that happens. When we take a look into that media.quality setter (html5.js), we can find following code (i'll skip irrelevant lines, just paste most important one that are invoked in this scenario):

set(input) {
   ...
  // Get current state
  const { currentTime, paused, preload, readyState, playbackRate } = player.media;

  // Set new source
  player.media.src = source.getAttribute('src');

  // Prevent loading if preload="none" and the current source isn't loaded (#1044)
  if (preload !== 'none' || readyState) {
    // Restore time
    player.once('loadedmetadata', () => {
      player.speed = playbackRate;
      player.currentTime = currentTime;

      // Resume playing
      if (!paused) {
        silencePromise(player.play());
      }
    });

    // Load new source
    player.media.load();
}

Now, two critical things happen here:

Ok, now we may wonder why it is set back to 1, if this whole logic is invoked after Plyr initialization (first instruction in our case). We would expect it to be already set to 2 even if it is snapshotted from destructuring right? To find that out we need to take a look at first instruction (step) which is already mentioned several times Plyr initialization.

When Plyr is initialized, its speed setter (plyr.js) is invoked (again, I'll skip irrelevant code, leave just last instruction):

set speed(input) {
    ...

    // Set media speed
    setTimeout(() => {
      this.media.playbackRate = speed;
    }, 0);
  }

As you can see speed on media object is set using setTimeout method with time interval set to 0, which according to specification (https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals#settimeout), does not mean it's invoked immediately:

If you specify a value of 0 (or omit the value), the function will run as soon as possible. (See the note below on why it runs "as soon as possible" and not "immediately".)

So now everything should be clear, we got some race condition happening here. To sum up, the speed from Plyr initialization (where we pass 2) is actually set after the value is snapshotted when we set the sources from the second instruction and then loadmetadata that happens at the end resets it back to 1.

Two additional things from my side that you may guys consider or reason about:

Steps to reproduce

I created a sample code sandbox with vanilla js where you can see that behaviour. Check the comments I added there, if you comment out size (quality), all of a sudden it works as expected. If you keep it, then in the console you can see that two events with speed change happen, and the 2nd one always reverts the value back to 1.

As an analogy you can try setting the initial volume as well, which works just fine as expected, because the logic of setting volume does not have those quirks of speed I described above.

https://codesandbox.io/s/plyr-speed-issue-jdy01

Environment

norecords commented 2 years ago

@bowczarek Thanks a lot, I was bangin' my head with this; As I use speed control for my webcams timelapse, I wasn't able to set the default speed.. I just removed size parameters from my list of videos, and now just work as expected. That's still the case on plyr v. 3.6.9