From 5f7b4dbaff41385e5d26b9d8e912d7632d4c658a Mon Sep 17 00:00:00 2001 From: Daniel Silhavy Date: Thu, 1 Aug 2024 12:00:14 +0200 Subject: [PATCH] Fixes some minor issues with the BOLA rule (#4536) --- src/core/Settings.js | 4 + src/core/events/CoreEvents.js | 2 + src/streaming/controllers/AbrController.js | 10 +- src/streaming/models/MetricsModel.js | 7 +- src/streaming/rules/abr/BolaRule.js | 129 ++++++++++-------- .../test-configurations/streams/all.json | 19 ++- 6 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/core/Settings.js b/src/core/Settings.js index 03ef79c39c..1a755c19f0 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -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, }; /** diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js index 89cec41ab4..9948be15aa 100644 --- a/src/core/events/CoreEvents.js +++ b/src/core/events/CoreEvents.js @@ -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'; } } diff --git a/src/streaming/controllers/AbrController.js b/src/streaming/controllers/AbrController.js index b0bd16a36f..b9503e749f 100644 --- a/src/streaming/controllers/AbrController.js +++ b/src/streaming/controllers/AbrController.js @@ -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) + ').'); diff --git a/src/streaming/models/MetricsModel.js b/src/streaming/models/MetricsModel.js index 985c47f044..59ea2989d2 100644 --- a/src/streaming/models/MetricsModel.js +++ b/src/streaming/models/MetricsModel.js @@ -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'; @@ -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 => { diff --git a/src/streaming/rules/abr/BolaRule.js b/src/streaming/rules/abr/BolaRule.js index 3179991e3a..c39a0d80e9 100644 --- a/src/streaming/rules/abr/BolaRule.js +++ b/src/streaming/rules/abr/BolaRule.js @@ -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 */ @@ -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; } } @@ -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 @@ -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 @@ -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; @@ -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 @@ -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 = { diff --git a/test/functional/config/test-configurations/streams/all.json b/test/functional/config/test-configurations/streams/all.json index c0c117f03e..3bcd04ec62 100644 --- a/test/functional/config/test-configurations/streams/all.json +++ b/test/functional/config/test-configurations/streams/all.json @@ -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",