Skip to content

Commit

Permalink
Fixes some minor issues with the BOLA rule (#4536)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsilhavy committed Aug 1, 2024
1 parent f33847c commit 5f7b4db
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 72 deletions.
4 changes: 4 additions & 0 deletions src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,10 @@ function Settings() {
'streaming.abr.rules.abandonRequestsRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES,
'streaming.abr.rules.l2ARule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES,
'streaming.abr.rules.loLPRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES,
'streaming.abr.maxBitrate.video': Events.SETTING_UPDATED_MAX_BITRATE,
'streaming.abr.maxBitrate.audio': Events.SETTING_UPDATED_MAX_BITRATE,
'streaming.abr.minBitrate.video': Events.SETTING_UPDATED_MIN_BITRATE,
'streaming.abr.minBitrate.audio': Events.SETTING_UPDATED_MIN_BITRATE,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/core/events/CoreEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class CoreEvents extends EventsBase {
this.SETTING_UPDATED_PLAYBACK_RATE_MIN = 'settingUpdatedPlaybackRateMin';
this.SETTING_UPDATED_PLAYBACK_RATE_MAX = 'settingUpdatedPlaybackRateMax';
this.SETTING_UPDATED_ABR_ACTIVE_RULES = 'settingUpdatedAbrActiveRules';
this.SETTING_UPDATED_MAX_BITRATE = 'settingUpdatedMaxBitrate';
this.SETTING_UPDATED_MIN_BITRATE = 'settingUpdatedMinBitrate';
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/streaming/controllers/AbrController.js
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,12 @@ function AbrController() {
const switchOnThreshold = bufferTimeDefault;
const switchOffThreshold = 0.5 * bufferTimeDefault;

const useBufferABR = abrRulesCollection.getBolaState(mediaType)
const newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules
abrRulesCollection.setBolaState(mediaType, newUseBufferABR);
const isUsingBolaRule = abrRulesCollection.getBolaState(mediaType)
const shouldUseBolaRule = bufferLevel >= (isUsingBolaRule ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules
abrRulesCollection.setBolaState(mediaType, shouldUseBolaRule);

if (newUseBufferABR !== useBufferABR) {
if (newUseBufferABR) {
if (shouldUseBolaRule !== isUsingBolaRule) {
if (shouldUseBolaRule) {
logger.info('[' + mediaType + '] switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
} else {
logger.info('[' + mediaType + '] switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
Expand Down
7 changes: 6 additions & 1 deletion src/streaming/models/MetricsModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import BufferLevel from '../vo/metrics/BufferLevel.js';
import BufferState from '../vo/metrics/BufferState.js';
import DVRInfo from '../vo/metrics/DVRInfo.js';
import DroppedFrames from '../vo/metrics/DroppedFrames.js';
import {ManifestUpdate, ManifestUpdateStreamInfo, ManifestUpdateRepresentationInfo} from '../vo/metrics/ManifestUpdate.js';
import {
ManifestUpdate,
ManifestUpdateStreamInfo,
ManifestUpdateRepresentationInfo
} from '../vo/metrics/ManifestUpdate.js';
import SchedulingInfo from '../vo/metrics/SchedulingInfo.js';
import EventBus from '../../core/EventBus.js';
import RequestsQueue from '../vo/metrics/RequestsQueue.js';
Expand Down Expand Up @@ -192,6 +196,7 @@ function MetricsModel(config) {
vo._serviceLocation = request.serviceLocation || null;
vo._fileLoaderType = request.fileLoaderType;
vo._resourceTimingValues = request.resourceTimingValues;
vo._streamId = request && request.representation && request.representation.mediaInfo && request.representation.mediaInfo.streamInfo ? request.representation.mediaInfo.streamInfo.id : null;

if (traces) {
traces.forEach(trace => {
Expand Down
129 changes: 70 additions & 59 deletions src/streaming/rules/abr/BolaRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ function BolaRule(config) {
eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChangeRequested, instance);
eventBus.on(MediaPlayerEvents.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance);
eventBus.on(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance);
eventBus.on(Events.SETTING_UPDATED_MAX_BITRATE, _onMinMaxBitrateUpdated, instance);
eventBus.on(Events.SETTING_UPDATED_MIN_BITRATE, _onMinMaxBitrateUpdated, instance);
}

/**
* If we rebuffer, we don't want the placeholder buffer to artificially raise BOLA quality
* If the buffer is empty, we don't want the placeholder buffer to artificially raise BOLA quality
* @param {object} e
* @private
*/
Expand Down Expand Up @@ -111,26 +113,77 @@ function BolaRule(config) {
if (bolaStateDict[streamId].hasOwnProperty(mediaType)) {
const bolaState = bolaStateDict[streamId][mediaType];
if (bolaState.state !== BOLA_STATE_ONE_BITRATE) {
bolaState.state = BOLA_STATE_STARTUP; // TODO: BOLA_STATE_SEEK?
bolaState.state = BOLA_STATE_STARTUP;
_clearBolaStateOnSeek(bolaState);
}
}
}
}

function _clearBolaStateOnSeek(bolaState) {
bolaState.placeholderBuffer = 0;
bolaState.mostAdvancedSegmentStart = NaN;
bolaState.lastSegmentWasReplacement = false;
bolaState.lastSegmentStart = NaN;
bolaState.lastSegmentDurationS = NaN;
bolaState.lastSegmentRequestTimeMs = NaN;
bolaState.lastSegmentFinishTimeMs = NaN;
}

/**
* Handle situations in which the downloaded quality differs from what the BOLA algorithm recommended
* @param e
* @private
*/
function _onMetricAdded(e) {
if (e && e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && e.value.trace && e.value.trace.length) {
const bolaState = bolaStateDict[e.streamId] && bolaStateDict[e.streamId][e.mediaType] ? bolaStateDict[e.streamId][e.mediaType] : null;
if (bolaState && bolaState.state !== BOLA_STATE_ONE_BITRATE) {
bolaState.lastSegmentRequestTimeMs = e.value.trequest.getTime();
bolaState.lastSegmentFinishTimeMs = e.value._tfinish.getTime();
_checkNewSegment(bolaState, e.mediaType);
try {
if (e && e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && e.value.trace && e.value.trace.length) {
const bolaState = bolaStateDict[e.value._streamId] && bolaStateDict[e.value._streamId][e.mediaType] ? bolaStateDict[e.value._streamId][e.mediaType] : null;
if (bolaState && bolaState.state !== BOLA_STATE_ONE_BITRATE) {
bolaState.lastSegmentRequestTimeMs = e.value.trequest.getTime();
bolaState.lastSegmentFinishTimeMs = e.value._tfinish.getTime();
_checkNewSegment(bolaState, e.mediaType);
}
}
} catch (e) {
logger.error(e);
}
}

/**
* When a new segment is downloaded, we get two notifications: onMediaFragmentLoaded() and onMetricAdded(). It is
* possible that the quality for the downloaded segment was lower (not higher) than the quality indicated by BOLA.
* This might happen because of other rules such as the DroppedFramesRule. When this happens, we trim the
* placeholder buffer to make BOLA more stable. This mechanism also avoids inflating the buffer when BOLA itself
* decides not to increase the quality to avoid oscillations.
*
* We should also check for replacement segments (fast switching). In this case, a segment is downloaded but does
* not grow the actual buffer. Fast switching might cause the buffer to deplete, causing BOLA to drop the bitrate.
* We avoid this by growing the placeholder buffer.
* @param bolaState
* @param mediaType
*/
function _checkNewSegment(bolaState, mediaType) {
if (!isNaN(bolaState.lastSegmentStart) && !isNaN(bolaState.lastSegmentRequestTimeMs) && !isNaN(bolaState.placeholderBuffer)) {
bolaState.placeholderBuffer *= PLACEHOLDER_BUFFER_DECAY;

// Find what maximum buffer corresponding to last segment was, and ensure placeholder is not relatively larger.
if (!isNaN(bolaState.lastSegmentFinishTimeMs)) {
const bufferLevel = dashMetrics.getCurrentBufferLevel(mediaType);
const bufferAtLastSegmentRequest = bufferLevel + 0.001 * (bolaState.lastSegmentFinishTimeMs - bolaState.lastSegmentRequestTimeMs); // estimate
const maxEffectiveBufferForLastSegment = _maxBufferLevelForRepresentation(bolaState, bolaState.currentRepresentation);
const maxPlaceholderBuffer = Math.max(0, maxEffectiveBufferForLastSegment - bufferAtLastSegmentRequest);
bolaState.placeholderBuffer = Math.min(maxPlaceholderBuffer, bolaState.placeholderBuffer);
}

// then see if we should grow placeholder buffer
if (bolaState.lastSegmentWasReplacement && !isNaN(bolaState.lastSegmentDurationS)) {
// compensate for segments that were downloaded but did not grow the buffer
bolaState.placeholderBuffer += bolaState.lastSegmentDurationS;
}

bolaState.lastSegmentStart = NaN;
bolaState.lastSegmentRequestTimeMs = NaN;
}
}

Expand Down Expand Up @@ -182,7 +235,7 @@ function BolaRule(config) {
}

/**
* NOTE: in live streaming, the real buffer level can drop below minimumBufferS, but bola should not stick to lowest bitrate by using a placeholder buffer level
* NOTE: in live streaming, the real buffer level can drop below minimumBufferS, but BOLA should not stick to lowest bitrate by using a placeholder buffer level
* @param bufferTimeDefault
* @param representations
* @param utilities
Expand Down Expand Up @@ -211,21 +264,6 @@ function BolaRule(config) {
return { gp: gp, Vp: Vp };
}

/**
*
* @param bolaState
* @private
*/
function _clearBolaStateOnSeek(bolaState) {
bolaState.placeholderBuffer = 0;
bolaState.mostAdvancedSegmentStart = NaN;
bolaState.lastSegmentWasReplacement = false;
bolaState.lastSegmentStart = NaN;
bolaState.lastSegmentDurationS = NaN;
bolaState.lastSegmentRequestTimeMs = NaN;
bolaState.lastSegmentFinishTimeMs = NaN;
}

/**
* If the buffer target is changed (can this happen mid-stream?), then adjust BOLA parameters accordingly.
* @param bolaState
Expand Down Expand Up @@ -268,7 +306,7 @@ function BolaRule(config) {
let quality = NaN;
let score = NaN;
for (let i = 0; i < bitrateCount; ++i) {
let s = (bolaState.Vp * (bolaState.utilities[i] + bolaState.gp) - bufferLevel) / bolaState.representations[i].bandwidth;
let s = (bolaState.Vp * (bolaState.utilities[i] - 1 + bolaState.gp) - bufferLevel) / bolaState.representations[i].bandwidth;
if (isNaN(score) || s >= score) {
score = s;
quality = i;
Expand Down Expand Up @@ -348,43 +386,14 @@ function BolaRule(config) {
}

/**
* When a new segment is downloaded, we get two notifications: onMediaFragmentLoaded() and onMetricAdded(). It is
* possible that the quality for the downloaded segment was lower (not higher) than the quality indicated by BOLA.
* This might happen because of other rules such as the DroppedFramesRule. When this happens, we trim the
* placeholder buffer to make BOLA more stable. This mechanism also avoids inflating the buffer when BOLA itself
* decides not to increase the quality to avoid oscillations.
*
* We should also check for replacement segments (fast switching). In this case, a segment is downloaded but does
* not grow the actual buffer. Fast switching might cause the buffer to deplete, causing BOLA to drop the bitrate.
* We avoid this by growing the placeholder buffer.
* @param bolaState
* @param mediaType
* We need to reset the Bola State once the min/max bitrate settings have been updated. Otherwise, the utility function works on outdated values
* @private
*/
function _checkNewSegment(bolaState, mediaType) {
if (!isNaN(bolaState.lastSegmentStart) && !isNaN(bolaState.lastSegmentRequestTimeMs) && !isNaN(bolaState.placeholderBuffer)) {
bolaState.placeholderBuffer *= PLACEHOLDER_BUFFER_DECAY;

// Find what maximum buffer corresponding to last segment was, and ensure placeholder is not relatively larger.
if (!isNaN(bolaState.lastSegmentFinishTimeMs)) {
const bufferLevel = dashMetrics.getCurrentBufferLevel(mediaType);
const bufferAtLastSegmentRequest = bufferLevel + 0.001 * (bolaState.lastSegmentFinishTimeMs - bolaState.lastSegmentRequestTimeMs); // estimate
const maxEffectiveBufferForLastSegment = _maxBufferLevelForRepresentation(bolaState, bolaState.currentRepresentation);
const maxPlaceholderBuffer = Math.max(0, maxEffectiveBufferForLastSegment - bufferAtLastSegmentRequest);
bolaState.placeholderBuffer = Math.min(maxPlaceholderBuffer, bolaState.placeholderBuffer);
}

// then see if we should grow placeholder buffer

if (bolaState.lastSegmentWasReplacement && !isNaN(bolaState.lastSegmentDurationS)) {
// compensate for segments that were downloaded but did not grow the buffer
bolaState.placeholderBuffer += bolaState.lastSegmentDurationS;
}

bolaState.lastSegmentStart = NaN;
bolaState.lastSegmentRequestTimeMs = NaN;
}
function _onMinMaxBitrateUpdated() {
resetInitialSettings()
}


/**
* The minimum buffer level that would cause BOLA to choose target quality rather than a lower bitrate
* @param bolaState
Expand Down Expand Up @@ -606,6 +615,8 @@ function BolaRule(config) {
eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChangeRequested, instance);
eventBus.off(MediaPlayerEvents.FRAGMENT_LOADING_ABANDONED, _onFragmentLoadingAbandoned, instance);
eventBus.off(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance);
eventBus.on(Events.SETTING_UPDATED_MAX_BITRATE, _onMinMaxBitrateUpdated, instance);
eventBus.on(Events.SETTING_UPDATED_MIN_BITRATE, _onMinMaxBitrateUpdated, instance);
}

instance = {
Expand Down
19 changes: 12 additions & 7 deletions test/functional/config/test-configurations/streams/all.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,25 +287,30 @@
]
},
{
"name": "1080p with PlayReady and Widevine DRM, single key",
"name": "Single-period, 1080p, H.264, 5 video, 3 audio, 3 text tracks, CMAF, cbcs encryption, single key, Widevine+PlayReady",
"type": "vod",
"url": "https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd",
"url": "https://media.axprod.net/TestVectors/Dash/protected_dash_1080p_h264_singlekey/manifest.mpd",
"drm": {
"com.widevine.alpha": {
"serverURL": "https://drm-widevine-licensing.axtest.net/AcquireLicense",
"serverURL": "https://drm-widevine-licensing.axprod.net/AcquireLicense",
"httpRequestHeaders": {
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.4lWwW46k-oWcah8oN18LPj5OLS5ZU-_AQv7fe0JhNjA"
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw"
},
"httpTimeout": 5000
},
"com.microsoft.playready": {
"serverURL": "https://drm-playready-licensing.axtest.net/AcquireLicense",
"serverURL": "https://drm-playready-licensing.axprod.net/AcquireLicense",
"httpRequestHeaders": {
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.4lWwW46k-oWcah8oN18LPj5OLS5ZU-_AQv7fe0JhNjA"
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw"
},
"httpTimeout": 5000
}
}
},
"includedTestfiles": [
"playback/*",
"audio/initial-audio",
"text/*"
]
},
{
"name": "1080p with W3C Clear Key, single key",
Expand Down

0 comments on commit 5f7b4db

Please sign in to comment.