Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamically choose between ThroughputRule and BolaRule #2083

Merged
merged 8 commits into from
Aug 17, 2017
6 changes: 3 additions & 3 deletions samples/dash-if-reference-player/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ app.controller('DashController', function ($scope, sources, contributors) {
$scope.scheduleWhilePausedSelected = true;
$scope.localStorageSelected = true;
$scope.fastSwitchSelected = true;
$scope.bolaSelected = false;
$scope.ABRStrategy = "abrDynamic";
////////////////////////////////////////
//
// Player Setup
Expand Down Expand Up @@ -483,8 +483,8 @@ app.controller('DashController', function ($scope, sources, contributors) {
$scope.player.setAutoPlay($scope.autoPlaySelected);
};

$scope.toggleBufferOccupancyABR = function () {
$scope.player.enableBufferOccupancyABR($scope.bolaSelected);
$scope.changeABRStrategy = function (strategy) {
$scope.player.setABRStrategy(strategy);
};

$scope.toggleUseCustomABRRules = function () {
Expand Down
22 changes: 16 additions & 6 deletions samples/dash-if-reference-player/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,25 @@
<input type="checkbox" id="fastSwitchCB" ng-model="fastSwitchSelected" ng-change="toggleFastSwitch()" ng-checked="fastSwitchSelected">
Fast Switching ABR
</label>
<label class="topcoat-checkbox" data-toggle="tooltip" data-placement="right"
title="BOLA is an ABR ruleset. When enabled, it will disable the default heuristics in Dash.js that depend strictly on average and real-time throughput measurements">
<input type="checkbox" id="bolaCB" ng-model="bolaSelected" ng-change="toggleBufferOccupancyABR()" ng-checked="bolaSelected">
Buffer Occupancy ABR
<label data-toggle="tooltip" data-placement="right"
title="Dynamically switch between BOLA and Throughput strategies.">
<input type="radio" id="abrDynamic" autocomplete="off" name="abrStrategy" checked="checked" ng-click="changeABRStrategy('abrDynamic')" >
ABR Strategy: Dynamic
</label>
<label data-toggle="tooltip" data-placement="right"
title="Choose bitrate based on buffer level.">
<input type="radio" id="abrBola" autocomplete="off" name="abrStrategy" ng-click="changeABRStrategy('abrBola')">
ABR Strategy: BOLA
</label>
<label data-toggle="tooltip" data-placement="right"
title="Choose bitrate based on recent throughput.">
<input type="radio" id="abrThroughput" autocomplete="off" name="abrStrategy" ng-click="changeABRStrategy('abrThroughput')">
ABR Strategy: Throughput
</label>
<label class="topcoat-checkbox" data-toggle="tooltip" data-placement="right"
title="ABR - Use custom abr rules">
title="ABR - Use custom ABR rules">
<input type="checkbox" id="customABRRules" ng-model="customABRRulesSelected" ng-change="toggleUseCustomABRRules()" ng-checked="customABRRulesSelected">
ABR - Use custom abr rules
Use Custom ABR Rules
</label>
</div>
</div>
Expand Down
57 changes: 43 additions & 14 deletions src/streaming/MediaPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1102,20 +1102,49 @@ function MediaPlayer() {
}

/**
* Enabling buffer-occupancy ABR will switch to the *experimental* implementation of BOLA,
* replacing the throughput-based ABR rule set (ThroughputRule, BufferOccupancyRule,
* InsufficientBufferRule and AbandonRequestsRule) with the buffer-occupancy-based
* BOLA rule set (BolaRule, BolaAbandonRule).
* Obsolete since version 2.6.0.
* Buffer-occupancy ABR is now switched on and off dynamically.
* @see {@link module:MediaPlayer#setABRStrategy setABRStrategy()}
*
* @see {@link http://arxiv.org/abs/1601.06748 BOLA WhitePaper.}
* @see {@link https://github.com/Dash-Industry-Forum/dash.js/wiki/BOLA-status More details about the implementation status.}
* @param {boolean} value
* @default false
* @memberof module:MediaPlayer
* @instance
*/
function enableBufferOccupancyABR(value) {
mediaPlayerModel.setBufferOccupancyABREnabled(value);
throw new Error('Calling obsolete function - enabledBufferOccupancyABR(' + value + ') has no effect.');
}

/**
* Sets the ABR strategy. Valid strategies are "abrDynamic", "abrBola" and "abrThroughput".
* The ABR strategy can also be changed during a streaming session.
* The call has no effect if an invalid method is passed.
*
* The BOLA strategy chooses bitrate based on current buffer level, with higher bitrates for higher buffer levels.
* The Throughput strategy chooses bitrate based on the recent throughput history.
* The Dynamic strategy switches smoothly between BOLA and Throughput in real time, playing to the strengths of both.
*
* @param {string} value
* @default "abrDynamic"
* @memberof module:MediaPlayer
* @instance
*/
function setABRStrategy(value) {
if (value === Constants.ABR_STRATEGY_DYNAMIC || value === Constants.ABR_STRATEGY_BOLA || value === Constants.ABR_STRATEGY_THROUGHPUT) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it time to create a public facing TYPES object accessible from outside Dash.js?

mediaPlayerModel.setABRStrategy(value);
} else {
log('Warning: Ignoring setABRStrategy(' + value + ') - unknown value.');
}
}

/**
* Returns the current ABR strategy being used.
* @return {string} "abrDynamic", "abrBola" or "abrThroughput"
* @see {@link module:MediaPlayer#setABRStrategy setABRStrategy()}
* @memberof module:MediaPlayer
* @instance
*/
function getABRStrategy() {
return mediaPlayerModel.getABRStrategy();
}

/**
Expand Down Expand Up @@ -1403,18 +1432,16 @@ function MediaPlayer() {
}

/**
* A threshold, in seconds, of when dashjs abr becomes less conservative since we have a
* larger "rich" buffer.
* The BufferOccupancyRule.js rule will override the ThroughputRule's decision when the
* buffer level surpasses this value and while it remains greater than this value.
* Obsolete since version 2.6.0.
* ABR rules now switch from Throughput to Buffer Occupancy mode when there is sufficient buffer.
* This renders the rich buffer mechanism redundant.
*
* @default 20 seconds
* @param {number} value
* @memberof module:MediaPlayer
* @instance
*/
function setRichBufferThreshold(value) {
mediaPlayerModel.setRichBufferThreshold(value);
throw new Error('Calling obsolete function - setRichBufferThreshold(' + value + ') has no effect.');
}

/**
Expand Down Expand Up @@ -2470,6 +2497,8 @@ function MediaPlayer() {
getAutoSwitchQualityFor: getAutoSwitchQualityFor,
setAutoSwitchQualityFor: setAutoSwitchQualityFor,
enableBufferOccupancyABR: enableBufferOccupancyABR,
setABRStrategy: setABRStrategy,
getABRStrategy: getABRStrategy,
useDefaultABRRules: useDefaultABRRules,
addABRCustomRule: addABRCustomRule,
removeABRCustomRule: removeABRCustomRule,
Expand Down
3 changes: 3 additions & 0 deletions src/streaming/constants/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class Constants {
this.SUGGESTED_PRESENTATION_DELAY = 'suggestedPresentationDelay';
this.SCHEME_ID_URI = 'schemeIdUri';
this.START_TIME = 'starttime';
this.ABR_STRATEGY_DYNAMIC = 'abrDynamic';
this.ABR_STRATEGY_BOLA = 'abrBola';
this.ABR_STRATEGY_THROUGHPUT = 'abrThroughput';
}

constructor () {
Expand Down
65 changes: 44 additions & 21 deletions src/streaming/controllers/AbrController.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@

import ABRRulesCollection from '../rules/abr/ABRRulesCollection';
import Constants from '../constants/Constants';
import MetricsConstants from '../constants/MetricsConstants';
import BitrateInfo from '../vo/BitrateInfo';
import FragmentModel from '../models/FragmentModel';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import MediaPlayerEvents from '../MediaPlayerEvents.js';
import FactoryMaker from '../../core/FactoryMaker';
import RulesContext from '../rules/RulesContext.js';
import SwitchRequest from '../rules/SwitchRequest.js';
Expand Down Expand Up @@ -84,6 +84,7 @@ function AbrController() {
switchHistoryDict,
droppedFramesHistory,
throughputHistory,
isUsingBufferOccupancyABRDict,
metricsModel,
dashMetrics,
useDeadTimeLatency;
Expand Down Expand Up @@ -111,13 +112,14 @@ function AbrController() {
streamProcessorDict[type] = streamProcessor;
abandonmentStateDict[type] = abandonmentStateDict[type] || {};
abandonmentStateDict[type].state = ALLOW_LOAD;
isUsingBufferOccupancyABRDict[type] = false;
eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, this);
if (type == Constants.VIDEO) {
eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, this);
eventBus.on(Events.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, this);
droppedFramesHistory = DroppedFramesHistory(context).create();
setElementSize();
}
eventBus.on(MediaPlayerEvents.METRIC_ADDED, onMetricAdded, this);
eventBus.on(Events.METRIC_ADDED, onMetricAdded, this);
throughputHistory = ThroughputHistory().create({
mediaPlayerModel: mediaPlayerModel
});
Expand All @@ -144,15 +146,16 @@ function AbrController() {
abandonmentStateDict = {};
streamProcessorDict = {};
switchHistoryDict = {};
isUsingBufferOccupancyABRDict = {};
limitBitrateByPortal = false;
useDeadTimeLatency = true;
usePixelRatioInLimitBitrateByPortal = false;
if (windowResizeEventCalled === undefined) {
windowResizeEventCalled = false;
}
eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, this);
eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, this);
eventBus.off(MediaPlayerEvents.METRIC_ADDED, onMetricAdded, this);
eventBus.off(Events.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, this);
eventBus.off(Events.METRIC_ADDED, onMetricAdded, this);
playbackIndex = undefined;
droppedFramesHistory = undefined;
throughputHistory = undefined;
Expand Down Expand Up @@ -203,9 +206,13 @@ function AbrController() {
}

function onMetricAdded(e) {
if (e.metric === 'HttpList' && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
if (e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
throughputHistory.push(e.mediaType, e.value, useDeadTimeLatency);
}

if (e.metric === MetricsConstants.BUFFER_LEVEL && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
updateIsUsingBufferOccupancyABR(e.mediaType, 0.001 * e.value.level);
}
}

function getTopQualityIndexFor(type, id) {
Expand Down Expand Up @@ -354,7 +361,7 @@ function AbrController() {
currentValue: oldQuality,
switchHistory: switchHistoryDict[type],
droppedFramesHistory: droppedFramesHistory,
hasRichBuffer: hasRichBuffer(type)
useBufferOccupancyABR: useBufferOccupancyABR(type)
});

if (droppedFramesHistory) {
Expand Down Expand Up @@ -395,7 +402,6 @@ function AbrController() {
changeQuality(type, oldQuality, newQuality, topQualityIdx, reason);
}
}

function changeQuality(type, oldQuality, newQuality, topQualityIdx, reason) {
if (type && streamProcessorDict[type]) {
var streamInfo = streamProcessorDict[type].getStreamInfo();
Expand Down Expand Up @@ -478,20 +484,37 @@ function AbrController() {
return infoList;
}

function hasRichBuffer(type) {
const metrics = metricsModel.getReadOnlyMetricsFor(type);
const bufferLevel = dashMetrics.getCurrentBufferLevel(metrics);
const bufferState = (metrics.BufferState.length > 0) ? metrics.BufferState[metrics.BufferState.length - 1] : null;
let isBufferRich = false;
function updateIsUsingBufferOccupancyABR(mediaType, bufferLevel) {
const strategy = mediaPlayerModel.getABRStrategy();

if (strategy === Constants.ABR_STRATEGY_BOLA) {
isUsingBufferOccupancyABRDict[mediaType] = true;
return;
} else if (strategy === Constants.ABR_STRATEGY_THROUGHPUT) {
isUsingBufferOccupancyABRDict[mediaType] = false;
return;
}
// else ABR_STRATEGY_DYNAMIC

let stableBufferTime = mediaPlayerModel.getStableBufferTime();
let switchOnThreshold = stableBufferTime;
let switchOffThreshold = 0.5 * stableBufferTime;

let useBufferABR = isUsingBufferOccupancyABRDict[mediaType];
let newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules
isUsingBufferOccupancyABRDict[mediaType] = newUseBufferABR;

// This will happen when another rule tries to switch down from highest quality index
// If there is enough buffer why not try to stay at high level
if (bufferState && bufferLevel > bufferState.target) {
// Are we currently over the buffer target by at least RICH_BUFFER_THRESHOLD?
isBufferRich = bufferLevel > ( bufferState.target + mediaPlayerModel.getRichBufferThreshold() );
if (newUseBufferABR !== useBufferABR) {
if (newUseBufferABR) {
log('AbrController (' + mediaType + ') switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
} else {
log('AbrController (' + mediaType + ') switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
}
}
}

return isBufferRich;
function useBufferOccupancyABR(mediaType) {
return isUsingBufferOccupancyABRDict[mediaType];
}

function getThroughputHistory() {
Expand Down Expand Up @@ -632,7 +655,7 @@ function AbrController() {
abrController: instance,
streamProcessor: streamProcessor,
currentRequest: e.request,
hasRichBuffer: hasRichBuffer(type)
useBufferOccupancyABR: useBufferOccupancyABR(type)
});
let switchRequest = abrRulesCollection.shouldAbandonFragment(rulesContext);
//Removed overrideFunc
Expand All @@ -650,7 +673,7 @@ function AbrController() {
switchHistoryDict[type].reset();
switchHistoryDict[type].push({oldValue: getQualityFor(type, streamController.getActiveStreamInfo()), newValue: switchRequest.quality, confidence: 1, reason: switchRequest.reason});
setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.quality, switchRequest.reason);
eventBus.trigger(Events.FRAGMENT_LOADING_ABANDONED, {streamProcessor: streamProcessorDict[type], request: request, mediaType: type});
eventBus.trigger(Events.FRAGMENT_LOADING_ABANDONED, {streamProcessor: streamProcessorDict[type], request: request, mediaType: type, newQuality: switchRequest.quality});

clearTimeout(abandonmentTimeout);
abandonmentTimeout = setTimeout(
Expand Down
Loading