Skip to content
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

implement lifecycle hooks and trigger pre/post setup hooks #3639

Merged
merged 10 commits into from
Nov 4, 2016
120 changes: 120 additions & 0 deletions docs/guides/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Hooks
Hooks exist so that users can "hook" on to certain video.js player lifecycle


## Current Hooks
Currently, the following hooks are avialable:

### beforesetup
`beforesetup` is called just before the player is created. This allows:
* modification of the options passed to the video.js function (`videojs('some-id, options)`)
* modification of the dom video element that will be used for the player

`beforesetup` hook functions should:
* take two arguments
1. videoEl: dom video element that video.js is going to use to create a player
2. options: options that video.js was intialized with and will later pass to the player during creation
* return options that will merge and override options that video.js with intialized with

Example: adding beforesetup hook
```js
var beforeSetup = function(videoEl, options) {
// videoEl.id will be some-id here, since that is what video.js
// was created with

videoEl.className += ' some-super-class';

// autoplay will be true here, since we passed in as such
(options.autoplay) {
options.autoplay = false
}

// options that are returned here will be merged with old options
// in this example options will now be
// {autoplay: false, controls: true}
return options;
};

videojs.hook('beforesetup', beforeSetup);
videojs('some-id', {autoplay: true, controls: true});
```

### setup
`setup` is called just after the player is created. This allows:
* plugin or custom functionalify to intialize on the player
* changes to the player object itself

`setup` hook functions:
* Take one argument
* player: the player that video.js created
* Don't have to return anything

Example: adding setup hook
```js
var setup = function(player) {
// initialize the foo plugin
player.foo();
};
var foo = function() {};

videojs.plugin('foo', foo);
videojs.hook('setup', setup);
var player = videojs('some-id', {autoplay: true, controls: true});
```

## Usage

### Adding
In order to use hooks you must first include video.js in the page or script that you are using. Then you add hooks using `videojs.hook(<name>, function)` before running the `videojs()` function.

Example: adding hooks
```js
videojs.hook('beforesetup', function(videoEl, options) {
// videoEl will be the element with id=vid1
// options will contain {autoplay: false}
});
videojs.hook('setup', function(player) {
// player will be the same player that is defined below
// as `var player`
});
var player = videojs('vid1', {autoplay: false});
```

After adding your hooks they will automatically be run at the correct time in the video.js lifecycle.

### Getting
To access the array of hooks that currently exists and will be run on the video.js object you can use the `videojs.hooks` function.

Example: getting all hooks attached to video.js
```js
var beforeSetupHooks = videojs.hooks('beforesetup');
var setupHooks = videojs.hooks('setup');
```

### Removing
To stop hooks from being executed during the video.js lifecycle you will remove them using `videojs.removeHook`.

Example: remove a hook that was defined by you
```js
var beforeSetup = function(videoEl, options) {};

// add the hook
videojs.hook('beforesetup', beforeSetup);

// remove that same hook
videojs.removeHook('beforesetup', beforeSetup);
```

You can also use `videojs.hooks` in conjunction with `videojs.removeHook` but it may have unexpected results if used during an asynchronous callbacks as other plugins/functionality may have added hooks.

Example: using `videojs.hooks` and `videojs.removeHook` to remove a hook
```js
// add the hook
videojs.hook('setup', function(videoEl, options) {});

var setupHooks = videojs.hooks('setup');

// remove the hook you just added
videojs.removeHook('setup', setupHooks[setupHooks.length - 1]);
```

