Video.js Blog

Pat O'Neill2017-01-26

Feature Spotlight: Advanced Plugins

Note: Advanced plugins are being introduced in Video.js 6.0 and are only supported from that version forward.

If you've been a Video.js user for a while, you're likely familiar with the concept of plugins: functions that become methods of any player you create. If you're not familiar with Video.js plugins, we have a comprehensive plugins guide available.

These plugins - which we'll call basic plugins - are lightweight and offer complete control of the player. That's really useful and it isn't changing - existing plugins should continue to work!

But what if you want a richer set of features? Or more guidance on how to structure your plugin? Or more tools out of the box that help manage complex plugin-rich players?

Well, until Video.js 6.0, you had to figure things out on your own.

Introducing Advanced Plugins

One of Video.js' strengths is its rich ecosystem of plugins; so, in the last few months, we wanted to focus our efforts on improving the plugin author experience.

While projects like the plugin generator and videojs-spellbook make becoming a plugin author easier than ever, the Video.js team thought it was important to provide a foundational API and set of conventions on which the future of Video.js plugins could be built.

Our solution is advanced plugins.

Advanced Plugins are Component-Like

One of the design goals for advanced plugins was to provide an API that was reminiscent of the existing components system. We achieved this in a number of ways.

At the lowest level, this included a name change for the plugin registration function from videojs.plugin to videojs.registerPlugin (taking a naming cue from videojs.registerComponent and videojs.registerTech).

Beyond a simple registration method name change, advanced plugins are class-based. A trivial example of an advanced plugin might look something like this:

const Plugin = videojs.getPlugin('plugin');

class HelloWorld extends Plugin {
  constructor(player) {
    super(player);
    this.player.addClass('hello-world');
  }
}

videojs.registerPlugin('helloWorld', HelloWorld);

This plugin can be initialized in the same way as a basic plugin - via a player method whose name matches the registered name of the plugin.

In the case of advanced plugins, this method is a factory function, which instantiates the plugin class and returns an instance.

It's useful to know that the player method that is created will always be a function. If a player already has an instance of an advanced plugin, its associated method will simply return the pre-existing instance rather than re-initialize it:

const player = videojs('my-player');
const instance = player.helloWorld();

// Logs: 'true'
videojs.log(instance === player.helloWorld());

The helloWorld method will return this plugin object until it is disposed - after which it will create a new plugin instance again.

Events

Similar to components, advanced plugins can listen to and trigger events via the on, one, off, and trigger methods.

This provides a loosely coupled communication channel for plugins and other objects (components, players, etc) to manage their own state and respond to changes in the state of one another.

Additional Event Data

The Video.js event system allows additional data to be passed to listeners as a second argument when triggering events (the first argument is the event object itself).

Plugin events pass a consistent set of properties in this object (including any custom properties passed to trigger):

  • instance: The plugin instance, which triggered the event.
  • name: The name of the plugin as a string (e.g. 'helloWorld').
  • plugin: The plugin class/constructor function (e.g. HelloWorld).

For example, a listener for an event on a plugin can expect something like this:

const player = videojs('my-player');
const instance = player.helloWorld();

instance.on('some-custom-event', (e, data) => {
  videojs.log(data.instance === instance); // true
  videojs.log(data.name === 'helloWorld'); // true
  videojs.log(data.plugin === videojs.getPlugin('helloWorld')); // true
  videojs.log(data.foo); // "bar"
});

instance.trigger('some-custom-event', {foo: 'bar'});

Lifecycle

Another similarity between plugins and components is the concept of a lifecycle - more specifically, setup and teardown processes.

We get the setup feature as a side effect of normal object creation in JavaScript, but we are left to our own devices when it comes to object destruction and ensuring that references between objects are cleaned up to avoid leaking memory.

Video.js components have long had a dispose method and event that deal with removing a component from the DOM and memory. Advanced plugins have the same feature:

const player = videojs('my-player');

const firstInstance = player.helloWorld();

// Logs: 'true'
videojs.log(firstInstance === player.helloWorld());

firstInstance.on('dispose', () => videojs.log('disposing a helloWorld instance'));

// Logs: 'disposing a helloWorld instance'
firstInstance.dispose();

const secondInstance = player.helloWorld(); 

// Logs: 'false'
videojs.log(firstInstance === secondInstance);

The pluginsetup Event

Plugins do have one lifecycle feature that components do not: the pluginsetup event.

This event is triggered on a player when a plugin is initialized on it:

const player = videojs('my-player');

player.on('pluginsetup', (e, hash) => {
  if (hash.name === 'helloWorld') {
    videojs.log('A helloWorld instance was created!');
  }
});

// Logs: 'A helloWorld instance was created!'
player.helloWorld();

React-inspired Statefulness

One of the exciting additions in Video.js for both advanced plugins and components is React-inspired statefulness. Essentially, this means that all plugin objects and component objects have a state property, which is a plain object that can be used to store variable state for that object. Then, there is a setState method that updates this object and triggers a statechanged event.

This system allows plugins and components to use their evented nature to communicate in-memory state changes through a consistent API:

// A static property of the constructor can be used to pre-populate state 
// for all instances.
HelloWorld.defaultState = {color: 'red'};

const player = videojs('my-player');
const instance = player.helloWorld();

instance.on('statechanged', (e) => {
  const {color} = e.changes;

  if (color) {
    videojs.log(`The helloWorld color changed from "${color.from}" to "${color.to}"!`);
  }
});

// Logs: 'The helloWorld color changed from "red" to "blue"!'
instance.setState({color: 'blue'}); 

Player Plugin Awareness

Finally, we couldn't add new plugin infrastructure without working on one of the more pernicious problems of managing complex combinations of plugins: the player can't report which plugins it has initialized - or not. To this end, the player has two new methods: hasPlugin and usingPlugin. These methods work for both types of plugins.

The hasPlugin Method

This method reports whether a plugin matching a given name is available on the player:

const player = videojs('my-player');

// Logs: 'true'
videojs.log(player.hasPlugin('helloWorld'));

// Logs: 'false'
videojs.log(player.hasPlugin('fooBar'));

This method ignores whether or not the plugin has been initialized and merely reports whether or not it has been registered.

The usingPlugin Method

This method reports not only whether a plugin is available on a player, but whether it is currently active on the player:

const player = videojs('my-player');

// Logs: 'false'
videojs.log(player.usingPlugin('helloWorld'));

player.helloWorld();

// Logs: 'true'
videojs.log(player.usingPlugin('helloWorld'));

One caveat to note here. While this works for both types of plugins, only advanced plugins can change this value more than once. A basic plugin has no built-in lifecycle or events; so, it's not possible to determine whether one has been "disposed".

Go Forth and Code

We hope these additions and improvements to the plugin architecture will make writing Video.js plugins more pleasurable and remove some of the low-level legwork involved in ensuring plugins aren't creating memory leaks and other problems.

The design of advanced plugins is such that we can add features as 6.0 matures and we get more community feedback. As always, we strongly encourage our users to give back to the Video.js project in whatever way they can.

For a more complete discussion of plugins generally, visit the Video.js plugins guide.