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;
......
...@@ -2,6 +2,7 @@ import sinon from 'sinon'; ...@@ -2,6 +2,7 @@ import sinon from 'sinon';
2 import QUnit from 'qunit'; 2 import QUnit from 'qunit';
3 import PlaylistLoader from '../src/playlist-loader'; 3 import PlaylistLoader from '../src/playlist-loader';
4 import videojs from 'video.js'; 4 import videojs from 'video.js';
5 import xhrFactory from '../src/xhr';
5 // Attempts to produce an absolute URL to a given relative path 6 // Attempts to produce an absolute URL to a given relative path
6 // based on window.location.href 7 // based on window.location.href
7 const urlTo = function(path) { 8 const urlTo = function(path) {
...@@ -27,6 +28,10 @@ QUnit.module('Playlist Loader', { ...@@ -27,6 +28,10 @@ QUnit.module('Playlist Loader', {
27 // fake timers 28 // fake timers
28 this.clock = sinon.useFakeTimers(); 29 this.clock = sinon.useFakeTimers();
29 videojs.xhr.XMLHttpRequest = this.sinonXhr; 30 videojs.xhr.XMLHttpRequest = this.sinonXhr;
31
32 this.fakeHls = {
33 xhr: xhrFactory()
34 };
30 }, 35 },
31 afterEach() { 36 afterEach() {
32 this.sinonXhr.restore(); 37 this.sinonXhr.restore();
...@@ -45,13 +50,13 @@ QUnit.test('throws if the playlist url is empty or undefined', function() { ...@@ -45,13 +50,13 @@ QUnit.test('throws if the playlist url is empty or undefined', function() {
45 }); 50 });
46 51
47 QUnit.test('starts without any metadata', function() { 52 QUnit.test('starts without any metadata', function() {
48 let loader = new PlaylistLoader('master.m3u8'); 53 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
49 54
50 QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); 55 QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
51 }); 56 });
52 57
53 QUnit.test('starts with no expired time', function() { 58 QUnit.test('starts with no expired time', function() {
54 let loader = new PlaylistLoader('media.m3u8'); 59 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
55 60
56 this.requests.pop().respond(200, null, 61 this.requests.pop().respond(200, null,
57 '#EXTM3U\n' + 62 '#EXTM3U\n' +
...@@ -64,7 +69,7 @@ QUnit.test('starts with no expired time', function() { ...@@ -64,7 +69,7 @@ QUnit.test('starts with no expired time', function() {
64 69
65 QUnit.test('requests the initial playlist immediately', function() { 70 QUnit.test('requests the initial playlist immediately', function() {
66 /* eslint-disable no-unused-vars */ 71 /* eslint-disable no-unused-vars */
67 let loader = new PlaylistLoader('master.m3u8'); 72 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
68 /* eslint-enable no-unused-vars */ 73 /* eslint-enable no-unused-vars */
69 74
70 QUnit.strictEqual(this.requests.length, 1, 'made a request'); 75 QUnit.strictEqual(this.requests.length, 1, 'made a request');
...@@ -74,7 +79,7 @@ QUnit.test('requests the initial playlist immediately', function() { ...@@ -74,7 +79,7 @@ QUnit.test('requests the initial playlist immediately', function() {
74 }); 79 });
75 80
76 QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { 81 QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() {
77 let loader = new PlaylistLoader('master.m3u8'); 82 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
78 let state; 83 let state;
79 84
80 loader.on('loadedplaylist', function() { 85 loader.on('loadedplaylist', function() {
...@@ -90,7 +95,7 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { ...@@ -90,7 +95,7 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() {
90 95
91 QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', function() { 96 QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
92 let loadedmetadatas = 0; 97 let loadedmetadatas = 0;
93 let loader = new PlaylistLoader('media.m3u8'); 98 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
94 99
95 loader.on('loadedmetadata', function() { 100 loader.on('loadedmetadata', function() {
96 loadedmetadatas++; 101 loadedmetadatas++;
...@@ -110,7 +115,7 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func ...@@ -110,7 +115,7 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func
110 115
111 QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist', 116 QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
112 function() { 117 function() {
113 let loader = new PlaylistLoader('media.m3u8'); 118 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
114 119
115 this.requests.pop().respond(200, null, 120 this.requests.pop().respond(200, null,
116 '#EXTM3U\n' + 121 '#EXTM3U\n' +
...@@ -124,7 +129,7 @@ function() { ...@@ -124,7 +129,7 @@ function() {
124 QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { 129 QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
125 let loadedPlaylist = 0; 130 let loadedPlaylist = 0;
126 let loadedMetadata = 0; 131 let loadedMetadata = 0;
127 let loader = new PlaylistLoader('master.m3u8'); 132 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
128 133
129 loader.on('loadedplaylist', function() { 134 loader.on('loadedplaylist', function() {
130 loadedPlaylist++; 135 loadedPlaylist++;
...@@ -157,7 +162,7 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { ...@@ -157,7 +162,7 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
157 }); 162 });
158 163
159 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { 164 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
160 let loader = new PlaylistLoader('live.m3u8'); 165 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
161 166
162 this.requests.pop().respond(200, null, 167 this.requests.pop().respond(200, null,
163 '#EXTM3U\n' + 168 '#EXTM3U\n' +
...@@ -173,7 +178,7 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi ...@@ -173,7 +178,7 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi
173 }); 178 });
174 179
175 QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { 180 QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() {
176 let loader = new PlaylistLoader('live.m3u8'); 181 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
177 182
178 this.requests.pop().respond(200, null, 183 this.requests.pop().respond(200, null,
179 '#EXTM3U\n' + 184 '#EXTM3U\n' +
...@@ -190,7 +195,7 @@ QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() ...@@ -190,7 +195,7 @@ QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function()
190 195
191 QUnit.test('does not increment expired seconds before firstplay is triggered', 196 QUnit.test('does not increment expired seconds before firstplay is triggered',
192 function() { 197 function() {
193 let loader = new PlaylistLoader('live.m3u8'); 198 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
194 199
195 this.requests.pop().respond(200, null, 200 this.requests.pop().respond(200, null,
196 '#EXTM3U\n' + 201 '#EXTM3U\n' +
...@@ -220,7 +225,7 @@ function() { ...@@ -220,7 +225,7 @@ function() {
220 }); 225 });
221 226
222 QUnit.test('increments expired seconds after a segment is removed', function() { 227 QUnit.test('increments expired seconds after a segment is removed', function() {
223 let loader = new PlaylistLoader('live.m3u8'); 228 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
224 229
225 loader.trigger('firstplay'); 230 loader.trigger('firstplay');
226 this.requests.pop().respond(200, null, 231 this.requests.pop().respond(200, null,
...@@ -251,7 +256,7 @@ QUnit.test('increments expired seconds after a segment is removed', function() { ...@@ -251,7 +256,7 @@ QUnit.test('increments expired seconds after a segment is removed', function() {
251 }); 256 });
252 257
253 QUnit.test('increments expired seconds after a discontinuity', function() { 258 QUnit.test('increments expired seconds after a discontinuity', function() {
254 let loader = new PlaylistLoader('live.m3u8'); 259 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
255 260
256 loader.trigger('firstplay'); 261 loader.trigger('firstplay');
257 this.requests.pop().respond(200, null, 262 this.requests.pop().respond(200, null,
...@@ -299,7 +304,7 @@ QUnit.test('increments expired seconds after a discontinuity', function() { ...@@ -299,7 +304,7 @@ QUnit.test('increments expired seconds after a discontinuity', function() {
299 304
300 QUnit.test('tracks expired seconds properly when two discontinuities expire at once', 305 QUnit.test('tracks expired seconds properly when two discontinuities expire at once',
301 function() { 306 function() {
302 let loader = new PlaylistLoader('live.m3u8'); 307 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
303 308
304 loader.trigger('firstplay'); 309 loader.trigger('firstplay');
305 this.requests.pop().respond(200, null, 310 this.requests.pop().respond(200, null,
...@@ -327,7 +332,7 @@ function() { ...@@ -327,7 +332,7 @@ function() {
327 332
328 QUnit.test('estimates expired if an entire window elapses between live playlist updates', 333 QUnit.test('estimates expired if an entire window elapses between live playlist updates',
329 function() { 334 function() {
330 let loader = new PlaylistLoader('live.m3u8'); 335 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
331 336
332 loader.trigger('firstplay'); 337 loader.trigger('firstplay');
333 this.requests.pop().respond(200, null, 338 this.requests.pop().respond(200, null,
...@@ -354,7 +359,7 @@ function() { ...@@ -354,7 +359,7 @@ function() {
354 359
355 QUnit.test('emits an error when an initial playlist request fails', function() { 360 QUnit.test('emits an error when an initial playlist request fails', function() {
356 let errors = []; 361 let errors = [];
357 let loader = new PlaylistLoader('master.m3u8'); 362 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
358 363
359 loader.on('error', function() { 364 loader.on('error', function() {
360 errors.push(loader.error); 365 errors.push(loader.error);
...@@ -367,7 +372,7 @@ QUnit.test('emits an error when an initial playlist request fails', function() { ...@@ -367,7 +372,7 @@ QUnit.test('emits an error when an initial playlist request fails', function() {
367 372
368 QUnit.test('errors when an initial media playlist request fails', function() { 373 QUnit.test('errors when an initial media playlist request fails', function() {
369 let errors = []; 374 let errors = [];
370 let loader = new PlaylistLoader('master.m3u8'); 375 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
371 376
372 loader.on('error', function() { 377 loader.on('error', function() {
373 errors.push(loader.error); 378 errors.push(loader.error);
...@@ -389,7 +394,7 @@ QUnit.test('errors when an initial media playlist request fails', function() { ...@@ -389,7 +394,7 @@ QUnit.test('errors when an initial media playlist request fails', function() {
389 QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload', 394 QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
390 function() { 395 function() {
391 /* eslint-disable no-unused-vars */ 396 /* eslint-disable no-unused-vars */
392 let loader = new PlaylistLoader('live.m3u8'); 397 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
393 /* eslint-enable no-unused-vars */ 398 /* eslint-enable no-unused-vars */
394 399
395 this.requests.pop().respond(200, null, 400 this.requests.pop().respond(200, null,
...@@ -414,7 +419,7 @@ function() { ...@@ -414,7 +419,7 @@ function() {
414 }); 419 });
415 420
416 QUnit.test('preserves segment metadata across playlist refreshes', function() { 421 QUnit.test('preserves segment metadata across playlist refreshes', function() {
417 let loader = new PlaylistLoader('live.m3u8'); 422 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
418 let segment; 423 let segment;
419 424
420 this.requests.pop().respond(200, null, 425 this.requests.pop().respond(200, null,
...@@ -446,7 +451,7 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { ...@@ -446,7 +451,7 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() {
446 }); 451 });
447 452
448 QUnit.test('clears the update timeout when switching quality', function() { 453 QUnit.test('clears the update timeout when switching quality', function() {
449 let loader = new PlaylistLoader('live-master.m3u8'); 454 let loader = new PlaylistLoader('live-master.m3u8', this.fakeHls);
450 let refreshes = 0; 455 let refreshes = 0;
451 456
452 // track the number of playlist refreshes triggered 457 // track the number of playlist refreshes triggered
...@@ -481,7 +486,7 @@ QUnit.test('clears the update timeout when switching quality', function() { ...@@ -481,7 +486,7 @@ QUnit.test('clears the update timeout when switching quality', function() {
481 486
482 QUnit.test('media-sequence updates are considered a playlist change', function() { 487 QUnit.test('media-sequence updates are considered a playlist change', function() {
483 /* eslint-disable no-unused-vars */ 488 /* eslint-disable no-unused-vars */
484 let loader = new PlaylistLoader('live.m3u8'); 489 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
485 /* eslint-enable no-unused-vars */ 490 /* eslint-enable no-unused-vars */
486 491
487 this.requests.pop().respond(200, null, 492 this.requests.pop().respond(200, null,
...@@ -505,7 +510,7 @@ QUnit.test('media-sequence updates are considered a playlist change', function() ...@@ -505,7 +510,7 @@ QUnit.test('media-sequence updates are considered a playlist change', function()
505 QUnit.test('emits an error if a media refresh fails', function() { 510 QUnit.test('emits an error if a media refresh fails', function() {
506 let errors = 0; 511 let errors = 0;
507 let errorResponseText = 'custom error message'; 512 let errorResponseText = 'custom error message';
508 let loader = new PlaylistLoader('live.m3u8'); 513 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
509 514
510 loader.on('error', function() { 515 loader.on('error', function() {
511 errors++; 516 errors++;
...@@ -527,7 +532,7 @@ QUnit.test('emits an error if a media refresh fails', function() { ...@@ -527,7 +532,7 @@ QUnit.test('emits an error if a media refresh fails', function() {
527 }); 532 });
528 533
529 QUnit.test('switches media playlists when requested', function() { 534 QUnit.test('switches media playlists when requested', function() {
530 let loader = new PlaylistLoader('master.m3u8'); 535 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
531 536
532 this.requests.pop().respond(200, null, 537 this.requests.pop().respond(200, null,
533 '#EXTM3U\n' + 538 '#EXTM3U\n' +
...@@ -556,7 +561,7 @@ QUnit.test('switches media playlists when requested', function() { ...@@ -556,7 +561,7 @@ QUnit.test('switches media playlists when requested', function() {
556 }); 561 });
557 562
558 QUnit.test('can switch playlists immediately after the master is downloaded', function() { 563 QUnit.test('can switch playlists immediately after the master is downloaded', function() {
559 let loader = new PlaylistLoader('master.m3u8'); 564 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
560 565
561 loader.on('loadedplaylist', function() { 566 loader.on('loadedplaylist', function() {
562 loader.media('high.m3u8'); 567 loader.media('high.m3u8');
...@@ -571,7 +576,7 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu ...@@ -571,7 +576,7 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu
571 }); 576 });
572 577
573 QUnit.test('can switch media playlists based on URI', function() { 578 QUnit.test('can switch media playlists based on URI', function() {
574 let loader = new PlaylistLoader('master.m3u8'); 579 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
575 580
576 this.requests.pop().respond(200, null, 581 this.requests.pop().respond(200, null,
577 '#EXTM3U\n' + 582 '#EXTM3U\n' +
...@@ -600,7 +605,7 @@ QUnit.test('can switch media playlists based on URI', function() { ...@@ -600,7 +605,7 @@ QUnit.test('can switch media playlists based on URI', function() {
600 }); 605 });
601 606
602 QUnit.test('aborts in-flight playlist refreshes when switching', function() { 607 QUnit.test('aborts in-flight playlist refreshes when switching', function() {
603 let loader = new PlaylistLoader('master.m3u8'); 608 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
604 609
605 this.requests.pop().respond(200, null, 610 this.requests.pop().respond(200, null,
606 '#EXTM3U\n' + 611 '#EXTM3U\n' +
...@@ -622,7 +627,7 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() { ...@@ -622,7 +627,7 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() {
622 }); 627 });
623 628
624 QUnit.test('switching to the active playlist is a no-op', function() { 629 QUnit.test('switching to the active playlist is a no-op', function() {
625 let loader = new PlaylistLoader('master.m3u8'); 630 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
626 631
627 this.requests.pop().respond(200, null, 632 this.requests.pop().respond(200, null,
628 '#EXTM3U\n' + 633 '#EXTM3U\n' +
...@@ -642,7 +647,7 @@ QUnit.test('switching to the active playlist is a no-op', function() { ...@@ -642,7 +647,7 @@ QUnit.test('switching to the active playlist is a no-op', function() {
642 }); 647 });
643 648
644 QUnit.test('switching to the active live playlist is a no-op', function() { 649 QUnit.test('switching to the active live playlist is a no-op', function() {
645 let loader = new PlaylistLoader('master.m3u8'); 650 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
646 651
647 this.requests.pop().respond(200, null, 652 this.requests.pop().respond(200, null,
648 '#EXTM3U\n' + 653 '#EXTM3U\n' +
...@@ -661,7 +666,7 @@ QUnit.test('switching to the active live playlist is a no-op', function() { ...@@ -661,7 +666,7 @@ QUnit.test('switching to the active live playlist is a no-op', function() {
661 }); 666 });
662 667
663 QUnit.test('switches back to loaded playlists without re-requesting them', function() { 668 QUnit.test('switches back to loaded playlists without re-requesting them', function() {
664 let loader = new PlaylistLoader('master.m3u8'); 669 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
665 670
666 this.requests.pop().respond(200, null, 671 this.requests.pop().respond(200, null,
667 '#EXTM3U\n' + 672 '#EXTM3U\n' +
...@@ -690,7 +695,7 @@ QUnit.test('switches back to loaded playlists without re-requesting them', funct ...@@ -690,7 +695,7 @@ QUnit.test('switches back to loaded playlists without re-requesting them', funct
690 695
691 QUnit.test('aborts outstanding requests if switching back to an already loaded playlist', 696 QUnit.test('aborts outstanding requests if switching back to an already loaded playlist',
692 function() { 697 function() {
693 let loader = new PlaylistLoader('master.m3u8'); 698 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
694 699
695 this.requests.pop().respond(200, null, 700 this.requests.pop().respond(200, null,
696 '#EXTM3U\n' + 701 '#EXTM3U\n' +
...@@ -724,7 +729,7 @@ function() { ...@@ -724,7 +729,7 @@ function() {
724 729
725 QUnit.test('does not abort requests when the same playlist is re-requested', 730 QUnit.test('does not abort requests when the same playlist is re-requested',
726 function() { 731 function() {
727 let loader = new PlaylistLoader('master.m3u8'); 732 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
728 733
729 this.requests.pop().respond(200, null, 734 this.requests.pop().respond(200, null,
730 '#EXTM3U\n' + 735 '#EXTM3U\n' +
...@@ -746,7 +751,7 @@ function() { ...@@ -746,7 +751,7 @@ function() {
746 }); 751 });
747 752
748 QUnit.test('throws an error if a media switch is initiated too early', function() { 753 QUnit.test('throws an error if a media switch is initiated too early', function() {
749 let loader = new PlaylistLoader('master.m3u8'); 754 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
750 755
751 QUnit.throws(function() { 756 QUnit.throws(function() {
752 loader.media('high.m3u8'); 757 loader.media('high.m3u8');
...@@ -762,7 +767,7 @@ QUnit.test('throws an error if a media switch is initiated too early', function( ...@@ -762,7 +767,7 @@ QUnit.test('throws an error if a media switch is initiated too early', function(
762 767
763 QUnit.test('throws an error if a switch to an unrecognized playlist is requested', 768 QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
764 function() { 769 function() {
765 let loader = new PlaylistLoader('master.m3u8'); 770 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
766 771
767 this.requests.pop().respond(200, null, 772 this.requests.pop().respond(200, null,
768 '#EXTM3U\n' + 773 '#EXTM3U\n' +
...@@ -775,7 +780,7 @@ function() { ...@@ -775,7 +780,7 @@ function() {
775 }); 780 });
776 781
777 QUnit.test('dispose cancels the refresh timeout', function() { 782 QUnit.test('dispose cancels the refresh timeout', function() {
778 let loader = new PlaylistLoader('live.m3u8'); 783 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
779 784
780 this.requests.pop().respond(200, null, 785 this.requests.pop().respond(200, null,
781 '#EXTM3U\n' + 786 '#EXTM3U\n' +
...@@ -790,7 +795,7 @@ QUnit.test('dispose cancels the refresh timeout', function() { ...@@ -790,7 +795,7 @@ QUnit.test('dispose cancels the refresh timeout', function() {
790 }); 795 });
791 796
792 QUnit.test('dispose aborts pending refresh requests', function() { 797 QUnit.test('dispose aborts pending refresh requests', function() {
793 let loader = new PlaylistLoader('live.m3u8'); 798 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
794 799
795 this.requests.pop().respond(200, null, 800 this.requests.pop().respond(200, null,
796 '#EXTM3U\n' + 801 '#EXTM3U\n' +
...@@ -807,7 +812,7 @@ QUnit.test('dispose aborts pending refresh requests', function() { ...@@ -807,7 +812,7 @@ QUnit.test('dispose aborts pending refresh requests', function() {
807 }); 812 });
808 813
809 QUnit.test('errors if requests take longer than 45s', function() { 814 QUnit.test('errors if requests take longer than 45s', function() {
810 let loader = new PlaylistLoader('media.m3u8'); 815 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
811 let errors = 0; 816 let errors = 0;
812 817
813 loader.on('error', function() { 818 loader.on('error', function() {
...@@ -820,7 +825,7 @@ QUnit.test('errors if requests take longer than 45s', function() { ...@@ -820,7 +825,7 @@ QUnit.test('errors if requests take longer than 45s', function() {
820 }); 825 });
821 826
822 QUnit.test('triggers an event when the active media changes', function() { 827 QUnit.test('triggers an event when the active media changes', function() {
823 let loader = new PlaylistLoader('master.m3u8'); 828 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
824 let mediaChanges = 0; 829 let mediaChanges = 0;
825 830
826 loader.on('mediachange', function() { 831 loader.on('mediachange', function() {
...@@ -861,7 +866,7 @@ QUnit.test('triggers an event when the active media changes', function() { ...@@ -861,7 +866,7 @@ QUnit.test('triggers an event when the active media changes', function() {
861 }); 866 });
862 867
863 QUnit.test('can get media index by playback position for non-live videos', function() { 868 QUnit.test('can get media index by playback position for non-live videos', function() {
864 let loader = new PlaylistLoader('media.m3u8'); 869 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
865 870
866 this.requests.shift().respond(200, null, 871 this.requests.shift().respond(200, null,
867 '#EXTM3U\n' + 872 '#EXTM3U\n' +
...@@ -886,7 +891,7 @@ QUnit.test('can get media index by playback position for non-live videos', funct ...@@ -886,7 +891,7 @@ QUnit.test('can get media index by playback position for non-live videos', funct
886 }); 891 });
887 892
888 QUnit.test('returns the lower index when calculating for a segment boundary', function() { 893 QUnit.test('returns the lower index when calculating for a segment boundary', function() {
889 let loader = new PlaylistLoader('media.m3u8'); 894 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
890 895
891 this.requests.shift().respond(200, null, 896 this.requests.shift().respond(200, null,
892 '#EXTM3U\n' + 897 '#EXTM3U\n' +
...@@ -903,7 +908,7 @@ QUnit.test('returns the lower index when calculating for a segment boundary', fu ...@@ -903,7 +908,7 @@ QUnit.test('returns the lower index when calculating for a segment boundary', fu
903 908
904 QUnit.test('accounts for non-zero starting segment time when calculating media index', 909 QUnit.test('accounts for non-zero starting segment time when calculating media index',
905 function() { 910 function() {
906 let loader = new PlaylistLoader('media.m3u8'); 911 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
907 912
908 this.requests.shift().respond(200, null, 913 this.requests.shift().respond(200, null,
909 '#EXTM3U\n' + 914 '#EXTM3U\n' +
...@@ -941,7 +946,7 @@ function() { ...@@ -941,7 +946,7 @@ function() {
941 }); 946 });
942 947
943 QUnit.test('prefers precise segment timing when tracking expired time', function() { 948 QUnit.test('prefers precise segment timing when tracking expired time', function() {
944 let loader = new PlaylistLoader('media.m3u8'); 949 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
945 950
946 loader.trigger('firstplay'); 951 loader.trigger('firstplay');
947 this.requests.shift().respond(200, null, 952 this.requests.shift().respond(200, null,
...@@ -975,7 +980,7 @@ QUnit.test('prefers precise segment timing when tracking expired time', function ...@@ -975,7 +980,7 @@ QUnit.test('prefers precise segment timing when tracking expired time', function
975 }); 980 });
976 981
977 QUnit.test('accounts for expired time when calculating media index', function() { 982 QUnit.test('accounts for expired time when calculating media index', function() {
978 let loader = new PlaylistLoader('media.m3u8'); 983 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
979 984
980 this.requests.shift().respond(200, null, 985 this.requests.shift().respond(200, null,
981 '#EXTM3U\n' + 986 '#EXTM3U\n' +
...@@ -1007,7 +1012,7 @@ QUnit.test('accounts for expired time when calculating media index', function() ...@@ -1007,7 +1012,7 @@ QUnit.test('accounts for expired time when calculating media index', function()
1007 }); 1012 });
1008 1013
1009 QUnit.test('does not misintrepret playlists missing newlines at the end', function() { 1014 QUnit.test('does not misintrepret playlists missing newlines at the end', function() {
1010 let loader = new PlaylistLoader('media.m3u8'); 1015 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
1011 1016
1012 // no newline 1017 // no newline
1013 this.requests.shift().respond(200, null, 1018 this.requests.shift().respond(200, null,
......
...@@ -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;
......