Browse Source

Added support for the throughput of the whole system to the ABR algorithm (#918)

* Added support for the throughput of the whole system to the ABR algorithm to allow it to adjust better to poor performance especially with flash.

* Added tests for systemBandwidth and throughput

* Replaced instances of (new Date()).getTime() with Date.now()

* Removed redundant test and added a comment to explain bandwidith and throughput setters

* Renamed startOfLoad_ to startOfAppend which is a more accurate name
pull/6/head
Jon-Carlos Rivera 9 years ago
committed by GitHub
parent
commit
1f2b9c78af
  1. 30
      src/segment-loader.js
  2. 46
      src/videojs-contrib-hls.js
  3. 4
      src/xhr.js
  4. 70
      test/master-playlist-controller.test.js
  5. 2
      test/test-helpers.js
  6. 78
      test/videojs-contrib-hls.test.js

30
src/segment-loader.js

@ -144,6 +144,7 @@ export default class SegmentLoader extends videojs.EventTarget {
// public properties
this.state = 'INIT';
this.bandwidth = settings.bandwidth;
this.throughput = {rate: 0, count: 0};
this.roundTrip = NaN;
this.resetStats_();
@ -754,6 +755,7 @@ export default class SegmentLoader extends videojs.EventTarget {
if (request === this.xhr_.segmentXhr) {
// the segment request is no longer outstanding
this.xhr_.segmentXhr = null;
segmentInfo.startOfAppend = Date.now();
// calculate the download bandwidth based on segment request
this.roundTrip = request.roundTripTime;
@ -903,6 +905,8 @@ export default class SegmentLoader extends videojs.EventTarget {
}
}
segmentInfo.byteLength = segmentInfo.bytes.byteLength;
this.sourceUpdater_.appendBuffer(segmentInfo.bytes,
this.handleUpdateEnd_.bind(this));
}
@ -919,6 +923,7 @@ export default class SegmentLoader extends videojs.EventTarget {
let currentTime = this.currentTime_();
this.pendingSegment_ = null;
this.recordThroughput_(segmentInfo);
// add segment metadata if it we have gained information during the
// last append
@ -1017,6 +1022,31 @@ export default class SegmentLoader extends videojs.EventTarget {
return timelineUpdated;
}
/**
* Records the current throughput of the decrypt, transmux, and append
* portion of the semgment pipeline. `throughput.rate` is a the cumulative
* moving average of the throughput. `throughput.count` is the number of
* data points in the average.
*
* @private
* @param {Object} segmentInfo the object returned by loadSegment
*/
recordThroughput_(segmentInfo) {
let rate = this.throughput.rate;
// Add one to the time to ensure that we don't accidentally attempt to divide
// by zero in the case where the throughput is ridiculously high
let segmentProcessingTime =
Date.now() - segmentInfo.startOfAppend + 1;
// Multiply by 8000 to convert from bytes/millisecond to bits/second
let segmentProcessingThroughput =
Math.floor((segmentInfo.byteLength / segmentProcessingTime) * 8 * 1000);
// This is just a cumulative moving average calculation:
// newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
this.throughput.rate +=
(segmentProcessingThroughput - rate) / (++this.throughput.count);
}
/**
* add a number of seconds to the currentTime when determining which
* segment to fetch in order to force the fetcher to advance in cases

46
src/videojs-contrib-hls.js

@ -122,7 +122,7 @@ Hls.STANDARD_PLAYLIST_SELECTOR = function() {
effectiveBitrate = variant.attributes.BANDWIDTH * BANDWIDTH_VARIANCE;
if (effectiveBitrate < this.bandwidth) {
if (effectiveBitrate < this.systemBandwidth) {
bandwidthPlaylists.push(variant);
// since the playlists are sorted in ascending order by
@ -399,12 +399,55 @@ class HlsHandler extends Component {
this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
}
},
throughput: {
get() {
return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate;
},
set(throughput) {
this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput;
// By setting `count` to 1 the throughput value becomes the starting value
// for the cumulative average
this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1;
}
},
bandwidth: {
get() {
return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
},
set(bandwidth) {
this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
// setting the bandwidth manually resets the throughput counter
// `count` is set to zero that current value of `rate` isn't included
// in the cumulative average
this.masterPlaylistController_.mainSegmentLoader_.throughput = {rate: 0, count: 0};
}
},
/**
* `systemBandwidth` is a combination of two serial processes bit-rates. The first
* is the network bitrate provided by `bandwidth` and the second is the bitrate of
* the entire process after that - decryption, transmuxing, and appending - provided
* by `throughput`.
*
* Since the two process are serial, the overall system bandwidth is given by:
* sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
*/
systemBandwidth: {
get() {
let invBandwidth = 1 / (this.bandwidth || 1);
let invThroughput;
if (this.throughput > 0) {
invThroughput = 1 / this.throughput;
} else {
invThroughput = 0;
}
let systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
return systemBitrate;
},
set() {
videojs.log.error('The "systemBandwidth" property is read-only');
}
}
});
@ -451,7 +494,6 @@ class HlsHandler extends Component {
// the bandwidth of the primary segment loader is our best
// estimate of overall bandwidth
this.on(this.masterPlaylistController_, 'progress', function() {
this.bandwidth = this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
this.tech_.trigger('progress');
});

4
src/xhr.js

@ -31,7 +31,7 @@ const xhrFactory = function() {
let request = videojsXHR(options, function(error, response) {
if (!error && request.response) {
request.responseTime = (new Date()).getTime();
request.responseTime = Date.now();
request.roundTripTime = request.responseTime - request.requestTime;
request.bytesReceived = request.response.byteLength || request.response.length;
if (!request.bandwidth) {
@ -63,7 +63,7 @@ const xhrFactory = function() {
callback(error, request);
});
request.requestTime = (new Date()).getTime();
request.requestTime = Date.now();
return request;
};

70
test/master-playlist-controller.test.js

@ -175,7 +175,8 @@ QUnit.test('if buffered, will request second segment byte range', function() {
this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
return videojs.createTimeRanges([[0, 20]]);
};
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment
standardXHRResponse(this.requests[1]);
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
@ -183,11 +184,11 @@ QUnit.test('if buffered, will request second segment byte range', function() {
QUnit.equal(this.requests[2].headers.Range, 'bytes=1823412-2299991');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
1024,
'1024 bytes downloaded');
});
QUnit.test('re-initializes the combined playlist loader when switching sources',
@ -262,18 +263,19 @@ function() {
this.player.tech_.on('progress', function() {
progressCount++;
});
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment
standardXHRResponse(this.requests.shift());
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
QUnit.equal(progressCount, 1, 'fired a progress event');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
1024,
'1024 bytes downloaded');
});
QUnit.test('updates the enabled track when switching audio groups', function() {
@ -470,6 +472,8 @@ QUnit.test('updates the combined segment loader on media changes', function() {
this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
updates.push(update);
};
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// downloading the new segment will update bandwidth and cause a
// playlist change
@ -481,12 +485,12 @@ QUnit.test('updates the combined segment loader on media changes', function() {
QUnit.ok(updates.length > 0, 'updated the segment list');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
QUnit.equal(
this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
1024,
'1024 bytes downloaded');
});
QUnit.test('selects a playlist after main/combined segment downloads', function() {
@ -535,7 +539,8 @@ QUnit.test('updates the duration after switching playlists', function() {
return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
};
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment 0
standardXHRResponse(this.requests[2]);
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
@ -546,11 +551,46 @@ QUnit.test('updates the duration after switching playlists', function() {
'updates the duration');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
1024,
'1024 bytes downloaded');
});
QUnit.test('playlist selection uses systemBandwidth', function() {
this.masterPlaylistController.mediaSource.trigger('sourceopen');
this.player.width(1000);
this.player.height(900);
// master
standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
QUnit.ok(/media3\.m3u8/i.test(this.requests[1].url), 'Selected the highest rendition');
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
this.clock.tick(1);
// segment 0
standardXHRResponse(this.requests[2]);
// 20ms have passed to upload 1kb that gives us a throughput of 1024 / 20 * 8 * 1000 = 409600
this.clock.tick(20);
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
// systemBandwidth is 1 / (1 / 8192000 + 1 / 409600) = ~390095
// media1
standardXHRResponse(this.requests[3]);
QUnit.ok(/media\.m3u8/i.test(this.requests[3].url), 'Selected the rendition < 390095');
QUnit.ok(this.masterPlaylistController.mediaSource.duration !== 0,
'updates the duration');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
1024,
'1024 bytes downloaded');
});
QUnit.test('removes request timeout when segment timesout on lowest rendition',

2
test/test-helpers.js

@ -289,7 +289,7 @@ export const standardXHRResponse = function(request, data) {
data = testDataManifests[manifestName];
}
request.response = new Uint8Array(16).buffer;
request.response = new Uint8Array(1024).buffer;
request.respond(200, {'Content-Type': contentType}, data);
};

78
test/videojs-contrib-hls.test.js

@ -157,7 +157,7 @@ QUnit.test('stats are reset on each new source', function() {
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, 'stat is set');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'stat is set');
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
@ -387,7 +387,7 @@ QUnit.test('starts downloading a segment on loadedmetadata', function() {
'the first segment is requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -465,10 +465,52 @@ QUnit.test('downloads media playlists after loading the master', function() {
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('setting bandwidth resets throughput', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.hls.throughput = 1000;
QUnit.strictEqual(this.player.tech_.hls.throughput,
1000,
'throughput is set');
this.player.tech_.hls.bandwidth = 20e10;
QUnit.strictEqual(this.player.tech_.hls.throughput,
0,
'throughput is reset when bandwidth is specified');
});
QUnit.test('a thoughput of zero is ignored in systemBandwidth', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.hls.bandwidth = 20e10;
QUnit.strictEqual(this.player.tech_.hls.throughput,
0,
'throughput is reset when bandwidth is specified');
QUnit.strictEqual(this.player.tech_.hls.systemBandwidth,
20e10,
'systemBandwidth is the same as bandwidth');
});
QUnit.test('systemBandwidth is a combination of thoughput and bandwidth', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.hls.bandwidth = 20e10;
this.player.tech_.hls.throughput = 20e10;
// 1 / ( 1 / 20e10 + 1 / 20e10) = 10e10
QUnit.strictEqual(this.player.tech_.hls.systemBandwidth,
10e10,
'systemBandwidth is the combination of bandwidth and throughput');
});
QUnit.test('upshifts if the initial bandwidth hint is high', function() {
this.player.src({
src: 'manifest/master.m3u8',
@ -498,7 +540,7 @@ QUnit.test('upshifts if the initial bandwidth hint is high', function() {
);
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -525,7 +567,7 @@ QUnit.test('downshifts if the initial bandwidth hint is low', function() {
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -1543,7 +1585,7 @@ QUnit.test('calling play() at the end of a video replays', function() {
QUnit.equal(seekTime, 0, 'seeked to the beginning');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -1626,7 +1668,7 @@ QUnit.test('seeking should abort an outstanding key request and create a new one
'urls should match');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -1672,7 +1714,7 @@ QUnit.test('switching playlists with an outstanding key request aborts request a
'http://media.example.com/fileSequence52-A.ts',
'requested the segment');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -2000,7 +2042,7 @@ QUnit.test('cleans up the buffer when loading live segments', function() {
'remove called with the right range');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -2053,7 +2095,7 @@ QUnit.test('cleans up the buffer based on currentTime when loading a live segmen
QUnit.deepEqual(removes[0], [0, 80 - 60], 'remove called with the right range');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -2092,7 +2134,7 @@ QUnit.test('cleans up the buffer when loading VOD segments', function() {
QUnit.deepEqual(removes[0], [0, 120 - 60], 'remove called with the right range');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
@ -2237,7 +2279,7 @@ QUnit.test('Allows overriding the global beforeRequest function', function() {
delete videojs.Hls.xhr.beforeRequest;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, 'seen above');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'seen above');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, 'one segment request');
});
@ -2336,7 +2378,7 @@ QUnit.test('stats are reset on dispose', function() {
// media
standardXHRResponse(this.requests.shift());
QUnit.equal(hls.stats.mediaBytesTransferred, 16, 'stat is set');
QUnit.equal(hls.stats.mediaBytesTransferred, 1024, 'stat is set');
hls.dispose();
QUnit.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset');
});
@ -2408,7 +2450,7 @@ QUnit.test('downloads additional playlists if required', function() {
// verify stats
QUnit.equal(hls.stats.bandwidth, 3000000, 'default');
QUnit.equal(hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(hls.stats.mediaRequests, 1, '1 request');
});
@ -2449,7 +2491,7 @@ QUnit.test('waits to download new segments until the media playlist is stable',
// verify stats
QUnit.equal(hls.stats.bandwidth, Infinity, 'default');
QUnit.equal(hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(hls.stats.mediaRequests, 1, '1 request');
});
@ -2566,7 +2608,11 @@ QUnit.test('blacklists playlist if key requests fail', function() {
'#EXT-X-ENDLIST\n');
// segment 1
if (/key\.php/i.test(this.requests[0].url)) {
standardXHRResponse(this.requests.pop());
} else {
standardXHRResponse(this.requests.shift());
}
// fail key
this.requests.shift().respond(404);
QUnit.ok(hls.playlists.media().excludeUntil > 0,
@ -2609,6 +2655,6 @@ QUnit.test('treats invalid keys as a key request failure and blacklists playlist
QUnit.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
// verify stats
QUnit.equal(hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
QUnit.equal(hls.stats.mediaRequests, 1, '1 request');
});
Loading…
Cancel
Save