From cc311ea3bf3a46968332e553ab279967e4054ff9 Mon Sep 17 00:00:00 2001 From: Brandon Casey Date: Fri, 27 May 2016 11:56:51 -0400 Subject: [PATCH 1/2] 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 --- src/master-playlist-controller.js | 37 ++++- src/segment-loader.js | 18 ++- src/videojs-contrib-hls.js | 20 ++- test/master-playlist-controller.test.js | 58 ++++++++ test/segment-loader.test.js | 87 +++++++++++- test/videojs-contrib-hls.test.js | 172 ++++++++++++++++++++++++ 6 files changed, 386 insertions(+), 6 deletions(-) diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index 2ff89698d..fecd1734e 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -169,6 +169,40 @@ export default class MasterPlaylistController extends videojs.EventTarget { this.masterPlaylistLoader_.load(); } + /** + * 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); } } diff --git a/src/segment-loader.js b/src/segment-loader.js index 79d57f076..8a2fe6f5b 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -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; @@ -152,6 +152,17 @@ export default class SegmentLoader extends videojs.EventTarget { this.hls_ = settings.hls; } + /** + * 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 */ @@ -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); diff --git a/src/videojs-contrib-hls.js b/src/videojs-contrib-hls.js index c8fadd520..6d5f62bda 100644 --- a/src/videojs-contrib-hls.js +++ b/src/videojs-contrib-hls.js @@ -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(); } } diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index 1ce0a5437..157510b27 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -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', diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index c86b82bb3..ec0edfec7 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -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() { diff --git a/test/videojs-contrib-hls.test.js b/test/videojs-contrib-hls.test.js index f4880b359..8c00e61ca 100644 --- a/test/videojs-contrib-hls.test.js +++ b/test/videojs-contrib-hls.test.js @@ -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'); }); From e45497bc45a3f9356c6e8c5a2de51a27bc2f1f7a Mon Sep 17 00:00:00 2001 From: Jon-Carlos Rivera Date: Fri, 27 May 2016 18:52:20 -0400 Subject: [PATCH 2/2] Percent buffered calculation now works correctly when the segment is not buffered and currentTime is within the segment (#715) --- src/ranges.js | 48 +++++++++++++++++++++++++++++++++++-------- test/ranges.test.js | 50 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/ranges.js b/src/ranges.js index a841d0ac2..926cb44a4 100644 --- a/src/ranges.js +++ b/src/ranges.js @@ -198,27 +198,58 @@ const bufferIntersection = function(bufferA, bufferB) { * covers adjusted according to currentTime * @param {TimeRanges} referenceRange - the original time range that the * segment covers + * @param {Number} currentTime - time in seconds where the current playback + * is at * @param {TimeRanges} buffered - the currently buffered time ranges * @returns {Number} percent of the segment currently buffered */ -const calculateBufferedPercent = function(segmentRange, referenceRange, buffered) { +const calculateBufferedPercent = function(adjustedRange, + referenceRange, + currentTime, + buffered) { let referenceDuration = referenceRange.end(0) - referenceRange.start(0); - let segmentDuration = segmentRange.end(0) - segmentRange.start(0); - let intersection = bufferIntersection(segmentRange, buffered); - let count = intersection.length; + let adjustedDuration = adjustedRange.end(0) - adjustedRange.start(0); + let bufferMissingFromAdjusted = referenceDuration - adjustedDuration; + let adjustedIntersection = bufferIntersection(adjustedRange, buffered); + let referenceIntersection = bufferIntersection(referenceRange, buffered); + let adjustedOverlap = 0; + let referenceOverlap = 0; + + let count = adjustedIntersection.length; while (count--) { - segmentDuration -= intersection.end(count) - intersection.start(count); + adjustedOverlap += adjustedIntersection.end(count) - + adjustedIntersection.start(count); + + // If the current overlap segment starts at currentTime, then increase the + // overlap duration so that it actually starts at the beginning of referenceRange + // by including the difference between the two Range's durations + // This is a work around for the way Flash has no buffer before currentTime + if (adjustedIntersection.start(count) === currentTime) { + adjustedOverlap += bufferMissingFromAdjusted; + } } - return (referenceDuration - segmentDuration) / referenceDuration * 100; + + count = referenceIntersection.length; + + while (count--) { + referenceOverlap += referenceIntersection.end(count) - + referenceIntersection.start(count); + } + + // Use whichever value is larger for the percentage-buffered since that value + // is likely more accurate because the only way + return Math.max(adjustedOverlap, referenceOverlap) / referenceDuration * 100; }; /** - * Return the amount of a segment specified by the mediaIndex overlaps - * the current buffered content. + * Return the amount of a range specified by the startOfSegment and segmentDuration + * overlaps the current buffered content. * * @param {Number} startOfSegment - the time where the segment begins * @param {Number} segmentDuration - the duration of the segment in seconds + * @param {Number} currentTime - time in seconds where the current playback + * is at * @param {TimeRanges} buffered - the state of the buffer * @returns {Number} percentage of the segment's time range that is * already in `buffered` @@ -254,6 +285,7 @@ const getSegmentBufferedPercent = function(startOfSegment, let percent = calculateBufferedPercent(adjustedSegmentRange, originalSegmentRange, + currentTime, buffered); // If the segment is reported as having a zero duration, return 0% diff --git a/test/ranges.test.js b/test/ranges.test.js index 1ec7f07ba..7058b028d 100644 --- a/test/ranges.test.js +++ b/test/ranges.test.js @@ -94,7 +94,7 @@ QUnit.test('detects time range end-point changed by updates', function() { QUnit.module('Segment Percent Buffered Calculations'); -QUnit.test('calculates the percent buffered for segments', function() { +QUnit.test('calculates the percent buffered for segments in the simple case', function() { let segmentStart = 10; let segmentDuration = 10; let currentTime = 0; @@ -108,8 +108,8 @@ QUnit.test('calculates the percent buffered for segments', function() { QUnit.equal(percentBuffered, 40, 'calculated the buffered amount correctly'); }); -QUnit.test('calculates the percent buffered for segments taking into account ' + - 'currentTime', function() { +QUnit.test('consider the buffer before currentTime to be filled if the segement begins at ' + + 'or before the currentTime', function() { let segmentStart = 10; let segmentDuration = 10; let currentTime = 15; @@ -123,6 +123,21 @@ QUnit.test('calculates the percent buffered for segments taking into account ' + QUnit.equal(percentBuffered, 90, 'calculated the buffered amount correctly'); }); +QUnit.test('does not consider the buffer before currentTime as filled if the segment ' + + 'begins after the currentTime', function() { + let segmentStart = 10; + let segmentDuration = 10; + let currentTime = 18; + let buffered = createTimeRanges([[19, 30]]); + let percentBuffered = Ranges.getSegmentBufferedPercent( + segmentStart, + segmentDuration, + currentTime, + buffered); + + QUnit.equal(percentBuffered, 10, 'calculated the buffered amount correctly'); +}); + QUnit.test('calculates the percent buffered for segments with multiple buffered ' + 'regions', function() { let segmentStart = 10; @@ -166,3 +181,32 @@ QUnit.test('calculates the percent buffered as 0 for zero-length segments', func QUnit.equal(percentBuffered, 0, 'calculated the buffered amount correctly'); }); + +QUnit.test('calculates the percent buffered as 0 for segments that do not overlap ' + + 'buffered regions taking into account currentTime', function() { + let segmentStart = 10; + let segmentDuration = 10; + let currentTime = 19; + let buffered = createTimeRanges([[20, 30]]); + let percentBuffered = Ranges.getSegmentBufferedPercent( + segmentStart, + segmentDuration, + currentTime, + buffered); + + QUnit.equal(percentBuffered, 0, 'calculated the buffered amount correctly'); +}); + +QUnit.test('calculates the percent buffered for segments that end before currentTime', function() { + let segmentStart = 10; + let segmentDuration = 10; + let currentTime = 19.6; + let buffered = createTimeRanges([[0, 19.5]]); + let percentBuffered = Ranges.getSegmentBufferedPercent( + segmentStart, + segmentDuration, + currentTime, + buffered); + + QUnit.equal(percentBuffered, 95, 'calculated the buffered amount correctly'); +});