# All files / src/controller abr-controller.js

50% Statements 75/150
33.93% Branches 38/112
100% Functions 1/1
48.98% Lines 72/147

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335

    1x

    142x
    142x

    142x
    142x
    142x
    142x
    142x
    142x

    15x
    15x

    3x
    3x

    20x
    20x

    18x

    18x

    18x

    18x

    18x

    16x
    16x

    16x

    77x

    3x
    3x

    74x

    36x
    36x

    1x
    1x

    1x

    1x

    1x

    1x

    1x
    1x
    1x
    1x
    1x
    1x

    1x
    1x

    1x

    1x
    1x

    1x

    1x
    1x
    1x

    1x

    1x
    1x

    1x
    1x

    2x
    12x

    12x

    12x
    12x
    12x

    12x

    12x
    2x

    10x

    12x
    12x

    12x

    12x

    1x

    1x

    2x

     /*
     * simple ABR Controller
     *  - compute next level based on last fragment bw heuristics
     *  - implement an abandon rules triggered if we have less than 2 frag buffered and if computed bw shows that we risk buffer stalling
     */

    import Event from '../events';
    import EventHandler from '../event-handler';
    import { BufferHelper } from '../utils/buffer-helper';
    import { ErrorDetails } from '../errors';
    import { logger } from '../utils/logger';
    import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';

    const { performance } = window;

    class AbrController extends EventHandler {
      constructor (hls) {
        super(hls, Event.FRAG_LOADING,
          Event.FRAG_LOADED,
          Event.FRAG_BUFFERED,
          Event.ERROR);
        this.lastLoadedFragLevel = 0;
        this._nextAutoLevel = -1;
        this.hls = hls;
        this.timer = null;
        this._bwEstimator = null;
        this.onCheck = this._abandonRulesCheck.bind(this);
      }

      destroy () {
        this.clearTimer();
        EventHandler.prototype.destroy.call(this);
      }

      onFragLoading (data) {
        const frag = data.frag;
        Iif (frag.type === 'main') {
          if (!this.timer) {
            this.fragCurrent = frag;
            this.timer = setInterval(this.onCheck, 100);
          }

          // lazy init of BwEstimator, rationale is that we use different params for Live/VoD
          // so we need to wait for stream manifest / playlist type to instantiate it.
          if (!this._bwEstimator) {
            const hls = this.hls;
            const config = hls.config;
            const level = frag.level;
            const isLive = hls.levels[level].details.live;

            let ewmaFast;
            let ewmaSlow;
            if (isLive) {
              ewmaFast = config.abrEwmaFastLive;
              ewmaSlow = config.abrEwmaSlowLive;
            } else {
              ewmaFast = config.abrEwmaFastVoD;
              ewmaSlow = config.abrEwmaSlowVoD;
            }
            this._bwEstimator = new EwmaBandWidthEstimator(hls, ewmaSlow, ewmaFast, config.abrEwmaDefaultEstimate);
          }
        }
      }

      _abandonRulesCheck () {
        /*
          monitor fragment retrieval time...
          we compute expected time of arrival of the complete fragment.
          we compare it to expected time of buffer starvation
        */
        const hls = this.hls;
        const video = hls.media;
        const frag = this.fragCurrent;

        if (!frag) {
          return;
        }

        const loader = frag.loader;
        const minAutoLevel = hls.minAutoLevel;

        // if loader has been destroyed or loading has been aborted, stop timer and return
        if (!loader || (loader.stats && loader.stats.aborted)) {
          logger.warn('frag loader destroy or aborted, disarm abandonRules');
          this.clearTimer();
          // reset forced auto level value so that next level will be selected
          this._nextAutoLevel = -1;
          return;
        }
        let stats = loader.stats;
        /* only monitor frag retrieval time if
        (video not paused OR first fragment being loaded(ready state === HAVE_NOTHING = 0)) AND autoswitching enabled AND not lowest level (=> means that we have several levels) */
        if (video && stats && ((!video.paused && (video.playbackRate !== 0)) || !video.readyState) && frag.autoLevel && frag.level) {
          const requestDelay = performance.now() - stats.trequest;
          const playbackRate = Math.abs(video.playbackRate);

          // monitor fragment load progress after half of expected fragment duration,to stabilize bitrate
          if (requestDelay > (500 * frag.duration / playbackRate)) {
            const levels = hls.levels;
            const loadRate = Math.max(1, stats.bw ? stats.bw / 8 : stats.loaded * 1000 / requestDelay); // byte/s; at least 1 byte/s to avoid division by zero

            // compute expected fragment length using frag duration and level bitrate. also ensure that expected len is gte than already loaded size
            const level = levels[frag.level];
            const levelBitrate = level.realBitrate ? Math.max(level.realBitrate, level.bitrate) : level.bitrate;
            const expectedLen = stats.total ? stats.total : Math.max(stats.loaded, Math.round(frag.duration * levelBitrate / 8));
            const pos = video.currentTime;
            const fragLoadedDelay = (expectedLen - stats.loaded) / loadRate;
            const bufferStarvationDelay = (BufferHelper.bufferInfo(video, pos, hls.config.maxBufferHole).end - pos) / playbackRate;

            // consider emergency switch down only if we have less than 2 frag buffered AND
            // time to finish loading current fragment is bigger than buffer starvation delay
            // ie if we risk buffer starvation if bw does not increase quickly
            if ((bufferStarvationDelay < (2 * frag.duration / playbackRate)) && (fragLoadedDelay > bufferStarvationDelay)) {
              let fragLevelNextLoadedDelay;
              let nextLoadLevel;
              // lets iterate through lower level and try to find the biggest one that could avoid rebuffering
              // we start from current level - 1 and we step down , until we find a matching level
              for (nextLoadLevel = frag.level - 1; nextLoadLevel > minAutoLevel; nextLoadLevel--) {
                // compute time to load next fragment at lower level
                // 0.8 : consider only 80% of current bw to be conservative
                // 8 = bits per byte (bps/Bps)
                const levelNextBitrate = levels[nextLoadLevel].realBitrate
                  ? Math.max(levels[nextLoadLevel].realBitrate, levels[nextLoadLevel].bitrate)
                  : levels[nextLoadLevel].bitrate;

                const fragLevelNextLoadedDelay = frag.duration * levelNextBitrate / (8 * 0.8 * loadRate);

                if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
                  // we found a lower level that be rebuffering free with current estimated bw !
                  break;
                }
              }
              // only emergency switch down if it takes less time to load new fragment at lowest level instead
              // of finishing loading current one ...
              if (fragLevelNextLoadedDelay < fragLoadedDelay) {
                logger.warn(`loading too slow, abort fragment loading and switch to level ${nextLoadLevel}:fragLoadedDelay[${nextLoadLevel}]<fragLoadedDelay[${frag.level - 1}];bufferStarvationDelay:${fragLevelNextLoadedDelay.toFixed(1)}<${fragLoadedDelay.toFixed(1)}:${bufferStarvationDelay.toFixed(1)}`);
                // force next load level in auto mode
                hls.nextLoadLevel = nextLoadLevel;
                // update bw estimate for this fragment before cancelling load (this will help reducing the bw)
                this._bwEstimator.sample(requestDelay, stats.loaded);
                // abort fragment loading
                loader.abort();
                // stop abandon rules timer
                this.clearTimer();
                hls.trigger(Event.FRAG_LOAD_EMERGENCY_ABORTED, { frag: frag, stats: stats });
              }
            }
          }
        }
      }

      onFragLoaded (data) {
        const frag = data.frag;
        if (frag.type === 'main' && Number.isFinite(frag.sn)) {
          // stop monitoring bw once frag loaded
          this.clearTimer();
          // store level id after successful fragment load
          this.lastLoadedFragLevel = frag.level;
          // reset forced auto level value so that next level will be selected
          this._nextAutoLevel = -1;

          // compute level average bitrate
          Iif (this.hls.config.abrMaxWithRealBitrate) {
            const level = this.hls.levels[frag.level];
            let loadedBytes = (level.loaded ? level.loaded.bytes : 0) + data.stats.loaded;
            let loadedDuration = (level.loaded ? level.loaded.duration : 0) + data.frag.duration;
            level.loaded = { bytes: loadedBytes, duration: loadedDuration };
            level.realBitrate = Math.round(8 * loadedBytes / loadedDuration);
          }
          // if fragment has been loaded to perform a bitrate test,
          Iif (data.frag.bitrateTest) {
            let stats = data.stats;
            stats.tparsed = stats.tbuffered = stats.tload;
            this.onFragBuffered(data);
          }
        }
      }

      onFragBuffered (data) {
        const stats = data.stats;
        const frag = data.frag;
        // only update stats on first frag buffering
        // if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
        // and leading to wrong bw estimation
        // on bitrate test, also only update stats once (if tload = tbuffered == on FRAG_LOADED)
        Iif (stats.aborted !== true && frag.type === 'main' && Number.isFinite(frag.sn) && ((!frag.bitrateTest || stats.tload === stats.tbuffered))) {
          // use tparsed-trequest instead of tbuffered-trequest to compute fragLoadingProcessing; rationale is that  buffer appending only happens once media is attached
          // in case we use config.startFragPrefetch while media is not attached yet, fragment might be parsed while media not attached yet, but it will only be buffered on media attached
          // as a consequence it could happen really late in the process. meaning that appending duration might appears huge ... leading to underestimated throughput estimation
          let fragLoadingProcessingMs = stats.tparsed - stats.trequest;
          logger.log(`latency/loading/parsing/append/kbps:${Math.round(stats.tfirst - stats.trequest)}/${Math.round(stats.tload - stats.tfirst)}/${Math.round(stats.tparsed - stats.tload)}/${Math.round(stats.tbuffered - stats.tparsed)}/${Math.round(8 * stats.loaded / (stats.tbuffered - stats.trequest))}`);
          this._bwEstimator.sample(fragLoadingProcessingMs, stats.loaded);
          stats.bwEstimate = this._bwEstimator.getEstimate();
          // if fragment has been loaded to perform a bitrate test, (hls.startLevel = -1), store bitrate test delay duration
          if (frag.bitrateTest) {
            this.bitrateTestDelay = fragLoadingProcessingMs / 1000;
          } else {
            this.bitrateTestDelay = 0;
          }
        }
      }

      onError (data) {
        // stop timer in case of frag loading error
        switch (data.details) {
        case ErrorDetails.FRAG_LOAD_ERROR:
        case ErrorDetails.FRAG_LOAD_TIMEOUT:
          this.clearTimer();
          break;
        default:
          break;
        }
      }

      clearTimer () {
        clearInterval(this.timer);
        this.timer = null;
      }

      // return next auto level
      get nextAutoLevel () {
        const forcedAutoLevel = this._nextAutoLevel;
        const bwEstimator = this._bwEstimator;
        // in case next auto level has been forced, and bw not available or not reliable, return forced value
        Iif (forcedAutoLevel !== -1 && (!bwEstimator || !bwEstimator.canEstimate())) {
          return forcedAutoLevel;
        }

        // compute next level using ABR logic
        let nextABRAutoLevel = this._nextABRAutoLevel;
        // if forced auto level has been defined, use it to cap ABR computed quality level
        Iif (forcedAutoLevel !== -1) {
          nextABRAutoLevel = Math.min(forcedAutoLevel, nextABRAutoLevel);
        }

        return nextABRAutoLevel;
      }
      get _nextABRAutoLevel () {
        let hls = this.hls;
        const { maxAutoLevel, levels, config, minAutoLevel } = hls;
        const video = hls.media;
        const currentLevel = this.lastLoadedFragLevel;
        const currentFragDuration = this.fragCurrent ? this.fragCurrent.duration : 0;
        const pos = (video ? video.currentTime : 0);

        // playbackRate is the absolute value of the playback rate; if video.playbackRate is 0, we use 1 to load as
        // if we're playing back at the normal rate.
        const playbackRate = ((video && (video.playbackRate !== 0)) ? Math.abs(video.playbackRate) : 1.0);
        const avgbw = this._bwEstimator ? this._bwEstimator.getEstimate() : config.abrEwmaDefaultEstimate;
        // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
        const bufferStarvationDelay = (BufferHelper.bufferInfo(video, pos, config.maxBufferHole).end - pos) / playbackRate;

        // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
        let bestLevel = this._findBestLevel(currentLevel, currentFragDuration, avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, config.abrBandWidthFactor, config.abrBandWidthUpFactor, levels);
        Iif (bestLevel >= 0) {
          return bestLevel;
        } else {
          logger.trace('rebuffering expected to happen, lets try to find a quality level minimizing the rebuffering');
          // not possible to get rid of rebuffering ... let's try to find level that will guarantee less than maxStarvationDelay of rebuffering
          // if no matching level found, logic will return 0
          let maxStarvationDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxStarvationDelay) : config.maxStarvationDelay;
          let bwFactor = config.abrBandWidthFactor;
          let bwUpFactor = config.abrBandWidthUpFactor;

          Eif (bufferStarvationDelay === 0) {
            // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
            let bitrateTestDelay = this.bitrateTestDelay;
            Iif (bitrateTestDelay) {
              // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
              // max video loading delay used in  automatic start level selection :
              // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
              // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
              // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
              const maxLoadingDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxLoadingDelay) : config.maxLoadingDelay;
              maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
              logger.trace(`bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`);
              // don't use conservative factor on bitrate test
              bwFactor = bwUpFactor = 1;
            }
          }
          bestLevel = this._findBestLevel(currentLevel, currentFragDuration, avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay + maxStarvationDelay, bwFactor, bwUpFactor, levels);
          return Math.max(bestLevel, 0);
        }
      }

      _findBestLevel (currentLevel, currentFragDuration, currentBw, minAutoLevel, maxAutoLevel, maxFetchDuration, bwFactor, bwUpFactor, levels) {
        for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
          let levelInfo = levels[i];

          Iif (!levelInfo) {
            continue;
          }

          const levelDetails = levelInfo.details;
          const avgDuration = levelDetails ? levelDetails.totalduration / levelDetails.fragments.length : currentFragDuration;
          const live = levelDetails ? levelDetails.live : false;

          let adjustedbw;
          // follow algorithm captured from stagefright :
          // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
          // Pick the highest bandwidth stream below or equal to estimated bandwidth.
          // consider only 80% of the available bandwidth, but if we are switching up,
          // be even more conservative (70%) to avoid overestimating and immediately
          // switching back.
          if (i <= currentLevel) {
            adjustedbw = bwFactor * currentBw;
          } else {
            adjustedbw = bwUpFactor * currentBw;
          }

          const bitrate = levels[i].realBitrate ? Math.max(levels[i].realBitrate, levels[i].bitrate) : levels[i].bitrate;
          const fetchDuration = bitrate * avgDuration / adjustedbw;

          logger.trace(`level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: ${i}/${Math.round(adjustedbw)}/${bitrate}/${avgDuration}/${maxFetchDuration}/${fetchDuration}`);
          // if adjusted bw is greater than level bitrate AND
          if (adjustedbw > bitrate &&
          // fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
          // we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
          // special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that _findBestLevel will return -1
            (!fetchDuration || (live && !this.bitrateTestDelay) || fetchDuration < maxFetchDuration)) {
            // as we are looping from highest to lowest, this will return the best achievable quality level
            return i;
          }
        }
        // not enough time budget even with quality level 0 ... rebuffering might happen
        return -1;
      }

      set nextAutoLevel (nextLevel) {
        this._nextAutoLevel = nextLevel;
      }
    }

    export default AbrController;