Merge pull request #633 from videojs/xhr-beforerequest
Expose the ability to set an `beforeRequest` function on xhr
Showing
6 changed files
with
183 additions
and
46 deletions
... | @@ -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; | ... | ... |
This diff is collapsed.
Click to expand it.
... | @@ -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; | ... | ... |
-
Please register or sign in to post a comment