Skip to content

Commit

Permalink
feat(xhr): add request and response hook API (#1393)
Browse files Browse the repository at this point in the history
* feat(xhr): add request and response hook API

* refactor for multiple hooks

* fix comment

* fix spacing

* use common callAllHooks fn

* fix xhr hook tests

* remove delete beforeRequest

* finish tests

* add docs to README

* remove decodeURIComponent from example

* rename callback sets

* move hooks to xhr namespace

* specify synchronous callbacks in docs
  • Loading branch information
adrums86 authored May 3, 2023
1 parent bdd842a commit 2356c34
Showing 5 changed files with 701 additions and 12 deletions.
103 changes: 91 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -30,7 +30,6 @@ Video.js Compatibility: 7.x, 8.x
- [Compatibility](#compatibility)
- [Browsers which support MSE](#browsers-which-support-mse)
- [Native only](#native-only)
- [Flash Support](#flash-support)
- [DRM](#drm)
- [Documentation](#documentation)
- [Options](#options)
@@ -637,12 +636,62 @@ player.tech().vhs.representations().forEach(function(rep) {
#### vhs.xhr
Type: `function`

The xhr function that is used by HLS internally is exposed on the per-
The xhr function that is used by VHS internally is exposed on the per-
player `vhs` object. While it is possible, we do not recommend replacing
the function with your own implementation. Instead, the `xhr` provides
the ability to specify a `beforeRequest` function that will be called
with an object containing the options that will be used to create the
xhr request.
the function with your own implementation. Instead, `xhr` provides
the ability to specify `onRequest` and `onResponse` hooks which take a
callback function as a parameter as well as `offRequest` and `offResponse`
functions which will remove a callback function from the `onRequest` or
`onResponse` set if it exists.

The `onRequest(callback)` function takes a `callback` function that will pass the xhr `request`
Object to that callback. These callbacks are called synchronously, in the order registered
and act as pre-request hooks for modifying the xhr `request` Object prior to making a request.

Example:
```javascript
const playerRequestHook = (request) => {
const requestUrl = new URL(request.uri);
requestUrl.searchParams.set('foo', 'bar');
request.uri = requestUrl.href;
};
player.tech().vhs.xhr.onRequest(playerRequestHook);
```

The `onResponse(callback)` function takes a `callback` function that will pass the xhr
`request`, `error`, and `response` Objects to that callback. These callbacks are called
in the order registered and act as post-request hooks for gathering data from the
xhr `request`, `error` and `response` Objects.

Example:
```javascript
const playerResponseHook = (request, error, response) => {
const bar = response.headers.foo
};
player.tech().vhs.xhr.onResponse(playerResponseHook);
```

The `offRequest` function takes a `callback` function, and will remove that function from
the collection of `onRequest` hooks if it exists.

Example:
```javascript
player.tech().vhs.xhr.offRequest(playerRequestHook);
```

The `offResponse` function takes a `callback` function, and will remove that function from
the collection of `offResponse` hooks if it exists.

Example:
```javascript
player.tech().vhs.xhr.offResponse(playerResponseHook);
```
Additionally a `beforeRequest` function can be defined,
that will be called with an object containing the options that will be used
to create the xhr request.

Note: any registered `onRequest` hooks, are called _after_ the `beforeRequest` function, so xhr
options modified by this function may be further modified by these hooks.

Example:
```javascript
@@ -653,13 +702,43 @@ player.tech().vhs.xhr.beforeRequest = function(options) {
};
```

The global `videojs.Vhs` also exposes an `xhr` property. Specifying a
`beforeRequest` function on that will allow you to intercept the options
for *all* requests in every player on a page. For consistency across
browsers the video source should be set at runtime once the video player
is ready.
The global `videojs.Vhs` also exposes an `xhr` property. Adding
`onRequest`, `onResponse` hooks and/or specifying a `beforeRequest`
function that will allow you to intercept the request Object, response
data and options for *all* requests in every player on a page. For
consistency across browsers the video source should be set at runtime
once the video player is ready.

Example:
```javascript
// Global request callback, will affect every player.
const globalRequestHook = (request) => {
const requestUrl = new URL(request.uri);
requestUrl.searchParams.set('foo', 'bar');
request.uri = requestUrl.href;
};
videojs.Vhs.xhr.onRequest(globalRequestHook);
```

```javascript
// Global response hook callback, will affect every player.
const globalResponseHook = (request, error, response) => {
const bar = response.headers.foo
};

videojs.Vhs.xhr.onResponse(globalResponseHook);
```

```javascript
// Remove a global onRequest callback.
videojs.Vhs.xhr.offRequest(globalRequestHook);
```

```javascript
// Remove a global onResponse callback.
videojs.Vhs.xhr.offResponse(globalResponseHook);
```

Example
```javascript
videojs.Vhs.xhr.beforeRequest = function(options) {
/*
137 changes: 137 additions & 0 deletions src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
@@ -427,6 +427,64 @@ const expandDataUri = (dataUri) => {
return dataUri;
};

/**
* Adds a request hook to an xhr object
*
* @param {Object} xhr object to add the onRequest hook to
* @param {function} callback hook function for an xhr request
*/
const addOnRequestHook = (xhr, callback) => {
if (!xhr._requestCallbackSet) {
xhr._requestCallbackSet = new Set();
}
xhr._requestCallbackSet.add(callback);
};

/**
* Adds a response hook to an xhr object
*
* @param {Object} xhr object to add the onResponse hook to
* @param {function} callback hook function for an xhr response
*/
const addOnResponseHook = (xhr, callback) => {
if (!xhr._responseCallbackSet) {
xhr._responseCallbackSet = new Set();
}
xhr._responseCallbackSet.add(callback);
};

/**
* Removes a request hook on an xhr object, deletes the onRequest set if empty.
*
* @param {Object} xhr object to remove the onRequest hook from
* @param {function} callback hook function to remove
*/
const removeOnRequestHook = (xhr, callback) => {
if (!xhr._requestCallbackSet) {
return;
}
xhr._requestCallbackSet.delete(callback);
if (!xhr._requestCallbackSet.size) {
delete xhr._requestCallbackSet;
}
};

/**
* Removes a response hook on an xhr object, deletes the onResponse set if empty.
*
* @param {Object} xhr object to remove the onResponse hook from
* @param {function} callback hook function to remove
*/
const removeOnResponseHook = (xhr, callback) => {
if (!xhr._responseCallbackSet) {
return;
}
xhr._responseCallbackSet.delete(callback);
if (!xhr._responseCallbackSet.size) {
delete xhr._responseCallbackSet;
}
};

/**
* Whether the browser has built-in HLS support.
*/
@@ -492,6 +550,42 @@ Vhs.isSupported = function() {
'your player\'s techOrder.');
};

/**
* A global function for setting an onRequest hook
*
* @param {function} callback for request modifiction
*/
Vhs.xhr.onRequest = function(callback) {
addOnRequestHook(Vhs.xhr, callback);
};

/**
* A global function for setting an onResponse hook
*
* @param {callback} callback for response data retrieval
*/
Vhs.xhr.onResponse = function(callback) {
addOnResponseHook(Vhs.xhr, callback);
};

/**
* Deletes a global onRequest callback if it exists
*
* @param {function} callback to delete from the global set
*/
Vhs.xhr.offRequest = function(callback) {
removeOnRequestHook(Vhs.xhr, callback);
};

/**
* Deletes a global onResponse callback if it exists
*
* @param {function} callback to delete from the global set
*/
Vhs.xhr.offResponse = function(callback) {
removeOnResponseHook(Vhs.xhr, callback);
};

const Component = videojs.getComponent('Component');

/**
@@ -1197,6 +1291,48 @@ class VhsHandler extends Component {
callback
});
}

/**
* Adds the onRequest, onResponse, offRequest and offResponse functions
* to the VhsHandler xhr Object.
*/
setupXhrHooks_() {
/**
* A player function for setting an onRequest hook
*
* @param {function} callback for request modifiction
*/
this.xhr.onRequest = (callback) => {
addOnRequestHook(this.xhr, callback);
};

/**
* A player function for setting an onResponse hook
*
* @param {callback} callback for response data retrieval
*/
this.xhr.onResponse = (callback) => {
addOnResponseHook(this.xhr, callback);
};

/**
* Deletes a player onRequest callback if it exists
*
* @param {function} callback to delete from the player set
*/
this.xhr.offRequest = (callback) => {
removeOnRequestHook(this.xhr, callback);
};

/**
* Deletes a player onResponse callback if it exists
*
* @param {function} callback to delete from the player set
*/
this.xhr.offResponse = (callback) => {
removeOnResponseHook(this.xhr, callback);
};
}
}

/**
@@ -1219,6 +1355,7 @@ const VhsSourceHandler = {

tech.vhs = new VhsHandler(source, tech, localOptions);
tech.vhs.xhr = xhrFactory();
tech.vhs.setupXhrHooks_();

tech.vhs.src(source.src, source.type);
return tech.vhs;
25 changes: 25 additions & 0 deletions src/xhr.js
Original file line number Diff line number Diff line change
@@ -56,6 +56,23 @@ const callbackWrapper = function(request, error, response, callback) {
callback(error, request);
};

/**
* Iterates over a Set of callback hooks and calls them in order
*
* @param {Set} hooks the hook set to iterate over
* @param {Object} request the xhr request object
* @param {Object} error the xhr error object
* @param {Object} response the xhr response object
*/
const callAllHooks = (hooks, request, error, response) => {
if (!hooks) {
return;
}
hooks.forEach((hookCallback) => {
hookCallback(request, error, response);
});
};

const xhrFactory = function() {
const xhr = function XhrFunction(options, callback) {
// Add a default timeout
@@ -66,6 +83,9 @@ const xhrFactory = function() {
// Allow an optional user-specified function to modify the option
// object before we construct the xhr request
const beforeRequest = XhrFunction.beforeRequest || videojs.Vhs.xhr.beforeRequest;
// onRequest and onResponse hooks as a Set, at either the player or global level.
const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs.Vhs.xhr._requestCallbackSet;
const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs.Vhs.xhr._responseCallbackSet;

if (beforeRequest && typeof beforeRequest === 'function') {
const newOptions = beforeRequest(options);
@@ -80,6 +100,8 @@ const xhrFactory = function() {
const xhrMethod = videojs.Vhs.xhr.original === true ? videojsXHR : videojs.Vhs.xhr;

const request = xhrMethod(options, function(error, response) {
// call all registered onResponse hooks
callAllHooks(_responseCallbackSet, request, error, response);
return callbackWrapper(request, error, response, callback);
});
const originalAbort = request.abort;
@@ -90,6 +112,9 @@ const xhrFactory = function() {
};
request.uri = options.uri;
request.requestTime = Date.now();
// call all registered onRequest hooks
callAllHooks(_requestCallbackSet, request);

return request;
};

Loading

0 comments on commit 2356c34

Please sign in to comment.