Skip to content

Commit 77357b1

Browse files
brandonocaseygkatsev
authored andcommitted
feat: implement player lifecycle hooks and trigger beforesetup/setup hooks (#3639)
Allows you to hook into `beforesetup` and `setup` hooks for all players that are created by videojs.
1 parent 11a096d commit 77357b1

File tree

3 files changed

+376
-2
lines changed

3 files changed

+376
-2
lines changed

docs/guides/hooks.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Hooks
2+
Hooks exist so that users can "hook" on to certain video.js player lifecycle
3+
4+
5+
## Current Hooks
6+
Currently, the following hooks are avialable:
7+
8+
### beforesetup
9+
`beforesetup` is called just before the player is created. This allows:
10+
* modification of the options passed to the video.js function (`videojs('some-id, options)`)
11+
* modification of the dom video element that will be used for the player
12+
13+
`beforesetup` hook functions should:
14+
* take two arguments
15+
1. videoEl: dom video element that video.js is going to use to create a player
16+
2. options: options that video.js was intialized with and will later pass to the player during creation
17+
* return options that will merge and override options that video.js with intialized with
18+
19+
Example: adding beforesetup hook
20+
```js
21+
var beforeSetup = function(videoEl, options) {
22+
// videoEl.id will be some-id here, since that is what video.js
23+
// was created with
24+
25+
videoEl.className += ' some-super-class';
26+
27+
// autoplay will be true here, since we passed in as such
28+
(options.autoplay) {
29+
options.autoplay = false
30+
}
31+
32+
// options that are returned here will be merged with old options
33+
// in this example options will now be
34+
// {autoplay: false, controls: true}
35+
return options;
36+
};
37+
38+
videojs.hook('beforesetup', beforeSetup);
39+
videojs('some-id', {autoplay: true, controls: true});
40+
```
41+
42+
### setup
43+
`setup` is called just after the player is created. This allows:
44+
* plugin or custom functionalify to intialize on the player
45+
* changes to the player object itself
46+
47+
`setup` hook functions:
48+
* Take one argument
49+
* player: the player that video.js created
50+
* Don't have to return anything
51+
52+
Example: adding setup hook
53+
```js
54+
var setup = function(player) {
55+
// initialize the foo plugin
56+
player.foo();
57+
};
58+
var foo = function() {};
59+
60+
videojs.plugin('foo', foo);
61+
videojs.hook('setup', setup);
62+
var player = videojs('some-id', {autoplay: true, controls: true});
63+
```
64+
65+
## Usage
66+
67+
### Adding
68+
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.
69+
70+
Example: adding hooks
71+
```js
72+
videojs.hook('beforesetup', function(videoEl, options) {
73+
// videoEl will be the element with id=vid1
74+
// options will contain {autoplay: false}
75+
});
76+
videojs.hook('setup', function(player) {
77+
// player will be the same player that is defined below
78+
// as `var player`
79+
});
80+
var player = videojs('vid1', {autoplay: false});
81+
```
82+
83+
After adding your hooks they will automatically be run at the correct time in the video.js lifecycle.
84+
85+
### Getting
86+
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.
87+
88+
Example: getting all hooks attached to video.js
89+
```js
90+
var beforeSetupHooks = videojs.hooks('beforesetup');
91+
var setupHooks = videojs.hooks('setup');
92+
```
93+
94+
### Removing
95+
To stop hooks from being executed during the video.js lifecycle you will remove them using `videojs.removeHook`.
96+
97+
Example: remove a hook that was defined by you
98+
```js
99+
var beforeSetup = function(videoEl, options) {};
100+
101+
// add the hook
102+
videojs.hook('beforesetup', beforeSetup);
103+
104+
// remove that same hook
105+
videojs.removeHook('beforesetup', beforeSetup);
106+
```
107+
108+
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.
109+
110+
Example: using `videojs.hooks` and `videojs.removeHook` to remove a hook
111+
```js
112+
// add the hook
113+
videojs.hook('setup', function(videoEl, options) {});
114+
115+
var setupHooks = videojs.hooks('setup');
116+
117+
// remove the hook you just added
118+
videojs.removeHook('setup', setupHooks[setupHooks.length - 1]);
119+
```
120+

src/js/video.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ if (typeof HTMLVideoElement === 'undefined' &&
6060
function videojs(id, options, ready) {
6161
let tag;
6262

63+
options = options || {};
64+
6365
// Allow for element or ID to be passed in
6466
// String ID
6567
if (typeof id === 'string') {
@@ -99,10 +101,81 @@ function videojs(id, options, ready) {
99101
}
100102

101103
// Element may have a player attr referring to an already created player instance.
102-
// If not, set up a new player and return the instance.
103-
return tag.player || Player.players[tag.playerId] || new Player(tag, options, ready);
104+
// If so return that otherwise set up a new player below
105+
if (tag.player || Player.players[tag.playerId]) {
106+
return tag.player || Player.players[tag.playerId];
107+
}
108+
109+
videojs.hooks('beforesetup').forEach(function(hookFunction) {
110+
const opts = hookFunction(tag, videojs.mergeOptions({}, options));
111+
112+
if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
113+
videojs.log.error('please return an object in beforesetup hooks');
114+
return;
115+
}
116+
117+
options = videojs.mergeOptions(options, opts);
118+
});
119+
120+
// If not, set up a new player
121+
const player = new Player(tag, options, ready);
122+
123+
videojs.hooks('setup').forEach((hookFunction) => hookFunction(player));
124+
125+
return player;
104126
}
105127

128+
/**
129+
* An Object that contains lifecycle hooks as keys which point to an array
130+
* of functions that are run when a lifecycle is triggered
131+
*/
132+
videojs.hooks_ = {};
133+
134+
/**
135+
* Get a list of hooks for a specific lifecycle
136+
*
137+
* @param {String} type the lifecyle to get hooks from
138+
* @param {Function=} optionally add a hook to the lifecycle that your are getting
139+
* @return {Array} an array of hooks, or an empty array if there are none
140+
*/
141+
videojs.hooks = function(type, fn) {
142+
videojs.hooks_[type] = videojs.hooks_[type] || [];
143+
if (fn) {
144+
videojs.hooks_[type] = videojs.hooks_[type].concat(fn);
145+
}
146+
return videojs.hooks_[type];
147+
};
148+
149+
/**
150+
* Add a function hook to a specific videojs lifecycle
151+
*
152+
* @param {String} type the lifecycle to hook the function to
153+
* @param {Function|Array} fn the function to attach
154+
*/
155+
videojs.hook = function(type, fn) {
156+
videojs.hooks(type, fn);
157+
};
158+
159+
/**
160+
* Remove a hook from a specific videojs lifecycle
161+
*
162+
* @param {String} type the lifecycle that the function hooked to
163+
* @param {Function} fn the hooked function to remove
164+
* @return {Boolean} the function that was removed or undef
165+
*/
166+
videojs.removeHook = function(type, fn) {
167+
const index = videojs.hooks(type).indexOf(fn);
168+
169+
if (index <= -1) {
170+
return false;
171+
}
172+
173+
videojs.hooks_[type] = videojs.hooks_[type].slice();
174+
videojs.hooks_[type].splice(index, 1);
175+
176+
return true;
177+
};
178+
106179
// Add default styles
107180
if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
108181
let style = Dom.$('.vjs-styles-defaults');

test/unit/video.test.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,184 @@ QUnit.test('should expose DOM functions', function(assert) {
166166
`videojs.${vjsName} is a reference to Dom.${domName}`);
167167
});
168168
});
169+
170+
QUnit.module('video.js:hooks ', {
171+
beforeEach() {
172+
videojs.hooks_ = {};
173+
}
174+
});
175+
176+
QUnit.test('should be able to add a hook', function(assert) {
177+
videojs.hook('foo', function() {});
178+
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook type');
179+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
180+
181+
videojs.hook('bar', function() {});
182+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hook types');
183+
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');
184+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
185+
186+
videojs.hook('bar', function() {});
187+
assert.equal(videojs.hooks_.bar.length, 2, 'should have 2 bar hooks');
188+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
189+
190+
videojs.hook('foo', function() {});
191+
videojs.hook('foo', function() {});
192+
videojs.hook('foo', function() {});
193+
assert.equal(videojs.hooks_.foo.length, 4, 'should have 4 foo hooks');
194+
assert.equal(videojs.hooks_.bar.length, 2, 'should have 2 bar hooks');
195+
});
196+
197+
QUnit.test('should be able to remove a hook', function(assert) {
198+
const noop = function() {};
199+
200+
videojs.hook('foo', noop);
201+
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
202+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
203+
204+
videojs.hook('bar', noop);
205+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
206+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
207+
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');
208+
209+
const fooRetval = videojs.removeHook('foo', noop);
210+
211+
assert.equal(fooRetval, true, 'should return true');
212+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
213+
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
214+
assert.equal(videojs.hooks_.bar.length, 1, 'should have 0 bar hook');
215+
216+
const barRetval = videojs.removeHook('bar', noop);
217+
218+
assert.equal(barRetval, true, 'should return true');
219+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
220+
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
221+
assert.equal(videojs.hooks_.bar.length, 0, 'should have 0 bar hook');
222+
223+
const errRetval = videojs.removeHook('bar', noop);
224+
225+
assert.equal(errRetval, false, 'should return false');
226+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
227+
assert.equal(videojs.hooks_.foo.length, 0, 'should have 0 foo hook');
228+
assert.equal(videojs.hooks_.bar.length, 0, 'should have 0 bar hook');
229+
});
230+
231+
QUnit.test('should be able get all hooks for a type', function(assert) {
232+
const noop = function() {};
233+
234+
videojs.hook('foo', noop);
235+
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
236+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
237+
238+
videojs.hook('bar', noop);
239+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
240+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
241+
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');
242+
243+
const fooHooks = videojs.hooks('foo');
244+
const barHooks = videojs.hooks('bar');
245+
246+
assert.deepEqual(videojs.hooks_.foo, fooHooks, 'should return the exact foo list from videojs.hooks_');
247+
assert.deepEqual(videojs.hooks_.bar, barHooks, 'should return the exact bar list from videojs.hooks_');
248+
});
249+
250+
QUnit.test('should be get all hooks for a type and add at the same time', function(assert) {
251+
const noop = function() {};
252+
253+
videojs.hook('foo', noop);
254+
assert.equal(Object.keys(videojs.hooks_).length, 1, 'should have 1 hook types');
255+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
256+
257+
videojs.hook('bar', noop);
258+
assert.equal(Object.keys(videojs.hooks_).length, 2, 'should have 2 hooks types');
259+
assert.equal(videojs.hooks_.foo.length, 1, 'should have 1 foo hook');
260+
assert.equal(videojs.hooks_.bar.length, 1, 'should have 1 bar hook');
261+
262+
const fooHooks = videojs.hooks('foo', noop);
263+
const barHooks = videojs.hooks('bar', noop);
264+
265+
assert.deepEqual(videojs.hooks_.foo.length, 2, 'foo should have two noop hooks');
266+
assert.deepEqual(videojs.hooks_.bar.length, 2, 'bar should have two noop hooks');
267+
assert.deepEqual(videojs.hooks_.foo, fooHooks, 'should return the exact foo list from videojs.hooks_');
268+
assert.deepEqual(videojs.hooks_.bar, barHooks, 'should return the exact bar list from videojs.hooks_');
269+
});
270+
271+
QUnit.test('should trigger beforesetup and setup during videojs setup', function(assert) {
272+
const vjsOptions = {techOrder: ['techFaker']};
273+
let setupCalled = false;
274+
let beforeSetupCalled = false;
275+
const beforeSetup = function(video, options) {
276+
beforeSetupCalled = true;
277+
assert.equal(setupCalled, false, 'setup should be called after beforesetup');
278+
assert.deepEqual(options, vjsOptions, 'options should be the same');
279+
assert.equal(video.id, 'test_vid_id', 'video id should be correct');
280+
};
281+
const setup = function(player) {
282+
setupCalled = true;
283+
284+
assert.equal(beforeSetupCalled, true, 'beforesetup should have been called already');
285+
assert.ok(player, 'created player from tag');
286+
assert.ok(player.id() === 'test_vid_id');
287+
assert.ok(videojs.getPlayers().test_vid_id === player,
288+
'added player to global reference');
289+
};
290+
291+
const fixture = document.getElementById('qunit-fixture');
292+
293+
fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';
294+
295+
const vid = document.getElementById('test_vid_id');
296+
297+
videojs.hook('beforesetup', beforeSetup);
298+
videojs.hook('setup', setup);
299+
300+
const player = videojs(vid, vjsOptions);
301+
302+
assert.ok(player.options_, 'returning null in beforesetup does not lose options');
303+
assert.equal(beforeSetupCalled, true, 'beforeSetup was called');
304+
assert.equal(setupCalled, true, 'setup was called');
305+
});
306+
307+
QUnit.test('beforesetup returns dont break videojs options', function(assert) {
308+
const vjsOptions = {techOrder: ['techFaker']};
309+
const fixture = document.getElementById('qunit-fixture');
310+
311+
fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';
312+
313+
const vid = document.getElementById('test_vid_id');
314+
315+
videojs.hook('beforesetup', function() {
316+
return null;
317+
});
318+
videojs.hook('beforesetup', function() {
319+
return '';
320+
});
321+
videojs.hook('beforesetup', function() {
322+
return [];
323+
});
324+
325+
const player = videojs(vid, vjsOptions);
326+
327+
assert.ok(player.options_, 'beforesetup should not destory options');
328+
assert.equal(player.options_.techOrder, vjsOptions.techOrder, 'options set by user should exist');
329+
});
330+
331+
QUnit.test('beforesetup options override videojs options', function(assert) {
332+
const vjsOptions = {techOrder: ['techFaker'], autoplay: false};
333+
const fixture = document.getElementById('qunit-fixture');
334+
335+
fixture.innerHTML += '<video id="test_vid_id"><source type="video/mp4"></video>';
336+
337+
const vid = document.getElementById('test_vid_id');
338+
339+
videojs.hook('beforesetup', function(options) {
340+
assert.equal(options.autoplay, false, 'false was passed to us');
341+
return {autoplay: true};
342+
});
343+
344+
const player = videojs(vid, vjsOptions);
345+
346+
assert.ok(player.options_, 'beforesetup should not destory options');
347+
assert.equal(player.options_.techOrder, vjsOptions.techOrder, 'options set by user should exist');
348+
assert.equal(player.options_.autoplay, true, 'autoplay should be set to true now');
349+
});

0 commit comments

Comments
 (0)