dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
117 lines (116 loc) • 22.3 kB
JavaScript
;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