sampotts / plyr

A simple HTML5, YouTube and Vimeo player

Home Page:https://plyr.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setting source programatically creates 170 duplicate event listeners on each call

catdna opened this issue · comments

Expected behaviour

When changing the source property of a Plyr instance, any existing event listeners should be removed (as the Plyr code seems to destory / recreate the Plyr instance internally !)

Actual behaviour

The number of event listeners assigned to a Plyr instance grows by approximately 170 each time the source property of the player instance is set programatically.

We use an audio player and allow the user to click on certain image / buttons / components etc which will then play different audio files assigned to each component - these cannot be determined before the page loads (so we cannot use a <source> element inside the <audio> element).

While changing the source programatically does work - I noticed the number of event listeners increasing at an alarming rate which appears to be the 'soft' destroy (in the Plyr source) isn't working as expected.

Steps to reproduce

I created a really simple Plnkr demo for this - it has an <audio> element and a <button> to set the source of the Plyr instance - it shows the number of event listeners before and after each call to set the source.

https://plnkr.co/edit/s44yTeU8XYYvao3k?preview

image

Environment

  • Browser: all (that I have access to)
  • Version: all up to date
  • Operating System: OSX
  • Version: 13.6.2

Console errors (if any)

none

Link to where the bug is happening

https://plnkr.co/edit/s44yTeU8XYYvao3k?preview

I tracked down the problem ... kind of ....

When the Plyr code changes the source, it eventually gets to:

src/js/source.js - in the this.destroy.call .... callback :

image

Which calls the ui.build.call(this) module / method :

image

Which then calls the src/js/listeners.js module listeners.media() method:

image

It appears that this is the area where the event listeners are continually added to the player elements - I didn't dive any further (it appears there's a toggle variable which should be used to remove any existing listener, but it's not working for some reason - maybe because the player instance has changed?

I'm kinda stuck now - I'm not sure of the performance impact this will have on our code on smaller devices (I don't have one to test on) - but our software is data driven (so there maybe pages with lots of source changes for a single player - at least 10 or more, and then the user can always run through that process again without reloading the page - and adding 170 event listeners on each player.source = ... call seems pretty costly to me.

Thanks
Chris

It looks like it's just the reference to the event listener functions that's being updated each time - the player.eventListeners array is the one which is just growing and growing - it appears the actual event listeners are being added / removed as expected - but the array is still holding references to the old functions.

I have a solution which works for us in our code which does not require changes to the Plyr source code (but it would be nice if it was fixed in the Plyr codebase !)

My solution is as follows:

  1. When creating an instance of the Plyr component, I wrap it in a javascript Proxy

  2. I use the proxy to detect any changes to the source property and clear down the eventListeners array before passing on the call to the underlying Plyr code

Here's my code:

const audioPlayerInstance = new Plyr(audioElement, {
  ...audioPlayerOptions // out of scope for this example
});

const playerProxyObject = new Proxy(audioPlayerInstance, {
  set(target, property, value) {
    if(property === 'source') {
      // clear down the plyr event listener array except for any registered on body / document
      target.eventListeners = [
        ...target.eventListeners.filter((listener) => listener.element === document || listener.element === document.body)
      ];
    }
    // now call the original plyr.source method
    return Reflect.set(target, property, value);
  }
});

return playerProxyObject;

Hope this helps someone!

Cheers
Chris