UNPKG

dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

117 lines (116 loc) 22.3 kB
'use strict';Object.defineProperty(exports,"__esModule",{value:true});var _MetricsConstants=require('../../constants/MetricsConstants');var _MetricsConstants2=_interopRequireDefault(_MetricsConstants);var _SwitchRequest=require('../SwitchRequest');var _SwitchRequest2=_interopRequireDefault(_SwitchRequest);var _FactoryMaker=require('../../../core/FactoryMaker');var _FactoryMaker2=_interopRequireDefault(_FactoryMaker);var _HTTPRequest=require('../../vo/metrics/HTTPRequest');var _EventBus=require('../../../core/EventBus');var _EventBus2=_interopRequireDefault(_EventBus);var _Events=require('../../../core/events/Events');var _Events2=_interopRequireDefault(_Events);var _Debug=require('../../../core/Debug');var _Debug2=_interopRequireDefault(_Debug);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj};}// BOLA_STATE_ONE_BITRATE : If there is only one bitrate (or initialization failed), always return NO_CHANGE. // BOLA_STATE_STARTUP : Set placeholder buffer such that we download fragments at most recently measured throughput. // BOLA_STATE_STEADY : Buffer primed, we switch to steady operation. // TODO: add BOLA_STATE_SEEK and tune BOLA behavior on seeking var BOLA_STATE_ONE_BITRATE=0;/** * The copyright in this software is being made available under the BSD License, * included below. This software may be subject to other third party and contributor * rights, including patent rights, and no such rights are granted under this license. * * Copyright (c) 2016, Dash Industry Forum. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * Neither the name of Dash Industry Forum nor the names of its * contributors may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */// For a description of the BOLA adaptive bitrate (ABR) algorithm, see http://arxiv.org/abs/1601.06748 var BOLA_STATE_STARTUP=1;var BOLA_STATE_STEADY=2;var MINIMUM_BUFFER_S=10;// BOLA should never add artificial delays if buffer is less than MINIMUM_BUFFER_S. var MINIMUM_BUFFER_PER_BITRATE_LEVEL_S=2;// E.g. if there are 5 bitrates, BOLA switches to top bitrate at buffer = 10 + 5 * 2 = 20s. // If Schedule Controller does not allow buffer to reach that level, it can be achieved through the placeholder buffer level. var PLACEHOLDER_BUFFER_DECAY=0.99;// Make sure placeholder buffer does not stick around too long. function BolaRule(config){config=config||{};var context=this.context;var dashMetrics=config.dashMetrics;var mediaPlayerModel=config.mediaPlayerModel;var eventBus=(0,_EventBus2.default)(context).getInstance();var instance=void 0,logger=void 0,bolaStateDict=void 0;function setup(){logger=(0,_Debug2.default)(context).getInstance().getLogger(instance);resetInitialSettings();eventBus.on(_Events2.default.BUFFER_EMPTY,onBufferEmpty,instance);eventBus.on(_Events2.default.PLAYBACK_SEEKING,onPlaybackSeeking,instance);eventBus.on(_Events2.default.PERIOD_SWITCH_STARTED,onPeriodSwitchStarted,instance);eventBus.on(_Events2.default.MEDIA_FRAGMENT_LOADED,onMediaFragmentLoaded,instance);eventBus.on(_Events2.default.METRIC_ADDED,onMetricAdded,instance);eventBus.on(_Events2.default.QUALITY_CHANGE_REQUESTED,onQualityChangeRequested,instance);eventBus.on(_Events2.default.FRAGMENT_LOADING_ABANDONED,onFragmentLoadingAbandoned,instance);}function utilitiesFromBitrates(bitrates){return bitrates.map(function(b){return Math.log(b);});// no need to worry about offset, utilities will be offset (uniformly) anyway later }// 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 function calculateBolaParameters(stableBufferTime,bitrates,utilities){var highestUtilityIndex=utilities.reduce(function(highestIndex,u,uIndex){return u>utilities[highestIndex]?uIndex:highestIndex;},0);if(highestUtilityIndex===0){// if highestUtilityIndex === 0, then always use lowest bitrate return null;}var bufferTime=Math.max(stableBufferTime,MINIMUM_BUFFER_S+MINIMUM_BUFFER_PER_BITRATE_LEVEL_S*bitrates.length);// TODO: Investigate if following can be better if utilities are not the default Math.log utilities. // If using Math.log utilities, we can choose Vp and gp to always prefer bitrates[0] at minimumBufferS and bitrates[max] at bufferTarget. // (Vp * (utility + gp) - bufferLevel) / bitrate has the maxima described when: // Vp * (utilities[0] + gp - 1) === minimumBufferS and Vp * (utilities[max] + gp - 1) === bufferTarget // giving: var gp=(utilities[highestUtilityIndex]-1)/(bufferTime/MINIMUM_BUFFER_S-1);var Vp=MINIMUM_BUFFER_S/gp;// note that expressions for gp and Vp assume utilities[0] === 1, which is true because of normalization return{gp:gp,Vp:Vp};}function getInitialBolaState(rulesContext){var initialState={};var mediaInfo=rulesContext.getMediaInfo();var bitrates=mediaInfo.bitrateList.map(function(b){return b.bandwidth;});var utilities=utilitiesFromBitrates(bitrates);utilities=utilities.map(function(u){return u-utilities[0]+1;});// normalize var stableBufferTime=mediaPlayerModel.getStableBufferTime();var params=calculateBolaParameters(stableBufferTime,bitrates,utilities);if(!params){// only happens when there is only one bitrate level initialState.state=BOLA_STATE_ONE_BITRATE;}else{initialState.state=BOLA_STATE_STARTUP;initialState.bitrates=bitrates;initialState.utilities=utilities;initialState.stableBufferTime=stableBufferTime;initialState.Vp=params.Vp;initialState.gp=params.gp;initialState.lastQuality=0;clearBolaStateOnSeek(initialState);}return initialState;}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. function checkBolaStateStableBufferTime(bolaState,mediaType){var stableBufferTime=mediaPlayerModel.getStableBufferTime();if(bolaState.stableBufferTime!==stableBufferTime){var params=calculateBolaParameters(stableBufferTime,bolaState.bitrates,bolaState.utilities);if(params.Vp!==bolaState.Vp||params.gp!==bolaState.gp){// correct placeholder buffer using two criteria: // 1. do not change effective buffer level at effectiveBufferLevel === MINIMUM_BUFFER_S ( === Vp * gp ) // 2. scale placeholder buffer by Vp subject to offset indicated in 1. var bufferLevel=dashMetrics.getCurrentBufferLevel(mediaType,true);var effectiveBufferLevel=bufferLevel+bolaState.placeholderBuffer;effectiveBufferLevel-=MINIMUM_BUFFER_S;effectiveBufferLevel*=params.Vp/bolaState.Vp;effectiveBufferLevel+=MINIMUM_BUFFER_S;bolaState.stableBufferTime=stableBufferTime;bolaState.Vp=params.Vp;bolaState.gp=params.gp;bolaState.placeholderBuffer=Math.max(0,effectiveBufferLevel-bufferLevel);}}}function getBolaState(rulesContext){var mediaType=rulesContext.getMediaType();var bolaState=bolaStateDict[mediaType];if(!bolaState){bolaState=getInitialBolaState(rulesContext);bolaStateDict[mediaType]=bolaState;}else if(bolaState.state!==BOLA_STATE_ONE_BITRATE){checkBolaStateStableBufferTime(bolaState,mediaType);}return bolaState;}// The core idea of BOLA. function getQualityFromBufferLevel(bolaState,bufferLevel){var bitrateCount=bolaState.bitrates.length;var quality=NaN;var score=NaN;for(var i=0;i<bitrateCount;++i){var s=(bolaState.Vp*(bolaState.utilities[i]+bolaState.gp)-bufferLevel)/bolaState.bitrates[i];if(isNaN(score)||s>=score){score=s;quality=i;}}return quality;}// maximum buffer level which prefers to download at quality rather than wait function maxBufferLevelForQuality(bolaState,quality){return bolaState.Vp*(bolaState.utilities[quality]+bolaState.gp);}// the minimum buffer level that would cause BOLA to choose quality rather than a lower bitrate function minBufferLevelForQuality(bolaState,quality){var qBitrate=bolaState.bitrates[quality];var qUtility=bolaState.utilities[quality];var min=0;for(var i=quality-1;i>=0;--i){// for each bitrate less than bitrates[quality], BOLA should prefer quality (unless other bitrate has higher utility) if(bolaState.utilities[i]<bolaState.utilities[quality]){var iBitrate=bolaState.bitrates[i];var iUtility=bolaState.utilities[i];var level=bolaState.Vp*(bolaState.gp+(qBitrate*iUtility-iBitrate*qUtility)/(qBitrate-iBitrate));min=Math.max(min,level);// we want min to be small but at least level(i) for all i }}return min;}/* * The placeholder buffer increases the effective buffer that is used to calculate the bitrate. * There are two main reasons we might want to increase the placeholder buffer: * * 1. When a segment finishes downloading, we would expect to get a call on getMaxIndex() regarding the quality for * the next segment. However, there might be a delay before the next call. E.g. when streaming live content, the * next segment might not be available yet. If the call to getMaxIndex() does happens after a delay, we don't * want the delay to change the BOLA decision - we only want to factor download time to decide on bitrate level. * * 2. It is possible to get a call to getMaxIndex() without having a segment download. The buffer target in dash.js * is different for top-quality segments and lower-quality segments. If getMaxIndex() returns a lower-than-top * quality, then the buffer controller might decide not to download a segment. When dash.js is ready for the next * segment, getMaxIndex() will be called again. We don't want this extra delay to factor in the bitrate decision. */function updatePlaceholderBuffer(bolaState,mediaType){var nowMs=Date.now();if(!isNaN(bolaState.lastSegmentFinishTimeMs)){// compensate for non-bandwidth-derived delays, e.g., live streaming availability, buffer controller var delay=0.001*(nowMs-bolaState.lastSegmentFinishTimeMs);bolaState.placeholderBuffer+=Math.max(0,delay);}else if(!isNaN(bolaState.lastCallTimeMs)){// no download after last call, compensate for delay between calls var _delay=0.001*(nowMs-bolaState.lastCallTimeMs);bolaState.placeholderBuffer+=Math.max(0,_delay);}bolaState.lastCallTimeMs=nowMs;bolaState.lastSegmentStart=NaN;bolaState.lastSegmentRequestTimeMs=NaN;bolaState.lastSegmentFinishTimeMs=NaN;checkBolaStateStableBufferTime(bolaState,mediaType);}function onBufferEmpty(){// if we rebuffer, we don't want the placeholder buffer to artificially raise BOLA quality for(var mediaType in bolaStateDict){if(bolaStateDict.hasOwnProperty(mediaType)&&bolaStateDict[mediaType].state===BOLA_STATE_STEADY){bolaStateDict[mediaType].placeholderBuffer=0;}}}function onPlaybackSeeking(){// TODO: 1. Verify what happens if we seek mid-fragment. // TODO: 2. If e.g. we have 10s fragments and seek, we might want to download the first fragment at a lower quality to restart playback quickly. for(var mediaType in bolaStateDict){if(bolaStateDict.hasOwnProperty(mediaType)){var bolaState=bolaStateDict[mediaType];if(bolaState.state!==BOLA_STATE_ONE_BITRATE){bolaState.state=BOLA_STATE_STARTUP;// TODO: BOLA_STATE_SEEK? clearBolaStateOnSeek(bolaState);}}}}function onPeriodSwitchStarted(){// TODO: does this have to be handled here? }function onMediaFragmentLoaded(e){if(e&&e.chunk&&e.chunk.mediaInfo){var bolaState=bolaStateDict[e.chunk.mediaInfo.type];if(bolaState&&bolaState.state!==BOLA_STATE_ONE_BITRATE){var start=e.chunk.start;if(isNaN(bolaState.mostAdvancedSegmentStart)||start>bolaState.mostAdvancedSegmentStart){bolaState.mostAdvancedSegmentStart=start;bolaState.lastSegmentWasReplacement=false;}else{bolaState.lastSegmentWasReplacement=true;}bolaState.lastSegmentStart=start;bolaState.lastSegmentDurationS=e.chunk.duration;bolaState.lastQuality=e.chunk.quality;checkNewSegment(bolaState,e.chunk.mediaInfo.type);}}}function onMetricAdded(e){if(e&&e.metric===_MetricsConstants2.default.HTTP_REQUEST&&e.value&&e.value.type===_HTTPRequest.HTTPRequest.MEDIA_SEGMENT_TYPE&&e.value.trace&&e.value.trace.length){var bolaState=bolaStateDict[e.mediaType];if(bolaState&&bolaState.state!==BOLA_STATE_ONE_BITRATE){bolaState.lastSegmentRequestTimeMs=e.value.trequest.getTime();bolaState.lastSegmentFinishTimeMs=e.value._tfinish.getTime();checkNewSegment(bolaState,e.mediaType);}}}/* * 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. */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)){var bufferLevel=dashMetrics.getCurrentBufferLevel(mediaType,true);var bufferAtLastSegmentRequest=bufferLevel+0.001*(bolaState.lastSegmentFinishTimeMs-bolaState.lastSegmentRequestTimeMs);// estimate var maxEffectiveBufferForLastSegment=maxBufferLevelForQuality(bolaState,bolaState.lastQuality);var 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 onQualityChangeRequested(e){// Useful to store change requests when abandoning a download. if(e){var bolaState=bolaStateDict[e.mediaType];if(bolaState&&bolaState.state!==BOLA_STATE_ONE_BITRATE){bolaState.abrQuality=e.newQuality;}}}function onFragmentLoadingAbandoned(e){if(e){var bolaState=bolaStateDict[e.mediaType];if(bolaState&&bolaState.state!==BOLA_STATE_ONE_BITRATE){// deflate placeholderBuffer - note that we want to be conservative when abandoning var bufferLevel=dashMetrics.getCurrentBufferLevel(e.mediaType,true);var wantEffectiveBufferLevel=void 0;if(bolaState.abrQuality>0){// deflate to point where BOLA just chooses newQuality over newQuality-1 wantEffectiveBufferLevel=minBufferLevelForQuality(bolaState,bolaState.abrQuality);}else{wantEffectiveBufferLevel=MINIMUM_BUFFER_S;}var maxPlaceholderBuffer=Math.max(0,wantEffectiveBufferLevel-bufferLevel);bolaState.placeholderBuffer=Math.min(bolaState.placeholderBuffer,maxPlaceholderBuffer);}}}function getMaxIndex(rulesContext){var switchRequest=(0,_SwitchRequest2.default)(context).create();if(!rulesContext||!rulesContext.hasOwnProperty('getMediaInfo')||!rulesContext.hasOwnProperty('getMediaType')||!rulesContext.hasOwnProperty('getScheduleController')||!rulesContext.hasOwnProperty('getStreamInfo')||!rulesContext.hasOwnProperty('getAbrController')||!rulesContext.hasOwnProperty('useBufferOccupancyABR')){return switchRequest;}var mediaInfo=rulesContext.getMediaInfo();var mediaType=rulesContext.getMediaType();var scheduleController=rulesContext.getScheduleController();var streamInfo=rulesContext.getStreamInfo();var abrController=rulesContext.getAbrController();var throughputHistory=abrController.getThroughputHistory();var streamId=streamInfo?streamInfo.id:null;var isDynamic=streamInfo&&streamInfo.manifestInfo&&streamInfo.manifestInfo.isDynamic;var useBufferOccupancyABR=rulesContext.useBufferOccupancyABR();switchRequest.reason=switchRequest.reason||{};if(!useBufferOccupancyABR){return switchRequest;}scheduleController.setTimeToLoadDelay(0);var bolaState=getBolaState(rulesContext);if(bolaState.state===BOLA_STATE_ONE_BITRATE){// shouldn't even have been called return switchRequest;}var bufferLevel=dashMetrics.getCurrentBufferLevel(mediaType,true);var throughput=throughputHistory.getAverageThroughput(mediaType,isDynamic);var safeThroughput=throughputHistory.getSafeAverageThroughput(mediaType,isDynamic);var latency=throughputHistory.getAverageLatency(mediaType);var quality=void 0;switchRequest.reason.state=bolaState.state;switchRequest.reason.throughput=throughput;switchRequest.reason.latency=latency;if(isNaN(throughput)){// isNaN(throughput) === isNaN(safeThroughput) === isNaN(latency) // still starting up - not enough information return switchRequest;}switch(bolaState.state){case BOLA_STATE_STARTUP:quality=abrController.getQualityForBitrate(mediaInfo,safeThroughput,latency);switchRequest.quality=quality;switchRequest.reason.throughput=safeThroughput;bolaState.placeholderBuffer=Math.max(0,minBufferLevelForQuality(bolaState,quality)-bufferLevel);bolaState.lastQuality=quality;if(!isNaN(bolaState.lastSegmentDurationS)&&bufferLevel>=bolaState.lastSegmentDurationS){bolaState.state=BOLA_STATE_STEADY;}break;// BOLA_STATE_STARTUP case BOLA_STATE_STEADY:// NB: The placeholder buffer is added to bufferLevel to come up with a bitrate. // This might lead BOLA to be too optimistic and to choose a bitrate that would lead to rebuffering - // if the real buffer bufferLevel runs out, the placeholder buffer cannot prevent rebuffering. // However, the InsufficientBufferRule takes care of this scenario. updatePlaceholderBuffer(bolaState,mediaType);quality=getQualityFromBufferLevel(bolaState,bufferLevel+bolaState.placeholderBuffer);// we want to avoid oscillations // We implement the "BOLA-O" variant: when network bandwidth lies between two encoded bitrate levels, stick to the lowest level. var qualityForThroughput=abrController.getQualityForBitrate(mediaInfo,safeThroughput,latency);if(quality>bolaState.lastQuality&&quality>qualityForThroughput){// only intervene if we are trying to *increase* quality to an *unsustainable* level // we are only avoid oscillations - do not drop below last quality quality=Math.max(qualityForThroughput,bolaState.lastQuality);}// We do not want to overfill buffer with low quality chunks. // Note that there will be no delay if buffer level is below MINIMUM_BUFFER_S, probably even with some margin higher than MINIMUM_BUFFER_S. var delayS=Math.max(0,bufferLevel+bolaState.placeholderBuffer-maxBufferLevelForQuality(bolaState,quality));// First reduce placeholder buffer, then tell schedule controller to pause. if(delayS<=bolaState.placeholderBuffer){bolaState.placeholderBuffer-=delayS;delayS=0;}else{delayS-=bolaState.placeholderBuffer;bolaState.placeholderBuffer=0;if(quality<abrController.getTopQualityIndexFor(mediaType,streamId)){// At top quality, allow schedule controller to decide how far to fill buffer. scheduleController.setTimeToLoadDelay(1000*delayS);}else{delayS=0;}}switchRequest.quality=quality;switchRequest.reason.throughput=throughput;switchRequest.reason.latency=latency;switchRequest.reason.bufferLevel=bufferLevel;switchRequest.reason.placeholderBuffer=bolaState.placeholderBuffer;switchRequest.reason.delay=delayS;bolaState.lastQuality=quality;// keep bolaState.state === BOLA_STATE_STEADY break;// BOLA_STATE_STEADY default:logger.debug('BOLA ABR rule invoked in bad state.');// should not arrive here, try to recover switchRequest.quality=abrController.getQualityForBitrate(mediaInfo,safeThroughput,latency);switchRequest.reason.state=bolaState.state;switchRequest.reason.throughput=safeThroughput;switchRequest.reason.latency=latency;bolaState.state=BOLA_STATE_STARTUP;clearBolaStateOnSeek(bolaState);}return switchRequest;}function resetInitialSettings(){bolaStateDict={};}function reset(){resetInitialSettings();eventBus.off(_Events2.default.BUFFER_EMPTY,onBufferEmpty,instance);eventBus.off(_Events2.default.PLAYBACK_SEEKING,onPlaybackSeeking,instance);eventBus.off(_Events2.default.PERIOD_SWITCH_STARTED,onPeriodSwitchStarted,instance);eventBus.off(_Events2.default.MEDIA_FRAGMENT_LOADED,onMediaFragmentLoaded,instance);eventBus.off(_Events2.default.METRIC_ADDED,onMetricAdded,instance);eventBus.off(_Events2.default.QUALITY_CHANGE_REQUESTED,onQualityChangeRequested,instance);eventBus.off(_Events2.default.FRAGMENT_LOADING_ABANDONED,onFragmentLoadingAbandoned,instance);}instance={getMaxIndex:getMaxIndex,reset:reset};setup();return instance;}BolaRule.__dashjs_factory_name='BolaRule';exports.default=_FactoryMaker2.default.getClassFactory(BolaRule); //# sourceMappingURL=BolaRule.js.map