277bea49 by jrivera

Expose the ability to set an `beforeRequest` function on the xhr object in order…

… to modify the options used to create the request object
* Make Xhr a factory function that returns a unique instance of the xhr function and expose an instance of it on each player's xhr object
* Keep the returned function is backward compatible with the previous xhr
* Add a `beforeRequest` function to the XHR that allows you to override options before the request
* The `beforeRequest` function can be specified on the global `videojs.Hls.xhr` function and it'll be used for all players unless overridden on a per-player level
1 parent 6fe9fa2f
...@@ -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,79 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func ...@@ -3363,6 +3363,79 @@ 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.ready(function() {
3370 this.hls.xhr.beforeRequest = function() {
3371 beforeRequestCalled = true;
3372 };
3373 });
3374 this.player.src({
3375 src: 'master.m3u8',
3376 type: 'application/vnd.apple.mpegurl'
3377 });
3378
3379 openMediaSource(this.player, this.clock);
3380 // master
3381 standardXHRResponse(this.requests.shift());
3382 // media
3383 standardXHRResponse(this.requests.shift());
3384
3385 QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
3386 });
3387
3388 QUnit.test('Allows specifying the beforeRequest function globally', function() {
3389 let beforeRequestCalled = false;
3390
3391 videojs.Hls.xhr.beforeRequest = function() {
3392 beforeRequestCalled = true;
3393 };
3394
3395 this.player.src({
3396 src: 'master.m3u8',
3397 type: 'application/vnd.apple.mpegurl'
3398 });
3399
3400 QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
3401
3402 delete videojs.Hls.xhr.beforeRequest;
3403 });
3404
3405 QUnit.test('Allows overriding the global beforeRequest function', function() {
3406 let beforeGlobalRequestCalled = 0;
3407 let beforeLocalRequestCalled = 0;
3408
3409 videojs.Hls.xhr.beforeRequest = function() {
3410 beforeGlobalRequestCalled++;
3411 };
3412
3413 this.player.src({
3414 src: 'master.m3u8',
3415 type: 'application/vnd.apple.mpegurl'
3416 });
3417 this.player.ready(function() {
3418 this.hls.xhr.beforeRequest = function() {
3419 beforeLocalRequestCalled++;
3420 };
3421 });
3422
3423 openMediaSource(this.player, this.clock);
3424 // master
3425 standardXHRResponse(this.requests.shift());
3426 // media
3427 standardXHRResponse(this.requests.shift());
3428 // ts
3429 standardXHRResponse(this.requests.shift());
3430
3431 QUnit.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
3432 'for the media playlist and media');
3433 QUnit.equal(beforeGlobalRequestCalled, 1, 'global beforeRequest was called once ' +
3434 'for the master playlist');
3435
3436 delete videojs.Hls.xhr.beforeRequest;
3437 });
3438
3366 QUnit.module('Buffer Inspection'); 3439 QUnit.module('Buffer Inspection');
3367 QUnit.test('detects time range end-point changed by updates', function() { 3440 QUnit.test('detects time range end-point changed by updates', function() {
3368 let edge; 3441 let edge;
......