cc311ea3 by Brandon Casey

Added bandwidth stats to HLS (#685)

* Added bandwidth stats to HLS
* moved bandwidth stats to reside on segment loaders
* added accessors for stats on master playlist controller
* added getters for stats using master playlist controller on hls
* updated tests to check for stats
1 parent 0fa22184
......@@ -170,6 +170,40 @@ export default class MasterPlaylistController extends videojs.EventTarget {
}
/**
* get the total number of media requests from the `audiosegmentloader_`
* and the `mainSegmentLoader_`
*
* @private
*/
mediaRequests_() {
return this.audioSegmentLoader_.mediaRequests +
this.mainSegmentLoader_.mediaRequests;
}
/**
* get the total time that media requests have spent trnasfering
* from the `audiosegmentloader_` and the `mainSegmentLoader_`
*
* @private
*/
mediaTransferDuration_() {
return this.audioSegmentLoader_.mediaTransferDuration +
this.mainSegmentLoader_.mediaTransferDuration;
}
/**
* get the total number of bytes transfered during media requests
* from the `audiosegmentloader_` and the `mainSegmentLoader_`
*
* @private
*/
mediaBytesTransferred_() {
return this.audioSegmentLoader_.mediaBytesTransferred +
this.mainSegmentLoader_.mediaBytesTransferred;
}
/**
* fill our internal list of HlsAudioTracks with data from
* the master playlist or use a default
*
......@@ -350,7 +384,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
if (media !== this.masterPlaylistLoader_.media()) {
this.masterPlaylistLoader_.media(media);
this.mainSegmentLoader_.sourceUpdater_.remove(this.tech_.currentTime() + 5, Infinity);
this.mainSegmentLoader_.sourceUpdater_.remove(this.tech_.currentTime() + 5,
Infinity);
}
}
......
......@@ -131,7 +131,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.state = 'INIT';
this.bandwidth = settings.bandwidth;
this.roundTrip = NaN;
this.bytesReceived = 0;
this.resetStats_();
// private properties
this.hasPlayed_ = settings.hasPlayed;
......@@ -153,6 +153,17 @@ export default class SegmentLoader extends videojs.EventTarget {
}
/**
* reset all of our media stats
*
* @private
*/
resetStats_() {
this.mediaBytesTransferred = 0;
this.mediaRequests = 0;
this.mediaTransferDuration = 0;
}
/**
* dispose of the SegmentLoader and reset to the default state
*/
dispose() {
......@@ -161,6 +172,7 @@ export default class SegmentLoader extends videojs.EventTarget {
if (this.sourceUpdater_) {
this.sourceUpdater_.dispose();
}
this.resetStats_();
}
/**
......@@ -625,7 +637,9 @@ export default class SegmentLoader extends videojs.EventTarget {
// calculate the download bandwidth based on segment request
this.roundTrip = request.roundTripTime;
this.bandwidth = request.bandwidth;
this.bytesReceived += request.bytesReceived || 0;
this.mediaBytesTransferred += request.bytesReceived || 0;
this.mediaRequests += 1;
this.mediaTransferDuration += request.roundTripTime || 0;
if (segment.key) {
segmentInfo.encryptedBytes = new Uint8Array(request.response);
......
......@@ -301,12 +301,16 @@ class HlsHandler extends Component {
this.tech_ = tech;
this.source_ = source;
this.stats = {};
// handle global & Source Handler level options
this.options_ = videojs.mergeOptions(videojs.options.hls || {}, options.hls);
this.setOptions_();
this.bytesReceived = 0;
// start playlist selection at a reasonable bandwidth for
// broadband internet
// 0.5 Mbps
this.bandwidth = this.options_.bandwidth || 4194304;
// listen for fullscreenchange events for this player so that we
// can adjust our quality selection quickly
......@@ -406,6 +410,19 @@ class HlsHandler extends Component {
}
});
Object.defineProperty(this.stats, 'bandwidth', {
get: () => this.bandwidth || 0
});
Object.defineProperty(this.stats, 'mediaRequests', {
get: () => this.masterPlaylistController_.mediaRequests_() || 0
});
Object.defineProperty(this.stats, 'mediaTransferDuration', {
get: () => this.masterPlaylistController_.mediaTransferDuration_() || 0
});
Object.defineProperty(this.stats, 'mediaBytesTransferred', {
get: () => this.masterPlaylistController_.mediaBytesTransferred_() || 0
});
this.tech_.one('canplay',
this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
......@@ -527,7 +544,6 @@ class HlsHandler extends Component {
this.masterPlaylistController_.dispose();
}
this.tech_.audioTracks().removeEventListener('change', this.audioTrackChange_);
super.dispose();
}
}
......
......@@ -64,6 +64,9 @@ QUnit.test('obeys none preload option', function() {
openMediaSource(this.player, this.clock);
QUnit.equal(this.requests.length, 0, 'no segment requests');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('obeys auto preload option', function() {
......@@ -76,6 +79,9 @@ QUnit.test('obeys auto preload option', function() {
openMediaSource(this.player, this.clock);
QUnit.equal(this.requests.length, 1, '1 segment request');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('obeys metadata preload option', function() {
......@@ -88,6 +94,9 @@ QUnit.test('obeys metadata preload option', function() {
openMediaSource(this.player, this.clock);
QUnit.equal(this.requests.length, 1, '1 segment request');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('clears some of the buffer for a fast quality change', function() {
......@@ -114,6 +123,9 @@ QUnit.test('clears some of the buffer for a fast quality change', function() {
QUnit.equal(removes.length, 1, 'removed buffered content');
QUnit.equal(removes[0].start, 7 + 5, 'removed from a bit after current time');
QUnit.equal(removes[0].end, Infinity, 'removed to the end');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('does not clear the buffer when no fast quality change occurs', function() {
......@@ -134,6 +146,8 @@ QUnit.test('does not clear the buffer when no fast quality change occurs', funct
this.masterPlaylistController.fastQualityChange_();
QUnit.equal(removes.length, 0, 'did not remove content');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('if buffered, will request second segment byte range', function() {
......@@ -163,6 +177,13 @@ QUnit.test('if buffered, will request second segment byte range', function() {
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
this.clock.tick(10 * 1000);
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.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
});
QUnit.test('re-initializes the combined playlist loader when switching sources',
......@@ -218,6 +239,8 @@ QUnit.test('updates the combined segment loader on live playlist refreshes', fun
this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
QUnit.equal(updates.length, 1, 'updated the segment list');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test(
......@@ -240,6 +263,13 @@ function() {
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.mediaRequests, 1, '1 segment request');
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
16,
'16 bytes downloaded');
});
QUnit.test('blacklists switching from video+audio playlists to audio only', function() {
......@@ -264,6 +294,9 @@ QUnit.test('blacklists switching from video+audio playlists to audio only', func
'selected video+audio');
audioPlaylist = this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
QUnit.equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth we set above');
});
QUnit.test('blacklists switching from audio-only playlists to video+audio', function() {
......@@ -290,6 +323,9 @@ QUnit.test('blacklists switching from audio-only playlists to video+audio', func
QUnit.equal(videoAudioPlaylist.excludeUntil,
Infinity,
'excluded incompatible playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
});
QUnit.test('blacklists switching from video-only playlists to video+audio', function() {
......@@ -317,6 +353,9 @@ QUnit.test('blacklists switching from video-only playlists to video+audio', func
QUnit.equal(videoAudioPlaylist.excludeUntil,
Infinity,
'excluded incompatible playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
});
QUnit.test('blacklists switching between playlists with incompatible audio codecs',
......@@ -343,6 +382,8 @@ function() {
alternatePlaylist =
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
QUnit.equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
});
QUnit.test('updates the combined segment loader on media changes', function() {
......@@ -369,6 +410,14 @@ QUnit.test('updates the combined segment loader on media changes', function() {
// media
standardXHRResponse(this.requests.shift());
QUnit.equal(updates.length, 1, 'updated the segment list');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, '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');
});
QUnit.test('selects a playlist after main/combined segment downloads', function() {
......@@ -392,6 +441,8 @@ QUnit.test('selects a playlist after main/combined segment downloads', function(
// and another
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
QUnit.strictEqual(calls, 3, 'selects after additional segments');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});
QUnit.test('updates the duration after switching playlists', function() {
......@@ -424,6 +475,13 @@ QUnit.test('updates the duration after switching playlists', function() {
QUnit.ok(selectedPlaylist, 'selected playlist');
QUnit.ok(this.masterPlaylistController.mediaSource.duration !== 0,
'updates the duration');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, Infinity, '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');
});
QUnit.test('seekable uses the intersection of alternate audio and combined tracks',
......
......@@ -105,6 +105,11 @@ QUnit.test('calling load is idempotent', function() {
this.requests.shift().respond(200, null, '');
loader.load();
QUnit.equal(this.requests.length, 0, 'load has no effect');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('calling load should unpause', function() {
......@@ -135,6 +140,11 @@ QUnit.test('calling load should unpause', function() {
loader.load();
QUnit.equal(loader.paused(), false, 'unpaused');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('regularly checks the buffer while unpaused', function() {
......@@ -159,6 +169,11 @@ QUnit.test('regularly checks the buffer while unpaused', function() {
currentTime = Config.GOAL_BUFFER_LENGTH;
this.clock.tick(10 * 1000);
QUnit.equal(this.requests.length, 1, 'requested another segment');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('does not check the buffer while paused', function() {
......@@ -177,6 +192,11 @@ QUnit.test('does not check the buffer while paused', function() {
this.clock.tick(10 * 1000);
QUnit.equal(this.requests.length, 0, 'did not make a request');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('calculates bandwidth after downloading a segment', function() {
......@@ -191,7 +211,12 @@ QUnit.test('calculates bandwidth after downloading a segment', function() {
QUnit.equal(loader.bandwidth, (10 / 100) * 8 * 1000, 'calculated bandwidth');
QUnit.equal(loader.roundTrip, 100, 'saves request round trip time');
QUnit.equal(loader.bytesReceived, 10, 'saves bytes received');
// TODO: Bandwidth Stat will be stale??
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('segment request timeouts reset bandwidth', function() {
......@@ -223,6 +248,10 @@ QUnit.test('appending a segment triggers progress', function() {
mediaSource.sourceBuffers[0].trigger('updateend');
QUnit.equal(progresses, 1, 'fired progress');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('only requests one segment at a time', function() {
......@@ -251,6 +280,11 @@ QUnit.test('only appends one segment at a time', function() {
QUnit.equal(mediaSource.sourceBuffers[0].updates_.filter(
update => update.append).length, 1, 'only one append');
QUnit.equal(this.requests.length, 0, 'only made one request');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('adjusts the playlist offset if no buffering progress is made', function() {
......@@ -288,6 +322,11 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
// so the loader should try the next segment
QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 20, '20 bytes');
QUnit.equal(loader.mediaTransferDuration, 2, '2 ms (clocks above)');
QUnit.equal(loader.mediaRequests, 2, '2 requests');
});
QUnit.test('never attempt to load a segment that ' +
......@@ -319,6 +358,11 @@ QUnit.test('never attempt to load a segment that ' +
// the loader should move on to the next segment
QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaTransferDuration, 1, '1 ms (clocks above)');
QUnit.equal(loader.mediaRequests, 1, '1 requests');
});
QUnit.test('adjusts the playlist offset if no buffering progress is made', function() {
......@@ -356,6 +400,11 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
// so the loader should try the next segment
QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 20, '20 bytes');
QUnit.equal(loader.mediaTransferDuration, 2, '2 ms (clocks above)');
QUnit.equal(loader.mediaRequests, 2, '2 requests');
});
QUnit.test('adjusts the playlist offset even when segment.end is set if no' +
......@@ -453,6 +502,10 @@ QUnit.test('abort does not cancel segment processing in progress', function() {
loader.abort();
QUnit.equal(loader.state, 'APPENDING', 'still appending');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('sets the timestampOffset on timeline change', function() {
......@@ -474,6 +527,10 @@ QUnit.test('sets the timestampOffset on timeline change', function() {
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
QUnit.equal(mediaSource.sourceBuffers[0].timestampOffset, 10, 'set timestampOffset');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 20, '20 bytes');
QUnit.equal(loader.mediaRequests, 2, '2 requests');
});
QUnit.test('tracks segment end times as they are buffered', function() {
......@@ -491,6 +548,10 @@ QUnit.test('tracks segment end times as they are buffered', function() {
]);
mediaSource.sourceBuffers[0].trigger('updateend');
QUnit.equal(playlist.segments[0].end, 9.5, 'updated duration');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('segment 404s should trigger an error', function() {
......@@ -548,6 +609,10 @@ QUnit.test('fires ended at the end of a playlist', function() {
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
QUnit.equal(endOfStreams, 1, 'triggered ended');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('live playlists do not trigger ended', function() {
......@@ -572,6 +637,10 @@ QUnit.test('live playlists do not trigger ended', function() {
mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
mediaSource.sourceBuffers[0].trigger('updateend');
QUnit.equal(endOfStreams, 0, 'did not trigger ended');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('respects the global withCredentials option', function() {
......@@ -781,6 +850,10 @@ QUnit.test('the key is saved to the segment in the correct format', function() {
QUnit.deepEqual(segment.key.bytes,
new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
'passed the specified segment key');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request was completed');
});
QUnit.test('supplies media sequence of current segment as the IV by default, if no IV ' +
......@@ -811,6 +884,10 @@ function() {
QUnit.deepEqual(segment.key.iv, new Uint32Array([0, 0, 0, 5]),
'the IV for the segment is the media sequence');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('segment with key has decrypted bytes appended during processing', function() {
......@@ -839,6 +916,10 @@ QUnit.test('segment with key has decrypted bytes appended during processing', fu
// Allow the decrypter's async stream to run the callback
this.clock.tick(1);
QUnit.ok(loader.pendingSegment_.bytes, 'decrypted bytes in segment');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 8, '8 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('calling load with an encrypted segment waits for both key and segment ' +
......@@ -864,6 +945,10 @@ QUnit.test('calling load with an encrypted segment waits for both key and segmen
keyRequest.response = new Uint32Array([0, 0, 0, 0]).buffer;
keyRequest.respond(200, null, '');
QUnit.equal(loader.state, 'DECRYPTING', 'moves to decrypting state');
// verify stats
QUnit.equal(loader.mediaBytesTransferred, 10, '10 bytes');
QUnit.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('key request timeouts reset bandwidth', function() {
......