Skip to content

Commit

Permalink
feat: add experimental buffer based ABR (#886)
Browse files Browse the repository at this point in the history
This adds a new option `experimentalBufferBasedABR` which turns it on. It uses a moving average playlist selector along with forward buffer checks that gate up and down switching. Right now it is turned off, and we are likely to fiddle with the implementation of this so it should not be relied on right now. We hope to eventually ship what this turns into as the new default ABR for VHS.

Co-authored-by: brandonocasey <brandonocasey@gmail.com>
  • Loading branch information
gkatsev and brandonocasey authored Sep 30, 2020
1 parent 89ead2c commit a05d032
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 72 deletions.
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ <h3>Options</h3>
<input id=partial type="checkbox">
Handle Partial (reloads player)
</label>
<label>
<input id=buffer-water type="checkbox">
[EXPERIMENTAL] Use Buffer Level for ABR (reloads player)
</label>
</div>

<h3>Load a URL</h3>
Expand Down
24 changes: 13 additions & 11 deletions scripts/index-demo-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
representationsEl.selectedIndex = selectedIndex;
};

['debug', 'autoplay', 'muted', 'minified', 'liveui', 'partial', 'url', 'type', 'keysystems'].forEach(function(name) {
['debug', 'autoplay', 'muted', 'minified', 'liveui', 'partial', 'url', 'type', 'keysystems', 'buffer-water'].forEach(function(name) {
stateEls[name] = document.getElementById(name);
});

Expand Down Expand Up @@ -251,13 +251,15 @@
stateEls.partial.addEventListener('change', function(event) {
saveState();

window.videojs.options = window.videojs.options || {};
window.videojs.options.vhs = window.videojs.options.vhs || {};
window.videojs.options.vhs.handlePartialData = event.target.checked;
// reload the player and scripts
stateEls.minified.dispatchEvent(newEvent('change'));
});

if (window.player) {
window.player.src(window.player.currentSource());
}
stateEls['buffer-water'].addEventListener('change', function(event) {
saveState();

// reload the player and scripts
stateEls.minified.dispatchEvent(newEvent('change'));
});

stateEls.liveui.addEventListener('change', function(event) {
Expand Down Expand Up @@ -296,8 +298,6 @@
videoEl.className = 'vjs-default-skin';
fixture.appendChild(videoEl);

stateEls.partial.dispatchEvent(newEvent('change'));

player = window.player = window.videojs(videoEl, {
plugins: {
httpSourceSelector: {
Expand All @@ -307,7 +307,9 @@
liveui: stateEls.liveui.checked,
html5: {
vhs: {
overrideNative: true
overrideNative: true,
handlePartialData: getInputValue(stateEls.partial),
experimentalBufferBasedABR: getInputValue(stateEls['buffer-water'])
}
}
});
Expand All @@ -329,7 +331,7 @@
sources.dispatchEvent(newEvent('change'));
}
player.on('loadedmetadata', function() {
if (player.vhs) {
if (player.tech_.vhs) {
window.vhs = player.tech_.vhs;
window.mpc = player.tech_.vhs.masterPlaylistController_;
window.mpc.masterPlaylistLoader_.on('mediachange', regenerateRepresentations);
Expand Down
8 changes: 7 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,11 @@ export default {
// How much of the buffer must be filled before we consider upswitching
BUFFER_LOW_WATER_LINE: 0,
MAX_BUFFER_LOW_WATER_LINE: 30,
BUFFER_LOW_WATER_LINE_RATE: 1

// TODO: Remove this when useBufferWaterLines is removed
EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16,

BUFFER_LOW_WATER_LINE_RATE: 1,
// If the buffer is greater than the high water line, we won't switch down
BUFFER_HIGH_WATER_LINE: 30
};
130 changes: 105 additions & 25 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ const shouldSwitchToMedia = function({
nextPlaylist,
forwardBuffer,
bufferLowWaterLine,
bufferHighWaterLine,
duration,
experimentalBufferBasedABR,
log
}) {
// we have no other playlist to switch to
Expand All @@ -58,31 +60,61 @@ const shouldSwitchToMedia = function({
return false;
}

const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;

// If the playlist is live, then we want to not take low water line into account.
// This is because in LIVE, the player plays 3 segments from the end of the
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
// in those segments, a viewer will never experience a rendition upswitch.
if (!currentPlaylist.endList) {
if (!currentPlaylist || !currentPlaylist.endList) {
log(`${sharedLogLine} as current playlist ` + (!currentPlaylist ? 'is not set' : 'is live'));
return true;
}

// no need to switch playlist is the same
if (nextPlaylist.id === currentPlaylist.id) {
return false;
}

const maxBufferLowWaterLine = experimentalBufferBasedABR ?
Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;

// For the same reason as LIVE, we ignore the low water line when the VOD
// duration is below the max potential low water line
if (duration < Config.MAX_BUFFER_LOW_WATER_LINE) {
if (duration < maxBufferLowWaterLine) {
log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`);
return true;
}

// we want to switch down to lower resolutions quickly to continue playback, but
if (nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH) {
const nextBandwidth = nextPlaylist.attributes.BANDWIDTH;
const currBandwidth = currentPlaylist.attributes.BANDWIDTH;

// when switching down, if our buffer is lower than the high water line,
// we can switch down
if (nextBandwidth < currBandwidth && (!experimentalBufferBasedABR || forwardBuffer < bufferHighWaterLine)) {
let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`;

if (experimentalBufferBasedABR) {
logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`;
}
log(logLine);
return true;
}

// ensure we have some buffer before we switch up to prevent us running out of
// buffer while loading a higher rendition.
if (forwardBuffer >= bufferLowWaterLine) {
// and if our buffer is higher than the low water line,
// we can switch up
if ((!experimentalBufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) {
let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`;

if (experimentalBufferBasedABR) {
logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`;
}
log(logLine);
return true;
}

log(`not ${sharedLogLine} as no switching criteria met`);

return false;
};

Expand Down Expand Up @@ -111,7 +143,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
enableLowInitialPlaylist,
sourceType,
cacheEncryptionKeys,
handlePartialData
handlePartialData,
experimentalBufferBasedABR
} = options;

if (!src) {
Expand All @@ -120,6 +153,7 @@ export class MasterPlaylistController extends videojs.EventTarget {

Vhs = externVhs;

this.experimentalBufferBasedABR = Boolean(experimentalBufferBasedABR);
this.withCredentials = withCredentials;
this.tech_ = tech;
this.vhs_ = tech.vhs;
Expand Down Expand Up @@ -318,7 +352,12 @@ export class MasterPlaylistController extends videojs.EventTarget {
selectedMedia = this.selectPlaylist();
}

if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) {
return;
}

this.initialMedia_ = selectedMedia;

this.masterPlaylistLoader_.media(this.initialMedia_);

// Under the standard case where a source URL is provided, loadedplaylist will
Expand Down Expand Up @@ -478,6 +517,27 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.tech_.trigger({type: 'usage', name: 'hls-playlist-cue-tags'});
}
}

shouldSwitchToMedia_(nextPlaylist) {
const currentPlaylist = this.masterPlaylistLoader_.media();
const buffered = this.tech_.buffered();
const forwardBuffer = buffered.length ?
buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0;

const bufferLowWaterLine = this.bufferLowWaterLine();
const bufferHighWaterLine = this.bufferHighWaterLine();

return shouldSwitchToMedia({
currentPlaylist,
nextPlaylist,
forwardBuffer,
bufferLowWaterLine,
bufferHighWaterLine,
duration: this.duration(),
experimentalBufferBasedABR: this.experimentalBufferBasedABR,
log: this.logger_
});
}
/**
* Register event handlers on the segment loaders. A helper function
* for construction time.
Expand All @@ -487,27 +547,23 @@ export class MasterPlaylistController extends videojs.EventTarget {
setupSegmentLoaderListeners_() {
this.mainSegmentLoader_.on('bandwidthupdate', () => {
const nextPlaylist = this.selectPlaylist();
const currentPlaylist = this.masterPlaylistLoader_.media();
const buffered = this.tech_.buffered();
const forwardBuffer = buffered.length ?
buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0;

const bufferLowWaterLine = this.bufferLowWaterLine();

if (shouldSwitchToMedia({
currentPlaylist,
nextPlaylist,
forwardBuffer,
bufferLowWaterLine,
duration: this.duration(),
log: this.logger_
})) {

if (this.shouldSwitchToMedia_(nextPlaylist)) {
this.masterPlaylistLoader_.media(nextPlaylist);
}

this.tech_.trigger('bandwidthupdate');
});

this.mainSegmentLoader_.on('progress', () => {
if (this.experimentalBufferBasedABR) {
const nextPlaylist = this.selectPlaylist();

if (this.shouldSwitchToMedia_(nextPlaylist)) {
this.masterPlaylistLoader_.media(nextPlaylist);
}
}

this.trigger('progress');
});

Expand Down Expand Up @@ -542,7 +598,26 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.onEndOfStream();
});

this.mainSegmentLoader_.on('earlyabort', () => {
this.mainSegmentLoader_.on('earlyabort', (event) => {
if (this.experimentalBufferBasedABR) {
const currentPlaylist = this.masterPlaylistLoader_.media();

// temporarily exclude the current playlist so that we can
// determine the next playlist that would be selected
// if this playlist were to be excluded.
currentPlaylist.excludeUntil = Infinity;

const nextPlaylist = this.selectPlaylist();

// un-exclude the current playlist for now
currentPlaylist.excludeUntil = null;

// if we shouldn't switch to the next playlist, do nothing
if (!this.shouldSwitchToMedia_(nextPlaylist)) {
this.logger_(`earlyabort triggered, but we will not be switching from ${currentPlaylist.id} -> ${nextPlaylist.id}.`);
return;
}
}
this.blacklistCurrentPlaylist({
message: 'Aborted early because there isn\'t enough bandwidth to complete the ' +
'request without rebuffering.'
Expand Down Expand Up @@ -1633,8 +1708,13 @@ export class MasterPlaylistController extends videojs.EventTarget {
const initial = Config.BUFFER_LOW_WATER_LINE;
const rate = Config.BUFFER_LOW_WATER_LINE_RATE;
const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE);

return Math.min(initial + currentTime * rate, max);
return Math.min(initial + currentTime * rate, this.experimentalBufferBasedABR ? newMax : max);
}

bufferHighWaterLine() {
return Config.BUFFER_HIGH_WATER_LINE;
}

}
Loading

0 comments on commit a05d032

Please sign in to comment.