diff --git a/README.md b/README.md index 3982adaec..3f260a904 100644 --- a/README.md +++ b/README.md @@ -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) { /* diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index e7833d285..fc0e60993 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -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; diff --git a/src/xhr.js b/src/xhr.js index a320ff6e7..4cb45fde6 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -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; }; diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index 13cf4e8a5..784df3b39 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -4036,6 +4036,351 @@ QUnit.test('Allows overriding the global beforeRequest function', function(asser delete videojs.Vhs.xhr.beforeRequest; }); +QUnit.test('Allows setting onRequest hooks globally', function(assert) { + let onRequestHookCallCount = 0; + let actualRequestUrl; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalRequestHook1 = (request) => { + const requestUrl = new URL(request.url); + + requestUrl.searchParams.set('foo', 'bar'); + request.url = decodeURIComponent(requestUrl.href); + actualRequestUrl = request.url; + onRequestHookCallCount++; + }; + const globalRequestHook2 = () => { + onRequestHookCallCount++; + }; + + videojs.Vhs.xhr.onRequest(globalRequestHook1); + videojs.Vhs.xhr.onRequest(globalRequestHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + + assert.equal(onRequestHookCallCount, 2, '2 onRequest hooks called'); + assert.equal(actualRequestUrl, 'http://localhost:9999/test/media2.m3u8?foo=bar', 'request url modified by onRequest hook'); + // remove global hooks for other tests + videojs.Vhs.xhr.offRequest(globalRequestHook1); + videojs.Vhs.xhr.offRequest(globalRequestHook2); +}); + +QUnit.test('Allows setting onRequest hooks on the player', function(assert) { + let onRequestHookCallCount = 0; + let actualRequestUrl; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const playerRequestHook1 = (request) => { + const requestUrl = new URL(request.url); + + requestUrl.searchParams.set('foo', 'bar'); + request.url = decodeURIComponent(requestUrl.href); + actualRequestUrl = request.url; + onRequestHookCallCount++; + }; + const playerRequestHook2 = () => { + onRequestHookCallCount++; + }; + + // Setup player level xhr hooks. + this.player.tech_.vhs.setupXhrHooks_(); + + this.player.tech_.vhs.xhr.onRequest(playerRequestHook1); + this.player.tech_.vhs.xhr.onRequest(playerRequestHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + + assert.equal(onRequestHookCallCount, 2, '2 onRequest hooks called'); + assert.equal(actualRequestUrl, 'http://localhost:9999/test/media2.m3u8?foo=bar', 'request url modified by onRequest hook'); + // remove player hooks for other tests + this.player.tech_.vhs.xhr.offRequest(playerRequestHook1); + this.player.tech_.vhs.xhr.offRequest(playerRequestHook2); +}); + +QUnit.test('Allows setting onRequest hooks globally and overriding with player hooks', function(assert) { + let onRequestHookCallCountGlobal = 0; + let onRequestHookCallCountPlayer = 0; + let actualRequestUrlGlobal; + let actualRequestUrlPlayer; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalRequestHook1 = (request) => { + const requestUrl = new URL(request.url); + + requestUrl.searchParams.set('foo', 'bar'); + request.url = decodeURIComponent(requestUrl.href); + actualRequestUrlGlobal = request.url; + onRequestHookCallCountGlobal++; + }; + const globalRequestHook2 = () => { + onRequestHookCallCountGlobal++; + }; + + videojs.Vhs.xhr.onRequest(globalRequestHook1); + videojs.Vhs.xhr.onRequest(globalRequestHook2); + + const playerRequestHook1 = (request) => { + const requestUrl = new URL(request.url); + + requestUrl.searchParams.set('bar', 'foo'); + request.url = decodeURIComponent(requestUrl.href); + actualRequestUrlPlayer = request.url; + onRequestHookCallCountPlayer++; + }; + const playerRequestHook2 = () => { + onRequestHookCallCountPlayer++; + }; + + // Setup player level xhr hooks. + this.player.tech_.vhs.setupXhrHooks_(); + + this.player.tech_.vhs.xhr.onRequest(playerRequestHook1); + this.player.tech_.vhs.xhr.onRequest(playerRequestHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + // remove player request hooks + this.player.tech_.vhs.xhr.offRequest(playerRequestHook1); + this.player.tech_.vhs.xhr.offRequest(playerRequestHook2); + + assert.equal(onRequestHookCallCountGlobal, 0, 'no onRequest global hooks called'); + assert.equal(actualRequestUrlGlobal, undefined, 'global request url undefined'); + assert.equal(onRequestHookCallCountPlayer, 2, '2 onRequest player hooks called'); + assert.equal(actualRequestUrlPlayer, 'http://localhost:9999/test/media2.m3u8?bar=foo', 'request url modified by player onRequest hook'); + + // media + this.standardXHRResponse(this.requests.shift()); + + assert.equal(onRequestHookCallCountGlobal, 2, '2 onRequest global hooks called'); + assert.equal(actualRequestUrlGlobal, 'http://localhost:9999/test/media2-00001.ts?foo=bar', 'request url modified by global onRequest hook'); + + videojs.Vhs.xhr.offRequest(globalRequestHook1); + videojs.Vhs.xhr.offRequest(globalRequestHook2); +}); + +QUnit.test('Allows removing onRequest hooks globally with offRequest', function(assert) { + let onRequestHookCallCount = 0; + let actualRequestUrl; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalRequestHook1 = (request) => { + const requestUrl = new URL(request.url); + + requestUrl.searchParams.set('foo', 'bar'); + request.url = decodeURIComponent(requestUrl.href); + actualRequestUrl = request.url; + onRequestHookCallCount++; + }; + const globalRequestHook2 = () => { + onRequestHookCallCount++; + }; + + videojs.Vhs.xhr.onRequest(globalRequestHook1); + videojs.Vhs.xhr.onRequest(globalRequestHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + + assert.equal(onRequestHookCallCount, 2, '2 onRequest hooks called'); + assert.equal(actualRequestUrl, 'http://localhost:9999/test/media2.m3u8?foo=bar', 'request url modified by onRequest hook'); + + videojs.Vhs.xhr.offRequest(globalRequestHook1); + + // media + this.standardXHRResponse(this.requests.shift()); + + assert.equal(onRequestHookCallCount, 3, '3 onRequest hooks called'); +}); + +QUnit.test('Allows setting onResponse hooks globally', function(assert) { + const done = assert.async(); + let onResponseHookCallCount = 0; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalResponseHook1 = (request, error, response) => { + onResponseHookCallCount++; + }; + const globalResponseHook2 = (request, error, response) => { + assert.equal(onResponseHookCallCount, 1, '1 onResponse hook called'); + assert.equal(response.url, 'http://localhost:9999/test/media2.m3u8', 'got expected response url'); + done(); + }; + + videojs.Vhs.xhr.onResponse(globalResponseHook1); + videojs.Vhs.xhr.onResponse(globalResponseHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + videojs.Vhs.xhr.offResponse(globalResponseHook1); + videojs.Vhs.xhr.offResponse(globalResponseHook2); +}); + +QUnit.test('Allows setting onResponse hooks on the player', function(assert) { + const done = assert.async(); + let onResponseHookCallCount = 0; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalResponseHook1 = (request, error, response) => { + onResponseHookCallCount++; + }; + const globalResponseHook2 = (request, error, response) => { + assert.equal(onResponseHookCallCount, 1, '1 onResponse hook called'); + assert.equal(response.url, 'http://localhost:9999/test/media2.m3u8', 'got expected response url'); + done(); + }; + + // Setup player level xhr hooks. + this.player.tech_.vhs.setupXhrHooks_(); + + this.player.tech_.vhs.xhr.onResponse(globalResponseHook1); + this.player.tech_.vhs.xhr.onResponse(globalResponseHook2); + + // main + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + this.player.tech_.vhs.xhr.offResponse(globalResponseHook1); + this.player.tech_.vhs.xhr.offResponse(globalResponseHook2); +}); + +QUnit.test('Allows setting onResponse hooks globally and overriding with player hooks', function(assert) { + const done = assert.async(); + let onResponseHookCallCountGlobal = 0; + let onResponseHookCallCountplayer = 0; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalResponseHook1 = (request, error, response) => { + onResponseHookCallCountGlobal++; + }; + + const globalResponseHook2 = (request, error, response) => { + assert.equal(onResponseHookCallCountGlobal, 1, 'no global onResponse hook called'); + assert.equal(response.url, 'http://localhost:9999/test/media2-00001.ts', 'got expected response url'); + done(); + }; + + const playerResponseHook1 = (request, error, response) => { + onResponseHookCallCountplayer++; + }; + const playerResponseHook2 = (request, error, response) => { + assert.equal(onResponseHookCallCountGlobal, 0, 'no global onResponse hook called'); + assert.equal(onResponseHookCallCountplayer, 1, '1 player onResponse hook called'); + assert.equal(response.url, 'http://localhost:9999/test/media2.m3u8', 'got expected response url'); + }; + + // Setup player level xhr hooks. + this.player.tech_.vhs.setupXhrHooks_(); + + videojs.Vhs.xhr.onResponse(globalResponseHook1); + videojs.Vhs.xhr.onResponse(globalResponseHook2); + this.player.tech_.vhs.xhr.onResponse(playerResponseHook1); + this.player.tech_.vhs.xhr.onResponse(playerResponseHook2); + // main + this.standardXHRResponse(this.requests.shift()); + + this.player.tech_.vhs.xhr.offResponse(playerResponseHook1); + this.player.tech_.vhs.xhr.offResponse(playerResponseHook2); + + // media + this.standardXHRResponse(this.requests.shift()); + + // ts + this.standardXHRResponse(this.requests.shift(), muxedSegment()); + + videojs.Vhs.xhr.offResponse(globalResponseHook1); + videojs.Vhs.xhr.offResponse(globalResponseHook2); +}); + +QUnit.test('Allows removing onResponse hooks globally with offResponse', function(assert) { + const done = assert.async(); + let onResponseHookCallCount = 0; + + this.player.src({ + src: 'main.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + const globalResponseHook1 = (request, error, response) => { + onResponseHookCallCount++; + }; + const globalResponseHook2 = (request, error, response) => { + assert.equal(onResponseHookCallCount, 0, '0 onResponse hooks called'); + assert.equal(response.url, 'http://localhost:9999/test/media2.m3u8', 'got expected response url'); + done(); + }; + + videojs.Vhs.xhr.onResponse(globalResponseHook1); + videojs.Vhs.xhr.onResponse(globalResponseHook2); + + // remove hook1 + videojs.Vhs.xhr.offResponse(globalResponseHook1); + + // main + this.standardXHRResponse(this.requests.shift()); + // media + this.standardXHRResponse(this.requests.shift()); + + videojs.Vhs.xhr.offResponse(globalResponseHook2); +}); + QUnit.test( 'passes useCueTags vhs option to main playlist controller', function(assert) { diff --git a/test/xhr.test.js b/test/xhr.test.js index 0c54c048c..7ab09f271 100644 --- a/test/xhr.test.js +++ b/test/xhr.test.js @@ -48,6 +48,109 @@ QUnit.test('xhr respects beforeRequest', function(assert) { assert.equal(this.requests.shift().url, 'global', 'url changed with global override'); }); +QUnit.test('calls global and player onRequest hooks respectively', function(assert) { + const defaultOptions = { + url: 'default' + }; + + this.xhr(defaultOptions); + let xhrRequest = this.requests.shift(); + + // create the global onRequest set and 2 hooks + videojs.Vhs.xhr._requestCallbackSet = new Set(); + const globalRequestHook1 = (request) => { + request.url = 'global'; + }; + const globalRequestHook2 = (request) => { + request.headers = { + foo: 'bar' + }; + }; + + // add them to the set + videojs.Vhs.xhr._requestCallbackSet.add(globalRequestHook1); + videojs.Vhs.xhr._requestCallbackSet.add(globalRequestHook2); + + this.xhr(defaultOptions); + xhrRequest = this.requests.shift(); + + assert.equal(xhrRequest.url, 'global', 'url changed with global onRequest hooks'); + assert.equal(xhrRequest.headers.foo, 'bar', 'headers changed with global onRequest hooks'); + + // create the player onRequest set and 2 hooks + this.xhr._requestCallbackSet = new Set(); + const playerRequestHook1 = (request) => { + request.url = 'player'; + }; + const playerRequestHook2 = (request) => { + request.headers = { + bar: 'foo' + }; + }; + + // add them to the set + this.xhr._requestCallbackSet.add(playerRequestHook1); + this.xhr._requestCallbackSet.add(playerRequestHook2); + + this.xhr(defaultOptions); + xhrRequest = this.requests.shift(); + + // player level request hooks override global + assert.equal(xhrRequest.url, 'player', 'url changed with player onRequest hooks'); + assert.equal(xhrRequest.headers.bar, 'foo', 'headers changed with player onRequest hooks'); + + // delete player level request hooks and check to ensure global are still used + delete this.xhr._requestCallbackSet; + this.xhr(defaultOptions); + xhrRequest = this.requests.shift(); + assert.equal(xhrRequest.url, 'global', 'url changed with player onRequest hooks'); + assert.equal(xhrRequest.headers.foo, 'bar', 'headers changed with player onRequest hooks'); + + delete videojs.Vhs.xhr._requestCallbackSet; + this.xhr(defaultOptions); + xhrRequest = this.requests.shift(); + assert.notEqual(xhrRequest.headers.foo, 'bar', 'headers the same without onRequest hooks'); +}); + +QUnit.test('xhr calls global and player onResponse hooks respectively', function(assert) { + const done = assert.async(); + const defaultOptions = { + url: 'default' + }; + let globalHookCallCount = 0; + + // Create global onResponse set and 2 hooks + videojs.Vhs.xhr._responseCallbackSet = new Set(); + const globalOnResponseHook1 = (request, error, response) => { + globalHookCallCount++; + }; + const globalOnResponseHook2 = (request, error, response) => { + globalHookCallCount++; + }; + + videojs.Vhs.xhr._responseCallbackSet.add(globalOnResponseHook1); + videojs.Vhs.xhr._responseCallbackSet.add(globalOnResponseHook2); + + // Create player onResponse set and 2 hooks + this.xhr._responseCallbackSet = new Set(); + const playerOnResponseHook1 = (request, error, response) => { + assert.equal(response.body, 'foo-bar', 'expected response body'); + assert.equal(response.method, 'GET', 'expected method'); + }; + const playerOnResponseHook2 = (request, error, response) => { + assert.equal(response.headers.foo, 'bar', 'expected headers'); + assert.equal(response.statusCode, 200, 'expected statusCode'); + assert.equal(globalHookCallCount, 0, 'global response hooks not called yet'); + done(); + }; + + this.xhr._responseCallbackSet.add(playerOnResponseHook1); + this.xhr._responseCallbackSet.add(playerOnResponseHook2); + + this.xhr(defaultOptions, () => { }); + this.requests.shift().respond(200, { foo: 'bar' }, 'foo-bar'); +}); + QUnit.test('byterangeStr works as expected', function(assert) { assert.equal(byterangeStr({offset: 20, length: 15}), 'bytes=20-34', 'as expected'); assert.equal(byterangeStr({offset: 0, length: 40}), 'bytes=0-39', 'as expected');