4b0e7317 by David LaPalomento

Merge pull request #178 from videojs/pre-segment-switch

Pre segment switch
2 parents de317697 014577a9
......@@ -29,12 +29,13 @@
"karma-firefox-launcher": "~0.1.3",
"karma-ie-launcher": "~0.1.1",
"karma-opera-launcher": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.1",
"karma-phantomjs-launcher": "^0.1.4",
"karma-qunit": "~0.1.1",
"karma-safari-launcher": "~0.1.1",
"karma-sauce-launcher": "~0.1.8",
"qunitjs": "^1.15.0",
"sinon": "1.10.2",
"video.js": "^4.7.2"
"video.js": "^4.9.0"
},
"dependencies": {
"pkcs7": "^0.2.2",
......
......@@ -58,6 +58,8 @@
haveMetadata = function(error, xhr, url) {
var parser, refreshDelay, update;
loader.setBandwidth(request || xhr);
// any in-flight request is now finished
request = null;
......@@ -200,6 +202,10 @@
});
};
loader.setBandwidth = function(xhr) {
loader.bandwidth = xhr.bandwidth;
};
// live playlist staleness timeout
loader.on('mediaupdatetimeout', function() {
if (loader.state !== 'HAVE_METADATA') {
......
......@@ -101,19 +101,58 @@ videojs.Hls.prototype.handleSourceOpen = function() {
sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader());
this.mediaIndex = 0;
if (this.playlists) {
this.playlists.dispose();
}
this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials);
this.playlists.on('loadedmetadata', videojs.bind(this, function() {
oldMediaPlaylist = this.playlists.media();
var selectedPlaylist, loaderHandler, newBitrate, segmentDuration,
segmentDlTime, setupEvents, threshold;
setupEvents = function() {
this.fillBuffer();
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
this.fillBuffer();
player.on('timeupdate', videojs.bind(this, this.fillBuffer));
player.on('timeupdate', videojs.bind(this, this.drainBuffer));
player.on('waiting', videojs.bind(this, this.drainBuffer));
player.trigger('loadedmetadata');
};
oldMediaPlaylist = this.playlists.media();
this.bandwidth = this.playlists.bandwidth;
selectedPlaylist = this.selectPlaylist();
newBitrate = selectedPlaylist.attributes &&
selectedPlaylist.attributes.BANDWIDTH;
segmentDuration = oldMediaPlaylist.segments &&
oldMediaPlaylist.segments[this.mediaIndex].duration ||
oldMediaPlaylist.targetDuration;
segmentDlTime = (segmentDuration * newBitrate) / this.bandwidth;
if (!segmentDlTime) {
segmentDlTime = Infinity;
}
// this threshold is to account for having a high latency on the manifest
// request which is a somewhat small file.
threshold = 10;
if (segmentDlTime <= threshold) {
this.playlists.media(selectedPlaylist);
loaderHandler = videojs.bind(this, function() {
setupEvents.call(this);
this.playlists.off('loadedplaylist', loaderHandler);
});
this.playlists.on('loadedplaylist', loaderHandler);
} else {
setupEvents.call(this);
}
}));
this.playlists.on('error', videojs.bind(this, function() {
......@@ -409,12 +448,27 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
this.loadSegment(segmentUri, offset);
};
/*
* Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
* Expects an object with:
* * `roundTripTime` - the round trip time for the request we're setting the time for
* * `bandwidth` - the bandwidth we want to set
* * `bytesReceived` - amount of bytes downloaded
* `bandwidth` is the only required property.
*/
videojs.Hls.prototype.setBandwidth = function(xhr) {
var tech = this;
// calculate the download bandwidth
tech.segmentXhrTime = xhr.roundTripTime;
tech.bandwidth = xhr.bandwidth;
tech.bytesReceived += xhr.bytesReceived || 0;
};
videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
var
tech = this,
player = this.player(),
settings = player.options().hls || {},
startTime = +new Date();
settings = player.options().hls || {};
// request the next segment
this.segmentXhr_ = videojs.Hls.xhr({
......@@ -448,10 +502,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
return;
}
// calculate the download bandwidth
tech.segmentXhrTime = (+new Date()) - startTime;
tech.bandwidth = (this.response.byteLength / tech.segmentXhrTime) * 8 * 1000;
tech.bytesReceived += this.response.byteLength;
tech.setBandwidth(this);
// package up all the work to append the segment
// if the segment is the start of a timestamp discontinuity,
......
......@@ -34,6 +34,7 @@
request = new window.XMLHttpRequest();
request.open(options.method, url);
request.url = url;
request.requestTime = new Date().getTime();
if (options.responseType) {
request.responseType = options.responseType;
......@@ -69,6 +70,13 @@
return callback.call(this, true, url);
}
if (this.response) {
this.responseTime = new Date().getTime();
this.roundTripTime = this.responseTime - this.requestTime;
this.bytesReceived = this.response.byteLength || this.response.length;
this.bandwidth = Math.floor((this.bytesReceived / this.roundTripTime) * 8 * 1000);
}
return callback.call(this, false, url);
};
request.send(null);
......
......@@ -5,19 +5,19 @@
"segments": [
{
"duration": 10,
"uri": "00001.ts"
"uri": "media-00001.ts"
},
{
"duration": 10,
"uri": "00002.ts"
"uri": "media-00002.ts"
},
{
"duration": 10,
"uri": "00003.ts"
"uri": "media-00003.ts"
},
{
"duration": 10,
"uri": "00004.ts"
"uri": "media-00004.ts"
}
],
"targetDuration": 10,
......
......@@ -2,12 +2,12 @@
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
media-00001.ts
#EXTINF:10,
00002.ts
media-00002.ts
#EXTINF:10,
00003.ts
media-00003.ts
#EXTINF:10,
00004.ts
media-00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
......
......@@ -2,12 +2,12 @@
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
media1-00001.ts
#EXTINF:10,
00002.ts
media1-00002.ts
#EXTINF:10,
00003.ts
media1-00003.ts
#EXTINF:10,
00004.ts
media1-00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
......
......@@ -2,12 +2,12 @@
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
media3-00001.ts
#EXTINF:10,
00002.ts
media3-00002.ts
#EXTINF:10,
00003.ts
media3-00003.ts
#EXTINF:10,
00004.ts
media3-00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
......
......@@ -260,7 +260,7 @@ test('starts downloading a segment on loadedmetadata', function() {
strictEqual(requests[1].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
'/manifest/media-00001.ts',
'the first segment is requested');
});
......@@ -349,8 +349,41 @@ test('downloads media playlists after loading the master', function() {
openMediaSource(player);
standardXHRResponse(requests[0]);
// set bandwidth to a high number, so, we don't switch;
player.hls.bandwidth = 500000;
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
strictEqual(requests[1].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/media.m3u8',
'media playlist requested');
strictEqual(requests[2].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/media-00001.ts',
'first segment requested');
});
test('downloads a second media playlist before playback, if bandwidth is high', function() {
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
player.hls.playlists.setBandwidth = function() {
player.hls.playlists.bandwidth = 100000;
};
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
standardXHRResponse(requests[3]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
strictEqual(requests[1].url,
......@@ -361,7 +394,12 @@ test('downloads media playlists after loading the master', function() {
strictEqual(requests[2].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
'/manifest/media1.m3u8',
'media playlist requested');
strictEqual(requests[3].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/media1-00001.ts',
'first segment requested');
});
......@@ -385,6 +423,10 @@ test('calculates the bandwidth after downloading a segment', function() {
openMediaSource(player);
standardXHRResponse(requests[0]);
// set the request time to be a bit earlier so our bandwidth calculations are not NaN
requests[1].requestTime = (new Date())-100;
standardXHRResponse(requests[1]);
ok(player.hls.bandwidth, 'bandwidth is calculated');
......@@ -407,10 +449,12 @@ test('selects a playlist after segment downloads', function() {
openMediaSource(player);
standardXHRResponse(requests[0]);
player.hls.bandwidth = 3000000;
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
strictEqual(calls, 1, 'selects after the initial segment');
strictEqual(calls, 2, 'selects after the initial segment');
player.currentTime = function() {
return 1;
};
......@@ -420,7 +464,8 @@ test('selects a playlist after segment downloads', function() {
player.trigger('timeupdate');
standardXHRResponse(requests[3]);
strictEqual(calls, 2, 'selects after additional segments');
strictEqual(calls, 3, 'selects after additional segments');
});
test('moves to the next segment if there is a network error', function() {
......@@ -433,6 +478,8 @@ test('moves to the next segment if there is a network error', function() {
openMediaSource(player);
standardXHRResponse(requests[0]);
player.hls.bandwidth = 3000000;
standardXHRResponse(requests[1]);
mediaIndex = player.hls.mediaIndex;
......@@ -486,6 +533,8 @@ test('downloads additional playlists if required', function() {
openMediaSource(player);
standardXHRResponse(requests[0]);
player.hls.bandwidth = 3000000;
standardXHRResponse(requests[1]);
// before an m3u8 is downloaded, no segments are available
player.hls.selectPlaylist = function() {
......@@ -661,7 +710,7 @@ test('downloads the next segment if the buffer is getting low', function() {
strictEqual(requests[2].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00002.ts',
'/manifest/media-00002.ts',
'made segment request');
});
......@@ -1161,6 +1210,8 @@ test('resets the switching algorithm if a request times out', function() {
});
openMediaSource(player);
standardXHRResponse(requests.shift()); // master
player.hls.bandwidth = 3000000;
standardXHRResponse(requests.shift()); // media.m3u8
// simulate a segment timeout
requests[0].timedout = true;
......@@ -1207,7 +1258,10 @@ test('remove event handlers on dispose', function() {
oldOn.call(player, type, handler);
};
player.off = function(type, handler) {
// ignore the top-level videojs removals that aren't relevant to HLS
if (type && type !== 'dispose') {
offhandlers++;
}
oldOff.call(player, type, handler);
};
player.src({
......@@ -1215,7 +1269,9 @@ test('remove event handlers on dispose', function() {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.hls.playlists.trigger('loadedmetadata');
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
player.dispose();
......