-
Notifications
You must be signed in to change notification settings - Fork 7.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Advanced Plugins for 6.0 #3690
Merged
Merged
Advanced Plugins for 6.0 #3690
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
f597301
First run at enhanced plugins
misteroneill e7f0087
Restore videojs.plugin as deprecated
misteroneill c1eb33c
Get closer to the previous API
misteroneill 5adbfc8
Move toward new class-based plugin architecture
misteroneill 77dc7f9
Move state to a mixin
misteroneill 4161b9a
Coordinate and test Plugin static methods
misteroneill 2df2732
Add basic plugin test
misteroneill dbdcf69
Fixes for events on plugins
misteroneill 7763bbd
Decorators -> Mixins
misteroneill a386a6d
Split plugin tests into multiple files
misteroneill 7285d1e
Resolve issues w/ events and add a bunch of class-based plugin tests
misteroneill c13fd7b
Rewrite plugins guide for 6.0
misteroneill 9ee98ba
Rename eventful to evented
misteroneill 7ddb681
Support custom event bus elements
misteroneill 2aceef0
Re-implement component as an evented object
misteroneill 8668f3e
Fix tests
misteroneill 6a5f8cc
Evented progress
misteroneill a6876ca
Evented tests
misteroneill 8e8fd9a
Roll back implementation of evented in components
misteroneill 43086f9
Remove plural static methods
misteroneill 2593edc
Plugin guide tweak
misteroneill 13c9c64
Make sure options.plugins is tested
misteroneill 1c4901f
Fix bad code in guide
misteroneill ce0e332
Documentation fixes
misteroneill eaff7f6
Add handleStateChanged method
misteroneill d0cce8c
Prevent Plugin from being instantiated directly
misteroneill 96a85fc
Documentation updates based on feedback
misteroneill 6a0cdab
Restore IE8 script
misteroneill 929affc
Don't wrap public methods
misteroneill 21d484a
Remove 'exclude' option
misteroneill de217df
DRY-er error testing
misteroneill 4a82b5c
English test names
misteroneill 1c7514c
Address stateful test feedback
misteroneill 19844be
Testing updates based on feedback
misteroneill 90b3907
Updates based on guide feedback
misteroneill 99bcef5
Use on instead of one for dispose listener
misteroneill 6a194fe
Proper capitalization for thrown errors
misteroneill e1ac672
Post-rebase cleanup
misteroneill f1694e2
Move handleStateChanged support to mixin
misteroneill 09a9830
Remove mistakenly-added permute module
misteroneill 414edb2
Replace plugin factory with plugin getter
misteroneill a245f91
Undo method removal. Too much.
misteroneill f411b33
'Class-based' to 'Advanced'
misteroneill 7342033
Lots of feedback changes, mostly docs
misteroneill 2f1f15c
Updates based on feedback
misteroneill 8eb90bf
Updates based on feedback
misteroneill 56de0b5
Documentation cleanup/improvement, fix dispose state issue
misteroneill 1c95599
Rename a test
misteroneill File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,58 +1,322 @@ | ||
# Plugins | ||
# Video.js Plugins | ||
|
||
If you've built something cool with Video.js, you can easily share it with the rest of the world by creating a plugin. Although, you can roll your own, you can also use [generator-videojs-plugin](https://github.com/dmlap/generator-videojs-plugin), a [Yeoman](http://yeoman.io) generator that provides scaffolding for video.js plugins including: | ||
One of the great strengths of Video.js is its ecosystem of plugins that allow authors from all over the world to share their video player customizations. This includes everything from the simplest UI tweaks to new [playback technologies and source handlers](tech.md)! | ||
|
||
* [Grunt](http://gruntjs.com) for build management | ||
* [npm](https://www.npmjs.org) for dependency management | ||
* [QUnit](http://qunitjs.com) for testing | ||
Because we view plugins as such an important part of Video.js, the organization is committed to maintaining a robust set of tools for plugin authorship: | ||
|
||
## Step 1: Write Some Javascript | ||
* [generator-videojs-plugin][generator] | ||
|
||
You may have already done this step. Code up something interesting and then wrap it in a function. At the most basic level, that's all a video.js plugin is. By convention, plugins take a hash of options as their first argument: | ||
A [Yeoman][yeoman] generator for scaffolding a Video.js plugin project. Additionally, it offers a set of [conventions for plugin authorship][standards] that, if followed, make authorship, contribution, and usage consistent and predictable. | ||
|
||
In short, the generator sets up plugin authors to focus on writing their plugin - not messing with tools. | ||
|
||
* [videojs-spellbook][spellbook] | ||
|
||
As of version 3, the plugin generator includes a new dependency: [videojs-spellbook][spellbook]. Spellbook is a kitchen sink plugin development tool: it builds plugins, creates tags, runs a development server, and more. | ||
|
||
The benefit of Spellbook is that you can run the generator _once_ and receive updates and bugfixes in Spellbook without having to run the generator again and deal with Yeoman conflicts and other headaches. | ||
|
||
As long as your plugin project follows the [conventions][standards], Spellbook should work on it! | ||
|
||
## Writing a Basic Plugin | ||
|
||
If you've written a Video.js plugin before, the basic plugin concept should be familiar. It's similar to a jQuery plugin in that the core idea is that you're adding a method to the player. | ||
|
||
### Write a JavaScript Function | ||
|
||
A basic plugin is a plain JavaScript function: | ||
|
||
```js | ||
function examplePlugin(options) { | ||
this.on('play', function(e) { | ||
console.log('playback has started!'); | ||
|
||
if (options.customClass) { | ||
this.addClass(options.customClass); | ||
} | ||
|
||
this.on('playing', function() { | ||
videojs.log('playback began!'); | ||
}); | ||
} | ||
``` | ||
|
||
By convention, plugins are passed an `options` object; however, you can realistically accept whatever arguments you want. This example plugin will add a custom class (whatever is passed in as `options.customClass`) and, whenever playback begins, it will log a message to the browser console. | ||
|
||
> **Note:** The value of `this` in the plugin function is the player instance; so, you have access to [its complete API][api-player]. | ||
|
||
### Register a Basic Plugin | ||
|
||
Now that we have a function that does something with a player, all that's left is to register the plugin with Video.js: | ||
|
||
```js | ||
videojs.registerPlugin('examplePlugin', examplePlugin); | ||
``` | ||
|
||
After that, any player will automatically have an `examplePlugin` method on its prototype! | ||
|
||
> **Note:** The only stipulation with the name of the plugin is that it cannot conflict with any existing plugin or player method. | ||
|
||
## Writing an Advanced Plugin | ||
|
||
Video.js 6 introduces advanced plugins: these are plugins that share a similar API with basic plugins, but are class-based and offer a range of extra features out of the box. | ||
|
||
While reading the following sections, you may want to refer to the [Plugin API docs][api-plugin] for more detail. | ||
|
||
### Write a JavaScript Class/Constructor | ||
|
||
If you're familiar with creating [components](components.md), this process is similar. An advanced plugin starts with a JavaScript class (a.k.a. a constructor function). | ||
|
||
If you're using ES6 already, you can use that syntax with your transpiler/language of choice (Babel, TypeScript, etc): | ||
|
||
```js | ||
const Plugin = videojs.getPlugin('plugin'); | ||
|
||
class ExamplePlugin extends Plugin { | ||
|
||
constructor(player, options) { | ||
super(player, options); | ||
|
||
if (options.customClass) { | ||
player.addClass(options.customClass); | ||
} | ||
|
||
player.on('playing', function() { | ||
videojs.log('playback began!'); | ||
}); | ||
} | ||
} | ||
``` | ||
|
||
Or with ES5: | ||
|
||
```js | ||
var Plugin = videojs.getPlugin('plugin'); | ||
|
||
var ExamplePlugin = videojs.extend(Plugin, { | ||
|
||
constructor: function(player, options) { | ||
Plugin.call(this, player, options); | ||
|
||
if (options.customClass) { | ||
player.addClass(options.customClass); | ||
} | ||
|
||
player.on('playing', function() { | ||
videojs.log('playback began!'); | ||
}); | ||
} | ||
}); | ||
``` | ||
|
||
For now, this example advanced plugin does the exact same thing as the basic plugin described above - not to worry, we will make it more interesting as we continue! | ||
|
||
### Register a Advanced Plugin | ||
|
||
The registration process for advanced plugins is identical to [the process for basic plugins](#register-a-basic-plugin). | ||
|
||
```js | ||
videojs.registerPlugin('examplePlugin', ExamplePlugin); | ||
``` | ||
|
||
> **Note:** Because ES6 classes are syntactic sugar on top of existing constructor function and prototype architecture in JavaScript, in all cases `registerPlugin`'s second argument is a function. | ||
|
||
### Key Differences from Basic Plugins | ||
|
||
Advanced plugins have two key differences from basic plugins that are important to understand before describing their advanced features. | ||
|
||
#### The Value of `this` | ||
|
||
With basic plugins, the value of `this` in the plugin function will be the _player_. | ||
|
||
With advanced plugins, the value of `this` is the _instance of the plugin class_. The player is passed to the plugin constructor as its first argument (and is automatically applied to the plugin instance as the `player` property) and any further arguments are passed after that. | ||
|
||
#### The Player Plugin Name Property | ||
|
||
Both basic plugins and advanced plugins are set up by calling a method on a player with a name matching the plugin (e.g., `player.examplePlugin()`). | ||
|
||
However, with advanced plugins, this method acts like a factory function and it is _replaced_ for the current player by a new function which returns the plugin instance: | ||
|
||
```js | ||
// `examplePlugin` has not been called, so it is a factory function. | ||
player.examplePlugin(); | ||
|
||
// `examplePlugin` is now a function that returns the same instance of | ||
// `ExamplePlugin` that was generated by the previous call. | ||
player.examplePlugin().someMethodName(); | ||
``` | ||
|
||
With basic plugins, the method does not change - it is always the same function. It is up to the authors of basic plugins to deal with multiple calls to their plugin function. | ||
|
||
### Features of Advanced Plugins | ||
|
||
Up to this point, our example advanced plugin is functionally identical to our example basic plugin. However, advanced plugins bring with them a great deal of benefit that is not built into basic plugins. | ||
|
||
#### Events | ||
|
||
Like components, advanced plugins offer an implementation of events. This includes: | ||
|
||
* The ability to listen for events on the plugin instance using `on` or `one`: | ||
|
||
```js | ||
player.examplePlugin().on('example-event', function() { | ||
videojs.log('example plugin received an example-event'); | ||
}); | ||
``` | ||
|
||
* The ability to `trigger` custom events on a plugin instance: | ||
|
||
```js | ||
player.examplePlugin().trigger('example-event'); | ||
``` | ||
|
||
* The ability to stop listening to custom events on a plugin instance using `off`: | ||
|
||
```js | ||
player.examplePlugin().off('example-event'); | ||
``` | ||
|
||
By offering a built-in events system, advanced plugins offer a wider range of options for code structure with a pattern familiar to most web developers. | ||
|
||
#### Statefulness | ||
|
||
A new concept introduced for advanced plugins is _statefulness_. This is similar to React components' `state` property and `setState` method. | ||
|
||
Advanced plugin instances each have a `state` property, which is a plain JavaScript object - it can contain any keys and values the plugin author wants. | ||
|
||
A default `state` can be provided by adding a static property to a plugin constructor: | ||
|
||
```js | ||
ExamplePlugin.defaultState = { | ||
customClass: 'default-custom-class' | ||
}; | ||
``` | ||
|
||
When it's activated, `this` will be the Video.js player your plugin is attached to. You can use anything you'd like in the [Video.js API](./api.md) when you're writing a plugin: change the `src`, mess up the DOM, or listen for and emit your own events. | ||
When the `state` is updated via the `setState` method, the plugin instance fires a `"statechanged"` event, but _only if something changed!_ This event can be used as a signal to update the DOM or perform some other action. The event object passed to listeners for this event includes, an object describing the changes that occurred on the `state` property: | ||
|
||
```js | ||
player.examplePlugin().on('statechanged', function(e) { | ||
if (e.changes && e.changes.customClass) { | ||
this.player | ||
.removeClass(e.changes.customClass.from) | ||
.addClass(e.changes.customClass.to); | ||
} | ||
}); | ||
|
||
player.examplePlugin().setState({customClass: 'another-custom-class'}); | ||
``` | ||
|
||
#### Lifecycle | ||
|
||
Like components, advanced plugins have a lifecycle. They can be created with their factory function and they can be destroyed using their `dispose` method: | ||
|
||
```js | ||
// set up a example plugin instance | ||
player.examplePlugin(); | ||
|
||
// dispose of it anytime thereafter | ||
player.examplePlugin().dispose(); | ||
``` | ||
|
||
The `dispose` method has several effects: | ||
|
||
## Step 2: Registering A Plugin | ||
* Triggers a `"dispose"` event on the plugin instance. | ||
* Cleans up all event listeners on the plugin instance, which helps avoid errors caused by events being triggered after an object is cleaned up. | ||
* Removes plugin state and references to the player to avoid memory leaks. | ||
* Reverts the player's named property (e.g. `player.examplePlugin`) _back_ to the original factory function, so the plugin can be set up again. | ||
|
||
It's time to give the rest of the world the opportunity to be awed by your genius. When your plugin is loaded, it needs to let Video.js know this amazing new functionality is now available: | ||
In addition, if the player is disposed, the disposal of all its advanced plugin instances will be triggered as well. | ||
|
||
### Advanced Example Advanced Plugin | ||
|
||
What follows is a complete ES6 advanced plugin that logs a custom message when the player's state changes between playing and paused. It uses all the described advanced features: | ||
|
||
```js | ||
videojs.plugin('examplePlugin', examplePlugin); | ||
import videojs from 'video.js'; | ||
|
||
const Plugin = videojs.getPlugin('plugin'); | ||
|
||
class Advanced extends Plugin { | ||
|
||
constructor(player, options) { | ||
super(player, options); | ||
|
||
// Whenever the player emits a playing or paused event, we update the | ||
// state if necessary. | ||
this.on(player, ['playing', 'paused'], this.updateState); | ||
this.on('statechanged', this.logState); | ||
} | ||
|
||
dispose() { | ||
super.dispose(); | ||
videojs.log('the advanced plugin is being disposed'); | ||
} | ||
|
||
updateState() { | ||
this.setState({playing: !player.paused()}); | ||
} | ||
|
||
logState(changed) { | ||
videojs.log(`the player is now ${this.state.playing ? 'playing' : 'paused'}`); | ||
} | ||
} | ||
|
||
videojs.registerPlugin('advanced', Advanced); | ||
|
||
const player = videojs('example-player'); | ||
|
||
player.advanced(); | ||
|
||
// This will begin playback, which will trigger a "playing" event, which will | ||
// update the state of the plugin, which will cause a message to be logged. | ||
player.play(); | ||
|
||
// This will pause playback, which will trigger a "paused" event, which will | ||
// update the state of the plugin, which will cause a message to be logged. | ||
player.pause(); | ||
|
||
player.advanced().dispose(); | ||
|
||
// This will begin playback, but the plugin has been disposed, so it will not | ||
// log any messages. | ||
player.play(); | ||
``` | ||
|
||
From this point on, your plugin will be added to the Video.js prototype and will show up as a property on every instance created. Make sure you choose a unique name that doesn't clash with any of the properties already in Video.js. Which leads us to... | ||
This example may be a bit pointless in reality, but it demonstrates the sort of flexibility offered by advanced plugins over basic plugins. | ||
|
||
## Setting up a Plugin | ||
|
||
## Step 3: Using A Plugin | ||
There are two ways to set up (or initialize) a plugin on a player. Both ways work identically for both basic and advanced plugins. | ||
|
||
There are two ways to initialize a plugin. If you're creating your video tag dynamically, you can specify the plugins you'd like to initialize with it and any options you want to pass to them: | ||
The first way is during creation of the player. Using the `plugins` option, a plugin can be automatically set up on a player: | ||
|
||
```js | ||
videojs('vidId', { | ||
videojs('example-player', { | ||
plugins: { | ||
examplePlugin: { | ||
exampleOption: true | ||
customClass: 'example-class' | ||
} | ||
} | ||
}); | ||
``` | ||
|
||
If you've already initialized your video tag, you can activate a plugin at any time by calling its setup function directly: | ||
Otherwise, a plugin can be manually set up: | ||
|
||
```js | ||
var video = videojs('cool-vid'); | ||
video.examplePlugin({ exampleOption: true }); | ||
var player = videojs('example-player'); | ||
player.examplePlugin({customClass: 'example-class'}); | ||
``` | ||
|
||
That's it. Head on over to the [Video.js wiki](https://github.com/videojs/video.js/wiki/Plugins) and add your plugin to the list so everyone else can check it out. | ||
These two methods are functionally identical - use whichever you prefer! | ||
|
||
## References | ||
|
||
## How should I use the Video.js icons in my plugin? | ||
* [Player API][api-player] | ||
* [Plugin API][api-plugin] | ||
* [Plugin Generator][generator] | ||
* [Plugin Conventions][standards] | ||
|
||
If you'd like to use any of the icons available in the [Video.js icon set](http://videojs.github.io/font/), please target them via the CSS class names instead of codepoints. The codepoints _may_ change between versions of the font, so using the class names ensures that your plugin will stay up to date with any font changes. | ||
[api-player]: http://docs.videojs.com/docs/api/player.html | ||
[api-plugin]: http://docs.videojs.com/docs/api/plugin.html | ||
[generator]: https://github.com/videojs/generator-videojs-plugin | ||
[spellbook]: https://github.com/videojs/spellbook | ||
[standards]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/standards.md | ||
[yeoman]: http://yeoman.io |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section and the next one seem to be talking about similar things it seems like they should be merged somehow. Maybe we should not mention the differences between the two plugin types at all and instead just talk about advanced plugins?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think people are familiar with basic plugins and, if they are, will have certain expectations as to how they work. These could be subtle differences, so I wanted to call them out here.