72adc094 by Jon-Carlos Rivera

Merge pull request #633 from videojs/xhr-beforerequest

Expose the ability to set an `beforeRequest` function on xhr
2 parents 6fe9fa2f 9dde5c62
...@@ -19,6 +19,7 @@ Play back HLS with video.js, even where it's not natively supported. ...@@ -19,6 +19,7 @@ Play back HLS with video.js, even where it's not natively supported.
19 - [hls.bandwidth](#hlsbandwidth) 19 - [hls.bandwidth](#hlsbandwidth)
20 - [hls.bytesReceived](#hlsbytesreceived) 20 - [hls.bytesReceived](#hlsbytesreceived)
21 - [hls.selectPlaylist](#hlsselectplaylist) 21 - [hls.selectPlaylist](#hlsselectplaylist)
22 - [hls.xhr](#hlsxhr)
22 - [Events](#events) 23 - [Events](#events)
23 - [loadedmetadata](#loadedmetadata) 24 - [loadedmetadata](#loadedmetadata)
24 - [loadedplaylist](#loadedplaylist) 25 - [loadedplaylist](#loadedplaylist)
...@@ -194,6 +195,44 @@ segment is downloaded. You can override this function to provide your ...@@ -194,6 +195,44 @@ segment is downloaded. You can override this function to provide your
194 adaptive streaming logic. You must, however, be sure to return a valid 195 adaptive streaming logic. You must, however, be sure to return a valid
195 media playlist object that is present in `player.hls.master`. 196 media playlist object that is present in `player.hls.master`.
196 197
198 #### hls.xhr
199 Type: `function`
200
201 The xhr function that is used by HLS internally is exposed on the per-
202 player `hls` object. While it is possible, we do not recommend replacing
203 the function with your own implementation. Instead, the `xhr` provides
204 the ability to specify a `beforeRequest` function that will be called
205 with an object containing the options that will be used to create the
206 xhr request.
207
208 Example:
209 ```javascript
210 player.hls.xhr.beforeRequest = function(options) {
211 options.uri = options.uri.replace('example.com', 'foo.com');
212
213 return options;
214 };
215 ```
216
217 The global `videojs.Hls` also exposes an `xhr` property. Specifying a
218 `beforeRequest` function on that will allow you to intercept the options
219 for *all* requests in every player on a page.
220
221 Example
222 ```javascript
223 videojs.Hls.xhr.beforeRequest = function(options) {
224 /*
225 * Modifications to requests that will affect every player.
226 */
227
228 return options;
229 };
230 ```
231
232 For information on the type of options that you can modify see the
233 documentation at [https://github.com/Raynos/xhr](https://github.com/Raynos/xhr).
234
235
197 ### Events 236 ### Events
198 Standard HTML video events are handled by video.js automatically and 237 Standard HTML video events are handled by video.js automatically and
199 are triggered on the player object. In addition, there are a couple 238 are triggered on the player object. In addition, there are a couple
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
6 * 6 *
7 */ 7 */
8 import resolveUrl from './resolve-url'; 8 import resolveUrl from './resolve-url';
9 import XhrModule from './xhr';
10 import {mergeOptions} from 'video.js'; 9 import {mergeOptions} from 'video.js';
11 import Stream from './stream'; 10 import Stream from './stream';
12 import m3u8 from './m3u8'; 11 import m3u8 from './m3u8';
...@@ -86,7 +85,7 @@ const updateSegments = function(original, update, offset) { ...@@ -86,7 +85,7 @@ const updateSegments = function(original, update, offset) {
86 }; 85 };
87 86
88 export default class PlaylistLoader extends Stream { 87 export default class PlaylistLoader extends Stream {
89 constructor(srcUrl, withCredentials) { 88 constructor(srcUrl, hls, withCredentials) {
90 super(); 89 super();
91 let loader = this; 90 let loader = this;
92 let dispose; 91 let dispose;
...@@ -95,6 +94,8 @@ export default class PlaylistLoader extends Stream { ...@@ -95,6 +94,8 @@ export default class PlaylistLoader extends Stream {
95 let playlistRequestError; 94 let playlistRequestError;
96 let haveMetadata; 95 let haveMetadata;
97 96
97 this.hls_ = hls;
98
98 // a flag that disables "expired time"-tracking this setting has 99 // a flag that disables "expired time"-tracking this setting has
99 // no effect when not playing a live stream 100 // no effect when not playing a live stream
100 this.trackExpiredTime_ = false; 101 this.trackExpiredTime_ = false;
...@@ -261,7 +262,7 @@ export default class PlaylistLoader extends Stream { ...@@ -261,7 +262,7 @@ export default class PlaylistLoader extends Stream {
261 } 262 }
262 263
263 // request the new playlist 264 // request the new playlist
264 request = XhrModule({ 265 request = this.hls_.xhr({
265 uri: resolveUrl(loader.master.uri, playlist.uri), 266 uri: resolveUrl(loader.master.uri, playlist.uri),
266 withCredentials 267 withCredentials
267 }, function(error, request) { 268 }, function(error, request) {
...@@ -298,7 +299,7 @@ export default class PlaylistLoader extends Stream { ...@@ -298,7 +299,7 @@ export default class PlaylistLoader extends Stream {
298 } 299 }
299 300
300 loader.state = 'HAVE_CURRENT_METADATA'; 301 loader.state = 'HAVE_CURRENT_METADATA';
301 request = XhrModule({ 302 request = this.hls_.xhr({
302 uri: resolveUrl(loader.master.uri, loader.media().uri), 303 uri: resolveUrl(loader.master.uri, loader.media().uri),
303 withCredentials 304 withCredentials
304 }, function(error, request) { 305 }, function(error, request) {
...@@ -310,7 +311,7 @@ export default class PlaylistLoader extends Stream { ...@@ -310,7 +311,7 @@ export default class PlaylistLoader extends Stream {
310 }); 311 });
311 312
312 // request the specified URL 313 // request the specified URL
313 request = XhrModule({ 314 request = this.hls_.xhr({
314 uri: srcUrl, 315 uri: srcUrl,
315 withCredentials 316 withCredentials
316 }, function(error, req) { 317 }, function(error, req) {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
5 */ 5 */
6 import PlaylistLoader from './playlist-loader'; 6 import PlaylistLoader from './playlist-loader';
7 import Playlist from './playlist'; 7 import Playlist from './playlist';
8 import xhr from './xhr'; 8 import xhrFactory from './xhr';
9 import {Decrypter, AsyncStream, decrypt} from './decrypter'; 9 import {Decrypter, AsyncStream, decrypt} from './decrypter';
10 import utils from './bin-utils'; 10 import utils from './bin-utils';
11 import {MediaSource, URL} from 'videojs-contrib-media-sources'; 11 import {MediaSource, URL} from 'videojs-contrib-media-sources';
...@@ -20,7 +20,7 @@ const Hls = { ...@@ -20,7 +20,7 @@ const Hls = {
20 AsyncStream, 20 AsyncStream,
21 decrypt, 21 decrypt,
22 utils, 22 utils,
23 xhr 23 xhr: xhrFactory()
24 }; 24 };
25 25
26 // the desired length of video to maintain in the buffer, in seconds 26 // the desired length of video to maintain in the buffer, in seconds
...@@ -416,6 +416,7 @@ export default class HlsHandler extends Component { ...@@ -416,6 +416,7 @@ export default class HlsHandler extends Component {
416 this.options_.withCredentials = videojs.options.hls.withCredentials; 416 this.options_.withCredentials = videojs.options.hls.withCredentials;
417 } 417 }
418 this.playlists = new Hls.PlaylistLoader(this.source_.src, 418 this.playlists = new Hls.PlaylistLoader(this.source_.src,
419 this.tech_.hls,
419 this.options_.withCredentials); 420 this.options_.withCredentials);
420 421
421 this.tech_.one('canplay', this.setupFirstPlay.bind(this)); 422 this.tech_.one('canplay', this.setupFirstPlay.bind(this));
...@@ -1224,7 +1225,7 @@ export default class HlsHandler extends Component { ...@@ -1224,7 +1225,7 @@ export default class HlsHandler extends Component {
1224 } 1225 }
1225 1226
1226 // request the next segment 1227 // request the next segment
1227 this.segmentXhr_ = Hls.xhr({ 1228 this.segmentXhr_ = this.tech_.hls.xhr({
1228 uri: segmentInfo.uri, 1229 uri: segmentInfo.uri,
1229 responseType: 'arraybuffer', 1230 responseType: 'arraybuffer',
1230 withCredentials: this.source_.withCredentials, 1231 withCredentials: this.source_.withCredentials,
...@@ -1504,7 +1505,7 @@ export default class HlsHandler extends Component { ...@@ -1504,7 +1505,7 @@ export default class HlsHandler extends Component {
1504 1505
1505 // request the key if the retry limit hasn't been reached 1506 // request the key if the retry limit hasn't been reached
1506 if (!key.bytes && !keyFailed(key)) { 1507 if (!key.bytes && !keyFailed(key)) {
1507 this.keyXhr_ = Hls.xhr({ 1508 this.keyXhr_ = this.tech_.hls.xhr({
1508 uri: this.playlistUriToUrl(key.uri), 1509 uri: this.playlistUriToUrl(key.uri),
1509 responseType: 'arraybuffer', 1510 responseType: 'arraybuffer',
1510 withCredentials: settings.withCredentials 1511 withCredentials: settings.withCredentials
...@@ -1563,6 +1564,14 @@ const HlsSourceHandler = function(mode) { ...@@ -1563,6 +1564,14 @@ const HlsSourceHandler = function(mode) {
1563 source, 1564 source,
1564 mode 1565 mode
1565 }); 1566 });
1567
1568 tech.hls.xhr = xhrFactory();
1569 // Use a global `before` function if specified on videojs.Hls.xhr
1570 // but still allow for a per-player override
1571 if (videojs.Hls.xhr.beforeRequest) {
1572 tech.hls.xhr.beforeRequest = videojs.Hls.xhr.beforeRequest;
1573 }
1574
1566 tech.hls.src(source.src); 1575 tech.hls.src(source.src);
1567 return tech.hls; 1576 return tech.hls;
1568 }, 1577 },
......
...@@ -2,48 +2,64 @@ ...@@ -2,48 +2,64 @@
2 * A wrapper for videojs.xhr that tracks bandwidth. 2 * A wrapper for videojs.xhr that tracks bandwidth.
3 */ 3 */
4 import {xhr as videojsXHR, mergeOptions} from 'video.js'; 4 import {xhr as videojsXHR, mergeOptions} from 'video.js';
5 const xhr = function(options, callback) { 5
6 // Add a default timeout for all hls requests 6 const xhrFactory = function() {
7 options = mergeOptions({ 7 const xhr = function XhrFunction(options, callback) {
8 timeout: 45e3 8 // Add a default timeout for all hls requests
9 }, options); 9 options = mergeOptions({
10 10 timeout: 45e3
11 let request = videojsXHR(options, function(error, response) { 11 }, options);
12 if (!error && request.response) { 12
13 request.responseTime = (new Date()).getTime(); 13 // Allow an optional user-specified function to modify the option
14 request.roundTripTime = request.responseTime - request.requestTime; 14 // object before we construct the xhr request
15 request.bytesReceived = request.response.byteLength || request.response.length; 15 if (XhrFunction.beforeRequest &&
16 if (!request.bandwidth) { 16 typeof XhrFunction.beforeRequest === 'function') {
17 request.bandwidth = 17 let newOptions = XhrFunction.beforeRequest(options);
18 Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); 18
19 if (newOptions) {
20 options = newOptions;
19 } 21 }
20 } 22 }
21 23
22 // videojs.xhr now uses a specific code 24 let request = videojsXHR(options, function(error, response) {
23 // on the error object to signal that a request has 25 if (!error && request.response) {
24 // timed out errors of setting a boolean on the request object 26 request.responseTime = (new Date()).getTime();
25 if (error || request.timedout) { 27 request.roundTripTime = request.responseTime - request.requestTime;
26 request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); 28 request.bytesReceived = request.response.byteLength || request.response.length;
27 } else { 29 if (!request.bandwidth) {
28 request.timedout = false; 30 request.bandwidth =
29 } 31 Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
32 }
33 }
30 34
31 // videojs.xhr no longer considers status codes outside of 200 and 0 35 // videojs.xhr now uses a specific code
32 // (for file uris) to be errors, but the old XHR did, so emulate that 36 // on the error object to signal that a request has
33 // behavior. Status 206 may be used in response to byterange requests. 37 // timed out errors of setting a boolean on the request object
34 if (!error && 38 if (error || request.timedout) {
35 response.statusCode !== 200 && 39 request.timedout = request.timedout || (error.code === 'ETIMEDOUT');
36 response.statusCode !== 206 && 40 } else {
37 response.statusCode !== 0) { 41 request.timedout = false;
38 error = new Error('XHR Failed with a response of: ' + 42 }
39 (request && (request.response || request.responseText))); 43
40 } 44 // videojs.xhr no longer considers status codes outside of 200 and 0
45 // (for file uris) to be errors, but the old XHR did, so emulate that
46 // behavior. Status 206 may be used in response to byterange requests.
47 if (!error &&
48 response.statusCode !== 200 &&
49 response.statusCode !== 206 &&
50 response.statusCode !== 0) {
51 error = new Error('XHR Failed with a response of: ' +
52 (request && (request.response || request.responseText)));
53 }
54
55 callback(error, request);
56 });
41 57
42 callback(error, request); 58 request.requestTime = (new Date()).getTime();
43 }); 59 return request;
60 };
44 61
45 request.requestTime = (new Date()).getTime(); 62 return xhr;
46 return request;
47 }; 63 };
48 64
49 export default xhr; 65 export default xhrFactory;
......
...@@ -3363,6 +3363,78 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func ...@@ -3363,6 +3363,78 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func
3363 window.getComputedStyle = oldGetComputedStyle; 3363 window.getComputedStyle = oldGetComputedStyle;
3364 }); 3364 });
3365 3365
3366 QUnit.test('Allows specifying the beforeRequest functionon the player', function() {
3367 let beforeRequestCalled = false;
3368
3369 this.player.src({
3370 src: 'master.m3u8',
3371 type: 'application/vnd.apple.mpegurl'
3372 });
3373 openMediaSource(this.player, this.clock);
3374
3375 this.player.hls.xhr.beforeRequest = function() {
3376 beforeRequestCalled = true;
3377 };
3378 // master
3379 standardXHRResponse(this.requests.shift());
3380 // media
3381 standardXHRResponse(this.requests.shift());
3382
3383 QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
3384 });
3385
3386 QUnit.test('Allows specifying the beforeRequest function globally', function() {
3387 let beforeRequestCalled = false;
3388
3389 videojs.Hls.xhr.beforeRequest = function() {
3390 beforeRequestCalled = true;
3391 };
3392
3393 this.player.src({
3394 src: 'master.m3u8',
3395 type: 'application/vnd.apple.mpegurl'
3396 });
3397 openMediaSource(this.player, this.clock);
3398 // master
3399 standardXHRResponse(this.requests.shift());
3400
3401 QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
3402
3403 delete videojs.Hls.xhr.beforeRequest;
3404 });
3405
3406 QUnit.test('Allows overriding the global beforeRequest function', function() {
3407 let beforeGlobalRequestCalled = 0;
3408 let beforeLocalRequestCalled = 0;
3409
3410 videojs.Hls.xhr.beforeRequest = function() {
3411 beforeGlobalRequestCalled++;
3412 };
3413
3414 this.player.src({
3415 src: 'master.m3u8',
3416 type: 'application/vnd.apple.mpegurl'
3417 });
3418 openMediaSource(this.player, this.clock);
3419
3420 this.player.hls.xhr.beforeRequest = function() {
3421 beforeLocalRequestCalled++;
3422 };
3423 // master
3424 standardXHRResponse(this.requests.shift());
3425 // media
3426 standardXHRResponse(this.requests.shift());
3427 // ts
3428 standardXHRResponse(this.requests.shift());
3429
3430 QUnit.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
3431 'for the media playlist and media');
3432 QUnit.equal(beforeGlobalRequestCalled, 1, 'global beforeRequest was called once ' +
3433 'for the master playlist');
3434
3435 delete videojs.Hls.xhr.beforeRequest;
3436 });
3437
3366 QUnit.module('Buffer Inspection'); 3438 QUnit.module('Buffer Inspection');
3367 QUnit.test('detects time range end-point changed by updates', function() { 3439 QUnit.test('detects time range end-point changed by updates', function() {
3368 let edge; 3440 let edge;
......