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.
- [hls.bandwidth](#hlsbandwidth)
- [hls.bytesReceived](#hlsbytesreceived)
- [hls.selectPlaylist](#hlsselectplaylist)
- [hls.xhr](#hlsxhr)
- [Events](#events)
- [loadedmetadata](#loadedmetadata)
- [loadedplaylist](#loadedplaylist)
......@@ -194,6 +195,44 @@ segment is downloaded. You can override this function to provide your
adaptive streaming logic. You must, however, be sure to return a valid
media playlist object that is present in `player.hls.master`.
#### hls.xhr
Type: `function`
The xhr function that is used by HLS internally is exposed on the per-
player `hls` 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.
Example:
```javascript
player.hls.xhr.beforeRequest = function(options) {
options.uri = options.uri.replace('example.com', 'foo.com');
return options;
};
```
The global `videojs.Hls` 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.
Example
```javascript
videojs.Hls.xhr.beforeRequest = function(options) {
/*
* Modifications to requests that will affect every player.
*/
return options;
};
```
For information on the type of options that you can modify see the
documentation at [https://github.com/Raynos/xhr](https://github.com/Raynos/xhr).
### Events
Standard HTML video events are handled by video.js automatically and
are triggered on the player object. In addition, there are a couple
......
......@@ -6,7 +6,6 @@
*
*/
import resolveUrl from './resolve-url';
import XhrModule from './xhr';
import {mergeOptions} from 'video.js';
import Stream from './stream';
import m3u8 from './m3u8';
......@@ -86,7 +85,7 @@ const updateSegments = function(original, update, offset) {
};
export default class PlaylistLoader extends Stream {
constructor(srcUrl, withCredentials) {
constructor(srcUrl, hls, withCredentials) {
super();
let loader = this;
let dispose;
......@@ -95,6 +94,8 @@ export default class PlaylistLoader extends Stream {
let playlistRequestError;
let haveMetadata;
this.hls_ = hls;
// a flag that disables "expired time"-tracking this setting has
// no effect when not playing a live stream
this.trackExpiredTime_ = false;
......@@ -261,7 +262,7 @@ export default class PlaylistLoader extends Stream {
}
// request the new playlist
request = XhrModule({
request = this.hls_.xhr({
uri: resolveUrl(loader.master.uri, playlist.uri),
withCredentials
}, function(error, request) {
......@@ -298,7 +299,7 @@ export default class PlaylistLoader extends Stream {
}
loader.state = 'HAVE_CURRENT_METADATA';
request = XhrModule({
request = this.hls_.xhr({
uri: resolveUrl(loader.master.uri, loader.media().uri),
withCredentials
}, function(error, request) {
......@@ -310,7 +311,7 @@ export default class PlaylistLoader extends Stream {
});
// request the specified URL
request = XhrModule({
request = this.hls_.xhr({
uri: srcUrl,
withCredentials
}, function(error, req) {
......
......@@ -5,7 +5,7 @@
*/
import PlaylistLoader from './playlist-loader';
import Playlist from './playlist';
import xhr from './xhr';
import xhrFactory from './xhr';
import {Decrypter, AsyncStream, decrypt} from './decrypter';
import utils from './bin-utils';
import {MediaSource, URL} from 'videojs-contrib-media-sources';
......@@ -20,7 +20,7 @@ const Hls = {
AsyncStream,
decrypt,
utils,
xhr
xhr: xhrFactory()
};
// the desired length of video to maintain in the buffer, in seconds
......@@ -416,6 +416,7 @@ export default class HlsHandler extends Component {
this.options_.withCredentials = videojs.options.hls.withCredentials;
}
this.playlists = new Hls.PlaylistLoader(this.source_.src,
this.tech_.hls,
this.options_.withCredentials);
this.tech_.one('canplay', this.setupFirstPlay.bind(this));
......@@ -1224,7 +1225,7 @@ export default class HlsHandler extends Component {
}
// request the next segment
this.segmentXhr_ = Hls.xhr({
this.segmentXhr_ = this.tech_.hls.xhr({
uri: segmentInfo.uri,
responseType: 'arraybuffer',
withCredentials: this.source_.withCredentials,
......@@ -1504,7 +1505,7 @@ export default class HlsHandler extends Component {
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
this.keyXhr_ = Hls.xhr({
this.keyXhr_ = this.tech_.hls.xhr({
uri: this.playlistUriToUrl(key.uri),
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
......@@ -1563,6 +1564,14 @@ const HlsSourceHandler = function(mode) {
source,
mode
});
tech.hls.xhr = xhrFactory();
// Use a global `before` function if specified on videojs.Hls.xhr
// but still allow for a per-player override
if (videojs.Hls.xhr.beforeRequest) {
tech.hls.xhr.beforeRequest = videojs.Hls.xhr.beforeRequest;
}
tech.hls.src(source.src);
return tech.hls;
},
......
......@@ -2,48 +2,64 @@
* A wrapper for videojs.xhr that tracks bandwidth.
*/
import {xhr as videojsXHR, mergeOptions} from 'video.js';
const xhr = function(options, callback) {
// Add a default timeout for all hls requests
options = mergeOptions({
timeout: 45e3
}, options);
let request = videojsXHR(options, function(error, response) {
if (!error && request.response) {
request.responseTime = (new Date()).getTime();
request.roundTripTime = request.responseTime - request.requestTime;
request.bytesReceived = request.response.byteLength || request.response.length;
if (!request.bandwidth) {
request.bandwidth =
Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
const xhrFactory = function() {
const xhr = function XhrFunction(options, callback) {
// Add a default timeout for all hls requests
options = mergeOptions({
timeout: 45e3
}, options);
// Allow an optional user-specified function to modify the option
// object before we construct the xhr request
if (XhrFunction.beforeRequest &&
typeof XhrFunction.beforeRequest === 'function') {
let newOptions = XhrFunction.beforeRequest(options);
if (newOptions) {
options = newOptions;
}
}
// videojs.xhr now uses a specific code
// on the error object to signal that a request has
// timed out errors of setting a boolean on the request object
if (error || request.timedout) {
request.timedout = request.timedout || (error.code === 'ETIMEDOUT');
} else {
request.timedout = false;
}
let request = videojsXHR(options, function(error, response) {
if (!error && request.response) {
request.responseTime = (new Date()).getTime();
request.roundTripTime = request.responseTime - request.requestTime;
request.bytesReceived = request.response.byteLength || request.response.length;
if (!request.bandwidth) {
request.bandwidth =
Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
}
}
// videojs.xhr no longer considers status codes outside of 200 and 0
// (for file uris) to be errors, but the old XHR did, so emulate that
// behavior. Status 206 may be used in response to byterange requests.
if (!error &&
response.statusCode !== 200 &&
response.statusCode !== 206 &&
response.statusCode !== 0) {
error = new Error('XHR Failed with a response of: ' +
(request && (request.response || request.responseText)));
}
// videojs.xhr now uses a specific code
// on the error object to signal that a request has
// timed out errors of setting a boolean on the request object
if (error || request.timedout) {
request.timedout = request.timedout || (error.code === 'ETIMEDOUT');
} else {
request.timedout = false;
}
// videojs.xhr no longer considers status codes outside of 200 and 0
// (for file uris) to be errors, but the old XHR did, so emulate that
// behavior. Status 206 may be used in response to byterange requests.
if (!error &&
response.statusCode !== 200 &&
response.statusCode !== 206 &&
response.statusCode !== 0) {
error = new Error('XHR Failed with a response of: ' +
(request && (request.response || request.responseText)));
}
callback(error, request);
});
callback(error, request);
});
request.requestTime = (new Date()).getTime();
return request;
};
request.requestTime = (new Date()).getTime();
return request;
return xhr;
};
export default xhr;
export default xhrFactory;
......
......@@ -3363,6 +3363,78 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func
window.getComputedStyle = oldGetComputedStyle;
});
QUnit.test('Allows specifying the beforeRequest functionon the player', function() {
let beforeRequestCalled = false;
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.hls.xhr.beforeRequest = function() {
beforeRequestCalled = true;
};
// master
standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
});
QUnit.test('Allows specifying the beforeRequest function globally', function() {
let beforeRequestCalled = false;
videojs.Hls.xhr.beforeRequest = function() {
beforeRequestCalled = true;
};
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
delete videojs.Hls.xhr.beforeRequest;
});
QUnit.test('Allows overriding the global beforeRequest function', function() {
let beforeGlobalRequestCalled = 0;
let beforeLocalRequestCalled = 0;
videojs.Hls.xhr.beforeRequest = function() {
beforeGlobalRequestCalled++;
};
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.hls.xhr.beforeRequest = function() {
beforeLocalRequestCalled++;
};
// master
standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
// ts
standardXHRResponse(this.requests.shift());
QUnit.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
'for the media playlist and media');
QUnit.equal(beforeGlobalRequestCalled, 1, 'global beforeRequest was called once ' +
'for the master playlist');
delete videojs.Hls.xhr.beforeRequest;
});
QUnit.module('Buffer Inspection');
QUnit.test('detects time range end-point changed by updates', function() {
let edge;
......