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() {
......
......@@ -139,6 +139,25 @@ QUnit.test('starts playing if autoplay is specified', function() {
QUnit.ok(!this.player.paused(), 'not paused');
});
QUnit.test('stats are reset on each new source', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
// make sure play() is called *after* the media source opens
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, 'stat is set');
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 0, 'stat is reset');
});
QUnit.test('XHR requests first byte range on play', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
......@@ -359,6 +378,10 @@ QUnit.test('starts downloading a segment on loadedmetadata', function() {
QUnit.strictEqual(this.requests[1].url,
absoluteUrl('manifest/media-00001.ts'),
'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.mediaRequests, 1, '1 request');
});
QUnit.test('re-initializes the handler for each source', function() {
......@@ -433,6 +456,10 @@ QUnit.test('downloads media playlists after loading the master', function() {
QUnit.strictEqual(this.requests[2].url,
absoluteUrl('manifest/media2-00001.ts'),
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('upshifts if the initial bandwidth hint is high', function() {
......@@ -462,6 +489,10 @@ QUnit.test('upshifts if the initial bandwidth hint is high', function() {
absoluteUrl('manifest/media2-00001.ts'),
'first segment requested'
);
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('downshifts if the initial bandwidth hint is low', function() {
......@@ -485,6 +516,10 @@ QUnit.test('downshifts if the initial bandwidth hint is low', function() {
QUnit.strictEqual(this.requests[2].url,
absoluteUrl('manifest/media1-00001.ts'),
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('buffer checks are noops until a media playlist is ready', function() {
......@@ -499,6 +534,7 @@ QUnit.test('buffer checks are noops until a media playlist is ready', function()
QUnit.strictEqual(this.requests[0].url,
'manifest/media.m3u8',
'media playlist requested');
});
QUnit.test('buffer checks are noops when only the master is ready', function() {
......@@ -533,6 +569,9 @@ QUnit.test('buffer checks are noops when only the master is ready', function() {
QUnit.strictEqual(this.requests[0].url,
absoluteUrl('manifest/media1.m3u8'),
'media playlist requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('selects a playlist below the current bandwidth', function() {
......@@ -556,6 +595,9 @@ QUnit.test('selects a playlist below the current bandwidth', function() {
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'the low bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 10, 'bandwidth set above');
});
QUnit.test('allows initial bandwidth to be provided', function() {
......@@ -574,6 +616,9 @@ QUnit.test('allows initial bandwidth to be provided', function() {
QUnit.equal(this.player.tech_.hls.bandwidth,
500,
'prefers user-specified initial bandwidth');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 500, 'bandwidth set above');
});
QUnit.test('raises the minimum bitrate for a stream proportionially', function() {
......@@ -598,6 +643,9 @@ QUnit.test('raises the minimum bitrate for a stream proportionially', function()
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'a lower bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 11, 'bandwidth set above');
});
QUnit.test('uses the lowest bitrate if no other is suitable', function() {
......@@ -619,6 +667,9 @@ QUnit.test('uses the lowest bitrate if no other is suitable', function() {
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'the lowest bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('selects the correct rendition by tech dimensions', function() {
......@@ -683,6 +734,9 @@ QUnit.test('selects the correct rendition by tech dimensions', function() {
QUnit.equal(playlist.attributes.BANDWIDTH,
440000,
'should have the expected bandwidth in case of multiple, if exact match');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 3000000, 'bandwidth set above');
});
QUnit.test('selects the highest bitrate playlist when the player dimensions are ' +
......@@ -713,6 +767,9 @@ QUnit.test('selects the highest bitrate playlist when the player dimensions are
QUnit.equal(playlist.attributes.BANDWIDTH,
1000,
'selected the highest bandwidth variant');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('filters playlists that are currently excluded', function() {
......@@ -748,6 +805,9 @@ QUnit.test('filters playlists that are currently excluded', function() {
QUnit.equal(playlist,
this.player.tech_.hls.playlists.master.playlists[0],
'expired the exclusion');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('does not blacklist compatible H.264 codec strings', function() {
......@@ -778,6 +838,9 @@ QUnit.test('does not blacklist compatible H.264 codec strings', function() {
QUnit.strictEqual(typeof master.playlists[1].excludeUntil,
'undefined',
'did not blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('does not blacklist compatible AAC codec strings', function() {
......@@ -808,6 +871,9 @@ QUnit.test('does not blacklist compatible AAC codec strings', function() {
QUnit.strictEqual(typeof master.playlists[1].excludeUntil,
'undefined',
'did not blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('cancels outstanding XHRs when seeking', function() {
......@@ -883,6 +949,9 @@ QUnit.test('segment 404 should trigger blacklisting of media', function() {
this.requests[2].respond(400);
QUnit.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
QUnit.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
});
QUnit.test('playlist 404 should blacklist media', function() {
......@@ -917,6 +986,8 @@ QUnit.test('playlist 404 should blacklist media', function() {
QUnit.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
QUnit.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('seeking in an empty playlist is a non-erroring noop', function() {
......@@ -965,6 +1036,9 @@ QUnit.test('fire loadedmetadata once we successfully load a playlist', function(
standardXHRResponse(this.requests.shift());
QUnit.equal(count, 1,
'loadedMedia triggered after successful recovery from 404');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
});
QUnit.test('sets seekable and duration for live playlists', function() {
......@@ -1221,6 +1295,9 @@ QUnit.test('resets the switching algorithm if a request times out', function() {
QUnit.strictEqual(this.player.tech_.hls.playlists.media(),
this.player.tech_.hls.playlists.master.playlists[1],
'reset to the lowest bitrate playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth is reset too');
});
QUnit.test('disposes the playlist loader', function() {
......@@ -1411,6 +1488,10 @@ QUnit.test('calling play() at the end of a video replays', function() {
this.player.tech_.trigger('play');
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.mediaRequests, 1, '1 request');
});
QUnit.test('keys are resolved relative to the master playlist', function() {
......@@ -1435,6 +1516,9 @@ QUnit.test('keys are resolved relative to the master playlist', function() {
QUnit.equal(this.requests[0].url,
absoluteUrl('video/playlist/keys/key.php'),
'resolves multiple relative paths');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('keys are resolved relative to their containing playlist', function() {
......@@ -1487,6 +1571,10 @@ QUnit.test('seeking should abort an outstanding key request and create a new one
'https://example.com/' +
this.player.tech_.hls.playlists.media().segments[1].key.uri,
'urls should match');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('switching playlists with an outstanding key request aborts request and ' +
......@@ -1529,6 +1617,9 @@ QUnit.test('switching playlists with an outstanding key request aborts request a
QUnit.equal(this.requests[1].url,
'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.mediaRequests, 1, '1 request');
});
QUnit.test('does not download segments if preload option set to none', function() {
......@@ -1549,6 +1640,9 @@ QUnit.test('does not download segments if preload option set to none', function(
return !(/m3u8$/).test(request.uri);
});
QUnit.equal(this.requests.length, 0, 'did not download any segments');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
// workaround https://bugzilla.mozilla.org/show_bug.cgi?id=548397
......@@ -1572,6 +1666,9 @@ QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', func
this.player.tech_.hls.selectPlaylist();
QUnit.ok(true, 'should not throw');
window.getComputedStyle = oldGetComputedStyle;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('resolves relative key URLs against the playlist', function() {
......@@ -1608,6 +1705,9 @@ QUnit.test('adds 1 default audio track if we have not parsed any, and the playli
QUnit.equal(this.player.audioTracks().length, 1, 'one audio track after load');
QUnit.ok(this.player.audioTracks()[0] instanceof HlsAudioTrack, 'audio track is an hls audio track');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('adds 1 default audio track if in flash mode', function() {
......@@ -1673,6 +1773,9 @@ QUnit.test('adds audio tracks if we have parsed some from a playlist', function(
QUnit.equal(vjsAudioTracks[1].enabled, false, 'main track is disabled');
QUnit.equal(hlsAudioTracks[1].enabled, false, 'main track is disabled');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('audio info from audioinfo event is stored on hls', function() {
......@@ -1754,6 +1857,9 @@ QUnit.test('audioinfo changes with three tracks, enabled track is blacklisted an
QUnit.equal(blacklistPlaylistCalls, 0, 'blacklist was not called on playlist');
QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged');
videojs.browser.IS_FIREFOX = oldIsFirefox;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('audioinfo changes with one track, blacklist playlist', function() {
......@@ -1787,6 +1893,9 @@ QUnit.test('audioinfo changes with one track, blacklist playlist', function() {
QUnit.equal(blacklistPlaylistCalls, 1, 'blacklist was called on playlist');
QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged');
videojs.browser.IS_FIREFOX = oldIsFirefox;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('audioinfo changes with three tracks, default is enabled, blacklisted playlist', function() {
......@@ -1835,6 +1944,9 @@ QUnit.test('audioinfo changes with three tracks, default is enabled, blacklisted
QUnit.equal(blacklistPlaylistCalls, 1, 'blacklist was called on playlist');
QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged');
videojs.browser.IS_FIREFOX = oldIsFirefox;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('cleans up the buffer when loading live segments', function() {
......@@ -1884,6 +1996,10 @@ QUnit.test('cleans up the buffer when loading live segments', function() {
QUnit.equal(removes.length, 1, 'remove called');
QUnit.deepEqual(removes[0], [0, seekable.start(0)],
'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.mediaRequests, 1, '1 request');
});
QUnit.test('cleans up the buffer based on currentTime when loading a live segment ' +
......@@ -1934,6 +2050,10 @@ QUnit.test('cleans up the buffer based on currentTime when loading a live segmen
QUnit.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
QUnit.equal(removes.length, 1, 'remove called');
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.mediaRequests, 1, '1 request');
});
QUnit.test('cleans up the buffer when loading VOD segments', function() {
......@@ -1969,6 +2089,10 @@ QUnit.test('cleans up the buffer when loading VOD segments', function() {
'media playlist requested');
QUnit.equal(removes.length, 1, 'remove called');
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.mediaRequests, 1, '1 request');
});
QUnit.test('when mediaGroup changes enabled track should not change', function() {
......@@ -2042,6 +2166,9 @@ QUnit.test('when mediaGroup changes enabled track should not change', function()
QUnit.equal(trackOne.enabled, false, 'track 1 - still disabled');
QUnit.equal(trackTwo.enabled, true, 'track 2 - still enabled');
QUnit.equal(trackThree.enabled, false, 'track 3 - disabled');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('Allows specifying the beforeRequest function on the player', function() {
......@@ -2062,6 +2189,9 @@ QUnit.test('Allows specifying the beforeRequest function on the player', functio
standardXHRResponse(this.requests.shift());
QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('Allows specifying the beforeRequest function globally', function() {
......@@ -2082,6 +2212,9 @@ QUnit.test('Allows specifying the beforeRequest function globally', function() {
QUnit.ok(beforeRequestCalled, 'beforeRequest was called');
delete videojs.Hls.xhr.beforeRequest;
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
});
QUnit.test('Allows overriding the global beforeRequest function', function() {
......@@ -2114,6 +2247,10 @@ QUnit.test('Allows overriding the global beforeRequest function', function() {
'for the master playlist');
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.mediaRequests, 1, 'one segment request');
});
QUnit.module('HLS Integration', {
......@@ -2151,6 +2288,26 @@ QUnit.test('aborts all in-flight work when disposed', function() {
});
});
QUnit.test('stats are reset on dispose', function() {
let hls = HlsSourceHandler('html5').handleSource({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
hls.mediaSource.trigger('sourceopen');
// master
standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
QUnit.equal(hls.stats.mediaBytesTransferred, 16, 'stat is set');
hls.dispose();
QUnit.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset');
});
QUnit.test('detects fullscreen and triggers a quality change', function() {
let qualityChanges = 0;
let hls = HlsSourceHandler('html5').handleSource({
......@@ -2215,6 +2372,11 @@ QUnit.test('downloads additional playlists if required', function() {
hls.playlists.media().resolvedUri,
'a new playlists was selected');
QUnit.ok(hls.playlists.media().segments, 'segments are now available');
// verify stats
QUnit.equal(hls.stats.bandwidth, 3000000, 'default');
QUnit.equal(hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('waits to download new segments until the media playlist is stable', function() {
......@@ -2251,6 +2413,11 @@ QUnit.test('waits to download new segments until the media playlist is stable',
standardXHRResponse(this.requests.shift());
this.clock.tick(10 * 1000);
QUnit.equal(this.requests.length, 1, 'resumes segment fetching');
// verify stats
QUnit.equal(hls.stats.bandwidth, Infinity, 'default');
QUnit.equal(hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('live playlist starts three target durations before live', function() {
......@@ -2286,6 +2453,7 @@ QUnit.test('live playlist starts three target durations before live', function()
'seeked to the seekable end');
QUnit.equal(this.requests.length, 1, 'begins buffering');
});
QUnit.module('HLS - Encryption', {
......@@ -2361,4 +2529,8 @@ QUnit.test('treats invalid keys as a key request failure and blacklists playlist
QUnit.ok(hls.playlists.media().excludeUntil > 0,
'blacklisted 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.mediaRequests, 1, '1 request');
});
......