77 changes: 75 additions & 2 deletions src/js/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ if (typeof HTMLVideoElement === 'undefined') {
function videojs(id, options, ready) {
let tag;

options = options || {};

// Allow for element or ID to be passed in
// String ID
if (typeof id === 'string') {
Expand Down Expand Up @@ -96,10 +98,81 @@ function videojs(id, options, ready) {
}

// Element may have a player attr referring to an already created player instance.
// If not, set up a new player and return the instance.
return tag.player || Player.players[tag.playerId] || new Player(tag, options, ready);
// If so return that otherwise set up a new player below
if (tag.player || Player.players[tag.playerId]) {
return tag.player || Player.players[tag.playerId];
}

videojs.hooks('beforesetup').forEach(function(hookFunction) {
const opts = hookFunction(tag, videojs.mergeOptions({}, options));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically, each handler that doesn't return an object is ignored?

Copy link
Contributor Author

@brandonocasey brandonocasey Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well they are still run, we just assume they don't want to change options

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, makes sense. Meant the response is ignored.


if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
videojs.log.error('please return an object in beforesetup hooks');
return;
}

options = videojs.mergeOptions(options, opts);
});

// If not, set up a new player
const player = new Player(tag, options, ready);

videojs.hooks('setup').forEach((hookFunction) => hookFunction(player));

return player;
}

/**
* An Object that contains lifecycle hooks as keys which point to an array
* of functions that are run when a lifecycle is triggered
*/
videojs.hooks_ = {};

/**
* Get a list of hooks for a specific lifecycle
*
* @param {String} type the lifecyle to get hooks from
* @param {Function=} optionally add a hook to the lifecycle that your are getting
* @return {Array} an array of hooks, or an empty array if there are none
*/
videojs.hooks = function(type, fn) {
videojs.hooks_[type] = videojs.hooks_[type] || [];
if (fn) {
videojs.hooks_[type] = videojs.hooks_[type].concat(fn);
}
return videojs.hooks_[type];
};

/**
* Add a function hook to a specific videojs lifecycle
*
* @param {String} type the lifecycle to hook the function to
* @param {Function|Array} fn the function to attach
*/
videojs.hook = function(type, fn) {
videojs.hooks(type, fn);
};

/**
* Remove a hook from a specific videojs lifecycle
*
* @param {String} type the lifecycle that the function hooked to
* @param {Function} fn the hooked function to remove
* @return {Boolean} the function that was removed or undef
*/
videojs.removeHook = function(type, fn) {
const index = videojs.hooks(type).indexOf(fn);

if (index <= -1) {
return false;
}

videojs.hooks_[type] = videojs.hooks_[type].slice();
videojs.hooks_[type].splice(index, 1);

return true;
};

// Add default styles
if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
let style = Dom.$('.vjs-styles-defaults');
Expand Down
181 changes: 181 additions & 0 deletions test/unit/video.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,184 @@ QUnit.test('should expose DOM functions', function(assert) {
`videojs.${vjsName} is a reference to Dom.${domName}`);
});
});

QUnit.module('video.js:hooks ', {
beforeEach() {
videojs.hooks_ = {};
}
});

QUnit.test('should be able to add a hook', function(assert) {
videojs.hook('foo', function() {});
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook type');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('bar', function() {});
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hook types');
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('bar', function() {});
assert.equal(videojs.hooks_.bar.length, 2, 'should have 2 bar hooks');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('foo', function() {});
videojs.hook('foo', function() {});
videojs.hook('foo', function() {});
assert.equal(videojs.hooks_.foo.length, 4, 'should have 4 foo hooks');
assert.equal(videojs.hooks_.bar.length, 2, 'should have 2 bar hooks');
});

QUnit.test('should be able to remove a hook', function(assert) {
const noop = function() {};

videojs.hook('foo', noop);
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('bar', noop);
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');

const fooRetval = videojs.removeHook('foo', noop);

assert.equal(fooRetval, true, 'should return true');
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
assert.equal(videojs.hooks_.bar.length, 1, 'should have 0 bar hook');

const barRetval = videojs.removeHook('bar', noop);

assert.equal(barRetval, true, 'should return true');
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
assert.equal(videojs.hooks_.bar.length, 0, 'should have 0 bar hook');

const errRetval = videojs.removeHook('bar', noop);

assert.equal(errRetval, false, 'should return false');
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
assert.equal(videojs.hooks_.bar.length, 0, 'should have 0 bar hook');
});

