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(xhr): add request and response hook API #1393

Merged
merged 13 commits into from
May 3, 2023
103 changes: 91 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
adrums86 marked this conversation as resolved.
Show resolved Hide resolved
- [DRM](#drm)
- [Documentation](#documentation)
- [Options](#options)
Expand Down Expand Up @@ -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
Expand All @@ -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) {
/*
Expand Down
137 changes: 137 additions & 0 deletions src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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');

/**
Expand Down Expand Up @@ -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);
};
}
}

/**
Expand All @@ -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;
Expand Down
25 changes: 25 additions & 0 deletions src/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
};

Expand Down
Loading