Skip to content

Commit

Permalink
Refactor the SwitchHistoryRule.js and allow parameter configuration v… (
Browse files Browse the repository at this point in the history
#4379)

* Refactor the SwitchHistoryRule.js and allow parameter configuration via Settings.js. Moreover, add message output to each ABR rule.
  • Loading branch information
dsilhavy committed Jan 31, 2024
1 parent a568ae9 commit deb8bf1
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 146 deletions.
53 changes: 33 additions & 20 deletions src/core/Settings.js
Expand Up @@ -206,7 +206,11 @@ import Events from './events/Events.js';
* }
* },
* switchHistoryRule: {
* active: true
* active: true,
* parameters: {
* sampleSize: 8,
* switchPercentageThreshold: 0.075
* }
* },
* droppedFramesRule: {
* active: true,
Expand Down Expand Up @@ -698,14 +702,19 @@ import Events from './events/Events.js';
* @property {object} [parameters={throughputSafetyFactor=0.7, segmentIgnoreCount=2}]
* Configures the rule specific parameters.
*
* - throughputSafetyFactor: The safety factor that is applied to the derived throughput, see example in the Description.
* - segmentIgnoreCount: This rule is not taken into account until the first segmentIgnoreCount media segments have been appended to the buffer.
* - `throughputSafetyFactor`: The safety factor that is applied to the derived throughput, see example in the Description.
* - `segmentIgnoreCount`: This rule is not taken into account until the first segmentIgnoreCount media segments have been appended to the buffer.
*/

/**
* @typedef {Object} SwitchHistoryRule
* @property {boolean} [active=true]
* Enable or disable the rule
* @property {object} [parameters={sampleSize=8, switchPercentageThreshold=0.075}]
* Configures the rule specific parameters.
*
* - `sampleSize`: Number of switch requests ("no switch", because of the selected Representation is already playing or "actual switches") required before the rule is applied
* - `switchPercentageThreshold`: Ratio of actual quality drops compared to no drops before a quality down-switch is triggered
*/

/**
Expand All @@ -715,8 +724,8 @@ import Events from './events/Events.js';
* @property {object} [parameters={minimumSampleSize=375, droppedFramesPercentageThreshold=0.15}]
* Configures the rule specific parameters.
*
* - minimumSampleSize: Sum of rendered and dropped frames required for each Representation before the rule kicks in.
* - droppedFramesPercentageThreshold: Minimum percentage of dropped frames to trigger a quality down switch. Values are defined in the range of 0 - 1.
* - `minimumSampleSize`: Sum of rendered and dropped frames required for each Representation before the rule kicks in.
* - `droppedFramesPercentageThreshold`: Minimum percentage of dropped frames to trigger a quality down switch. Values are defined in the range of 0 - 1.
*/

/**
Expand All @@ -726,9 +735,9 @@ import Events from './events/Events.js';
* @property {object} [parameters={abandonDurationMultiplier=1.8, minSegmentDownloadTimeThresholdInMs=500, minThroughputSamplesThreshold=6}]
* Configures the rule specific parameters.
*
* - abandonDurationMultiplier: Factor to multiply with the segment duration to compare against the estimated remaining download time of the current segment. See code example above.
* - minSegmentDownloadTimeThresholdInMs: The AbandonRequestRule only kicks if the download time of the current segment exceeds this value.
* - minThroughputSamplesThreshold: Minimum throughput samples (equivalent to number of progress events) required before the AbandonRequestRule kicks in.
* - `abandonDurationMultiplier`: Factor to multiply with the segment duration to compare against the estimated remaining download time of the current segment. See code example above.
* - `minSegmentDownloadTimeThresholdInMs`: The AbandonRequestRule only kicks if the download time of the current segment exceeds this value.
* - `minThroughputSamplesThreshold`: Minimum throughput samples (equivalent to number of progress events) required before the AbandonRequestRule kicks in.
*/

/**
Expand Down Expand Up @@ -769,19 +778,19 @@ import Events from './events/Events.js';
* It should be between 0 and 1, with lower values giving less rebuffering (but also lower quality)
* @property {object} [sampleSettings = {live=3,vod=4,enableSampleSizeAdjustment=true,decreaseScale=0.7,increaseScale=1.3,maxMeasurementsToKeep=20,averageLatencySampleAmount=4}]
* When deriving the throughput based on the arithmetic or harmonic mean these settings define:
* - live: Number of throughput samples to use (sample size) for live streams
* - vod: Number of throughput samples to use (sample size) for VoD streams
* - enableSampleSizeAdjustment: Adjust the sample sizes if throughput samples vary a lot
* - decreaseScale: Increase sample size by one if the ratio of current and previous sample is below or equal this value
* - increaseScale: Increase sample size by one if the ratio of current and previous sample is higher or equal this value
* - maxMeasurementsToKeep: Number of samples to keep before sliding samples out of the window
* - averageLatencySampleAmount: Number of latency samples to use (sample size)
* - `live`: Number of throughput samples to use (sample size) for live streams
* - `vod`: Number of throughput samples to use (sample size) for VoD streams
* - `enableSampleSizeAdjustment`: Adjust the sample sizes if throughput samples vary a lot
* - `decreaseScale`: Increase sample size by one if the ratio of current and previous sample is below or equal this value
* - `increaseScale`: Increase sample size by one if the ratio of current and previous sample is higher or equal this value
* - `maxMeasurementsToKeep`: Number of samples to keep before sliding samples out of the window
* - `averageLatencySampleAmount`: Number of latency samples to use (sample size)
* @property {object} [ewma={throughputSlowHalfLifeSeconds=8,throughputFastHalfLifeSeconds=3,latencySlowHalfLifeCount=2,latencyFastHalfLifeCount=1}]
* When deriving the throughput based on the exponential weighted moving average these settings define:
* - throughputSlowHalfLifeSeconds: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues
* - throughputFastHalfLifeSeconds: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues
* - latencySlowHalfLifeCount: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues
* - latencyFastHalfLifeCount: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues
* - `throughputSlowHalfLifeSeconds`: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues
* - `throughputFastHalfLifeSeconds`: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues
* - `latencySlowHalfLifeCount`: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues
* - `latencyFastHalfLifeCount`: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues
*/

/**
Expand Down Expand Up @@ -1143,7 +1152,11 @@ function Settings() {
}
},
switchHistoryRule: {
active: true
active: true,
parameters: {
sampleSize: 8,
switchPercentageThreshold: 0.075
}
},
droppedFramesRule: {
active: true,
Expand Down
55 changes: 22 additions & 33 deletions src/streaming/controllers/AbrController.js
Expand Up @@ -67,7 +67,7 @@ function AbrController() {
cmsdModel,
domStorage,
playbackRepresentationId,
switchHistoryDict,
switchRequestHistory,
droppedFramesHistory,
throughputController,
dashMetrics,
Expand All @@ -83,13 +83,13 @@ function AbrController() {
*/
function initialize() {
droppedFramesHistory = DroppedFramesHistory(context).create();
switchRequestHistory = SwitchRequestHistory(context).create();
abrRulesCollection = ABRRulesCollection(context).create({
dashMetrics,
customParametersModel,
mediaPlayerModel,
settings
});

abrRulesCollection.initialize();

eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance);
Expand All @@ -109,18 +109,11 @@ function AbrController() {
if (!streamProcessorDict[streamId]) {
streamProcessorDict[streamId] = {};
}

if (!switchHistoryDict[streamId]) {
switchHistoryDict[streamId] = {};
}
streamProcessorDict[streamId][type] = streamProcessor;

if (!abandonmentStateDict[streamId]) {
abandonmentStateDict[streamId] = {};
}

switchHistoryDict[streamId][type] = SwitchRequestHistory(context).create();
streamProcessorDict[streamId][type] = streamProcessor;

abandonmentStateDict[streamId][type] = {};
abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD;

Expand All @@ -143,10 +136,6 @@ function AbrController() {
delete streamProcessorDict[streamId][type];
}

if (switchHistoryDict[streamId] && switchHistoryDict[streamId][type]) {
delete switchHistoryDict[streamId][type];
}

if (abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type]) {
delete abandonmentStateDict[streamId][type];
}
Expand All @@ -159,7 +148,6 @@ function AbrController() {
function resetInitialSettings() {
abandonmentStateDict = {};
streamProcessorDict = {};
switchHistoryDict = {};

if (windowResizeEventCalled === undefined) {
windowResizeEventCalled = false;
Expand All @@ -168,8 +156,13 @@ function AbrController() {
droppedFramesHistory.reset();
}

if (switchRequestHistory) {
switchRequestHistory.reset();
}

playbackRepresentationId = undefined;
droppedFramesHistory = undefined;
switchRequestHistory = undefined;
clearTimeout(abandonmentTimeout);
abandonmentTimeout = null;
}
Expand Down Expand Up @@ -508,8 +501,8 @@ function AbrController() {
})[0];
if (request) {
abandonmentStateDict[streamId][type].state = MetricsConstants.ABANDON_LOAD;
switchHistoryDict[streamId][type].reset();
setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.representation, switchRequest.reason, switchRequest.rule);
switchRequestHistory.reset();
setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.representation, switchRequest.reason);

clearTimeout(abandonmentTimeout);
abandonmentTimeout = setTimeout(
Expand Down Expand Up @@ -601,7 +594,6 @@ function AbrController() {
}
}

// ABR is turned off, do nothing
if (!settings.get().streaming.abr.autoSwitchBitrate[type]) {
return false;
}
Expand All @@ -611,7 +603,7 @@ function AbrController() {
const rulesContext = RulesContext(context).create({
abrController: instance,
throughputController,
switchHistory: switchHistoryDict[streamId][type],
switchRequestHistory,
droppedFramesHistory,
streamProcessor,
videoModel
Expand All @@ -623,13 +615,13 @@ function AbrController() {
}

let newRepresentation = switchRequest.representation;
switchHistoryDict[streamId][type].push({
oldRepresentation: currentRepresentation,
switchRequestHistory.push({
currentRepresentation,
newRepresentation
});

if (newRepresentation.id !== currentRepresentation.id && (abandonmentStateDict[streamId][type].state === MetricsConstants.ALLOW_LOAD || newRepresentation.absoluteIndex < currentRepresentation.absoluteIndex)) {
_changeQuality(streamId, type, currentRepresentation, newRepresentation, switchRequest.reason, switchRequest.rule);
_changeQuality(currentRepresentation, newRepresentation, switchRequest.reason);
return true;
}

Expand All @@ -638,7 +630,6 @@ function AbrController() {
logger.error(e);
return false;
}

}

/**
Expand All @@ -650,18 +641,17 @@ function AbrController() {
* @param {string} reason
* @param {string} rule
*/
function setPlaybackQuality(type, streamInfo, representation, reason = null, rule = null) {
function setPlaybackQuality(type, streamInfo, representation, reason = {}) {
if (!streamInfo || !streamInfo.id || !type || !streamProcessorDict || !streamProcessorDict[streamInfo.id] || !streamProcessorDict[streamInfo.id][type] || !representation) {
return;
}

const streamProcessor = streamProcessorDict[streamInfo.id][type];
const streamId = streamInfo.id;
const currentRepresentation = streamProcessor.getRepresentation();


if (!currentRepresentation || representation.id !== currentRepresentation.id) {
_changeQuality(streamId, type, currentRepresentation, representation, reason, rule);
_changeQuality(currentRepresentation, representation, reason);
}
}

Expand All @@ -678,21 +668,21 @@ function AbrController() {

/**
* Changes the internal qualityDict values according to the new quality
* @param {string} streamId
* @param {string} type
* @param {Representation} oldRepresentation
* @param {Representation} newRepresentation
* @param {string} reason
* @private
*/
function _changeQuality(streamId, type, oldRepresentation, newRepresentation, reason, rule) {
function _changeQuality(oldRepresentation, newRepresentation, reason) {
const streamId = newRepresentation.mediaInfo.streamInfo.id;
const type = newRepresentation.mediaInfo.type;
if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
const bufferLevel = dashMetrics.getCurrentBufferLevel(type);
const isAdaptationSetSwitch = oldRepresentation !== null && !adapter.areMediaInfosEqual(oldRepresentation.mediaInfo, newRepresentation.mediaInfo);

const oldBitrate = oldRepresentation ? oldRepresentation.bitrateInKbit : 0;
logger.info('Stream ID: ' + streamId + ' [' + type + '],' + (rule ? rule : '') + ' switch from bitrate ' + oldBitrate + ' to bitrate ' + newRepresentation.bitrateInKbit + ' (buffer: ' + bufferLevel + ') ' + (reason ? JSON.stringify(reason) : '.'));
logger.info(`[AbrController]: Switching quality in period ${streamId} for media type ${type}. Switch from bitrate ${oldBitrate} to bitrate ${newRepresentation.bitrateInKbit}. Current buffer level: ${bufferLevel}. Reason:` + (reason ? JSON.stringify(reason) : '/'));

eventBus.trigger(Events.QUALITY_CHANGE_REQUESTED,
{
Expand Down Expand Up @@ -786,10 +776,9 @@ function AbrController() {
if (streamProcessorDict[streamId]) {
delete streamProcessorDict[streamId];
}
if (switchHistoryDict[streamId]) {
delete switchHistoryDict[streamId];
if (switchRequestHistory) {
switchRequestHistory.clearForStream(streamId);
}

if (abandonmentStateDict[streamId]) {
delete abandonmentStateDict[streamId];
}
Expand Down
24 changes: 12 additions & 12 deletions src/streaming/rules/DroppedFramesHistory.js
Expand Up @@ -13,9 +13,7 @@ function DroppedFramesHistory() {
}

if (!values[streamId]) {
values[streamId] = [];
lastDroppedFrames[streamId] = 0;
lastTotalFrames[streamId] = 0;
_initializeForStream(streamId);
}

let droppedVideoFrames = playbackQuality && playbackQuality.droppedVideoFrames ? playbackQuality.droppedVideoFrames : 0;
Expand All @@ -40,18 +38,20 @@ function DroppedFramesHistory() {

}

function _initializeForStream(streamId) {
values[streamId] = [];
lastDroppedFrames[streamId] = 0;
lastTotalFrames[streamId] = 0;
}

function getFrameHistory(streamId) {
return values[streamId];
}

function clearForStream(streamId) {
try {
delete values[streamId];
delete lastDroppedFrames[streamId];
delete lastTotalFrames[streamId];
} catch (e) {

}
delete values[streamId];
delete lastDroppedFrames[streamId];
delete lastTotalFrames[streamId];
}

function reset() {
Expand All @@ -61,9 +61,9 @@ function DroppedFramesHistory() {
}

return {
push,
getFrameHistory,
clearForStream,
getFrameHistory,
push,
reset
};
}
Expand Down
8 changes: 4 additions & 4 deletions src/streaming/rules/RulesContext.js
Expand Up @@ -37,7 +37,7 @@ function RulesContext(config) {
let instance;
const abrController = config.abrController;
const throughputController = config.throughputController;
const switchHistory = config.switchHistory;
const switchRequestHistory = config.switchRequestHistory;
const droppedFramesHistory = config.droppedFramesHistory;
const currentRequest = config.currentRequest;
const scheduleController = config.streamProcessor ? config.streamProcessor.getScheduleController() : null;
Expand Down Expand Up @@ -74,8 +74,8 @@ function RulesContext(config) {
return throughputController;
}

function getSwitchHistory() {
return switchHistory;
function getSwitchRequestHistory() {
return switchRequestHistory;
}

function getVideoModel() {
Expand All @@ -99,7 +99,7 @@ function RulesContext(config) {
getRepresentation,
getScheduleController,
getStreamInfo,
getSwitchHistory,
getSwitchRequestHistory,
getThroughputController,
getVideoModel,
};
Expand Down

0 comments on commit deb8bf1

Please sign in to comment.