Skip to content

Commit

Permalink
feat: implement player lifecycle hooks and trigger beforesetup/setup …
Browse files Browse the repository at this point in the history
…hooks (#3639)

Allows you to hook into `beforesetup` and `setup` hooks for all players that are created by videojs.
  • Loading branch information
brandonocasey authored and gkatsev committed Nov 4, 2016
1 parent 11a096d commit 77357b1
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 2 deletions.
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 @@ -60,6 +60,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 @@ -99,10 +101,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));

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;
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) {
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) {
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');
});

0 comments on commit 77357b1

Please sign in to comment.