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:
parent
ad9546cad8
commit
774f9e7f45
@ -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
|
||||
|
@ -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
93
src/js/utils/hooks.js
Normal 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
|
||||
};
|
101
src/js/video.js
101
src/js/video.js
@ -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()) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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_);
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user