From deb8bf18600b41bb73d08d137d18080e09e58236 Mon Sep 17 00:00:00 2001 From: Daniel Silhavy Date: Wed, 31 Jan 2024 14:50:22 +0100 Subject: [PATCH] =?UTF-8?q?Refactor=20the=20SwitchHistoryRule.js=20and=20a?= =?UTF-8?q?llow=20parameter=20configuration=20v=E2=80=A6=20(#4379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor the SwitchHistoryRule.js and allow parameter configuration via Settings.js. Moreover, add message output to each ABR rule. --- src/core/Settings.js | 53 +++++--- src/streaming/controllers/AbrController.js | 55 ++++----- src/streaming/rules/DroppedFramesHistory.js | 24 ++-- src/streaming/rules/RulesContext.js | 8 +- src/streaming/rules/SwitchRequestHistory.js | 114 ++++++++++++++---- .../rules/abr/AbandonRequestsRule.js | 4 +- src/streaming/rules/abr/DroppedFramesRule.js | 16 +-- src/streaming/rules/abr/SwitchHistoryRule.js | 51 +++----- src/streaming/rules/abr/ThroughputRule.js | 6 +- test/unit/mocks/RulesContextMock.js | 4 +- .../streaming.rules.SwitchRequestHistory.js | 67 ++++++++++ .../streaming.rules.abr.SwitchHistoryRule.js | 2 +- 12 files changed, 258 insertions(+), 146 deletions(-) create mode 100644 test/unit/streaming.rules.SwitchRequestHistory.js diff --git a/src/core/Settings.js b/src/core/Settings.js index 647ab752fb..d8c601b082 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -206,7 +206,11 @@ import Events from './events/Events.js'; * } * }, * switchHistoryRule: { - * active: true + * active: true, + * parameters: { + * sampleSize: 8, + * switchPercentageThreshold: 0.075 + * } * }, * droppedFramesRule: { * active: true, @@ -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 */ /** @@ -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. */ /** @@ -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. */ /** @@ -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 */ /** @@ -1143,7 +1152,11 @@ function Settings() { } }, switchHistoryRule: { - active: true + active: true, + parameters: { + sampleSize: 8, + switchPercentageThreshold: 0.075 + } }, droppedFramesRule: { active: true, diff --git a/src/streaming/controllers/AbrController.js b/src/streaming/controllers/AbrController.js index c72c2182ff..e723ab5bc2 100644 --- a/src/streaming/controllers/AbrController.js +++ b/src/streaming/controllers/AbrController.js @@ -67,7 +67,7 @@ function AbrController() { cmsdModel, domStorage, playbackRepresentationId, - switchHistoryDict, + switchRequestHistory, droppedFramesHistory, throughputController, dashMetrics, @@ -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); @@ -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; @@ -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]; } @@ -159,7 +148,6 @@ function AbrController() { function resetInitialSettings() { abandonmentStateDict = {}; streamProcessorDict = {}; - switchHistoryDict = {}; if (windowResizeEventCalled === undefined) { windowResizeEventCalled = false; @@ -168,8 +156,13 @@ function AbrController() { droppedFramesHistory.reset(); } + if (switchRequestHistory) { + switchRequestHistory.reset(); + } + playbackRepresentationId = undefined; droppedFramesHistory = undefined; + switchRequestHistory = undefined; clearTimeout(abandonmentTimeout); abandonmentTimeout = null; } @@ -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( @@ -601,7 +594,6 @@ function AbrController() { } } - // ABR is turned off, do nothing if (!settings.get().streaming.abr.autoSwitchBitrate[type]) { return false; } @@ -611,7 +603,7 @@ function AbrController() { const rulesContext = RulesContext(context).create({ abrController: instance, throughputController, - switchHistory: switchHistoryDict[streamId][type], + switchRequestHistory, droppedFramesHistory, streamProcessor, videoModel @@ -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; } @@ -638,7 +630,6 @@ function AbrController() { logger.error(e); return false; } - } /** @@ -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); } } @@ -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, { @@ -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]; } diff --git a/src/streaming/rules/DroppedFramesHistory.js b/src/streaming/rules/DroppedFramesHistory.js index e35ae4ef86..cc83fb572e 100644 --- a/src/streaming/rules/DroppedFramesHistory.js +++ b/src/streaming/rules/DroppedFramesHistory.js @@ -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; @@ -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() { @@ -61,9 +61,9 @@ function DroppedFramesHistory() { } return { - push, - getFrameHistory, clearForStream, + getFrameHistory, + push, reset }; } diff --git a/src/streaming/rules/RulesContext.js b/src/streaming/rules/RulesContext.js index 09ded55895..d71ef8a9fa 100644 --- a/src/streaming/rules/RulesContext.js +++ b/src/streaming/rules/RulesContext.js @@ -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; @@ -74,8 +74,8 @@ function RulesContext(config) { return throughputController; } - function getSwitchHistory() { - return switchHistory; + function getSwitchRequestHistory() { + return switchRequestHistory; } function getVideoModel() { @@ -99,7 +99,7 @@ function RulesContext(config) { getRepresentation, getScheduleController, getStreamInfo, - getSwitchHistory, + getSwitchRequestHistory, getThroughputController, getVideoModel, }; diff --git a/src/streaming/rules/SwitchRequestHistory.js b/src/streaming/rules/SwitchRequestHistory.js index 4242e2c4bc..4557858631 100644 --- a/src/streaming/rules/SwitchRequestHistory.js +++ b/src/streaming/rules/SwitchRequestHistory.js @@ -30,62 +30,124 @@ */ import FactoryMaker from '../../core/FactoryMaker.js'; - -const SWITCH_REQUEST_HISTORY_DEPTH = 8; // must be > SwitchHistoryRule SAMPLE_SIZE to enable rule +import Settings from '../../core/Settings.js'; function SwitchRequestHistory() { - let switchRequests = {}; // running total - let srHistory = []; // history of each switch + let switchRequests = {}; + let switchRequestHistory = {}; + const context = this.context; + const settings = Settings(context).getInstance(); function push(switchRequest) { - if (!switchRequests[switchRequest.oldRepresentation.id]) { - switchRequests[switchRequest.oldRepresentation.id] = { - noDrops: 0, - drops: 0, - dropSize: 0 - }; + const currentRepresentation = switchRequest.currentRepresentation; + const newRepresentation = switchRequest.newRepresentation; + + // Don`t compare quality switches between different periods + if (currentRepresentation.mediaInfo.streamInfo.id !== newRepresentation.mediaInfo.streamInfo.id) { + return; + } + + const streamId = currentRepresentation.mediaInfo.streamInfo.id; + if (!switchRequests[streamId]) { + _initializeForStream(streamId) + } + + const mediaType = currentRepresentation.mediaInfo.type; + if (!switchRequests[streamId][mediaType]) { + _initializeForMediaType(streamId, mediaType); + } + + const currentRepresentationId = switchRequest.currentRepresentation.id + if (!switchRequests[streamId][mediaType][currentRepresentationId]) { + _initializeForRepresentation(streamId, mediaType, currentRepresentationId); } // Set switch details - let indexDiff = switchRequest.newRepresentation.absoluteIndex - switchRequest.oldRepresentation.absoluteIndex; + let indexDiff = switchRequest.newRepresentation.absoluteIndex - switchRequest.currentRepresentation.absoluteIndex; let drop = (indexDiff < 0) ? 1 : 0; let dropSize = drop ? -indexDiff : 0; let noDrop = drop ? 0 : 1; // Update running totals - switchRequests[switchRequest.oldRepresentation.id].drops += drop; - switchRequests[switchRequest.oldRepresentation.id].dropSize += dropSize; - switchRequests[switchRequest.oldRepresentation.id].noDrops += noDrop; + switchRequests[streamId][mediaType][switchRequest.currentRepresentation.id].drops += drop; + switchRequests[streamId][mediaType][switchRequest.currentRepresentation.id].dropSize += dropSize; + switchRequests[streamId][mediaType][switchRequest.currentRepresentation.id].noDrops += noDrop; // Save to history - srHistory.push({ - id: switchRequest.oldRepresentation.id, + switchRequestHistory[streamId][mediaType].push({ + id: switchRequest.currentRepresentation.id, noDrop: noDrop, drop: drop, dropSize: dropSize }); + // Remove outdated entries from history + const removedHistorySample = _adjustSwitchRequestHistory(streamId, mediaType); + + // Adjust current values based on the removed sample + if (removedHistorySample) { + _adjustSwitchRequestDrops(streamId, mediaType, removedHistorySample) + } + + } + + function _initializeForStream(streamId) { + switchRequests[streamId] = {}; + switchRequestHistory[streamId] = {}; + } + + function _initializeForMediaType(streamId, mediaType) { + switchRequests[streamId][mediaType] = {}; + switchRequestHistory[streamId][mediaType] = []; + } + + function _initializeForRepresentation(streamId, mediaType, representationId) { + switchRequests[streamId][mediaType][representationId] = { + noDrops: 0, + drops: 0, + dropSize: 0 + }; + } + + function _adjustSwitchRequestHistory(streamId, mediaType) { // Shift the earliest switch off srHistory and readjust to keep depth of running totals constant - if (srHistory.length > SWITCH_REQUEST_HISTORY_DEPTH) { - let srHistoryFirst = srHistory.shift(); - switchRequests[srHistoryFirst.id].drops -= srHistoryFirst.drop; - switchRequests[srHistoryFirst.id].dropSize -= srHistoryFirst.dropSize; - switchRequests[srHistoryFirst.id].noDrops -= srHistoryFirst.noDrop; + if (switchRequestHistory[streamId][mediaType].length > settings.get().streaming.abr.rules.switchHistoryRule.parameters.sampleSize) { + return switchRequestHistory[streamId][mediaType].shift(); + } + + return null + } + + function _adjustSwitchRequestDrops(streamId, mediaType, removedHistorySample) { + switchRequests[streamId][mediaType][removedHistorySample.id].drops -= removedHistorySample.drop; + switchRequests[streamId][mediaType][removedHistorySample.id].dropSize -= removedHistorySample.dropSize; + switchRequests[streamId][mediaType][removedHistorySample.id].noDrops -= removedHistorySample.noDrop; + } + + function getSwitchRequests(streamId, mediaType) { + if (streamId === null || typeof streamId === 'undefined' + || mediaType === null || typeof mediaType === 'undefined' + || !switchRequests[streamId] || !switchRequests[streamId][mediaType]) { + return {} } + + return switchRequests[streamId][mediaType]; } - function getSwitchRequests() { - return switchRequests; + function clearForStream(streamId) { + delete switchRequests[streamId]; + delete switchRequestHistory[streamId]; } function reset() { - switchRequests = []; - srHistory = []; + switchRequests = {}; + switchRequestHistory = {}; } return { - push, + clearForStream, getSwitchRequests, + push, reset }; } diff --git a/src/streaming/rules/abr/AbandonRequestsRule.js b/src/streaming/rules/abr/AbandonRequestsRule.js index ebf09b5b8a..c70a8e470c 100644 --- a/src/streaming/rules/abr/AbandonRequestsRule.js +++ b/src/streaming/rules/abr/AbandonRequestsRule.js @@ -119,10 +119,10 @@ function AbandonRequestsRule(config) { if (remainingBytesToDownload > totalBytesForOptimalRepresentation) { switchRequest.representation = optimalRepresentationForBitrate; switchRequest.reason = { - throughputInKbit + throughputInKbit, + message: `[AbandonRequestRule][${mediaType} is asking to abandon and switch to quality to ${optimalRepresentationForBitrate.absoluteIndex}. The measured bandwidth was ${throughputInKbit} kbit/s` } abandonDict[request.index] = true; - logger.info(`[AbandonRequestRule][${mediaType} is asking to abandon and switch to quality to ${optimalRepresentationForBitrate.absoluteIndex}. The measured bandwidth was ${throughputInKbit} kbit/s`); } } diff --git a/src/streaming/rules/abr/DroppedFramesRule.js b/src/streaming/rules/abr/DroppedFramesRule.js index 4cd087c1c3..4b8a031532 100644 --- a/src/streaming/rules/abr/DroppedFramesRule.js +++ b/src/streaming/rules/abr/DroppedFramesRule.js @@ -1,18 +1,12 @@ import FactoryMaker from '../../../core/FactoryMaker.js'; import SwitchRequest from '../SwitchRequest.js'; -import Debug from '../../../core/Debug.js'; import Settings from '../../../core/Settings.js'; function DroppedFramesRule() { const context = this.context; const settings = Settings(context).getInstance(); - let instance, - logger; - - function setup() { - logger = Debug(context).getInstance().getLogger(instance); - } + let instance; function getSwitchRequest(rulesContext) { const switchRequest = SwitchRequest(context).create(); @@ -49,14 +43,16 @@ function DroppedFramesRule() { if (totalFrames > settings.get().streaming.abr.rules.droppedFramesRule.parameters.minimumSampleSize && droppedFrames / totalFrames > settings.get().streaming.abr.rules.droppedFramesRule.parameters.droppedFramesPercentageThreshold) { newRepresentation = representations[i - 1]; - logger.debug('index: ' + newRepresentation.absoluteIndex + ' Dropped Frames: ' + droppedFrames + ' Total Frames: ' + totalFrames); break; } } } if (newRepresentation) { switchRequest.representation = newRepresentation; - switchRequest.reason = { droppedFrames }; + switchRequest.reason = { + droppedFrames, + message: `[DroppedFramesRule]: Switching to index ${newRepresentation.absoluteIndex}. Dropped Frames: ${droppedFrames}, Total Frames: ${totalFrames}` + }; } return switchRequest; @@ -66,8 +62,6 @@ function DroppedFramesRule() { getSwitchRequest }; - setup(); - return instance; } diff --git a/src/streaming/rules/abr/SwitchHistoryRule.js b/src/streaming/rules/abr/SwitchHistoryRule.js index a2069363a9..a5c176671a 100644 --- a/src/streaming/rules/abr/SwitchHistoryRule.js +++ b/src/streaming/rules/abr/SwitchHistoryRule.js @@ -1,24 +1,12 @@ import FactoryMaker from '../../../core/FactoryMaker.js'; -import Debug from '../../../core/Debug.js'; import SwitchRequest from '../SwitchRequest.js'; +import Settings from '../../../core/Settings.js'; function SwitchHistoryRule() { const context = this.context; - - let instance, - logger; - - //MAX_SWITCH is the number of drops made. It doesn't consider the size of the drop. - const MAX_SWITCH = 0.075; - - //Before this number of switch requests(no switch or actual), don't apply the rule. - //must be < SwitchRequestHistory SWITCH_REQUEST_HISTORY_DEPTH to enable rule - const SAMPLE_SIZE = 6; - - function setup() { - logger = Debug(context).getInstance().getLogger(instance); - } + const settings = Settings(context).getInstance(); + let instance; function getSwitchRequest(rulesContext) { const switchRequest = SwitchRequest(context).create(); @@ -27,35 +15,30 @@ function SwitchHistoryRule() { if (!rulesContext) { return switchRequest; } - const switchRequestHistory = rulesContext ? rulesContext.getSwitchHistory() : null; - const switchRequests = switchRequestHistory ? switchRequestHistory.getSwitchRequests() : []; + + const streamId = rulesContext.getStreamInfo().id; + const mediaType = rulesContext.getMediaType(); + const switchRequestHistory = rulesContext ? rulesContext.getSwitchRequestHistory() : null; + const switchRequests = switchRequestHistory ? switchRequestHistory.getSwitchRequests(streamId, mediaType) : {}; const abrController = rulesContext.getAbrController(); const mediaInfo = rulesContext.getMediaInfo(); + const representations = abrController.getPossibleVoRepresentations(mediaInfo, true); let drops = 0; let noDrops = 0; - let dropSize = 0; - - switchRequest.rule = this.getClassName(); - - const representations = abrController.getPossibleVoRepresentations(mediaInfo, true); for (let i = 0; i < representations.length; i++) { - const currentRepresentation = representations[i]; - if (currentRepresentation && switchRequests[currentRepresentation.id]) { - drops += switchRequests[currentRepresentation.id].drops; - noDrops += switchRequests[currentRepresentation.id].noDrops; - dropSize += switchRequests[currentRepresentation.id].dropSize; + const currentPossibleRepresentation = representations[i]; + if (currentPossibleRepresentation && switchRequests[currentPossibleRepresentation.id]) { + drops += switchRequests[currentPossibleRepresentation.id].drops; + noDrops += switchRequests[currentPossibleRepresentation.id].noDrops; - if (drops + noDrops >= SAMPLE_SIZE && (drops / noDrops > MAX_SWITCH)) { - const targetRepresentation = (i > 0 && switchRequests[currentRepresentation.id].drops > 0) ? representations[i - 1] : currentRepresentation; - switchRequest.representation = targetRepresentation; + if (drops + noDrops >= settings.get().streaming.abr.rules.switchHistoryRule.parameters.sampleSize && (drops / noDrops > settings.get().streaming.abr.rules.switchHistoryRule.parameters.switchPercentageThreshold)) { + switchRequest.representation = (i > 0 && switchRequests[currentPossibleRepresentation.id].drops > 0) ? representations[i - 1] : currentPossibleRepresentation; switchRequest.reason = { - index: switchRequest.quality, drops: drops, noDrops: noDrops, - dropSize: dropSize + message: `[SwitchHistoryRule]: Switch to index: ${switchRequest.representation.absoluteIndex} samples: ${(drops + noDrops)} drops: ${drops}` }; - logger.debug('Switch history rule index: ' + switchRequest.representation.absoluteIndex + ' samples: ' + (drops + noDrops) + ' drops: ' + drops); break; } } @@ -68,7 +51,7 @@ function SwitchHistoryRule() { getSwitchRequest }; - setup(); + return instance; } diff --git a/src/streaming/rules/abr/ThroughputRule.js b/src/streaming/rules/abr/ThroughputRule.js index 674017db91..260280bccf 100644 --- a/src/streaming/rules/abr/ThroughputRule.js +++ b/src/streaming/rules/abr/ThroughputRule.js @@ -69,7 +69,11 @@ function ThroughputRule(config) { if (abrController.getAbandonmentStateFor(streamId, mediaType) === MetricsConstants.ALLOW_LOAD) { if (currentBufferState.state === MetricsConstants.BUFFER_LOADED || isDynamic) { switchRequest.representation = abrController.getOptimalRepresentationForBitrate(mediaInfo, throughput, true); - switchRequest.reason = { throughput: throughput, latency: latency }; + switchRequest.reason = { + throughput, + latency, + message: `[ThroughputRule]: Switching to Representation with bitrate ${switchRequest.representation.bitrateInKbit} kbit/s. Throughput: ${throughput}` + }; scheduleController.setTimeToLoadDelay(0); } } diff --git a/test/unit/mocks/RulesContextMock.js b/test/unit/mocks/RulesContextMock.js index 68c752a0b8..d21f46ba7e 100644 --- a/test/unit/mocks/RulesContextMock.js +++ b/test/unit/mocks/RulesContextMock.js @@ -4,7 +4,7 @@ function SwitchRequestHistoryMock() { this.getSwitchRequests = function () { return { 1: { - drops: 7, + drops: 10, noDrops: 0, dropSize: 4 } @@ -35,7 +35,7 @@ function RulesContextMock() { } }; }; - this.getSwitchHistory = function () { + this.getSwitchRequestHistory = function () { return new SwitchRequestHistoryMock(); }; this.getVoRepresentation = function () { diff --git a/test/unit/streaming.rules.SwitchRequestHistory.js b/test/unit/streaming.rules.SwitchRequestHistory.js new file mode 100644 index 0000000000..e812f88f49 --- /dev/null +++ b/test/unit/streaming.rules.SwitchRequestHistory.js @@ -0,0 +1,67 @@ +import {expect} from 'chai'; +import SwitchRequestHistory from '../../src/streaming/rules/SwitchRequestHistory.js'; + +describe('SwitchRequestHistory', () => { + + describe('Unit Tests', () => { + + describe('getSwitchRequests()', () => { + let switchRequestHistory; + + beforeEach(() => { + switchRequestHistory = SwitchRequestHistory({}).create() + }) + + it('should return an empty object if streamId is not provided', () => { + expect(switchRequestHistory.getSwitchRequests(null, 'video')).to.be.empty + }) + + it('should throw a TypeError if mediaInfo is not provided', () => { + expect(switchRequestHistory.getSwitchRequests('id', null)).to.be.empty + }) + + it('should not throw an error if streamId and mediaType are defined but there is no entry', () => { + expect(switchRequestHistory.getSwitchRequests('id', 'video')).to.be.empty + }) + }) + }) + + describe('Integration Tests', () => { + + describe('push() and getSwitchRequests()', () => { + let switchRequestHistory; + + beforeEach(() => { + switchRequestHistory = SwitchRequestHistory({}).create() + }) + + it('should create an entry and make it available', () => { + const mediaInfo = { + streamInfo: { + id: 1 + }, + type: 'video' + } + const currentRepresentation = { + mediaInfo, + id: 'id_1', + absoluteIndex: 4 + } + const newRepresentation = { + mediaInfo, + id: 'id_2', + absoluteIndex: 2 + } + const switchRequest = { currentRepresentation, newRepresentation }; + switchRequestHistory.push(switchRequest); + + const switchRequests = switchRequestHistory.getSwitchRequests(mediaInfo.streamInfo.id, mediaInfo.type) + expect(switchRequests).to.not.be.empty + expect(switchRequests).to.have.property(currentRepresentation.id) + expect(switchRequests[currentRepresentation.id].drops).to.be.equal(1) + expect(switchRequests[currentRepresentation.id].noDrops).to.be.equal(0) + expect(switchRequests[currentRepresentation.id].dropSize).to.be.equal(2) + }) + }) + }) +}); diff --git a/test/unit/streaming.rules.abr.SwitchHistoryRule.js b/test/unit/streaming.rules.abr.SwitchHistoryRule.js index e19f404a30..dc891aace5 100644 --- a/test/unit/streaming.rules.abr.SwitchHistoryRule.js +++ b/test/unit/streaming.rules.abr.SwitchHistoryRule.js @@ -13,7 +13,7 @@ describe('SwitchHistoryRule', function () { expect(switchRequest.representation).to.be.equal(SwitchRequest.NO_CHANGE); }); - it('should return an switchRequest with quality equals 0 when one switchRequest equals to {drops: 7, noDrops: 0, dropSize: 4}, a division by zero occurs', function () { + it('should return an switchRequest with quality equals 0 when one switchRequest equals to {drops: 10, noDrops: 0, dropSize: 4}, a division by zero occurs', function () { let rulesContextMock = new RulesContextMock(); const switchRequest = switchHistoryRule.getSwitchRequest(rulesContextMock);