QUnit.test('should be able get all hooks for a type', function(assert) {
const noop = function() {};

videojs.hook('foo', noop);
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('bar', noop);
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');

const fooHooks = videojs.hooks('foo');
const barHooks = videojs.hooks('bar');

assert.deepEqual(videojs.hooks_.foo, fooHooks, 'should return the exact foo list from videojs.hooks_');
assert.deepEqual(videojs.hooks_.bar, barHooks, 'should return the exact bar list from videojs.hooks_');
});

QUnit.test('should be get all hooks for a type and add at the same time', function(assert) {
const noop = function() {};

videojs.hook('foo', noop);
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');

videojs.hook('bar', noop);
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');

const fooHooks = videojs.hooks('foo', noop);
const barHooks = videojs.hooks('bar', noop);

assert.deepEqual(videojs.hooks_.foo.length, 2, 'foo should have two noop hooks');
assert.deepEqual(videojs.hooks_.bar.length, 2, 'bar should have two noop hooks');
assert.deepEqual(videojs.hooks_.foo, fooHooks, 'should return the exact foo list from videojs.hooks_');
assert.deepEqual(videojs.hooks_.bar, barHooks, 'should return the exact bar list from videojs.hooks_');
});

QUnit.test('should trigger beforesetup and setup during videojs setup', function(assert) {
const vjsOptions = {techOrder: ['techFaker']};
let setupCalled = false;
let beforeSetupCalled = false;
const beforeSetup = function(video, options) {
beforeSetupCalled = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about asserting that setupCalled is still false in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, also added a check for beforeSetupCalled in the setup function

assert.equal(setupCalled, false, 'setup should be called after beforesetup');
assert.deepEqual(options, vjsOptions, 'options should be the same');
assert.equal(video.id, 'test_vid_id', 'video id should be correct');
};
const setup = function(player) {
setupCalled = true;

assert.equal(beforeSetupCalled, true, 'beforesetup should have been called already');
assert.ok(player, 'created player from tag');
assert.ok(player.id() === 'test_vid_id');
assert.ok(videojs.getPlayers().test_vid_id === player,
'added player to global reference');
};

const fixture = document.getElementById('qunit-fixture');

fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';

const vid = document.getElementById('test_vid_id');

videojs.hook('beforesetup', beforeSetup);
videojs.hook('setup', setup);

const player = videojs(vid, vjsOptions);

assert.ok(player.options_, 'returning null in beforesetup does not lose options');
assert.equal(beforeSetupCalled, true, 'beforeSetup was called');
assert.equal(setupCalled, true, 'setup was called');
});

QUnit.test('beforesetup returns dont break videojs options', function(assert) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a positive test case for this as well?
Something like: beforesetup hooks modify options properly and merge each item in the list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

const vjsOptions = {techOrder: ['techFaker']};
const fixture = document.getElementById('qunit-fixture');

fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';

const vid = document.getElementById('test_vid_id');

videojs.hook('beforesetup', function() {
return null;
});
videojs.hook('beforesetup', function() {
return '';
});
videojs.hook('beforesetup', function() {
return [];
});

const player = videojs(vid, vjsOptions);

assert.ok(player.options_, 'beforesetup should not destory options');
assert.equal(player.options_.techOrder, vjsOptions.techOrder, 'options set by user should exist');
});

QUnit.test('beforesetup options override videojs options', function(assert) {
const vjsOptions = {techOrder: ['techFaker'], autoplay: false};
const fixture = document.getElementById('qunit-fixture');

fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';

const vid = document.getElementById('test_vid_id');

videojs.hook('beforesetup', function(options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add another hook that say adds fluid: true option?

assert.equal(options.autoplay, false, 'false was passed to us');
return {autoplay: true};
});

const player = videojs(vid, vjsOptions);

assert.ok(player.options_, 'beforesetup should not destory options');
assert.equal(player.options_.techOrder, vjsOptions.techOrder, 'options set by user should exist');
assert.equal(player.options_.autoplay, true, 'autoplay should be set to true now');
});