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

feat(hooks): Error hooks #7349

Merged
merged 6 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions docs/guides/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ Hooks exist so that users can globally hook into certain Video.js lifecycle mome
* [Example](#example)
* [setup](#setup)
* [Example](#example-1)
* [beforeerror](#beforeerror)
* [Example](#example-2)
* [error](#error)
* [Example](#example-3)
* [Usage](#usage)
* [Adding](#adding)
* [Example](#example-2)
* [Example](#example-4)
* [Adding Once](#adding-once)
* [Example](#example-3)
* [Example](#example-5)
* [Getting](#getting)
* [Example](#example-4)
* [Example](#example-6)
* [Removing](#removing)
* [Example](#example-5)
* [Example](#example-7)

## Current Hooks

Expand Down Expand Up @@ -99,6 +103,53 @@ videojs.hook('setup', function(player) {
videojs('some-id', {autoplay: true, controls: true});
```

### beforeerror

`beforeerror` occurs just as we get an error on the player. This allows plugins or other custom code to intercept the error and modify it to be something else.
`error` can be [one of multiple things](https://docs.videojs.com/mediaerror#MediaError), most commonly an object with a `code` property or `null` which means that the current error should be cleared.

`beforeerror` hook functions:

* Take two arguments:
1. The `player` that the error is happening on.
1. The `error` object that was passed in.
* Return an error object that should replace the error

#### Example

```js
videojs.hook('beforeerror', function(player, err) {
const error = player.error();

// prevent current error from being cleared out
if (err === null) {
return error;
}

// but allow changing to a new error
return err;
});
```

### error

`error` occurs after the player has errored out, after `beforeerror` has allowed updating the error, and after an `error` event has been triggered on the player in question. It is purely an informative event which allows you to get all errors from all players.

`error` hook functions:

* Take two arguments:
1. `player`: the player that the error occurred on
1. `error`: the Error object that was resolved with the `beforeerror` hooks
* Don't have to return anything

#### Example

```js
videojs.hook('error', function(player, err) {
console.log(`player ${player.id()} has errored out with code ${err.code} ${err.message}`);
});
```

## Usage

### Adding
Expand Down
22 changes: 22 additions & 0 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import * as middleware from './tech/middleware.js';
import {ALL as TRACK_TYPES} from './tracks/track-types';
import filterSource from './utils/filter-source';
import {getMimetype, findMimetype} from './utils/mimetypes';
import {hooks} from './utils/hooks';
import {isObject} from './utils/obj';
import keycode from 'keycode';

// The following imports are used only to ensure that the corresponding modules
Expand Down Expand Up @@ -3949,6 +3951,23 @@ class Player extends Component {
return this.error_ || null;
}

// allow hooks to modify error object
hooks('beforeerror').forEach((hookFunction) => {
const newErr = hookFunction(this, err);

if (!(
(isObject(newErr) && !Array.isArray(newErr)) ||
typeof newErr === 'string' ||
typeof newErr === 'number' ||
newErr === null
)) {
this.log.error('please return a value that MediaError expects in beforeerror hooks');
return;
}

err = newErr;
});

// Suppress the first error message for no compatible source until
// user interaction
if (this.options_.suppressNotSupportedError &&
Expand Down Expand Up @@ -3991,6 +4010,9 @@ class Player extends Component {
*/
this.trigger('error');

// notify hooks of the per player error
hooks('error').forEach((hookFunction) => hookFunction(this, this.error_));
Copy link
Contributor

Choose a reason for hiding this comment

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

we should use the err here so that a hook function does not modify player.error_ and cause weird things to happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

player.error() returns player.error_ directly, so, I don't see an issue with returning it here too.


return;
}

Expand Down
93 changes: 93 additions & 0 deletions src/js/utils/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* An Object that contains lifecycle hooks as keys which point to an array
* of functions that are run when a lifecycle is triggered
*
* @private
*/
const hooks_ = {};

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

/**
* Add a function hook to a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle to hook the function to.
*
* @param {Function|Function[]}
* The function or array of functions to attach.
*/
const hook = function(type, fn) {
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
*/
const removeHook = function(type, fn) {
const index = hooks(type).indexOf(fn);

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

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

return true;
};

/**
* Add a function hook that will only run once to a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle to hook the function to.
*
* @param {Function|Function[]}
* The function or array of functions to attach.
*/
const hookOnce = function(type, fn) {
hooks(type, [].concat(fn).map(original => {
const wrapper = (...args) => {
removeHook(type, wrapper);
return original(...args);
};

return wrapper;
}));
};

export {
hooks_,
hooks,
hook,
hookOnce,
removeHook
};
101 changes: 14 additions & 87 deletions src/js/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
*/
import {version} from '../../package.json';
import window from 'global/window';
import {
hooks_,
hooks,
hook,
hookOnce,
removeHook
} from './utils/hooks';
import * as setup from './setup';
import * as stylesheet from './utils/stylesheet.js';
import Component from './component';
Expand Down Expand Up @@ -155,7 +162,7 @@ function videojs(id, options, ready) {

options = options || {};

videojs.hooks('beforesetup').forEach((hookFunction) => {
hooks('beforesetup').forEach((hookFunction) => {
const opts = hookFunction(el, mergeOptions(options));

if (!isObject(opts) || Array.isArray(opts)) {
Expand All @@ -172,96 +179,16 @@ function videojs(id, options, ready) {

player = new PlayerComponent(el, options, ready);

videojs.hooks('setup').forEach((hookFunction) => hookFunction(player));
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
*
* @private
*/
videojs.hooks_ = {};

/**
* Get a list of hooks for a specific lifecycle
*
* @param {string} type
* the lifecyle to get hooks from
*
* @param {Function|Function[]} [fn]
* Optionally add a hook (or hooks) 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|Function[]}
* The function or array of functions to attach.
*/
videojs.hook = function(type, fn) {
videojs.hooks(type, fn);
};

/**
* Add a function hook that will only run once to a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle to hook the function to.
*
* @param {Function|Function[]}
* The function or array of functions to attach.
*/
videojs.hookOnce = function(type, fn) {
videojs.hooks(type, [].concat(fn).map(original => {
const wrapper = (...args) => {
videojs.removeHook(type, wrapper);
return original(...args);
};

return wrapper;
}));
};

/**
* 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;
};
videojs.hooks_ = hooks_;
videojs.hooks = hooks;
videojs.hook = hook;
videojs.hookOnce = hookOnce;
videojs.removeHook = removeHook;

// Add default styles
if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && Dom.isReal()) {
Expand Down
Loading