1
0
mirror of https://github.com/videojs/video.js.git synced 2025-02-02 11:34:50 +02:00

feat(hooks): Error hooks (#7349)

Adding beforeerror and error hooks that make it easier to know when errors occurred on all players and allows intercepting and modifying errors.
This commit is contained in:
Gary Katsevman 2021-07-28 13:32:38 -04:00 committed by GitHub
parent ad9546cad8
commit 774f9e7f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 288 additions and 94 deletions

View File

@ -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)
* [Adding Once](#adding-once)
* [Example](#example-3)
* [Getting](#getting)
* [Example](#example-4)
* [Removing](#removing)
* [Adding Once](#adding-once)
* [Example](#example-5)
* [Getting](#getting)
* [Example](#example-6)
* [Removing](#removing)
* [Example](#example-7)
## Current Hooks
@ -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

View File

@ -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
@ -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 &&
@ -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_));
return;
}

93
src/js/utils/hooks.js Normal file
View File

@ -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
};

View File

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

View File

@ -1181,6 +1181,105 @@ QUnit.test('player should handle different error types', function(assert) {
player.dispose();
});
QUnit.test('beforeerror hook allows us to modify errors', function(assert) {
const player = TestHelpers.makePlayer({});
const beforeerrorHook = function(p, err) {
assert.equal(player, p, 'the players match');
assert.equal(err.code, 4, 'we got code 4 in beforeerror hook');
return { code: 1 };
};
const errorHook = function(p, err) {
assert.equal(player, p, 'the players match');
assert.equal(err.code, 1, 'we got code 1 in error hook');
};
videojs.hook('beforeerror', beforeerrorHook);
videojs.hook('error', errorHook);
player.error({code: 4});
player.dispose();
videojs.removeHook('beforeerror', beforeerrorHook);
videojs.removeHook('error', errorHook);
});
QUnit.test('beforeerror hook logs a warning if the incorrect type is returned', function(assert) {
const player = TestHelpers.makePlayer({});
const stub = sinon.stub(player.log, 'error');
let errorReturnValue;
const beforeerrorHook = function(p, err) {
return errorReturnValue;
};
videojs.hook('beforeerror', beforeerrorHook);
stub.reset();
errorReturnValue = {code: 4};
player.error({code: 4});
assert.ok(stub.notCalled, '{code: 4} is supported');
stub.reset();
errorReturnValue = 1;
player.error({code: 4});
assert.ok(stub.notCalled, 'number is supported');
stub.reset();
errorReturnValue = null;
player.error({code: 4});
assert.ok(stub.notCalled, 'null is supported');
stub.reset();
errorReturnValue = 'hello';
player.error({code: 4});
assert.ok(stub.notCalled, 'string is supported');
stub.reset();
errorReturnValue = new Error('hello');
player.error({code: 4});
assert.ok(stub.notCalled, 'Error object is supported');
stub.reset();
errorReturnValue = [1, 2, 3];
player.error({code: 4});
assert.ok(stub.called, 'array is not supported');
stub.reset();
errorReturnValue = undefined;
player.error({code: 4});
assert.ok(stub.called, 'undefined is not supported');
stub.reset();
errorReturnValue = true;
player.error({code: 4});
assert.ok(stub.called, 'booleans are not supported');
videojs.removeHook('beforeerror', beforeerrorHook);
player.dispose();
});
QUnit.test('player should trigger error related hooks', function(assert) {
const player = TestHelpers.makePlayer({});
const beforeerrorHook = function(p, err) {
assert.equal(player, p, 'the players match');
assert.equal(err.code, 4, 'we got code 4 in beforeerror hook');
return err;
};
const errorHook = function(p, err) {
assert.equal(player, p, 'the players match');
assert.equal(err.code, 4, 'we got code 4 in error hook');
};
videojs.hook('beforeerror', beforeerrorHook);
videojs.hook('error', errorHook);
player.error({code: 4});
player.dispose();
videojs.removeHook('beforeerror', beforeerrorHook);
videojs.removeHook('error', errorHook);
});
QUnit.test('Data attributes on the video element should persist in the new wrapper element', function(assert) {
const dataId = 123;

View File

@ -4,12 +4,14 @@ import document from 'global/document';
import sinon from 'sinon';
import log from '../../src/js/utils/log.js';
const clearObj = (obj) => Object.keys(obj).forEach((key) => delete obj[key]);
QUnit.module('video.js:hooks ', {
beforeEach() {
videojs.hooks_ = {};
clearObj(videojs.hooks_);
},
afterEach() {
videojs.hooks_ = {};
clearObj(videojs.hooks_);
}
});