diff --git a/index.d.ts b/index.d.ts index d10a4fc0e5..ec35dc9da2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1176,15 +1176,31 @@ declare namespace dashjs { abr?: { limitBitrateByPortal?: boolean; usePixelRatioInLimitBitrateByPortal?: boolean; - activeRules?: { - throughputRule?: boolean, - bolaRule?: boolean, - insufficientBufferRule?: boolean, - switchHistoryRule?: boolean, - droppedFramesRule?: boolean, - abandonRequestsRule?: boolean - l2ARule?: boolean - loLPRule?: boolean + rules?: { + throughputRule?: { + active?: boolean + }, + bolaRule?: { + active?: boolean + }, + insufficientBufferRule?: { + active?: boolean + }, + switchHistoryRule?: { + active?: boolean + }, + droppedFramesRule?: { + active?: boolean + }, + abandonRequestsRule?: { + active?: boolean + } + l2ARule?: { + active?: boolean + } + loLPRule?: { + active?: boolean + } }, throughput?: { averageCalculationMode?: ThroughputCalculationModes, diff --git a/samples/abr/abr.html b/samples/abr/abr.html index 89b0f1b679..f97c5f977f 100644 --- a/samples/abr/abr.html +++ b/samples/abr/abr.html @@ -29,13 +29,25 @@ player.updateSettings({ streaming: { abr: { - activeRules: { - throughputRule: true, - bolaRule: false, - insufficientBufferRule: true, - switchHistoryRule: false, - droppedFramesRule: false, - abandonRequestsRule: false + rules: { + throughputRule: { + active: true + } , + bolaRule: { + active: false + }, + insufficientBufferRule: { + active: true + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: false + } } } } diff --git a/samples/abr/custom-abr-rules.html b/samples/abr/custom-abr-rules.html index e0b4646112..c2642f8802 100644 --- a/samples/abr/custom-abr-rules.html +++ b/samples/abr/custom-abr-rules.html @@ -32,12 +32,24 @@ player.updateSettings({ abr: { activeRules: { - throughputRule: false, - bolaRule: false, - insufficientBufferRule: false, - switchHistoryRule: false, - droppedFramesRule: false, - abandonRequestsRule: false + throughputRule: { + active: false + }, + bolaRule: { + active: false + }, + insufficientBufferRule: { + active: false + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: false + } } } }); diff --git a/samples/dash-if-reference-player/app/main.js b/samples/dash-if-reference-player/app/main.js index f7937546f1..0f314f8a85 100644 --- a/samples/dash-if-reference-player/app/main.js +++ b/samples/dash-if-reference-player/app/main.js @@ -574,15 +574,31 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' $scope.player.updateSettings({ streaming: { abr: { - activeRules: { - throughputRule: $scope.activeAbrRules.throughputRule, - bolaRule: $scope.activeAbrRules.bolaRule, - insufficientBufferRule: $scope.activeAbrRules.insufficientBufferRule, - switchHistoryRule: $scope.activeAbrRules.switchHistoryRule, - droppedFramesRule: $scope.activeAbrRules.droppedFramesRule, - abandonRequestsRule: $scope.activeAbrRules.abandonRequestsRule, - l2ARule: $scope.activeAbrRules.l2ARule, - loLPRule: $scope.activeAbrRules.loLPRule, + rules: { + throughputRule: { + active: $scope.activeAbrRules.throughputRule + }, + bolaRule: { + active: $scope.activeAbrRules.bolaRule + }, + insufficientBufferRule: { + active: $scope.activeAbrRules.insufficientBufferRule, + }, + switchHistoryRule: { + active: $scope.activeAbrRules.switchHistoryRule + }, + droppedFramesRule: { + active: $scope.activeAbrRules.droppedFramesRule + }, + abandonRequestsRule: { + active: $scope.activeAbrRules.abandonRequestsRule + }, + l2ARule: { + active: $scope.activeAbrRules.l2ARule + }, + loLPRule: { + active: $scope.activeAbrRules.loLPRule + } } } } @@ -2071,14 +2087,14 @@ app.controller('DashController', ['$scope', '$window', 'sources', 'contributors' function setAbrRules() { var currentConfig = $scope.player.getSettings(); - $scope.activeAbrRules.throughputRule = currentConfig.streaming.abr.activeRules.throughputRule; - $scope.activeAbrRules.bolaRule = currentConfig.streaming.abr.activeRules.bolaRule; - $scope.activeAbrRules.insufficientBufferRule = currentConfig.streaming.abr.activeRules.insufficientBufferRule; - $scope.activeAbrRules.switchHistoryRule = currentConfig.streaming.abr.activeRules.switchHistoryRule; - $scope.activeAbrRules.droppedFramesRule = currentConfig.streaming.abr.activeRules.droppedFramesRule; - $scope.activeAbrRules.abandonRequestsRule = currentConfig.streaming.abr.activeRules.abandonRequestsRule; - $scope.activeAbrRules.loLPRule = currentConfig.streaming.abr.activeRules.loLPRule; - $scope.activeAbrRules.l2ARule = currentConfig.streaming.abr.activeRules.l2ARule; + $scope.activeAbrRules.throughputRule = currentConfig.streaming.abr.rules.throughputRule.active; + $scope.activeAbrRules.bolaRule = currentConfig.streaming.abr.rules.bolaRule.active; + $scope.activeAbrRules.insufficientBufferRule = currentConfig.streaming.abr.rules.insufficientBufferRule.active; + $scope.activeAbrRules.switchHistoryRule = currentConfig.streaming.abr.rules.switchHistoryRule.active; + $scope.activeAbrRules.droppedFramesRule = currentConfig.streaming.abr.rules.droppedFramesRule.active; + $scope.activeAbrRules.abandonRequestsRule = currentConfig.streaming.abr.rules.abandonRequestsRule.active; + $scope.activeAbrRules.loLPRule = currentConfig.streaming.abr.rules.loLPRule.active; + $scope.activeAbrRules.l2ARule = currentConfig.streaming.abr.rules.l2ARule.active; } function setAdditionalPlaybackOptions() { diff --git a/samples/low-latency/testplayer/main.js b/samples/low-latency/testplayer/main.js index 135b0ca61b..be08a6719a 100644 --- a/samples/low-latency/testplayer/main.js +++ b/samples/low-latency/testplayer/main.js @@ -104,15 +104,31 @@ App.prototype._applyParameters = function () { mode: settings.catchupMechanism }, abr: { - activeRules: { - throughputRule: settings.abrThroughputRule, - bolaRule: settings.abrBolaRule, - insufficientBufferRule: settings.abrInsufficientBufferRule, - switchHistoryRule: settings.abrSwitchHistoryRule, - droppedFramesRule: settings.abrDroppedFramesRule, - abandonRequestsRule: settings.abrAbandonRequestRule, - l2ARule: settings.abrL2ARule, - loLPRule: settings.abrLoLPRule, + rules: { + throughputRule: { + active: settings.abrThroughputRule + }, + bolaRule: { + active: settings.abrBolaRule + }, + insufficientBufferRule: { + active: settings.abrInsufficientBufferRule + }, + switchHistoryRule: { + active: settings.abrSwitchHistoryRule + }, + droppedFramesRule: { + active: settings.abrDroppedFramesRule + }, + abandonRequestsRule: { + active: settings.abrAbandonRequestRule + }, + l2ARule: { + active: settings.abrL2ARule + }, + loLPRule: { + active: settings.abrLoLPRule + }, }, throughput: { averageCalculationMode: settings.throughputEstimation, diff --git a/src/core/Settings.js b/src/core/Settings.js index 7cbebfa0b6..06fd780ef3 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -191,15 +191,32 @@ import Events from './events/Events.js'; * abr: { * limitBitrateByPortal: false, * usePixelRatioInLimitBitrateByPortal: false, - * activeRules: { - * throughputRule: true, - * bolaRule: true, - * insufficientBufferRule: true, - * switchHistoryRule: true, - * droppedFramesRule: true, - * abandonRequestsRule: true, - * l2ARule: false, - * loLPRule: false + * rules: { + * throughputRule: { + * active: true + * }, + * bolaRule: { + * active: true + * }, + * insufficientBufferRule: { + * active: true + * }, + * switchHistoryRule: { + * active: true + * }, + * droppedFramesRule: { + * active: true + * }, + * abandonRequestsRule: { + * active: true + * }, + * l2ARule: { + * active: false + * }, + * loLPRule: { + * active: false + * } + * * }, * throughput: { * averageCalculationMode: Constants.THROUGHPUT_CALCULATION_MODES.EWMA, @@ -607,7 +624,7 @@ import Events from './events/Events.js'; * Sets whether to take into account the device's pixel ratio when defining the portal dimensions. * * Useful on, for example, retina displays. - * @property {object} [activeRules={throughputRule: true, bolaRule: true, insufficientBufferRule: true,switchHistoryRule: true,droppedFramesRule: true,abandonRequestsRule: true, l2ARule: false, loLPRule: false}] + * @property {object} [activeRules={throughputRule: {active: true}, bolaRule: {active: true}, insufficientBufferRule: {active: true},switchHistoryRule: {active: true},droppedFramesRule: {active: true},abandonRequestsRule: {active: true}, l2ARule: {active: false}, loLPRule: {active: false}}] * Enable/Disable individual ABR rules. Note that if the throughputRule and the bolaRule are activated at the same time we switch to a dynamic mode. * In the dynamic mode either ThroughputRule or BolaRule are active but not both at the same time. * @@ -851,14 +868,14 @@ function Settings() { 'streaming.liveCatchup.enabled': Events.SETTING_UPDATED_CATCHUP_ENABLED, 'streaming.liveCatchup.playbackRate.min': Events.SETTING_UPDATED_PLAYBACK_RATE_MIN, 'streaming.liveCatchup.playbackRate.max': Events.SETTING_UPDATED_PLAYBACK_RATE_MAX, - 'streaming.abr.activeRules.throughputRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.bolaRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.insufficientBufferRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.switchHistoryRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.droppedFramesRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.abandonRequestsRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.l2ARule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, - 'streaming.abr.activeRules.loLPRule': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + 'streaming.abr.rules.throughputRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + 'streaming.abr.rules.bolaRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + 'streaming.abr.rules.insufficientBufferRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + 'streaming.abr.rules.switchHistoryRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + 'streaming.abr.rules.droppedFramesRule.active': Events.SETTING_UPDATED_ABR_ACTIVE_RULES, + '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, }; /** @@ -1016,15 +1033,36 @@ function Settings() { limitBitrateByPortal: false, usePixelRatioInLimitBitrateByPortal: false, enableSupplementalPropertyAdaptationSetSwitching: true, - activeRules: { - throughputRule: true, - bolaRule: true, - insufficientBufferRule: true, - switchHistoryRule: true, - droppedFramesRule: true, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: true + }, + switchHistoryRule: { + active: true + }, + droppedFramesRule: { + active: true + }, + abandonRequestsRule: { + active: true, + parameters: { + abandonDurationMultiplier: 1.8, + minSegmentDownloadTimeThresholdInMs: 500, + minThroughputSamplesThreshold: 6 + } + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } }, throughput: { averageCalculationMode: Constants.THROUGHPUT_CALCULATION_MODES.EWMA, diff --git a/src/streaming/controllers/AbrController.js b/src/streaming/controllers/AbrController.js index 23af2ee547..febff46091 100644 --- a/src/streaming/controllers/AbrController.js +++ b/src/streaming/controllers/AbrController.js @@ -127,7 +127,7 @@ function AbrController() { // Do not change current value if it has been set before const currentState = abrRulesCollection.getBolaState(type) if (currentState === undefined) { - abrRulesCollection.setBolaState(type, settings.get().streaming.abr.activeRules.bolaRule && !_shouldApplyDynamicAbrStrategy()); + abrRulesCollection.setBolaState(type, settings.get().streaming.abr.rules.bolaRule.active && !_shouldApplyDynamicAbrStrategy()); } } @@ -720,7 +720,7 @@ function AbrController() { * @private */ function _shouldApplyDynamicAbrStrategy() { - return settings.get().streaming.abr.activeRules.bolaRule && settings.get().streaming.abr.activeRules.throughputRule + return settings.get().streaming.abr.rules.bolaRule.active && settings.get().streaming.abr.rules.throughputRule.active } /** diff --git a/src/streaming/controllers/ThroughputController.js b/src/streaming/controllers/ThroughputController.js index 4e954e6c78..e17445fe8d 100644 --- a/src/streaming/controllers/ThroughputController.js +++ b/src/streaming/controllers/ThroughputController.js @@ -93,7 +93,7 @@ function ThroughputController() { } /** - * Push new values to the throughput model once a HTTP request completed + * Push new values to the throughput model once an HTTP request completed * @param {object} e * @private */ diff --git a/src/streaming/net/HTTPLoader.js b/src/streaming/net/HTTPLoader.js index 08e6ada949..a2dafef08e 100644 --- a/src/streaming/net/HTTPLoader.js +++ b/src/streaming/net/HTTPLoader.js @@ -171,6 +171,7 @@ function HTTPLoader(cfg) { t: event.throughput }); + requestObject.traces = traces; lastTraceTime = currentTime; lastTraceReceivedCount = event.loaded; } @@ -234,7 +235,7 @@ function HTTPLoader(cfg) { } else { _onerror(); } - }); + }); }; /** @@ -294,14 +295,14 @@ function HTTPLoader(cfg) { return new Promise((resolve) => { _applyRequestInterceptors(httpRequest).then((_httpRequest) => { httpRequest = _httpRequest; - + httpRequest.customData.onload = _onload; httpRequest.customData.onloadend = _onloadend; httpRequest.customData.onerror = _onloadend; httpRequest.customData.onprogress = _onprogress; httpRequest.customData.onabort = _onabort; httpRequest.customData.ontimeout = _ontimeout; - + httpResponse.resourceTiming.startTime = Date.now(); loader.load(httpRequest, httpResponse); resolve(); @@ -353,10 +354,11 @@ function HTTPLoader(cfg) { let httpRequest; // CommonMediaLibrary.request.CommonMediaRequest let httpResponse; // CommonMediaLibrary.request.CommonMediaResponse - + requestObject.bytesLoaded = NaN; requestObject.bytesTotal = NaN; requestObject.firstByteDate = null; + requestObject.traces = []; firstProgress = true; requestStartTime = new Date(); lastTraceTime = requestStartTime; diff --git a/src/streaming/rules/abr/ABRRulesCollection.js b/src/streaming/rules/abr/ABRRulesCollection.js index 6870aff918..4f60b9de52 100644 --- a/src/streaming/rules/abr/ABRRulesCollection.js +++ b/src/streaming/rules/abr/ABRRulesCollection.js @@ -115,13 +115,13 @@ function ABRRulesCollection(config) { function _handleRuleUpdate(ruleName, rulesCollection) { // we use camel case in the settings while the rules start with a capital latter. Not ideal but convert between these formats const attribute = ruleName.charAt(0).toLowerCase() + ruleName.slice(1); - if (settings.get().streaming.abr.activeRules[attribute] && !_arrayContainsRule(rulesCollection, ruleName)) { + if (settings.get().streaming.abr.rules[attribute].active && !_arrayContainsRule(rulesCollection, ruleName)) { rulesCollection.push( _createRuleInstance(ruleName) ); return rulesCollection - } else if (!settings.get().streaming.abr.activeRules[attribute]) { + } else if (!settings.get().streaming.abr.rules[attribute].active) { return _removeRuleFromArray(rulesCollection, ruleName) } diff --git a/src/streaming/rules/abr/AbandonRequestsRule.js b/src/streaming/rules/abr/AbandonRequestsRule.js index b317d73b9c..ebf09b5b8a 100644 --- a/src/streaming/rules/abr/AbandonRequestsRule.js +++ b/src/streaming/rules/abr/AbandonRequestsRule.js @@ -31,122 +31,57 @@ import SwitchRequest from '../SwitchRequest.js'; import FactoryMaker from '../../../core/FactoryMaker.js'; import Debug from '../../../core/Debug.js'; +import Settings from '../../../core/Settings.js'; function AbandonRequestsRule(config) { config = config || {}; const mediaPlayerModel = config.mediaPlayerModel; const dashMetrics = config.dashMetrics; - const ABANDON_MULTIPLIER = 1.8; - const GRACE_TIME_THRESHOLD = 500; - const MIN_LENGTH_TO_AVERAGE = 5; - const context = this.context; + const settings = Settings(context).getInstance(); let instance, logger, - fragmentDict, - abandonDict, - throughputArray; + abandonDict; function setup() { logger = Debug(context).getInstance().getLogger(instance); reset(); } - function _setFragmentRequestDict(type, id) { - fragmentDict[type] = fragmentDict[type] || {}; - fragmentDict[type][id] = fragmentDict[type][id] || {}; - } - - function _storeLastRequestThroughputByType(type, throughput) { - throughputArray[type] = throughputArray[type] || []; - throughputArray[type].push(throughput); - } function shouldAbandon(rulesContext) { const switchRequest = SwitchRequest(context).create(); switchRequest.rule = this.getClassName(); try { - if (!rulesContext) { return switchRequest } + const request = rulesContext.getCurrentRequest(); - const mediaInfo = rulesContext.getMediaInfo(); - const mediaType = rulesContext.getMediaType(); - const req = rulesContext.getCurrentRequest(); - - if (!isNaN(req.index)) { - _setFragmentRequestDict(mediaType, req.index); + if (!isNaN(request.index)) { - const stableBufferTime = mediaPlayerModel.getBufferTimeDefault(); - const bufferLevel = dashMetrics.getCurrentBufferLevel(mediaType); - if ( bufferLevel > stableBufferTime ) { + // In case we abandoned already or do not have enough information to proceed we return here + if (request.firstByteDate === null || abandonDict.hasOwnProperty(request.index)) { return switchRequest; } - const fragmentInfo = fragmentDict[mediaType][req.index]; - if (fragmentInfo === null || req.firstByteDate === null || abandonDict.hasOwnProperty(fragmentInfo.id)) { + // Do not abandon if the buffer level is larger than the stable time + const stableBufferTime = mediaPlayerModel.getBufferTimeDefault(); + const mediaType = rulesContext.getMediaType(); + const bufferLevel = dashMetrics.getCurrentBufferLevel(mediaType); + if (bufferLevel > stableBufferTime) { return switchRequest; } - // setup some init info based on first progress event - if (fragmentInfo.firstByteTime === undefined) { - throughputArray[mediaType] = []; - fragmentInfo.firstByteTime = req.firstByteDate.getTime(); - fragmentInfo.segmentDuration = req.duration; - fragmentInfo.bytesTotal = req.bytesTotal; - fragmentInfo.id = req.index; - } - fragmentInfo.bytesLoaded = req.bytesLoaded; - fragmentInfo.elapsedTime = new Date().getTime() - fragmentInfo.firstByteTime; - - // Store throughput for each progress event we are getting, see ABR controller - if (fragmentInfo.bytesLoaded > 0 && fragmentInfo.elapsedTime > 0) { - _storeLastRequestThroughputByType(mediaType, Math.round(fragmentInfo.bytesLoaded * 8 / fragmentInfo.elapsedTime)); - } - - // Activate rule once we have enough samples and initial startup time has elapsed - if (throughputArray[mediaType].length >= MIN_LENGTH_TO_AVERAGE && - fragmentInfo.elapsedTime > GRACE_TIME_THRESHOLD && - fragmentInfo.bytesLoaded < fragmentInfo.bytesTotal) { - - const requestedRepresentation = req.representation; - const totalSampledValue = throughputArray[mediaType].reduce((a, b) => a + b, 0); - fragmentInfo.measuredBandwidthInKbps = Math.round(totalSampledValue / throughputArray[mediaType].length); - fragmentInfo.estimatedTimeOfDownload = +((fragmentInfo.bytesTotal * 8 / fragmentInfo.measuredBandwidthInKbps) / 1000).toFixed(2); - - // We do not abandon if the estimated download time is below a threshold, or we are on the lowest quality anyway. - const representation = rulesContext.getRepresentation(); - const abrController = rulesContext.getAbrController(); - if (fragmentInfo.estimatedTimeOfDownload < fragmentInfo.segmentDuration * ABANDON_MULTIPLIER || abrController.isPlayingAtLowestQuality(representation)) { - return switchRequest; - } - - if (!abandonDict.hasOwnProperty(fragmentInfo.id)) { - const abrController = rulesContext.getAbrController(); - const bytesRemaining = fragmentInfo.bytesTotal - fragmentInfo.bytesLoaded; - const newRepresentation = abrController.getOptimalRepresentationForBitrate(mediaInfo, fragmentInfo.measuredBandwidthInKbps, true); - const estimatedBytesForNewPresentation = fragmentInfo.bytesTotal * newRepresentation.bitrateInKbit / requestedRepresentation.bitrateInKbit; - - if (bytesRemaining > estimatedBytesForNewPresentation) { - switchRequest.representation = newRepresentation; - switchRequest.reason = { - throughput: fragmentInfo.measuredBandwidthInKbps, - fragmentID: fragmentInfo.id - } - abandonDict[fragmentInfo.id] = fragmentInfo; - logger.debug('[' + mediaType + '] frag id', fragmentInfo.id, ' is asking to abandon and switch to quality to ', newRepresentation.absoluteIndex, ' measured bandwidth was', fragmentInfo.measuredBandwidthInKbps); - delete fragmentDict[mediaType][fragmentInfo.id]; - } - } - } - - // Done loading we can delete the fragment from the dict - else if (fragmentInfo.bytesLoaded === fragmentInfo.bytesTotal) { - delete fragmentDict[mediaType][fragmentInfo.id]; + // Activate rule once we have enough samples, the initial startup time has elapsed and the download is not finished yet + const elapsedTimeSinceFirstByteInMs = Date.now() - request.firstByteDate.getTime(); + if (request.traces.length >= settings.get().streaming.abr.rules.abandonRequestsRule.parameters.minThroughputSamplesThreshold && + elapsedTimeSinceFirstByteInMs > settings.get().streaming.abr.rules.abandonRequestsRule.parameters.minSegmentDownloadTimeThresholdInMs && + request.bytesLoaded < request.bytesTotal) { + return _getSwitchRequest(rulesContext, request, switchRequest); } } @@ -157,10 +92,45 @@ function AbandonRequestsRule(config) { } } + function _getSwitchRequest(rulesContext, request, switchRequest) { + const mediaInfo = rulesContext.getMediaInfo(); + const mediaType = rulesContext.getMediaType(); + + // Use the traces of the request to derive the mean throughput. We ignore the first entries as they contain the latency of the request. + const downloadedBytes = request.traces.reduce((prev, curr) => prev + curr.b[0], 0) - request.traces[0].b[0]; + const downloadTimeInMs = Math.max(request.traces.reduce((prev, curr) => prev + curr.d, 0) - request.traces[0].d, 1); + const throughputInKbit = Math.round((8 * downloadedBytes) / downloadTimeInMs) + const estimatedTimeOfDownloadInSeconds = Number((request.bytesTotal * 8 / throughputInKbit) / 1000).toFixed(2); + + // We do not abandon if the estimated download time is below a constant multiple of the segment duration, or if we are on the lowest quality anyway. + const representation = rulesContext.getRepresentation(); + const abrController = rulesContext.getAbrController(); + if (estimatedTimeOfDownloadInSeconds < request.duration * settings.get().streaming.abr.rules.abandonRequestsRule.parameters.abandonDurationMultiplier || abrController.isPlayingAtLowestQuality(representation)) { + return switchRequest; + } + + if (!abandonDict.hasOwnProperty(request.index)) { + const abrController = rulesContext.getAbrController(); + const remainingBytesToDownload = request.bytesTotal - request.bytesLoaded; + const optimalRepresentationForBitrate = abrController.getOptimalRepresentationForBitrate(mediaInfo, throughputInKbit, true); + const currentRequestedRepresentation = request.representation; + const totalBytesForOptimalRepresentation = request.bytesTotal * optimalRepresentationForBitrate.bitrateInKbit / currentRequestedRepresentation.bitrateInKbit; + + if (remainingBytesToDownload > totalBytesForOptimalRepresentation) { + switchRequest.representation = optimalRepresentationForBitrate; + switchRequest.reason = { + throughputInKbit + } + 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`); + } + } + + return switchRequest + } + function reset() { - fragmentDict = {}; abandonDict = {}; - throughputArray = []; } instance = { diff --git a/test/unit/streaming.rules.abr.ABRRulesCollection.js b/test/unit/streaming.rules.abr.ABRRulesCollection.js index 1907499ad0..dd4970f165 100644 --- a/test/unit/streaming.rules.abr.ABRRulesCollection.js +++ b/test/unit/streaming.rules.abr.ABRRulesCollection.js @@ -25,15 +25,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: true, - bolaRule: true, - insufficientBufferRule: true, - switchHistoryRule: true, - droppedFramesRule: true, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: true + }, + switchHistoryRule: { + active: true + }, + droppedFramesRule: { + active: true + }, + abandonRequestsRule: { + active: true + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -65,15 +81,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: true, - bolaRule: true, - insufficientBufferRule: true, - switchHistoryRule: true, - droppedFramesRule: true, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: true + }, + switchHistoryRule: { + active: true + }, + droppedFramesRule: { + active: true + }, + abandonRequestsRule: { + active: true + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -102,15 +134,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: false, - bolaRule: false, - insufficientBufferRule: false, - switchHistoryRule: false, - droppedFramesRule: false, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: false + }, + bolaRule: { + active: false + }, + insufficientBufferRule: { + active: false + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: true + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -140,15 +188,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: false, - bolaRule: false, - insufficientBufferRule: true, - switchHistoryRule: true, - droppedFramesRule: false, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: false + }, + bolaRule: { + active: false + }, + insufficientBufferRule: { + active: true + }, + switchHistoryRule: { + active: true + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: true + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -174,15 +238,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: true, - bolaRule: true, - insufficientBufferRule: false, - switchHistoryRule: false, - droppedFramesRule: false, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: false + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: false + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -205,15 +285,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: false, - bolaRule: false, - insufficientBufferRule: true, - switchHistoryRule: true, - droppedFramesRule: false, - abandonRequestsRule: false, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: false + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: false + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } } @@ -233,15 +329,31 @@ describe('ABRRulesCollection', function () { settings.update({ streaming: { abr: { - activeRules: { - throughputRule: true, - bolaRule: true, - insufficientBufferRule: false, - switchHistoryRule: false, - droppedFramesRule: false, - abandonRequestsRule: true, - l2ARule: false, - loLPRule: false + rules: { + throughputRule: { + active: true + }, + bolaRule: { + active: true + }, + insufficientBufferRule: { + active: false + }, + switchHistoryRule: { + active: false + }, + droppedFramesRule: { + active: false + }, + abandonRequestsRule: { + active: true + }, + l2ARule: { + active: false + }, + loLPRule: { + active: false + } } } }